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

Add support for complex file extensions like ".coffee.md" or ".html.erb" #3285

Merged
merged 2 commits into from
Mar 29, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 64 additions & 23 deletions src/language/LanguageManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@
* console.log("Language " + language.getName() + " is now available!");
* });
*
* The extension can also contain dots:
* LanguageManager.defineLanguage("literatecoffeescript", {
* name: "Literate CoffeeScript",
* mode: "coffeescript",
* fileExtensions: ["litcoffee", "coffee.md"]
* });
*
* You can also specify file names:
* LanguageManager.defineLanguage("makefile", {
* name: "Make",
Expand Down Expand Up @@ -133,21 +140,6 @@ define(function (require, exports, module) {
return true;
}

/**
* Lowercases the file extension and ensures it doesn't start with a dot.
* @param {!string} extension The file extension
* @return {string} The normalized file extension
*/
function _normalizeFileExtension(extension) {
// Remove a leading dot if present
if (extension.charAt(0) === ".") {
extension = extension.substr(1);
}

// Make checks below case-INsensitive
return extension.toLowerCase();
}

/**
* Monkey-patch CodeMirror to prevent modes from being overwritten by extensions.
* We may rely on the tokens provided by some of these modes.
Expand Down Expand Up @@ -180,7 +172,8 @@ define(function (require, exports, module) {

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

// If no language was found for the file name, use the file extension instead
if (!language) {
console.log("Called LanguageManager.getLanguageForPath with an unhandled " + (extension ? "file extension" : "file name") + ":", extension || filename);
// Split the file name into parts:
// "foo.coffee.md" => ["foo", "coffee", "md"]
// ".profile.bak" => ["", "profile", "bak"]
// "1. Vacation.txt" => ["1", " Vacation", "txt"]
parts = fileName.split(".");

// A leading dot does not indicate a file extension, but marks the file as hidden => remove it
if (parts[0] === "") {
// ["", "profile", "bak"] => ["profile", "bak"]
parts.shift();
}

// The first part is assumed to be the title, not the extension => remove it
// ["foo", "coffee", "md"] => ["coffee", "md"]
// ["profile", "bak"] => ["bak"]
// ["1", " Vacation", "txt"] => [" Vacation", "txt"]
parts.shift();

// Join the remaining parts into a file extension until none are left or a language was found
while (!language && parts.length) {
// First iteration:
// ["coffee", "md"] => "coffee.md"
// ["bak"] => "bak"
// [" Vacation", "txt"] => " Vacation.txt"
// Second iteration (assuming no language was found for "coffee.md"):
// ["md"] => "md"
// ["txt"] => "txt"
extension = parts.join(".");
language = _fileExtensionToLanguageMap[extension];
// Remove the first part
// First iteration:
// ["coffee", "md"] => ["md"]
// ["bak"] => []
// [" Vacation", "txt"] => ["txt"]
// Second iteration:
// ["md"] => []
// ["txt"] => []
parts.shift();
}
}

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

/**
* Sets the identifier for this language or prints an error to the console.
* @param {!string} id Identifier for this language, use only letters a-z and _ inbetween (i.e. "cpp", "foo_bar")
* @param {!string} id Identifier for this language, use only letters a-z or digits 0-9, and _ inbetween (i.e. "cpp", "foo_bar", "c99")
* @return {boolean} Whether the ID was valid and set or not
*/
Language.prototype._setId = function (id) {
Expand Down Expand Up @@ -425,7 +458,14 @@ define(function (require, exports, module) {
* @return {boolean} Whether adding the file extension was successful or not
*/
Language.prototype.addFileExtension = function (extension) {
extension = _normalizeFileExtension(extension);
// Remove a leading dot if present
if (extension.charAt(0) === ".") {
extension = extension.substr(1);
}

// Make checks below case-INsensitive
extension = extension.toLowerCase();

if (this._fileExtensions.indexOf(extension) === -1) {
this._fileExtensions.push(extension);

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

if (this._fileNames.indexOf(name) === -1) {
Expand Down Expand Up @@ -600,7 +641,7 @@ define(function (require, exports, module) {
/**
* Defines a language.
*
* @param {!string} id Unique identifier for this language, use only letters a-z, numbers and _ inbetween (i.e. "cpp", "foo_bar")
* @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")
* @param {!Object} definition An object describing the language
* @param {!string} definition.name Human-readable name of the language, as it's commonly referred to (i.e. "C++")
* @param {Array.<string>} definition.fileExtensions List of file extensions used by this language (i.e. ["php", "php3"])
Expand Down
15 changes: 15 additions & 0 deletions test/spec/LanguageManager-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,21 @@ define(function (require, exports, module) {
expect(LanguageManager.getLanguageForPath("foo.doesNotExist")).toBe(unknown);
});

it("should map complex file extensions to languages", function () {
var ruby = LanguageManager.getLanguage("ruby"),
html = LanguageManager.getLanguage("html"),
unknown = LanguageManager.getLanguage("unknown");

expect(LanguageManager.getLanguageForPath("foo.html.erb")).toBe(unknown);
expect(LanguageManager.getLanguageForPath("foo.erb")).toBe(unknown);

html.addFileExtension("html.erb");
ruby.addFileExtension("erb");

expect(LanguageManager.getLanguageForPath("foo.html.erb")).toBe(html);
expect(LanguageManager.getLanguageForPath("foo.erb")).toBe(ruby);
});

it("should map file names to languages", function () {
var coffee = LanguageManager.getLanguage("coffeescript"),
unknown = LanguageManager.getLanguage("unknown");
Expand Down