diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx
index 4f3ef44f5..a9efdb245 100644
--- a/client/homebrew/editor/editor.jsx
+++ b/client/homebrew/editor/editor.jsx
@@ -151,12 +151,19 @@ const Editor = createClass({
// definition lists
if(line.includes('::')){
- const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
+ if(/^:*$/.test(line) == true){ return };
+ const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
let match;
while ((match = regex.exec(line)) != null){
- codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[0]) }, { line: lineNumber, ch: line.indexOf(match[0]) + match[0].length }, { className: 'define' });
- codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'term' });
- codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[2]) }, { line: lineNumber, ch: line.indexOf(match[2]) + match[2].length }, { className: 'definition' });
+ codeMirror.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' });
+ codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
+ codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
+ const ddIndex = match.indices[2][0];
+ let colons = /::/g;
+ let colonMatches = colons.exec(match[2]);
+ if(colonMatches !== null){
+ codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight'} )
+ }
}
}
diff --git a/client/homebrew/editor/editor.less b/client/homebrew/editor/editor.less
index b165f91db..d7950ead3 100644
--- a/client/homebrew/editor/editor.less
+++ b/client/homebrew/editor/editor.less
@@ -55,6 +55,16 @@
vertical-align : sub;
font-size : 0.9em;
}
+ .dl-highlight {
+ &.dl-colon-highlight {
+ font-weight : bold;
+ color : #949494;
+ background : #E5E5E5;
+ border-radius : 3px;
+ }
+ &.dt-highlight { color : rgb(96, 117, 143); }
+ &.dd-highlight { color : rgb(97, 57, 178); }
+ }
}
.brewJump {
diff --git a/package.json b/package.json
index e0c65e306..0cf6fe773 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace",
"test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace",
"test:mustache-syntax:injection": "jest '.*(mustache-syntax).*' -t '^Injection:.*' --verbose --noStackTrace",
+ "test:marked-extensions": "jest tests/markdown/marked-extensions.test.js --verbose --noStackTrace",
"test:route": "jest tests/routes/static-pages.test.js --verbose",
"phb": "node scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build",
diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js
index 09f810907..f82ec3c32 100644
--- a/shared/naturalcrit/markdown.js
+++ b/shared/naturalcrit/markdown.js
@@ -294,10 +294,10 @@ const superSubScripts = {
}
};
-const definitionLists = {
- name : 'definitionLists',
+const definitionListsInline = {
+ name : 'definitionListsInline',
level : 'block',
- start(src) { return src.match(/^.*?::.*/m)?.index; }, // Hint to Marked.js to stop and check for a match
+ start(src) { return src.match(/^[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
let match;
@@ -312,7 +312,7 @@ const definitionLists = {
}
if(definitions.length) {
return {
- type : 'definitionLists',
+ type : 'definitionListsInline',
raw : src.slice(0, endIndex),
definitions
};
@@ -321,11 +321,54 @@ const definitionLists = {
renderer(token) {
return `
${token.definitions.reduce((html, def)=>{
return `${html}- ${this.parser.parseInline(def.dt)}
`
- + `- ${this.parser.parseInline(def.dd)}
\n`;
+ + `- ${this.parser.parseInline(def.dd)}
`;
}, '')}
`;
}
};
+const definitionListsMultiline = {
+ name : 'definitionListsMultiline',
+ level : 'block',
+ start(src) { return src.match(/^[^\n]*\n::/m)?.index; }, // Hint to Marked.js to stop and check for a match
+ tokenizer(src, tokens) {
+ const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
+ let match;
+ let endIndex = 0;
+ const definitions = [];
+ while (match = regex.exec(src)) {
+ if(match[1]) {
+ definitions.push({
+ dt : this.lexer.inlineTokens(match[1].trim()),
+ dds : []
+ });
+ }
+ if(match[2]) {
+ definitions[definitions.length - 1].dds.push(
+ this.lexer.inlineTokens(match[2].trim().replace(/\s/g, ' '))
+ );
+ }
+ endIndex = regex.lastIndex;
+ }
+ if(definitions.length) {
+ return {
+ type : 'definitionListsMultiline',
+ raw : src.slice(0, endIndex),
+ definitions
+ };
+ }
+ },
+ renderer(token) {
+ let returnVal = ``;
+ token.definitions.forEach((def)=>{
+ const dds = def.dds.map((s)=>{
+ return `\n- ${this.parser.parseInline(s).trim()}
`;
+ }).join('');
+ returnVal += `- ${this.parser.parseInline(def.dt)}
${dds}\n`;
+ });
+ returnVal = returnVal.trim();
+ return `${returnVal}
`;
+ }
+};
//v=====--------------------< Variable Handling >-------------------=====v// 242 lines
const replaceVar = function(input, hoist=false, allowUnresolved=false) {
@@ -572,7 +615,7 @@ function MarkedVariables() {
//^=====--------------------< Variable Handling >-------------------=====^//
Marked.use(MarkedVariables());
-Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts] });
+Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionListsInline, definitionListsMultiline, superSubScripts] });
Marked.use(mustacheInjectBlock);
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
diff --git a/tests/markdown/marked-extensions.test.js b/tests/markdown/marked-extensions.test.js
new file mode 100644
index 000000000..8d6c3c1c4
--- /dev/null
+++ b/tests/markdown/marked-extensions.test.js
@@ -0,0 +1,79 @@
+/* eslint-disable max-lines */
+
+const Markdown = require('naturalcrit/markdown.js');
+
+describe('Inline Definition Lists', ()=>{
+ test('No Term 1 Definition', function() {
+ const source = ':: My First Definition\n\n';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- My First Definition
');
+ });
+
+ test('Single Definition Term', function() {
+ const source = 'My term :: My First Definition\n\n';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- My term
- My First Definition
');
+ });
+
+ test('Multiple Definition Terms', function() {
+ const source = 'Term 1::Definition of Term 1\nTerm 2::Definition of Term 2\n\n';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- Term 1
- Definition of Term 1
- Term 2
- Definition of Term 2
');
+ });
+});
+
+describe('Multiline Definition Lists', ()=>{
+ test('Single Term, Single Definition', function() {
+ const source = 'Term 1\n::Definition 1\n\n';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- Term 1
\n- Definition 1
');
+ });
+
+ test('Single Term, Plural Definitions', function() {
+ const source = 'Term 1\n::Definition 1\n::Definition 2\n\n';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- Term 1
\n- Definition 1
\n- Definition 2
');
+ });
+
+ test('Multiple Term, Single Definitions', function() {
+ const source = 'Term 1\n::Definition 1\n\nTerm 2\n::Definition 1\n\n';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- Term 1
\n- Definition 1
\n- Term 2
\n- Definition 1
');
+ });
+
+ test('Multiple Term, Plural Definitions', function() {
+ const source = 'Term 1\n::Definition 1\n::Definition 2\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- Term 1
\n- Definition 1
\n- Definition 2
\n- Term 2
\n- Definition 1
\n- Definition 2
');
+ });
+
+ test('Single Term, Single multi-line definition', function() {
+ const source = 'Term 1\n::Definition 1\nand more and\nmore and more\n\n';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- Term 1
\n- Definition 1 and more and more and more
');
+ });
+
+ test('Single Term, Plural multi-line definitions', function() {
+ const source = 'Term 1\n::Definition 1\nand more and more\n::Definition 2\nand more\nand more\n::Definition 3\n\n';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- Term 1
\n- Definition 1 and more and more
\n- Definition 2 and more and more
\n- Definition 3
');
+ });
+
+ test('Multiple Term, Single multi-line definition', function() {
+ const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- Term 1
\n- Definition 1 and more and more
\n- Term 2
\n- Definition 1
\n- Definition 2
');
+ });
+
+ test('Multiple Term, Single multi-line definition, followed by an inline dl', function() {
+ const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n::Inline Definition (no term)';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- Term 1
\n- Definition 1 and more and more
\n- Term 2
\n- Definition 1
\n- Definition 2
- Inline Definition (no term)
');
+ });
+
+ test('Multiple Term, Single multi-line definition, followed by paragraph', function() {
+ const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\nParagraph';
+ const rendered = Markdown.render(source).trim();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('- Term 1
\n- Definition 1 and more and more
\n- Term 2
\n- Definition 1
\n- Definition 2
Paragraph
');
+ });
+});