Add markdown support [#29]

This commit is contained in:
David Evans 2018-01-15 00:10:26 +00:00
parent 0f22dc7f94
commit f8a757c508
8 changed files with 323 additions and 5 deletions

View File

@ -199,6 +199,20 @@ A -> B: this is much easier
A <- B: than writing the whole name A <- B: than writing the whole name
``` ```
### Markdown
<img src="screenshots/Markdown.png" alt="Markdown preview" width="200" align="right" />
```
define 'Name with
**bold** and _italic_' as A
define 'Also `code`
and ~strikeout~' as B
A -> B: '_**basic markdown
is supported!**_'
```
### Alternative Agent Ordering ### Alternative Agent Ordering
<img src="screenshots/AlternativeAgentOrdering.png" alt="Alternative Agent Ordering preview" width="200" align="right" /> <img src="screenshots/AlternativeAgentOrdering.png" alt="Alternative Agent Ordering preview" width="200" align="right" />

View File

@ -1334,16 +1334,105 @@ define('sequence/Tokeniser',['./CodeMirrorMode'], (CMMode) => {
define('sequence/MarkdownParser',[],() => { define('sequence/MarkdownParser',[],() => {
'use strict'; 'use strict';
const STYLES = [
{
begin: /[\s_~`]\*(?=\S)/g,
end: /\S\*(?=[\s_~`])/g,
attrs: {'font-style': 'italic'},
}, {
begin: /[\s*~`]_(?=\S)/g,
end: /\S_(?=[\s*~`])/g,
attrs: {'font-style': 'italic'},
}, {
begin: /[\s_~`]\*\*(?=\S)/g,
end: /\S\*\*(?=[\s_~`])/g,
attrs: {'font-weight': 'bolder'},
}, {
begin: /[\s*~`]__(?=\S)/g,
end: /\S__(?=[\s*~`])/g,
attrs: {'font-weight': 'bolder'},
}, {
begin: /[\s_*`]~(?=\S)/g,
end: /\S~(?=[\s_*`])/g,
attrs: {'text-decoration': 'line-through'},
}, {
begin: /[\s_*~.]`(?=\S)/g,
end: /\S`(?=[\s_*~.])/g,
attrs: {'font-family': 'monospace'},
},
];
function findNext(line, p, active) {
const virtLine = ' ' + line + ' ';
let styleIndex = -1;
let bestStart = virtLine.length;
let bestEnd = 0;
STYLES.forEach(({begin, end, attrs}, ind) => {
const search = active[ind] ? end : begin;
search.lastIndex = p;
const m = search.exec(virtLine);
if(m && (
m.index < bestStart ||
(m.index === bestStart && search.lastIndex > bestEnd)
)) {
styleIndex = ind;
bestStart = m.index;
bestEnd = search.lastIndex;
}
});
return {styleIndex, start: bestStart, end: bestEnd - 1};
}
function combineAttrs(activeCount, active) {
if(!activeCount) {
return null;
}
const attrs = {};
active.forEach((on, ind) => {
if(on) {
Object.assign(attrs, STYLES[ind].attrs);
}
});
return attrs;
}
function parseMarkdown(text) { function parseMarkdown(text) {
if(!text) { if(!text) {
return []; return [];
} }
const active = STYLES.map(() => false);
let activeCount = 0;
let attrs = null;
const lines = text.split('\n'); const lines = text.split('\n');
const result = []; const result = [];
const attrs = null;
lines.forEach((line) => { lines.forEach((line) => {
result.push([{text: line, attrs}]); const parts = [];
let p = 0;
while(true) {
const {styleIndex, start, end} = findNext(line, p, active);
if(styleIndex === -1) {
break;
}
if(active[styleIndex]) {
active[styleIndex] = false;
-- activeCount;
} else {
active[styleIndex] = true;
++ activeCount;
}
if(start > p) {
parts.push({text: line.substring(p, start), attrs});
}
attrs = combineAttrs(activeCount, active);
p = end;
}
if(p < line.length) {
parts.push({text: line.substr(p), attrs});
}
result.push(parts);
}); });
return result; return result;
} }

File diff suppressed because one or more lines are too long

View File

@ -301,6 +301,17 @@ A -> B: this is much easier
A <- B: than writing the whole name A <- B: than writing the whole name
</pre> </pre>
<h3 id="Markdown">Markdown</h3>
<pre class="example" data-lang="sequence">
define 'Name with
**bold** and _italic_' as A
define 'Also `code`
and ~strikeout~' as B
A -> B: '_**basic markdown
is supported!**_'
</pre>
<h3 id="AlternativeAgentOrdering">Alternative Agent Ordering</h3> <h3 id="AlternativeAgentOrdering">Alternative Agent Ordering</h3>
<pre class="example" data-lang="sequence"> <pre class="example" data-lang="sequence">
define Baz, Foo define Baz, Foo

BIN
screenshots/Markdown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -199,6 +199,26 @@
code: 'title {Title}', code: 'title {Title}',
preview: 'headers box\ntitle Title\nA -> B', preview: 'headers box\ntitle Title\nA -> B',
}, },
{
title: 'Bold markdown',
code: '**{text}**',
preview: 'A -> B: **bold**',
},
{
title: 'Italic markdown',
code: '_{text}_',
preview: 'A -> B: _italic_',
},
{
title: 'Strikeout markdown',
code: '~{text}~',
preview: 'A -> B: ~strikeout~',
},
{
title: 'Monospace markdown',
code: '`{text}`',
preview: 'A -> B: `mono`',
},
{ {
title: 'Monospace theme', title: 'Monospace theme',
code: 'theme monospace', code: 'theme monospace',

View File

@ -1,16 +1,105 @@
define(() => { define(() => {
'use strict'; 'use strict';
const STYLES = [
{
begin: /[\s_~`]\*(?=\S)/g,
end: /\S\*(?=[\s_~`])/g,
attrs: {'font-style': 'italic'},
}, {
begin: /[\s*~`]_(?=\S)/g,
end: /\S_(?=[\s*~`])/g,
attrs: {'font-style': 'italic'},
}, {
begin: /[\s_~`]\*\*(?=\S)/g,
end: /\S\*\*(?=[\s_~`])/g,
attrs: {'font-weight': 'bolder'},
}, {
begin: /[\s*~`]__(?=\S)/g,
end: /\S__(?=[\s*~`])/g,
attrs: {'font-weight': 'bolder'},
}, {
begin: /[\s_*`]~(?=\S)/g,
end: /\S~(?=[\s_*`])/g,
attrs: {'text-decoration': 'line-through'},
}, {
begin: /[\s_*~.]`(?=\S)/g,
end: /\S`(?=[\s_*~.])/g,
attrs: {'font-family': 'monospace'},
},
];
function findNext(line, p, active) {
const virtLine = ' ' + line + ' ';
let styleIndex = -1;
let bestStart = virtLine.length;
let bestEnd = 0;
STYLES.forEach(({begin, end, attrs}, ind) => {
const search = active[ind] ? end : begin;
search.lastIndex = p;
const m = search.exec(virtLine);
if(m && (
m.index < bestStart ||
(m.index === bestStart && search.lastIndex > bestEnd)
)) {
styleIndex = ind;
bestStart = m.index;
bestEnd = search.lastIndex;
}
});
return {styleIndex, start: bestStart, end: bestEnd - 1};
}
function combineAttrs(activeCount, active) {
if(!activeCount) {
return null;
}
const attrs = {};
active.forEach((on, ind) => {
if(on) {
Object.assign(attrs, STYLES[ind].attrs);
}
});
return attrs;
}
function parseMarkdown(text) { function parseMarkdown(text) {
if(!text) { if(!text) {
return []; return [];
} }
const active = STYLES.map(() => false);
let activeCount = 0;
let attrs = null;
const lines = text.split('\n'); const lines = text.split('\n');
const result = []; const result = [];
const attrs = null;
lines.forEach((line) => { lines.forEach((line) => {
result.push([{text: line, attrs}]); const parts = [];
let p = 0;
while(true) {
const {styleIndex, start, end} = findNext(line, p, active);
if(styleIndex === -1) {
break;
}
if(active[styleIndex]) {
active[styleIndex] = false;
-- activeCount;
} else {
active[styleIndex] = true;
++ activeCount;
}
if(start > p) {
parts.push({text: line.substring(p, start), attrs});
}
attrs = combineAttrs(activeCount, active);
p = end;
}
if(p < line.length) {
parts.push({text: line.substr(p), attrs});
}
result.push(parts);
}); });
return result; return result;
} }

View File

@ -27,6 +27,101 @@ defineDescribe('Markdown Parser', [
]); ]);
}); });
it('recognises bold styling', () => {
const formatted = parser('a **b** c __d__ e');
expect(formatted).toEqual([[
{text: 'a ', attrs: null},
{text: 'b', attrs: {'font-weight': 'bolder'}},
{text: ' c ', attrs: null},
{text: 'd', attrs: {'font-weight': 'bolder'}},
{text: ' e', attrs: null},
]]);
});
it('ignores styling marks inside words', () => {
const formatted = parser('a**b**c__d__e');
expect(formatted).toEqual([[
{text: 'a**b**c__d__e', attrs: null},
]]);
});
it('continues styling across lines', () => {
const formatted = parser('a **b\nc** d');
expect(formatted).toEqual([[
{text: 'a ', attrs: null},
{text: 'b', attrs: {'font-weight': 'bolder'}},
], [
{text: 'c', attrs: {'font-weight': 'bolder'}},
{text: ' d', attrs: null},
]]);
});
it('recognises italic styling', () => {
const formatted = parser('a *b* c _d_ e');
expect(formatted).toEqual([[
{text: 'a ', attrs: null},
{text: 'b', attrs: {'font-style': 'italic'}},
{text: ' c ', attrs: null},
{text: 'd', attrs: {'font-style': 'italic'}},
{text: ' e', attrs: null},
]]);
});
it('recognises strikethrough styling', () => {
const formatted = parser('a ~b~ c');
expect(formatted).toEqual([[
{text: 'a ', attrs: null},
{text: 'b', attrs: {'text-decoration': 'line-through'}},
{text: ' c', attrs: null},
]]);
});
it('recognises monospace styling', () => {
const formatted = parser('a `b` c');
expect(formatted).toEqual([[
{text: 'a ', attrs: null},
{text: 'b', attrs: {'font-family': 'monospace'}},
{text: ' c', attrs: null},
]]);
});
it('allows dots around monospace styling', () => {
const formatted = parser('a.`b`.c');
expect(formatted).toEqual([[
{text: 'a.', attrs: null},
{text: 'b', attrs: {'font-family': 'monospace'}},
{text: '.c', attrs: null},
]]);
});
it('recognises combined styling', () => {
const formatted = parser('a **_b_ c**');
expect(formatted).toEqual([[
{text: 'a ', attrs: null},
{text: 'b', attrs: {
'font-weight': 'bolder',
'font-style': 'italic',
}},
{text: ' c', attrs: {'font-weight': 'bolder'}},
]]);
});
it('allows complex interactions between styles', () => {
const formatted = parser('_a **b_ ~c~**');
expect(formatted).toEqual([[
{text: 'a ', attrs: {'font-style': 'italic'}},
{text: 'b', attrs: {
'font-weight': 'bolder',
'font-style': 'italic',
}},
{text: ' ', attrs: {'font-weight': 'bolder'}},
{text: 'c', attrs: {
'font-weight': 'bolder',
'text-decoration': 'line-through',
}},
]]);
});
describe('SVGTextBlock interaction', () => { describe('SVGTextBlock interaction', () => {
let hold = null; let hold = null;
let block = null; let block = null;