Skip to content
This repository was archived by the owner on Sep 6, 2021. It is now read-only.

Commit bcb5f41

Browse files
committed
Merge pull request #3285 from DennisKehrig/dk/complex-file-extensions
Add support for complex file extensions like ".coffee.md" or ".html.erb"
2 parents 38f18a9 + 1e3a759 commit bcb5f41

File tree

2 files changed

+79
-23
lines changed

2 files changed

+79
-23
lines changed

src/language/LanguageManager.js

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@
4747
* console.log("Language " + language.getName() + " is now available!");
4848
* });
4949
*
50+
* The extension can also contain dots:
51+
* LanguageManager.defineLanguage("literatecoffeescript", {
52+
* name: "Literate CoffeeScript",
53+
* mode: "coffeescript",
54+
* fileExtensions: ["litcoffee", "coffee.md"]
55+
* });
56+
*
5057
* You can also specify file names:
5158
* LanguageManager.defineLanguage("makefile", {
5259
* name: "Make",
@@ -133,21 +140,6 @@ define(function (require, exports, module) {
133140
return true;
134141
}
135142

136-
/**
137-
* Lowercases the file extension and ensures it doesn't start with a dot.
138-
* @param {!string} extension The file extension
139-
* @return {string} The normalized file extension
140-
*/
141-
function _normalizeFileExtension(extension) {
142-
// Remove a leading dot if present
143-
if (extension.charAt(0) === ".") {
144-
extension = extension.substr(1);
145-
}
146-
147-
// Make checks below case-INsensitive
148-
return extension.toLowerCase();
149-
}
150-
151143
/**
152144
* Monkey-patch CodeMirror to prevent modes from being overwritten by extensions.
153145
* We may rely on the tokens provided by some of these modes.
@@ -180,7 +172,8 @@ define(function (require, exports, module) {
180172

181173
/**
182174
* Resolves a language ID to a Language object.
183-
* @param {!string} id Identifier for this language, use only letters a-z and _ inbetween (i.e. "cpp", "foo_bar")
175+
* File names have a higher priority than file extensions.
176+
* @param {!string} id Identifier for this language, use only letters a-z or digits 0-9 and _ inbetween (i.e. "cpp", "foo_bar", "c99")
184177
* @return {Language} The language with the provided identifier or undefined
185178
*/
186179
function getLanguage(id) {
@@ -193,12 +186,52 @@ define(function (require, exports, module) {
193186
* @return {Language} The language for the provided file type or the fallback language
194187
*/
195188
function getLanguageForPath(path) {
196-
var extension = _normalizeFileExtension(PathUtils.filenameExtension(path)),
197-
filename = PathUtils.filename(path).toLowerCase(),
198-
language = extension ? _fileExtensionToLanguageMap[extension] : _fileNameToLanguageMap[filename];
189+
var fileName = PathUtils.filename(path).toLowerCase(),
190+
language = _fileNameToLanguageMap[fileName],
191+
extension,
192+
parts;
199193

194+
// If no language was found for the file name, use the file extension instead
200195
if (!language) {
201-
console.log("Called LanguageManager.getLanguageForPath with an unhandled " + (extension ? "file extension" : "file name") + ":", extension || filename);
196+
// Split the file name into parts:
197+
// "foo.coffee.md" => ["foo", "coffee", "md"]
198+
// ".profile.bak" => ["", "profile", "bak"]
199+
// "1. Vacation.txt" => ["1", " Vacation", "txt"]
200+
parts = fileName.split(".");
201+
202+
// A leading dot does not indicate a file extension, but marks the file as hidden => remove it
203+
if (parts[0] === "") {
204+
// ["", "profile", "bak"] => ["profile", "bak"]
205+
parts.shift();
206+
}
207+
208+
// The first part is assumed to be the title, not the extension => remove it
209+
// ["foo", "coffee", "md"] => ["coffee", "md"]
210+
// ["profile", "bak"] => ["bak"]
211+
// ["1", " Vacation", "txt"] => [" Vacation", "txt"]
212+
parts.shift();
213+
214+
// Join the remaining parts into a file extension until none are left or a language was found
215+
while (!language && parts.length) {
216+
// First iteration:
217+
// ["coffee", "md"] => "coffee.md"
218+
// ["bak"] => "bak"
219+
// [" Vacation", "txt"] => " Vacation.txt"
220+
// Second iteration (assuming no language was found for "coffee.md"):
221+
// ["md"] => "md"
222+
// ["txt"] => "txt"
223+
extension = parts.join(".");
224+
language = _fileExtensionToLanguageMap[extension];
225+
// Remove the first part
226+
// First iteration:
227+
// ["coffee", "md"] => ["md"]
228+
// ["bak"] => []
229+
// [" Vacation", "txt"] => ["txt"]
230+
// Second iteration:
231+
// ["md"] => []
232+
// ["txt"] => []
233+
parts.shift();
234+
}
202235
}
203236

204237
return language || _fallbackLanguage;
@@ -287,7 +320,7 @@ define(function (require, exports, module) {
287320

288321
/**
289322
* Sets the identifier for this language or prints an error to the console.
290-
* @param {!string} id Identifier for this language, use only letters a-z and _ inbetween (i.e. "cpp", "foo_bar")
323+
* @param {!string} id Identifier for this language, use only letters a-z or digits 0-9, and _ inbetween (i.e. "cpp", "foo_bar", "c99")
291324
* @return {boolean} Whether the ID was valid and set or not
292325
*/
293326
Language.prototype._setId = function (id) {
@@ -425,7 +458,14 @@ define(function (require, exports, module) {
425458
* @return {boolean} Whether adding the file extension was successful or not
426459
*/
427460
Language.prototype.addFileExtension = function (extension) {
428-
extension = _normalizeFileExtension(extension);
461+
// Remove a leading dot if present
462+
if (extension.charAt(0) === ".") {
463+
extension = extension.substr(1);
464+
}
465+
466+
// Make checks below case-INsensitive
467+
extension = extension.toLowerCase();
468+
429469
if (this._fileExtensions.indexOf(extension) === -1) {
430470
this._fileExtensions.push(extension);
431471

@@ -446,6 +486,7 @@ define(function (require, exports, module) {
446486
* @return {boolean} Whether adding the file name was successful or not
447487
*/
448488
Language.prototype.addFileName = function (name) {
489+
// Make checks below case-INsensitive
449490
name = name.toLowerCase();
450491

451492
if (this._fileNames.indexOf(name) === -1) {
@@ -600,7 +641,7 @@ define(function (require, exports, module) {
600641
/**
601642
* Defines a language.
602643
*
603-
* @param {!string} id Unique identifier for this language, use only letters a-z, numbers and _ inbetween (i.e. "cpp", "foo_bar")
644+
* @param {!string} id Unique identifier for this language, use only letters a-z or digits 0-9, and _ inbetween (i.e. "cpp", "foo_bar", "c99")
604645
* @param {!Object} definition An object describing the language
605646
* @param {!string} definition.name Human-readable name of the language, as it's commonly referred to (i.e. "C++")
606647
* @param {Array.<string>} definition.fileExtensions List of file extensions used by this language (i.e. ["php", "php3"])

test/spec/LanguageManager-test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,21 @@ define(function (require, exports, module) {
136136
expect(LanguageManager.getLanguageForPath("foo.doesNotExist")).toBe(unknown);
137137
});
138138

139+
it("should map complex file extensions to languages", function () {
140+
var ruby = LanguageManager.getLanguage("ruby"),
141+
html = LanguageManager.getLanguage("html"),
142+
unknown = LanguageManager.getLanguage("unknown");
143+
144+
expect(LanguageManager.getLanguageForPath("foo.html.erb")).toBe(unknown);
145+
expect(LanguageManager.getLanguageForPath("foo.erb")).toBe(unknown);
146+
147+
html.addFileExtension("html.erb");
148+
ruby.addFileExtension("erb");
149+
150+
expect(LanguageManager.getLanguageForPath("foo.html.erb")).toBe(html);
151+
expect(LanguageManager.getLanguageForPath("foo.erb")).toBe(ruby);
152+
});
153+
139154
it("should map file names to languages", function () {
140155
var coffee = LanguageManager.getLanguage("coffeescript"),
141156
unknown = LanguageManager.getLanguage("unknown");

0 commit comments

Comments
 (0)