Add markdown link support [#54]

This commit is contained in:
David Evans 2018-05-07 00:19:18 +01:00
parent 011d8c6979
commit 3e4110193a
9 changed files with 329 additions and 195 deletions

View File

@ -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

View File

@ -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));
} }

View File

@ -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;
}); });
} }

View File

@ -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');

View File

@ -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));
} }

View File

@ -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;

View File

@ -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&lt;sup&gt;2&lt;/sup&gt; Re **Bo** _It_ `Mo` I&lt;sup&gt;2&lt;/sup&gt;
**_`Comb`_** &lt;u&gt;&lt;o&gt;~Strike~&lt;/o&gt;&lt;/u&gt; I&lt;sub&gt;2&lt;/sub&gt; **_`Comb`_** &lt;u&gt;&lt;o&gt;~Strike~&lt;/o&gt;&lt;/u&gt; I&lt;sub&gt;2&lt;/sub&gt;
&lt;highlight&gt;Back&lt;/highlight&gt; &lt;red&gt;Text&lt;/red&gt; &lt;highlight&gt;Back&lt;/highlight&gt; &lt;red&gt;Text&lt;/red&gt; [link](http://www.example.com)
\&lt;b&gt;esc&lt;/b&gt; \&lt;b&gt;esc&lt;/b&gt;
\\&lt;b&gt;no-esc&lt;/b&gt; 😎 \\&lt;b&gt;no-esc&lt;/b&gt; 😎
" "
</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">&lt;b&gt;esc&lt;/b&gt;</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">&lt;b&gt;esc&lt;/b&gt;</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

View File

@ -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;