Add markdown support [#29]
This commit is contained in:
parent
0f22dc7f94
commit
f8a757c508
14
README.md
14
README.md
|
@ -199,6 +199,20 @@ A -> B: this is much easier
|
|||
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
|
||||
|
||||
<img src="screenshots/AlternativeAgentOrdering.png" alt="Alternative Agent Ordering preview" width="200" align="right" />
|
||||
|
|
|
@ -1334,16 +1334,105 @@ define('sequence/Tokeniser',['./CodeMirrorMode'], (CMMode) => {
|
|||
define('sequence/MarkdownParser',[],() => {
|
||||
'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) {
|
||||
if(!text) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const active = STYLES.map(() => false);
|
||||
let activeCount = 0;
|
||||
let attrs = null;
|
||||
const lines = text.split('\n');
|
||||
const result = [];
|
||||
const attrs = null;
|
||||
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;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
11
library.htm
11
library.htm
|
@ -301,6 +301,17 @@ A -> B: this is much easier
|
|||
A <- B: than writing the whole name
|
||||
</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>
|
||||
<pre class="example" data-lang="sequence">
|
||||
define Baz, Foo
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -199,6 +199,26 @@
|
|||
code: 'title {Title}',
|
||||
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',
|
||||
code: 'theme monospace',
|
||||
|
|
|
@ -1,16 +1,105 @@
|
|||
define(() => {
|
||||
'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) {
|
||||
if(!text) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const active = STYLES.map(() => false);
|
||||
let activeCount = 0;
|
||||
let attrs = null;
|
||||
const lines = text.split('\n');
|
||||
const result = [];
|
||||
const attrs = null;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
let hold = null;
|
||||
let block = null;
|
||||
|
|
Loading…
Reference in New Issue