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

Commit

Permalink
Adds code folding support for handlebars template files (#12675)
Browse files Browse the repository at this point in the history
* added fold range finder for handlebars template files.

* update to test handlebars file

* Added test.hbs file and unit tests for handlebars template files

* added space after module comment

* Addresses code review comments

* added license header and stringutils

* adds handlebars fold helper to htmlmixed mode

* added more handlebars/mustache-related helper tags e.g., tags introduced by ~, ^ or #*
updated tests to capture these cases
  • Loading branch information
thehogfather authored and ficristo committed Oct 5, 2016
1 parent f57c991 commit 281a4df
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 3 deletions.
205 changes: 205 additions & 0 deletions src/extensions/default/CodeFolding/foldhelpers/handlebarsFold.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Copyright (c) 2016 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/

/**
* Fold range finder for handlebars/mustache template type files.
* @author Patrick Oladimeji
* @date 14/08/2016 22:04:21
*/

define(function (require, exports, module) {
"use strict";
var CodeMirror = brackets.getModule("thirdparty/CodeMirror/lib/codemirror"),
_ = brackets.getModule("thirdparty/lodash"),
StringUtils = brackets.getModule("utils/StringUtils");

/**
* Utility function for scanning the text in a document until a certain condition is met
* @param {object} cm The code mirror object representing the document
* @param {string} startCh The start character position for the scan operation
* @param {number} startLine The start line position for the scan operation
* @param {function (string): boolean} condition A predicate function that takes in the text seen so far and returns true if the scanning process should be halted
* @returns {{from:CodeMirror.Pos, to: CodeMirror.Pos, string: string}} An object representing the range of text scanned.
*/
function scanTextUntil(cm, startCh, startLine, condition) {
var line = cm.getLine(startLine),
seen = "",
characterIndex = startCh,
currentLine = startLine,
range;
while (currentLine <= cm.lastLine()) {
if (line.length === 0) {
characterIndex = 0;
line = cm.getLine(++currentLine);
} else {
seen = seen.concat(line[characterIndex] || "");
if (condition(seen)) {
range = {
from: {ch: startCh, line: startLine},
to: {ch: characterIndex, line: currentLine},
string: seen
};
return range;
} else if (characterIndex >= line.length) {
seen = seen.concat(cm.lineSeparator());
if (condition(seen)) {
range = {
from: {ch: startCh, line: startLine},
to: {ch: characterIndex, line: currentLine},
string: seen
};
return range;
}
characterIndex = 0;
line = cm.getLine(++currentLine);
} else {
++characterIndex;
}
}
}
}

/**
* Utility function used to detect the end of a helper name when scanning a series of text.
* The end of a helper name is signalled by a space character or the `}`
* @param {string} seen The string seen so far
* @returns {boolean} True when the end of a helper name has been detected.
*/
function endHelperName(seen) {
return (/\s$/).test(seen) || StringUtils.endsWith(seen, "}");
}

/**
* Returns a predicate function that returns true when a specific character is found
* @param {string} character the character to use in the match function
* @returns {function} A function that checks if the last character of the parameter string matches the parameter character
*/
function readUntil(character) {
return function (seen) {
return seen[seen.length - 1] === character;
};
}

function getRange(cm, start) {
var currentLine = start.line,
text = cm.getLine(currentLine) || "",
i = 0,
tagStack = [],
braceStack = [],
found,
openTag,
openPos,
currentCharacter,
openTagIndex = text.indexOf("{{"),
range;

if (openTagIndex < 0 || text[openTagIndex + 2] === "/") {
return;
}

found = scanTextUntil(cm, openTagIndex + 2, currentLine, endHelperName);
if (!found) {
return;
}

openPos = {
from: {line: currentLine, ch: openTagIndex},
to: found.to
};
openTag = found.string.substring(0, found.string.length - 1);
if (openTag[0] === "#" || openTag[0] === "~" || openTag[0] === "^") {
found = scanTextUntil(cm, openPos.to.ch, openPos.to.line, function (seen) {
return seen.length > 1 && seen.substr(-2) === "}}";
});
if (found) {
openPos.to = {line: found.to.line, ch: found.to.ch + 1};
}
tagStack.push(openTag.substr(1));
} else {
braceStack.push("{{");
}

i = found.to.ch;
currentLine = found.to.line;

while (currentLine <= cm.lastLine()) {
text = cm.getLine(currentLine);
currentCharacter = (text && text[i]) || "";
switch (currentCharacter) {
case "{":
if (text[i + 1] === "{") {
found = scanTextUntil(cm, i + 2, currentLine, endHelperName);
if (found) {
var tag = found.string.substring(0, found.string.length - 1);
if (tag[0] === "#" || tag[0] === "~" || tag[0] === "^") {
tagStack.push(tag.substr(1));
} else if (tag[0] === "/" &&
(_.last(tagStack) === tag.substr(1) || _.last(tagStack) === "*" + tag.substr(1))) {
tagStack.pop();
if (tagStack.length === 0 && braceStack.length === 0) {
range = {
from: openPos.to,
to: {ch: i, line: currentLine}
};
return range;
}
} else {
braceStack.push("{{");
}
}
}
break;
case "}":
if (text[i + 1] === "}") {
braceStack.pop();
if (braceStack.length === 0 && tagStack.length === 0) {
range = {
from: openPos.to,
to: {ch: i, line: currentLine}
};
return range;
}
}
break;
case "\"":
case "'":
found = scanTextUntil(cm, i + 1, currentLine, readUntil(text[i]));
if (found) {
i = found.to.ch;
currentLine = found.to.line;
}
break;
default:
break;
}

++i;
if (i >= text.length) {
++currentLine;
i = 0;
}
}
}

module.exports = getRange;
});
6 changes: 4 additions & 2 deletions src/extensions/default/CodeFolding/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ define(function (require, exports, module) {
var foldGutter = require("foldhelpers/foldgutter"),
foldCode = require("foldhelpers/foldcode"),
indentFold = require("foldhelpers/indentFold"),
handlebarsFold = require("foldhelpers/handlebarsFold"),
selectionFold = require("foldhelpers/foldSelected");


Expand Down Expand Up @@ -393,8 +394,9 @@ define(function (require, exports, module) {
return prefs.getSetting("alwaysUseIndentFold");
}, indentFold);

CodeMirror.registerHelper("fold", "django", CodeMirror.helpers.fold.brace);
CodeMirror.registerHelper("fold", "tornado", CodeMirror.helpers.fold.brace);
CodeMirror.registerHelper("fold", "handlebars", handlebarsFold);
CodeMirror.registerHelper("fold", "htmlhandlebars", handlebarsFold);
CodeMirror.registerHelper("fold", "htmlmixed", handlebarsFold);

EditorManager.on("activeEditorChange.CodeFolding", onActiveEditorChanged);
DocumentManager.on("documentRefreshed.CodeFolding", function (event, doc) {
Expand Down
47 changes: 47 additions & 0 deletions src/extensions/default/CodeFolding/unittest-files/test.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{{!--
comments
go
here
--}}

{{#test}}


{{/test}}

<h1>Comments</h1>

<div id="comments">

{{#each comments}}
{{../permalink

}}

{{#if title}}
{{../permalink}}
{{/if}}
{{/each}}

{{#each comments}}
<h2><a href="/posts/{{../permalink}}#{{id}}">{{title}}</a></h2>
<div>
{{
body
}}
<section class="person">
{{~person}}
<div>Name: {{firstName}} {{lastName}}</div>
<div>Email: {{email}}</div>
<div>Phone: {{phone}}</div>
{{/person}}
{{#*inline "myPartial"}}
My Content
{{/inline}}
{{^relationships}}
Relationship details go here
{{/relationships}}
</section>
</div>
{{/each}}
</div>
9 changes: 8 additions & 1 deletion src/extensions/default/CodeFolding/unittests.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ define(function (require, exports, module) {
sameLevelFoldableLines: [8, 24],
firstSelection: {start: {line: 3, ch: 0}, end: {line: 10, ch: 0}},
secondSelection: {start: {line: 6, ch: 0}, end: {line: 17, ch: 4}}
},
hbs: {
filePath: testDocumentDirectory + "test.hbs",
foldableLines: [1, 7, 14, 16, 17, 21, 26, 28, 29, 32, 33, 38, 41],
sameLevelFoldableLines: [1, 7, 14],
firstSelection: {start: {line: 2, ch: 0}, end: {line: 10, ch: 0}},
secondSelection: {start: {line: 5, ch: 0}, end: {line: 8, ch: 4}}
}
},
open = "open",
Expand Down Expand Up @@ -163,7 +170,7 @@ define(function (require, exports, module) {
/**
* Helper function to return the fold markers on the current codeMirror instance
*
* @returns {[[Type]]} [[Description]]
* @returns {Array<object>} An array of objects containing the line and the type of marker.
*/
function getGutterFoldMarks() {
testEditor = EditorManager.getCurrentFullEditor();
Expand Down

0 comments on commit 281a4df

Please sign in to comment.