Add markdown link support [#54]
This commit is contained in:
parent
011d8c6979
commit
3e4110193a
|
@ -4224,6 +4224,10 @@
|
||||||
attrs: {'filter': 'highlight'},
|
attrs: {'filter': 'highlight'},
|
||||||
begin: {matcher: /<highlight>/g, skip: 0},
|
begin: {matcher: /<highlight>/g, skip: 0},
|
||||||
end: {matcher: /<\/highlight>/g, skip: 0},
|
end: {matcher: /<\/highlight>/g, skip: 0},
|
||||||
|
}, {
|
||||||
|
all: {matcher: /\[([^\]]+)\]\(([^)]+)\)/g, skip: 0},
|
||||||
|
attrs: (m) => ({'href': m[2], 'text-decoration': 'underline'}),
|
||||||
|
text: (m) => m[1],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -4232,62 +4236,73 @@
|
||||||
|
|
||||||
const ESC = -2;
|
const ESC = -2;
|
||||||
|
|
||||||
function findNext(line, p, active) {
|
function pickBest(best, styleIndex, search, match) {
|
||||||
const virtLine = ' ' + line + ' ';
|
if(!match) {
|
||||||
let styleIndex = -1;
|
return best;
|
||||||
let bestStart = virtLine.length;
|
|
||||||
let bestEnd = 0;
|
|
||||||
|
|
||||||
STYLES.forEach(({begin, end}, ind) => {
|
|
||||||
const search = active[ind] ? end : begin;
|
|
||||||
search.matcher.lastIndex = p + 1 - search.skip;
|
|
||||||
const m = search.matcher.exec(virtLine);
|
|
||||||
const beginInd = m ? (m.index + search.skip) : Number.POSITIVE_INFINITY;
|
|
||||||
if(
|
|
||||||
beginInd < bestStart ||
|
|
||||||
(beginInd === bestStart && search.matcher.lastIndex > bestEnd)
|
|
||||||
) {
|
|
||||||
styleIndex = ind;
|
|
||||||
bestStart = beginInd;
|
|
||||||
bestEnd = search.matcher.lastIndex;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const escIndex = virtLine.indexOf('\u001B', p + 1);
|
|
||||||
if(escIndex !== -1 && escIndex < bestStart) {
|
|
||||||
styleIndex = ESC;
|
|
||||||
bestStart = escIndex;
|
|
||||||
bestEnd = escIndex + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(styleIndex === -1) {
|
const start = match.index + search.skip;
|
||||||
return null;
|
const end = search.matcher.lastIndex;
|
||||||
|
if(start < best.start || (start === best.start && end > best.end)) {
|
||||||
|
return {end, match, start, styleIndex};
|
||||||
}
|
}
|
||||||
|
return best;
|
||||||
return {end: bestEnd - 1, start: bestStart - 1, styleIndex};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function combineAttrs(activeCount, active) {
|
function findNext(line, p, active) {
|
||||||
if(!activeCount) {
|
const virtLine = ' ' + line + ' ';
|
||||||
|
const pos = p + 1;
|
||||||
|
let best = {
|
||||||
|
end: 0,
|
||||||
|
match: null,
|
||||||
|
start: virtLine.length,
|
||||||
|
styleIndex: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const escIndex = virtLine.indexOf('\u001B', pos);
|
||||||
|
if(escIndex !== -1) {
|
||||||
|
best = {
|
||||||
|
end: escIndex + 1,
|
||||||
|
match: null,
|
||||||
|
start: escIndex,
|
||||||
|
styleIndex: ESC,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
STYLES.forEach(({all, begin, end}, ind) => {
|
||||||
|
const search = all || (active[ind] === null ? begin : end);
|
||||||
|
search.matcher.lastIndex = pos - search.skip;
|
||||||
|
best = pickBest(best, ind, search, search.matcher.exec(virtLine));
|
||||||
|
});
|
||||||
|
|
||||||
|
if(best.styleIndex === -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- best.end;
|
||||||
|
-- best.start;
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function combineAttrs(active) {
|
||||||
const attrs = {};
|
const attrs = {};
|
||||||
const decorations = [];
|
const decorations = [];
|
||||||
active.forEach((on, ind) => {
|
let any = false;
|
||||||
if(!on) {
|
active.forEach((activeAttrs) => {
|
||||||
|
if(!activeAttrs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const activeAttrs = STYLES[ind].attrs;
|
|
||||||
const decoration = activeAttrs['text-decoration'];
|
const decoration = activeAttrs['text-decoration'];
|
||||||
if(decoration && !decorations.includes(decoration)) {
|
if(decoration && !decorations.includes(decoration)) {
|
||||||
decorations.push(decoration);
|
decorations.push(decoration);
|
||||||
}
|
}
|
||||||
Object.assign(attrs, activeAttrs);
|
Object.assign(attrs, activeAttrs);
|
||||||
|
any = true;
|
||||||
});
|
});
|
||||||
if(decorations.length > 1) {
|
if(decorations.length > 1) {
|
||||||
attrs['text-decoration'] = decorations.join(' ');
|
attrs['text-decoration'] = decorations.join(' ');
|
||||||
}
|
}
|
||||||
return attrs;
|
return any ? attrs : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shrinkWhitespace(text) {
|
function shrinkWhitespace(text) {
|
||||||
|
@ -4298,29 +4313,44 @@
|
||||||
return text.replace(WHITE_END, '');
|
return text.replace(WHITE_END, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function findStyles(line, active, toggleCallback, textCallback) {
|
function getOrCall(v, params) {
|
||||||
|
if(typeof v === 'function') {
|
||||||
|
return v(...params);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findStyles(line, active, textCallback) {
|
||||||
let ln = line;
|
let ln = line;
|
||||||
let p = 0;
|
let p = 0;
|
||||||
let s = 0;
|
let s = 0;
|
||||||
let match = null;
|
for(let next = null; (next = findNext(ln, p, active));) {
|
||||||
while((match = findNext(ln, p, active))) {
|
const {styleIndex, start, end, match} = next;
|
||||||
const {styleIndex, start, end} = match;
|
|
||||||
if(styleIndex === ESC) {
|
if(styleIndex === ESC) {
|
||||||
ln = ln.substr(0, start) + ln.substr(end);
|
ln = ln.substr(0, start) + ln.substr(end);
|
||||||
p = start + 1;
|
p = start + 1;
|
||||||
} else {
|
continue;
|
||||||
if(start > s) {
|
|
||||||
textCallback(ln.substring(s, start));
|
|
||||||
}
|
|
||||||
active[styleIndex] = !active[styleIndex];
|
|
||||||
toggleCallback(styleIndex);
|
|
||||||
s = end;
|
|
||||||
p = end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textCallback(ln.substring(s, start));
|
||||||
|
|
||||||
|
if(active[styleIndex] === null) {
|
||||||
|
const style = STYLES[styleIndex];
|
||||||
|
|
||||||
|
active[styleIndex] = getOrCall(style.attrs, [match]);
|
||||||
|
if(style.all) {
|
||||||
|
textCallback(getOrCall(style.text, [match]));
|
||||||
|
active[styleIndex] = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
active[styleIndex] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = end;
|
||||||
|
p = end;
|
||||||
}
|
}
|
||||||
if(s < ln.length) {
|
textCallback(ln.substr(s));
|
||||||
textCallback(ln.substr(s));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMarkdown(markdown) {
|
function parseMarkdown(markdown) {
|
||||||
|
@ -4328,21 +4358,15 @@
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const active = STYLES.map(() => false);
|
const active = STYLES.map(() => null);
|
||||||
let activeCount = 0;
|
|
||||||
let attrs = null;
|
|
||||||
const lines = trimCollapsible(markdown).split('\n');
|
const lines = trimCollapsible(markdown).split('\n');
|
||||||
return lines.map((line) => {
|
return lines.map((line) => {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
findStyles(
|
findStyles(shrinkWhitespace(trimCollapsible(line)), active, (text) => {
|
||||||
shrinkWhitespace(trimCollapsible(line)),
|
if(text) {
|
||||||
active,
|
parts.push({attrs: combineAttrs(active), text});
|
||||||
(styleIndex) => {
|
}
|
||||||
activeCount += active[styleIndex] ? 1 : -1;
|
});
|
||||||
attrs = combineAttrs(activeCount, active);
|
|
||||||
},
|
|
||||||
(text) => parts.push({attrs, text})
|
|
||||||
);
|
|
||||||
return parts;
|
return parts;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -6963,7 +6987,16 @@
|
||||||
formattedLine.forEach(({text, attrs}) => {
|
formattedLine.forEach(({text, attrs}) => {
|
||||||
let element = text;
|
let element = text;
|
||||||
if(attrs) {
|
if(attrs) {
|
||||||
element = svg.el('tspan').attrs(attrs).add(text);
|
if(attrs.href) {
|
||||||
|
element = svg.el('a').attrs({
|
||||||
|
'cursor': 'pointer',
|
||||||
|
'rel': 'nofollow',
|
||||||
|
'target': '_blank',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
element = svg.el('tspan');
|
||||||
|
}
|
||||||
|
element.attrs(attrs).add(text);
|
||||||
if(attrs.filter) {
|
if(attrs.filter) {
|
||||||
element.attr('filter', svg.getTextFilter(attrs.filter));
|
element.attr('filter', svg.getTextFilter(attrs.filter));
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -4224,6 +4224,10 @@
|
||||||
attrs: {'filter': 'highlight'},
|
attrs: {'filter': 'highlight'},
|
||||||
begin: {matcher: /<highlight>/g, skip: 0},
|
begin: {matcher: /<highlight>/g, skip: 0},
|
||||||
end: {matcher: /<\/highlight>/g, skip: 0},
|
end: {matcher: /<\/highlight>/g, skip: 0},
|
||||||
|
}, {
|
||||||
|
all: {matcher: /\[([^\]]+)\]\(([^)]+)\)/g, skip: 0},
|
||||||
|
attrs: (m) => ({'href': m[2], 'text-decoration': 'underline'}),
|
||||||
|
text: (m) => m[1],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -4232,62 +4236,73 @@
|
||||||
|
|
||||||
const ESC = -2;
|
const ESC = -2;
|
||||||
|
|
||||||
function findNext(line, p, active) {
|
function pickBest(best, styleIndex, search, match) {
|
||||||
const virtLine = ' ' + line + ' ';
|
if(!match) {
|
||||||
let styleIndex = -1;
|
return best;
|
||||||
let bestStart = virtLine.length;
|
|
||||||
let bestEnd = 0;
|
|
||||||
|
|
||||||
STYLES.forEach(({begin, end}, ind) => {
|
|
||||||
const search = active[ind] ? end : begin;
|
|
||||||
search.matcher.lastIndex = p + 1 - search.skip;
|
|
||||||
const m = search.matcher.exec(virtLine);
|
|
||||||
const beginInd = m ? (m.index + search.skip) : Number.POSITIVE_INFINITY;
|
|
||||||
if(
|
|
||||||
beginInd < bestStart ||
|
|
||||||
(beginInd === bestStart && search.matcher.lastIndex > bestEnd)
|
|
||||||
) {
|
|
||||||
styleIndex = ind;
|
|
||||||
bestStart = beginInd;
|
|
||||||
bestEnd = search.matcher.lastIndex;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const escIndex = virtLine.indexOf('\u001B', p + 1);
|
|
||||||
if(escIndex !== -1 && escIndex < bestStart) {
|
|
||||||
styleIndex = ESC;
|
|
||||||
bestStart = escIndex;
|
|
||||||
bestEnd = escIndex + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(styleIndex === -1) {
|
const start = match.index + search.skip;
|
||||||
return null;
|
const end = search.matcher.lastIndex;
|
||||||
|
if(start < best.start || (start === best.start && end > best.end)) {
|
||||||
|
return {end, match, start, styleIndex};
|
||||||
}
|
}
|
||||||
|
return best;
|
||||||
return {end: bestEnd - 1, start: bestStart - 1, styleIndex};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function combineAttrs(activeCount, active) {
|
function findNext(line, p, active) {
|
||||||
if(!activeCount) {
|
const virtLine = ' ' + line + ' ';
|
||||||
|
const pos = p + 1;
|
||||||
|
let best = {
|
||||||
|
end: 0,
|
||||||
|
match: null,
|
||||||
|
start: virtLine.length,
|
||||||
|
styleIndex: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const escIndex = virtLine.indexOf('\u001B', pos);
|
||||||
|
if(escIndex !== -1) {
|
||||||
|
best = {
|
||||||
|
end: escIndex + 1,
|
||||||
|
match: null,
|
||||||
|
start: escIndex,
|
||||||
|
styleIndex: ESC,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
STYLES.forEach(({all, begin, end}, ind) => {
|
||||||
|
const search = all || (active[ind] === null ? begin : end);
|
||||||
|
search.matcher.lastIndex = pos - search.skip;
|
||||||
|
best = pickBest(best, ind, search, search.matcher.exec(virtLine));
|
||||||
|
});
|
||||||
|
|
||||||
|
if(best.styleIndex === -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- best.end;
|
||||||
|
-- best.start;
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function combineAttrs(active) {
|
||||||
const attrs = {};
|
const attrs = {};
|
||||||
const decorations = [];
|
const decorations = [];
|
||||||
active.forEach((on, ind) => {
|
let any = false;
|
||||||
if(!on) {
|
active.forEach((activeAttrs) => {
|
||||||
|
if(!activeAttrs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const activeAttrs = STYLES[ind].attrs;
|
|
||||||
const decoration = activeAttrs['text-decoration'];
|
const decoration = activeAttrs['text-decoration'];
|
||||||
if(decoration && !decorations.includes(decoration)) {
|
if(decoration && !decorations.includes(decoration)) {
|
||||||
decorations.push(decoration);
|
decorations.push(decoration);
|
||||||
}
|
}
|
||||||
Object.assign(attrs, activeAttrs);
|
Object.assign(attrs, activeAttrs);
|
||||||
|
any = true;
|
||||||
});
|
});
|
||||||
if(decorations.length > 1) {
|
if(decorations.length > 1) {
|
||||||
attrs['text-decoration'] = decorations.join(' ');
|
attrs['text-decoration'] = decorations.join(' ');
|
||||||
}
|
}
|
||||||
return attrs;
|
return any ? attrs : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shrinkWhitespace(text) {
|
function shrinkWhitespace(text) {
|
||||||
|
@ -4298,29 +4313,44 @@
|
||||||
return text.replace(WHITE_END, '');
|
return text.replace(WHITE_END, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function findStyles(line, active, toggleCallback, textCallback) {
|
function getOrCall(v, params) {
|
||||||
|
if(typeof v === 'function') {
|
||||||
|
return v(...params);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findStyles(line, active, textCallback) {
|
||||||
let ln = line;
|
let ln = line;
|
||||||
let p = 0;
|
let p = 0;
|
||||||
let s = 0;
|
let s = 0;
|
||||||
let match = null;
|
for(let next = null; (next = findNext(ln, p, active));) {
|
||||||
while((match = findNext(ln, p, active))) {
|
const {styleIndex, start, end, match} = next;
|
||||||
const {styleIndex, start, end} = match;
|
|
||||||
if(styleIndex === ESC) {
|
if(styleIndex === ESC) {
|
||||||
ln = ln.substr(0, start) + ln.substr(end);
|
ln = ln.substr(0, start) + ln.substr(end);
|
||||||
p = start + 1;
|
p = start + 1;
|
||||||
} else {
|
continue;
|
||||||
if(start > s) {
|
|
||||||
textCallback(ln.substring(s, start));
|
|
||||||
}
|
|
||||||
active[styleIndex] = !active[styleIndex];
|
|
||||||
toggleCallback(styleIndex);
|
|
||||||
s = end;
|
|
||||||
p = end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textCallback(ln.substring(s, start));
|
||||||
|
|
||||||
|
if(active[styleIndex] === null) {
|
||||||
|
const style = STYLES[styleIndex];
|
||||||
|
|
||||||
|
active[styleIndex] = getOrCall(style.attrs, [match]);
|
||||||
|
if(style.all) {
|
||||||
|
textCallback(getOrCall(style.text, [match]));
|
||||||
|
active[styleIndex] = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
active[styleIndex] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = end;
|
||||||
|
p = end;
|
||||||
}
|
}
|
||||||
if(s < ln.length) {
|
textCallback(ln.substr(s));
|
||||||
textCallback(ln.substr(s));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMarkdown(markdown) {
|
function parseMarkdown(markdown) {
|
||||||
|
@ -4328,21 +4358,15 @@
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const active = STYLES.map(() => false);
|
const active = STYLES.map(() => null);
|
||||||
let activeCount = 0;
|
|
||||||
let attrs = null;
|
|
||||||
const lines = trimCollapsible(markdown).split('\n');
|
const lines = trimCollapsible(markdown).split('\n');
|
||||||
return lines.map((line) => {
|
return lines.map((line) => {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
findStyles(
|
findStyles(shrinkWhitespace(trimCollapsible(line)), active, (text) => {
|
||||||
shrinkWhitespace(trimCollapsible(line)),
|
if(text) {
|
||||||
active,
|
parts.push({attrs: combineAttrs(active), text});
|
||||||
(styleIndex) => {
|
}
|
||||||
activeCount += active[styleIndex] ? 1 : -1;
|
});
|
||||||
attrs = combineAttrs(activeCount, active);
|
|
||||||
},
|
|
||||||
(text) => parts.push({attrs, text})
|
|
||||||
);
|
|
||||||
return parts;
|
return parts;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -6963,7 +6987,16 @@
|
||||||
formattedLine.forEach(({text, attrs}) => {
|
formattedLine.forEach(({text, attrs}) => {
|
||||||
let element = text;
|
let element = text;
|
||||||
if(attrs) {
|
if(attrs) {
|
||||||
element = svg.el('tspan').attrs(attrs).add(text);
|
if(attrs.href) {
|
||||||
|
element = svg.el('a').attrs({
|
||||||
|
'cursor': 'pointer',
|
||||||
|
'rel': 'nofollow',
|
||||||
|
'target': '_blank',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
element = svg.el('tspan');
|
||||||
|
}
|
||||||
|
element.attrs(attrs).add(text);
|
||||||
if(attrs.filter) {
|
if(attrs.filter) {
|
||||||
element.attr('filter', svg.getTextFilter(attrs.filter));
|
element.attr('filter', svg.getTextFilter(attrs.filter));
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,10 @@ const STYLES = [
|
||||||
attrs: {'filter': 'highlight'},
|
attrs: {'filter': 'highlight'},
|
||||||
begin: {matcher: /<highlight>/g, skip: 0},
|
begin: {matcher: /<highlight>/g, skip: 0},
|
||||||
end: {matcher: /<\/highlight>/g, skip: 0},
|
end: {matcher: /<\/highlight>/g, skip: 0},
|
||||||
|
}, {
|
||||||
|
all: {matcher: /\[([^\]]+)\]\(([^)]+)\)/g, skip: 0},
|
||||||
|
attrs: (m) => ({'href': m[2], 'text-decoration': 'underline'}),
|
||||||
|
text: (m) => m[1],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -67,62 +71,73 @@ const WHITE_END = /^[\t-\r ]+|[\t-\r ]+$/g;
|
||||||
|
|
||||||
const ESC = -2;
|
const ESC = -2;
|
||||||
|
|
||||||
function findNext(line, p, active) {
|
function pickBest(best, styleIndex, search, match) {
|
||||||
const virtLine = ' ' + line + ' ';
|
if(!match) {
|
||||||
let styleIndex = -1;
|
return best;
|
||||||
let bestStart = virtLine.length;
|
|
||||||
let bestEnd = 0;
|
|
||||||
|
|
||||||
STYLES.forEach(({begin, end}, ind) => {
|
|
||||||
const search = active[ind] ? end : begin;
|
|
||||||
search.matcher.lastIndex = p + 1 - search.skip;
|
|
||||||
const m = search.matcher.exec(virtLine);
|
|
||||||
const beginInd = m ? (m.index + search.skip) : Number.POSITIVE_INFINITY;
|
|
||||||
if(
|
|
||||||
beginInd < bestStart ||
|
|
||||||
(beginInd === bestStart && search.matcher.lastIndex > bestEnd)
|
|
||||||
) {
|
|
||||||
styleIndex = ind;
|
|
||||||
bestStart = beginInd;
|
|
||||||
bestEnd = search.matcher.lastIndex;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const escIndex = virtLine.indexOf('\u001B', p + 1);
|
|
||||||
if(escIndex !== -1 && escIndex < bestStart) {
|
|
||||||
styleIndex = ESC;
|
|
||||||
bestStart = escIndex;
|
|
||||||
bestEnd = escIndex + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(styleIndex === -1) {
|
const start = match.index + search.skip;
|
||||||
return null;
|
const end = search.matcher.lastIndex;
|
||||||
|
if(start < best.start || (start === best.start && end > best.end)) {
|
||||||
|
return {end, match, start, styleIndex};
|
||||||
}
|
}
|
||||||
|
return best;
|
||||||
return {end: bestEnd - 1, start: bestStart - 1, styleIndex};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function combineAttrs(activeCount, active) {
|
function findNext(line, p, active) {
|
||||||
if(!activeCount) {
|
const virtLine = ' ' + line + ' ';
|
||||||
|
const pos = p + 1;
|
||||||
|
let best = {
|
||||||
|
end: 0,
|
||||||
|
match: null,
|
||||||
|
start: virtLine.length,
|
||||||
|
styleIndex: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const escIndex = virtLine.indexOf('\u001B', pos);
|
||||||
|
if(escIndex !== -1) {
|
||||||
|
best = {
|
||||||
|
end: escIndex + 1,
|
||||||
|
match: null,
|
||||||
|
start: escIndex,
|
||||||
|
styleIndex: ESC,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
STYLES.forEach(({all, begin, end}, ind) => {
|
||||||
|
const search = all || (active[ind] === null ? begin : end);
|
||||||
|
search.matcher.lastIndex = pos - search.skip;
|
||||||
|
best = pickBest(best, ind, search, search.matcher.exec(virtLine));
|
||||||
|
});
|
||||||
|
|
||||||
|
if(best.styleIndex === -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- best.end;
|
||||||
|
-- best.start;
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function combineAttrs(active) {
|
||||||
const attrs = {};
|
const attrs = {};
|
||||||
const decorations = [];
|
const decorations = [];
|
||||||
active.forEach((on, ind) => {
|
let any = false;
|
||||||
if(!on) {
|
active.forEach((activeAttrs) => {
|
||||||
|
if(!activeAttrs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const activeAttrs = STYLES[ind].attrs;
|
|
||||||
const decoration = activeAttrs['text-decoration'];
|
const decoration = activeAttrs['text-decoration'];
|
||||||
if(decoration && !decorations.includes(decoration)) {
|
if(decoration && !decorations.includes(decoration)) {
|
||||||
decorations.push(decoration);
|
decorations.push(decoration);
|
||||||
}
|
}
|
||||||
Object.assign(attrs, activeAttrs);
|
Object.assign(attrs, activeAttrs);
|
||||||
|
any = true;
|
||||||
});
|
});
|
||||||
if(decorations.length > 1) {
|
if(decorations.length > 1) {
|
||||||
attrs['text-decoration'] = decorations.join(' ');
|
attrs['text-decoration'] = decorations.join(' ');
|
||||||
}
|
}
|
||||||
return attrs;
|
return any ? attrs : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shrinkWhitespace(text) {
|
function shrinkWhitespace(text) {
|
||||||
|
@ -133,29 +148,44 @@ function trimCollapsible(text) {
|
||||||
return text.replace(WHITE_END, '');
|
return text.replace(WHITE_END, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function findStyles(line, active, toggleCallback, textCallback) {
|
function getOrCall(v, params) {
|
||||||
|
if(typeof v === 'function') {
|
||||||
|
return v(...params);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findStyles(line, active, textCallback) {
|
||||||
let ln = line;
|
let ln = line;
|
||||||
let p = 0;
|
let p = 0;
|
||||||
let s = 0;
|
let s = 0;
|
||||||
let match = null;
|
for(let next = null; (next = findNext(ln, p, active));) {
|
||||||
while((match = findNext(ln, p, active))) {
|
const {styleIndex, start, end, match} = next;
|
||||||
const {styleIndex, start, end} = match;
|
|
||||||
if(styleIndex === ESC) {
|
if(styleIndex === ESC) {
|
||||||
ln = ln.substr(0, start) + ln.substr(end);
|
ln = ln.substr(0, start) + ln.substr(end);
|
||||||
p = start + 1;
|
p = start + 1;
|
||||||
} else {
|
continue;
|
||||||
if(start > s) {
|
|
||||||
textCallback(ln.substring(s, start));
|
|
||||||
}
|
|
||||||
active[styleIndex] = !active[styleIndex];
|
|
||||||
toggleCallback(styleIndex);
|
|
||||||
s = end;
|
|
||||||
p = end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textCallback(ln.substring(s, start));
|
||||||
|
|
||||||
|
if(active[styleIndex] === null) {
|
||||||
|
const style = STYLES[styleIndex];
|
||||||
|
|
||||||
|
active[styleIndex] = getOrCall(style.attrs, [match]);
|
||||||
|
if(style.all) {
|
||||||
|
textCallback(getOrCall(style.text, [match]));
|
||||||
|
active[styleIndex] = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
active[styleIndex] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = end;
|
||||||
|
p = end;
|
||||||
}
|
}
|
||||||
if(s < ln.length) {
|
textCallback(ln.substr(s));
|
||||||
textCallback(ln.substr(s));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function parseMarkdown(markdown) {
|
export default function parseMarkdown(markdown) {
|
||||||
|
@ -163,21 +193,15 @@ export default function parseMarkdown(markdown) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const active = STYLES.map(() => false);
|
const active = STYLES.map(() => null);
|
||||||
let activeCount = 0;
|
|
||||||
let attrs = null;
|
|
||||||
const lines = trimCollapsible(markdown).split('\n');
|
const lines = trimCollapsible(markdown).split('\n');
|
||||||
return lines.map((line) => {
|
return lines.map((line) => {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
findStyles(
|
findStyles(shrinkWhitespace(trimCollapsible(line)), active, (text) => {
|
||||||
shrinkWhitespace(trimCollapsible(line)),
|
if(text) {
|
||||||
active,
|
parts.push({attrs: combineAttrs(active), text});
|
||||||
(styleIndex) => {
|
}
|
||||||
activeCount += active[styleIndex] ? 1 : -1;
|
});
|
||||||
attrs = combineAttrs(activeCount, active);
|
|
||||||
},
|
|
||||||
(text) => parts.push({attrs, text})
|
|
||||||
);
|
|
||||||
return parts;
|
return parts;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,6 +205,16 @@ describe('Markdown Parser', () => {
|
||||||
]]);
|
]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recognises link styling', () => {
|
||||||
|
const formatted = parser('a [b](c) d');
|
||||||
|
|
||||||
|
expect(formatted).toEqual([[
|
||||||
|
{attrs: null, text: 'a '},
|
||||||
|
{attrs: {'href': 'c', 'text-decoration': 'underline'}, text: 'b'},
|
||||||
|
{attrs: null, text: ' d'},
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
it('allows dots around monospace styling', () => {
|
it('allows dots around monospace styling', () => {
|
||||||
const formatted = parser('a.`b`.c');
|
const formatted = parser('a.`b`.c');
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,16 @@ function populateSvgTextLine(svg, node, formattedLine) {
|
||||||
formattedLine.forEach(({text, attrs}) => {
|
formattedLine.forEach(({text, attrs}) => {
|
||||||
let element = text;
|
let element = text;
|
||||||
if(attrs) {
|
if(attrs) {
|
||||||
element = svg.el('tspan').attrs(attrs).add(text);
|
if(attrs.href) {
|
||||||
|
element = svg.el('a').attrs({
|
||||||
|
'cursor': 'pointer',
|
||||||
|
'rel': 'nofollow',
|
||||||
|
'target': '_blank',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
element = svg.el('tspan');
|
||||||
|
}
|
||||||
|
element.attrs(attrs).add(text);
|
||||||
if(attrs.filter) {
|
if(attrs.filter) {
|
||||||
element.attr('filter', svg.getTextFilter(attrs.filter));
|
element.attr('filter', svg.getTextFilter(attrs.filter));
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,27 @@ describe('SVGTextBlock', () => {
|
||||||
.toEqual('foo<tspan filter="url(#local-foobar)">bar</tspan>');
|
.toEqual('foo<tspan filter="url(#local-foobar)">bar</tspan>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('converts href attributes into <a> tags', () => {
|
||||||
|
block.set({formatted: [[
|
||||||
|
{text: 'foo'},
|
||||||
|
{attrs: {'href': 'foo', 'zig': 'zag'}, text: 'bar'},
|
||||||
|
]]});
|
||||||
|
|
||||||
|
expect(hold.childNodes[0].innerHTML).toContain('foo<a ');
|
||||||
|
expect(hold.childNodes[0].innerHTML).toContain(' href="foo"');
|
||||||
|
expect(hold.childNodes[0].innerHTML).toContain('>bar</a>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds basic link attributes to <a>s', () => {
|
||||||
|
block.set({formatted: [[
|
||||||
|
{attrs: {'href': 'foo'}, text: 'bar'},
|
||||||
|
]]});
|
||||||
|
|
||||||
|
expect(hold.childNodes[0].innerHTML).toContain(' cursor="pointer"');
|
||||||
|
expect(hold.childNodes[0].innerHTML).toContain(' target="_blank"');
|
||||||
|
expect(hold.childNodes[0].innerHTML).toContain(' rel="nofollow"');
|
||||||
|
});
|
||||||
|
|
||||||
it('re-uses text nodes when possible, adding more if needed', () => {
|
it('re-uses text nodes when possible, adding more if needed', () => {
|
||||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||||
const [line0, line1] = hold.childNodes;
|
const [line0, line1] = hold.childNodes;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<svg width="132.474609375" height="150" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-66.2373046875 -145 132.474609375 150"><metadata>title "
|
<svg width="132.474609375" height="150" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-66.2373046875 -145 132.474609375 150"><metadata>title "
|
||||||
Re **Bo** _It_ `Mo` I<sup>2</sup>
|
Re **Bo** _It_ `Mo` I<sup>2</sup>
|
||||||
**_`Comb`_** <u><o>~Strike~</o></u> I<sub>2</sub>
|
**_`Comb`_** <u><o>~Strike~</o></u> I<sub>2</sub>
|
||||||
<highlight>Back</highlight> <red>Text</red>
|
<highlight>Back</highlight> <red>Text</red> [link](http://www.example.com)
|
||||||
\<b>esc</b>
|
\<b>esc</b>
|
||||||
\\<b>no-esc</b> 😎
|
\\<b>no-esc</b> 😎
|
||||||
"
|
"
|
||||||
</metadata><defs><filter id="R0highlight"><feMorphology in="SourceAlpha" operator="dilate" radius="4"></feMorphology><feGaussianBlur edgeMode="none" stdDeviation="3, 1.5"></feGaussianBlur><feComponentTransfer><feFuncA intercept="-70" slope="100" type="linear"></feFuncA></feComponentTransfer><feComponentTransfer><feFuncR intercept="1" slope="0" type="linear"></feFuncR><feFuncG intercept="0.875" slope="0" type="linear"></feFuncG><feFuncB intercept="0" slope="0" type="linear"></feFuncB><feFuncA slope="0.8" type="linear"></feFuncA></feComponentTransfer><feMerge><feMergeNode></feMergeNode><feMergeNode in="SourceGraphic"></feMergeNode></feMerge></filter></defs><defs><mask id="R0FullMask" maskUnits="userSpaceOnUse"><rect fill="#FFFFFF" height="150" width="132.474609375" x="-66.2373046875" y="-145"></rect></mask><mask id="R0LineMask" maskUnits="userSpaceOnUse"><rect fill="#FFFFFF" height="150" width="132.474609375" x="-66.2373046875" y="-145"></rect></mask></defs><g></g><g><text x="0" class="title" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" y="-120">Re <tspan font-weight="bolder">Bo</tspan> <tspan font-style="italic">It</tspan> <tspan font-family="Courier New,Liberation Mono,monospace">Mo</tspan> I<tspan baseline-shift="70%" font-size="0.6em">2</tspan></text><text x="0" class="title" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" y="-94"><tspan font-style="italic" font-weight="bolder" font-family="Courier New,Liberation Mono,monospace">Comb</tspan> <tspan text-decoration="line-through overline underline">Strike</tspan> I<tspan baseline-shift="-20%" font-size="0.6em">2</tspan></text><text x="0" class="title" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" y="-68"><tspan filter="url(#R0highlight)">Back</tspan> <tspan fill="#DD0000">Text</tspan></text><text x="0" class="title" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" y="-42"><b>esc</b></text><text x="0" class="title" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" y="-16">\<tspan font-weight="bolder">no-esc</tspan> 😎</text></g><g></g><g mask="url(#R0FullMask)"><g mask="url(#R0LineMask)"></g><g></g><g></g></g></svg>
|
</metadata><defs><filter id="R0highlight"><feMorphology in="SourceAlpha" operator="dilate" radius="4"></feMorphology><feGaussianBlur edgeMode="none" stdDeviation="3, 1.5"></feGaussianBlur><feComponentTransfer><feFuncA intercept="-70" slope="100" type="linear"></feFuncA></feComponentTransfer><feComponentTransfer><feFuncR intercept="1" slope="0" type="linear"></feFuncR><feFuncG intercept="0.875" slope="0" type="linear"></feFuncG><feFuncB intercept="0" slope="0" type="linear"></feFuncB><feFuncA slope="0.8" type="linear"></feFuncA></feComponentTransfer><feMerge><feMergeNode></feMergeNode><feMergeNode in="SourceGraphic"></feMergeNode></feMerge></filter></defs><defs><mask id="R0FullMask" maskUnits="userSpaceOnUse"><rect fill="#FFFFFF" height="150" width="132.474609375" x="-66.2373046875" y="-145"></rect></mask><mask id="R0LineMask" maskUnits="userSpaceOnUse"><rect fill="#FFFFFF" height="150" width="132.474609375" x="-66.2373046875" y="-145"></rect></mask></defs><g></g><g><text x="0" class="title" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" y="-120">Re <tspan font-weight="bolder">Bo</tspan> <tspan font-style="italic">It</tspan> <tspan font-family="Courier New,Liberation Mono,monospace">Mo</tspan> I<tspan baseline-shift="70%" font-size="0.6em">2</tspan></text><text x="0" class="title" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" y="-94"><tspan font-style="italic" font-weight="bolder" font-family="Courier New,Liberation Mono,monospace">Comb</tspan> <tspan text-decoration="line-through overline underline">Strike</tspan> I<tspan baseline-shift="-20%" font-size="0.6em">2</tspan></text><text x="0" class="title" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" y="-68"><tspan filter="url(#R0highlight)">Back</tspan> <tspan fill="#DD0000">Text</tspan> <a cursor="pointer" rel="nofollow" target="_blank" href="http://www.example.com" text-decoration="underline">link</a></text><text x="0" class="title" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" y="-42"><b>esc</b></text><text x="0" class="title" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" y="-16">\<tspan font-weight="bolder">no-esc</tspan> 😎</text></g><g></g><g mask="url(#R0FullMask)"><g mask="url(#R0LineMask)"></g><g></g><g></g></g></svg>
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.9 KiB |
|
@ -389,6 +389,10 @@ html, body {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg a:active, svg a:hover {
|
||||||
|
fill: #0080CC;
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.drop-target:after {
|
.drop-target:after {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
Loading…
Reference in New Issue