Add support for red text and highlighted text in markdown
This commit is contained in:
parent
dcad48ec90
commit
b23e278729
|
@ -60,8 +60,58 @@
|
||||||
// No-op
|
// No-op
|
||||||
}
|
}
|
||||||
|
|
||||||
addDefs() {
|
addDefs(builder, textBuilder) {
|
||||||
// No-op
|
// Thanks, https://stackoverflow.com/a/12263962/1180785
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=603157
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=917766
|
||||||
|
textBuilder('highlight', () => this.svg.el('filter')
|
||||||
|
.add(
|
||||||
|
// Morph makes characters consistent
|
||||||
|
this.svg.el('feMorphology').attrs({
|
||||||
|
'in': 'SourceAlpha',
|
||||||
|
'operator': 'dilate',
|
||||||
|
'radius': '4',
|
||||||
|
}),
|
||||||
|
// Blur+thresh makes edges smooth
|
||||||
|
this.svg.el('feGaussianBlur').attrs({
|
||||||
|
'edgeMode': 'none',
|
||||||
|
'stdDeviation': '3, 1.5',
|
||||||
|
}),
|
||||||
|
this.svg.el('feComponentTransfer').add(
|
||||||
|
this.svg.el('feFuncA').attrs({
|
||||||
|
'intercept': -70,
|
||||||
|
'slope': 100,
|
||||||
|
'type': 'linear',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// Add colour
|
||||||
|
this.svg.el('feComponentTransfer').add(
|
||||||
|
this.svg.el('feFuncR').attrs({
|
||||||
|
'intercept': 1,
|
||||||
|
'slope': 0,
|
||||||
|
'type': 'linear',
|
||||||
|
}),
|
||||||
|
this.svg.el('feFuncG').attrs({
|
||||||
|
'intercept': 0.9375,
|
||||||
|
'slope': 0,
|
||||||
|
'type': 'linear',
|
||||||
|
}),
|
||||||
|
this.svg.el('feFuncB').attrs({
|
||||||
|
'intercept': 0.09375,
|
||||||
|
'slope': 0,
|
||||||
|
'type': 'linear',
|
||||||
|
}),
|
||||||
|
this.svg.el('feFuncA').attrs({
|
||||||
|
'slope': 0.7,
|
||||||
|
'type': 'linear',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// Draw text on top
|
||||||
|
this.svg.el('feMerge').add(
|
||||||
|
this.svg.el('feMergeNode'),
|
||||||
|
this.svg.el('feMergeNode').attr('in', 'SourceGraphic')
|
||||||
|
)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitleAttrs() {
|
getTitleAttrs() {
|
||||||
|
@ -3837,7 +3887,7 @@
|
||||||
start: /"/y,
|
start: /"/y,
|
||||||
},
|
},
|
||||||
{baseToken: {v: '...'}, start: /\.\.\./y},
|
{baseToken: {v: '...'}, start: /\.\.\./y},
|
||||||
{end: /(?=[ \t\r\n:+~\-*!<>,])|$/y, start: /(?=[^ \t\r\n:+~\-*!<>,])/y},
|
{end: /(?=[ \t\r\n:+~\-*!<,])|$/y, start: /(?=[^ \t\r\n:+~\-*!<,])/y},
|
||||||
{
|
{
|
||||||
end: /(?=[^~\-<>x])|[-~]x|[<>](?=x)|$/y,
|
end: /(?=[^~\-<>x])|[-~]x|[<>](?=x)|$/y,
|
||||||
includeEnd: true,
|
includeEnd: true,
|
||||||
|
@ -4109,28 +4159,36 @@
|
||||||
const STYLES = [
|
const STYLES = [
|
||||||
{
|
{
|
||||||
attrs: {'font-style': 'italic'},
|
attrs: {'font-style': 'italic'},
|
||||||
begin: /[\s_~`]\*(?=\S)/g,
|
begin: {matcher: /[\s_~`>]\*(?=\S)/g, skip: 1},
|
||||||
end: /\S\*(?=[\s_~`])/g,
|
end: {matcher: /\S\*(?=[\s_~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-style': 'italic'},
|
attrs: {'font-style': 'italic'},
|
||||||
begin: /[\s*~`]_(?=\S)/g,
|
begin: {matcher: /[\s*~`>]_(?=\S)/g, skip: 1},
|
||||||
end: /\S_(?=[\s*~`])/g,
|
end: {matcher: /\S_(?=[\s*~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-weight': 'bolder'},
|
attrs: {'font-weight': 'bolder'},
|
||||||
begin: /[\s_~`]\*\*(?=\S)/g,
|
begin: {matcher: /[\s_~`>]\*\*(?=\S)/g, skip: 1},
|
||||||
end: /\S\*\*(?=[\s_~`])/g,
|
end: {matcher: /\S\*\*(?=[\s_~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-weight': 'bolder'},
|
attrs: {'font-weight': 'bolder'},
|
||||||
begin: /[\s*~`]__(?=\S)/g,
|
begin: {matcher: /[\s*~`>]__(?=\S)/g, skip: 1},
|
||||||
end: /\S__(?=[\s*~`])/g,
|
end: {matcher: /\S__(?=[\s*~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'text-decoration': 'line-through'},
|
attrs: {'text-decoration': 'line-through'},
|
||||||
begin: /[\s_*`]~(?=\S)/g,
|
begin: {matcher: /[\s_*`>]~(?=\S)/g, skip: 1},
|
||||||
end: /\S~(?=[\s_*`])/g,
|
end: {matcher: /\S~(?=[\s_*`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-family': 'Courier New,Liberation Mono,monospace'},
|
attrs: {'font-family': 'Courier New,Liberation Mono,monospace'},
|
||||||
begin: /[\s_*~.]`(?=\S)/g,
|
begin: {matcher: /[\s_*~.>]`(?=\S)/g, skip: 1},
|
||||||
end: /\S`(?=[\s_*~.])/g,
|
end: {matcher: /\S`(?=[\s_*~.<])/g, skip: 1},
|
||||||
|
}, {
|
||||||
|
attrs: {'fill': '#DD0000'},
|
||||||
|
begin: {matcher: /<red>/g, skip: 0},
|
||||||
|
end: {matcher: /<\/red>/g, skip: 0},
|
||||||
|
}, {
|
||||||
|
attrs: {'filter': 'highlight'},
|
||||||
|
begin: {matcher: /<highlight>/g, skip: 0},
|
||||||
|
end: {matcher: /<\/highlight>/g, skip: 0},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -4142,19 +4200,20 @@
|
||||||
|
|
||||||
STYLES.forEach(({begin, end}, ind) => {
|
STYLES.forEach(({begin, end}, ind) => {
|
||||||
const search = active[ind] ? end : begin;
|
const search = active[ind] ? end : begin;
|
||||||
search.lastIndex = p;
|
search.matcher.lastIndex = p + 1 - search.skip;
|
||||||
const m = search.exec(virtLine);
|
const m = search.matcher.exec(virtLine);
|
||||||
if(m && (
|
const beginInd = m ? (m.index + search.skip) : Number.POSITIVE_INFINITY;
|
||||||
m.index < bestStart ||
|
if(
|
||||||
(m.index === bestStart && search.lastIndex > bestEnd)
|
beginInd < bestStart ||
|
||||||
)) {
|
(beginInd === bestStart && search.matcher.lastIndex > bestEnd)
|
||||||
|
) {
|
||||||
styleIndex = ind;
|
styleIndex = ind;
|
||||||
bestStart = m.index;
|
bestStart = beginInd;
|
||||||
bestEnd = search.lastIndex;
|
bestEnd = search.matcher.lastIndex;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {end: bestEnd - 1, start: bestStart, styleIndex};
|
return {end: bestEnd - 1, start: bestStart - 1, styleIndex};
|
||||||
}
|
}
|
||||||
|
|
||||||
function combineAttrs(activeCount, active) {
|
function combineAttrs(activeCount, active) {
|
||||||
|
@ -6776,7 +6835,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
detach() {
|
detach() {
|
||||||
|
if(this.element.parentNode) {
|
||||||
this.element.parentNode.removeChild(this.element);
|
this.element.parentNode.removeChild(this.element);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6830,11 +6891,14 @@
|
||||||
throw new Error('Invalid formatted text line: ' + formattedLine);
|
throw new Error('Invalid formatted text line: ' + formattedLine);
|
||||||
}
|
}
|
||||||
formattedLine.forEach(({text, attrs}) => {
|
formattedLine.forEach(({text, attrs}) => {
|
||||||
|
let element = text;
|
||||||
if(attrs) {
|
if(attrs) {
|
||||||
node.add(svg.el('tspan').attrs(attrs).add(text));
|
element = svg.el('tspan').attrs(attrs).add(text);
|
||||||
} else {
|
if(attrs.filter) {
|
||||||
node.add(text);
|
element.attr('filter', svg.getTextFilter(attrs.filter));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
node.add(element);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7282,12 +7346,40 @@
|
||||||
this.dom = domWrapper;
|
this.dom = domWrapper;
|
||||||
this.body = this.el('svg').attr('xmlns', NS).attr('version', '1.1');
|
this.body = this.el('svg').attr('xmlns', NS).attr('version', '1.1');
|
||||||
const fn = (textSizerFactory || defaultTextSizerFactory);
|
const fn = (textSizerFactory || defaultTextSizerFactory);
|
||||||
|
this.textFilters = new Map();
|
||||||
this.textSizer = new TextSizerWrapper(fn(this));
|
this.textSizer = new TextSizerWrapper(fn(this));
|
||||||
|
|
||||||
this.txt = this.txt.bind(this);
|
this.txt = this.txt.bind(this);
|
||||||
this.el = this.el.bind(this);
|
this.el = this.el.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetTextFilters() {
|
||||||
|
this.textFilters.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerTextFilter(name, id) {
|
||||||
|
this.textFilters.set(name, {id, used: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
getTextFilter(name) {
|
||||||
|
const filter = this.textFilters.get(name);
|
||||||
|
if(!filter) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
filter.used = true;
|
||||||
|
return 'url(#' + filter.id + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsedTextFilterNames() {
|
||||||
|
const used = [];
|
||||||
|
for(const [name, filter] of this.textFilters) {
|
||||||
|
if(filter.used) {
|
||||||
|
used.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return used;
|
||||||
|
}
|
||||||
|
|
||||||
linearGradient(attrs, stops) {
|
linearGradient(attrs, stops) {
|
||||||
return this.el('linearGradient')
|
return this.el('linearGradient')
|
||||||
.attrs(attrs)
|
.attrs(attrs)
|
||||||
|
@ -7508,6 +7600,7 @@
|
||||||
this.components = components || getComponents();
|
this.components = components || getComponents();
|
||||||
this.svg = new SVG(new DOMWrapper(document), textSizerFactory);
|
this.svg = new SVG(new DOMWrapper(document), textSizerFactory);
|
||||||
this.knownThemeDefs = new Set();
|
this.knownThemeDefs = new Set();
|
||||||
|
this.knownTextFilterDefs = new Map();
|
||||||
this.knownDefs = new Set();
|
this.knownDefs = new Set();
|
||||||
this.highlights = new Map();
|
this.highlights = new Map();
|
||||||
this.collapsed = new Set();
|
this.collapsed = new Set();
|
||||||
|
@ -7524,6 +7617,7 @@
|
||||||
this.prepareMeasurementsStage.bind(this);
|
this.prepareMeasurementsStage.bind(this);
|
||||||
this.renderStage = this.renderStage.bind(this);
|
this.renderStage = this.renderStage.bind(this);
|
||||||
this.addThemeDef = this.addThemeDef.bind(this);
|
this.addThemeDef = this.addThemeDef.bind(this);
|
||||||
|
this.addThemeTextDef = this.addThemeTextDef.bind(this);
|
||||||
this.addDef = this.addDef.bind(this);
|
this.addDef = this.addDef.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7582,6 +7676,15 @@
|
||||||
return namespacedName;
|
return namespacedName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addThemeTextDef(name, generator) {
|
||||||
|
const namespacedName = this.namespace + name;
|
||||||
|
if(!this.knownTextFilterDefs.has(name)) {
|
||||||
|
const def = generator().attr('id', namespacedName);
|
||||||
|
this.knownTextFilterDefs.set(name, def);
|
||||||
|
}
|
||||||
|
this.svg.registerTextFilter(name, namespacedName);
|
||||||
|
}
|
||||||
|
|
||||||
addDef(name, generator) {
|
addDef(name, generator) {
|
||||||
let nm = name;
|
let nm = name;
|
||||||
let gen = generator;
|
let gen = generator;
|
||||||
|
@ -7963,6 +8066,7 @@
|
||||||
_reset(theme) {
|
_reset(theme) {
|
||||||
if(theme) {
|
if(theme) {
|
||||||
this.knownThemeDefs.clear();
|
this.knownThemeDefs.clear();
|
||||||
|
this.knownTextFilterDefs.clear();
|
||||||
this.themeDefs.empty();
|
this.themeDefs.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8060,7 +8164,11 @@
|
||||||
this._reset(themeChanged);
|
this._reset(themeChanged);
|
||||||
|
|
||||||
this.metaCode.nodeValue = sequence.meta.code;
|
this.metaCode.nodeValue = sequence.meta.code;
|
||||||
this.theme.addDefs(this.addThemeDef);
|
this.svg.resetTextFilters();
|
||||||
|
this.theme.addDefs(this.addThemeDef, this.addThemeTextDef);
|
||||||
|
for(const def of this.knownTextFilterDefs.values()) {
|
||||||
|
def.detach();
|
||||||
|
}
|
||||||
|
|
||||||
this.title.set({
|
this.title.set({
|
||||||
attrs: Object.assign({
|
attrs: Object.assign({
|
||||||
|
@ -8094,6 +8202,10 @@
|
||||||
sequence.stages.forEach(this.renderStage);
|
sequence.stages.forEach(this.renderStage);
|
||||||
const bottomY = this.checkAgentRange(['[', ']'], this.currentY);
|
const bottomY = this.checkAgentRange(['[', ']'], this.currentY);
|
||||||
|
|
||||||
|
this.svg.getUsedTextFilterNames().forEach((name) => {
|
||||||
|
this.themeDefs.add(this.knownTextFilterDefs.get(name));
|
||||||
|
});
|
||||||
|
|
||||||
const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0);
|
const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0);
|
||||||
this.updateBounds(stagesHeight);
|
this.updateBounds(stagesHeight);
|
||||||
|
|
||||||
|
@ -8962,7 +9074,7 @@
|
||||||
this.random.reset();
|
this.random.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
addDefs(builder) {
|
addDefs(builder, textBuilder) {
|
||||||
builder('sketch_font', () => {
|
builder('sketch_font', () => {
|
||||||
const style = this.svg.el('style', null);
|
const style = this.svg.el('style', null);
|
||||||
// Font must be embedded for exporting as SVG / PNG
|
// Font must be embedded for exporting as SVG / PNG
|
||||||
|
@ -8974,6 +9086,8 @@
|
||||||
);
|
);
|
||||||
return style;
|
return style;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
super.addDefs(builder, textBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
vary(range, centre = 0) {
|
vary(range, centre = 0) {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -60,8 +60,58 @@
|
||||||
// No-op
|
// No-op
|
||||||
}
|
}
|
||||||
|
|
||||||
addDefs() {
|
addDefs(builder, textBuilder) {
|
||||||
// No-op
|
// Thanks, https://stackoverflow.com/a/12263962/1180785
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=603157
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=917766
|
||||||
|
textBuilder('highlight', () => this.svg.el('filter')
|
||||||
|
.add(
|
||||||
|
// Morph makes characters consistent
|
||||||
|
this.svg.el('feMorphology').attrs({
|
||||||
|
'in': 'SourceAlpha',
|
||||||
|
'operator': 'dilate',
|
||||||
|
'radius': '4',
|
||||||
|
}),
|
||||||
|
// Blur+thresh makes edges smooth
|
||||||
|
this.svg.el('feGaussianBlur').attrs({
|
||||||
|
'edgeMode': 'none',
|
||||||
|
'stdDeviation': '3, 1.5',
|
||||||
|
}),
|
||||||
|
this.svg.el('feComponentTransfer').add(
|
||||||
|
this.svg.el('feFuncA').attrs({
|
||||||
|
'intercept': -70,
|
||||||
|
'slope': 100,
|
||||||
|
'type': 'linear',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// Add colour
|
||||||
|
this.svg.el('feComponentTransfer').add(
|
||||||
|
this.svg.el('feFuncR').attrs({
|
||||||
|
'intercept': 1,
|
||||||
|
'slope': 0,
|
||||||
|
'type': 'linear',
|
||||||
|
}),
|
||||||
|
this.svg.el('feFuncG').attrs({
|
||||||
|
'intercept': 0.9375,
|
||||||
|
'slope': 0,
|
||||||
|
'type': 'linear',
|
||||||
|
}),
|
||||||
|
this.svg.el('feFuncB').attrs({
|
||||||
|
'intercept': 0.09375,
|
||||||
|
'slope': 0,
|
||||||
|
'type': 'linear',
|
||||||
|
}),
|
||||||
|
this.svg.el('feFuncA').attrs({
|
||||||
|
'slope': 0.7,
|
||||||
|
'type': 'linear',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// Draw text on top
|
||||||
|
this.svg.el('feMerge').add(
|
||||||
|
this.svg.el('feMergeNode'),
|
||||||
|
this.svg.el('feMergeNode').attr('in', 'SourceGraphic')
|
||||||
|
)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitleAttrs() {
|
getTitleAttrs() {
|
||||||
|
@ -3837,7 +3887,7 @@
|
||||||
start: /"/y,
|
start: /"/y,
|
||||||
},
|
},
|
||||||
{baseToken: {v: '...'}, start: /\.\.\./y},
|
{baseToken: {v: '...'}, start: /\.\.\./y},
|
||||||
{end: /(?=[ \t\r\n:+~\-*!<>,])|$/y, start: /(?=[^ \t\r\n:+~\-*!<>,])/y},
|
{end: /(?=[ \t\r\n:+~\-*!<,])|$/y, start: /(?=[^ \t\r\n:+~\-*!<,])/y},
|
||||||
{
|
{
|
||||||
end: /(?=[^~\-<>x])|[-~]x|[<>](?=x)|$/y,
|
end: /(?=[^~\-<>x])|[-~]x|[<>](?=x)|$/y,
|
||||||
includeEnd: true,
|
includeEnd: true,
|
||||||
|
@ -4109,28 +4159,36 @@
|
||||||
const STYLES = [
|
const STYLES = [
|
||||||
{
|
{
|
||||||
attrs: {'font-style': 'italic'},
|
attrs: {'font-style': 'italic'},
|
||||||
begin: /[\s_~`]\*(?=\S)/g,
|
begin: {matcher: /[\s_~`>]\*(?=\S)/g, skip: 1},
|
||||||
end: /\S\*(?=[\s_~`])/g,
|
end: {matcher: /\S\*(?=[\s_~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-style': 'italic'},
|
attrs: {'font-style': 'italic'},
|
||||||
begin: /[\s*~`]_(?=\S)/g,
|
begin: {matcher: /[\s*~`>]_(?=\S)/g, skip: 1},
|
||||||
end: /\S_(?=[\s*~`])/g,
|
end: {matcher: /\S_(?=[\s*~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-weight': 'bolder'},
|
attrs: {'font-weight': 'bolder'},
|
||||||
begin: /[\s_~`]\*\*(?=\S)/g,
|
begin: {matcher: /[\s_~`>]\*\*(?=\S)/g, skip: 1},
|
||||||
end: /\S\*\*(?=[\s_~`])/g,
|
end: {matcher: /\S\*\*(?=[\s_~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-weight': 'bolder'},
|
attrs: {'font-weight': 'bolder'},
|
||||||
begin: /[\s*~`]__(?=\S)/g,
|
begin: {matcher: /[\s*~`>]__(?=\S)/g, skip: 1},
|
||||||
end: /\S__(?=[\s*~`])/g,
|
end: {matcher: /\S__(?=[\s*~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'text-decoration': 'line-through'},
|
attrs: {'text-decoration': 'line-through'},
|
||||||
begin: /[\s_*`]~(?=\S)/g,
|
begin: {matcher: /[\s_*`>]~(?=\S)/g, skip: 1},
|
||||||
end: /\S~(?=[\s_*`])/g,
|
end: {matcher: /\S~(?=[\s_*`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-family': 'Courier New,Liberation Mono,monospace'},
|
attrs: {'font-family': 'Courier New,Liberation Mono,monospace'},
|
||||||
begin: /[\s_*~.]`(?=\S)/g,
|
begin: {matcher: /[\s_*~.>]`(?=\S)/g, skip: 1},
|
||||||
end: /\S`(?=[\s_*~.])/g,
|
end: {matcher: /\S`(?=[\s_*~.<])/g, skip: 1},
|
||||||
|
}, {
|
||||||
|
attrs: {'fill': '#DD0000'},
|
||||||
|
begin: {matcher: /<red>/g, skip: 0},
|
||||||
|
end: {matcher: /<\/red>/g, skip: 0},
|
||||||
|
}, {
|
||||||
|
attrs: {'filter': 'highlight'},
|
||||||
|
begin: {matcher: /<highlight>/g, skip: 0},
|
||||||
|
end: {matcher: /<\/highlight>/g, skip: 0},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -4142,19 +4200,20 @@
|
||||||
|
|
||||||
STYLES.forEach(({begin, end}, ind) => {
|
STYLES.forEach(({begin, end}, ind) => {
|
||||||
const search = active[ind] ? end : begin;
|
const search = active[ind] ? end : begin;
|
||||||
search.lastIndex = p;
|
search.matcher.lastIndex = p + 1 - search.skip;
|
||||||
const m = search.exec(virtLine);
|
const m = search.matcher.exec(virtLine);
|
||||||
if(m && (
|
const beginInd = m ? (m.index + search.skip) : Number.POSITIVE_INFINITY;
|
||||||
m.index < bestStart ||
|
if(
|
||||||
(m.index === bestStart && search.lastIndex > bestEnd)
|
beginInd < bestStart ||
|
||||||
)) {
|
(beginInd === bestStart && search.matcher.lastIndex > bestEnd)
|
||||||
|
) {
|
||||||
styleIndex = ind;
|
styleIndex = ind;
|
||||||
bestStart = m.index;
|
bestStart = beginInd;
|
||||||
bestEnd = search.lastIndex;
|
bestEnd = search.matcher.lastIndex;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {end: bestEnd - 1, start: bestStart, styleIndex};
|
return {end: bestEnd - 1, start: bestStart - 1, styleIndex};
|
||||||
}
|
}
|
||||||
|
|
||||||
function combineAttrs(activeCount, active) {
|
function combineAttrs(activeCount, active) {
|
||||||
|
@ -6776,7 +6835,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
detach() {
|
detach() {
|
||||||
|
if(this.element.parentNode) {
|
||||||
this.element.parentNode.removeChild(this.element);
|
this.element.parentNode.removeChild(this.element);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6830,11 +6891,14 @@
|
||||||
throw new Error('Invalid formatted text line: ' + formattedLine);
|
throw new Error('Invalid formatted text line: ' + formattedLine);
|
||||||
}
|
}
|
||||||
formattedLine.forEach(({text, attrs}) => {
|
formattedLine.forEach(({text, attrs}) => {
|
||||||
|
let element = text;
|
||||||
if(attrs) {
|
if(attrs) {
|
||||||
node.add(svg.el('tspan').attrs(attrs).add(text));
|
element = svg.el('tspan').attrs(attrs).add(text);
|
||||||
} else {
|
if(attrs.filter) {
|
||||||
node.add(text);
|
element.attr('filter', svg.getTextFilter(attrs.filter));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
node.add(element);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7282,12 +7346,40 @@
|
||||||
this.dom = domWrapper;
|
this.dom = domWrapper;
|
||||||
this.body = this.el('svg').attr('xmlns', NS).attr('version', '1.1');
|
this.body = this.el('svg').attr('xmlns', NS).attr('version', '1.1');
|
||||||
const fn = (textSizerFactory || defaultTextSizerFactory);
|
const fn = (textSizerFactory || defaultTextSizerFactory);
|
||||||
|
this.textFilters = new Map();
|
||||||
this.textSizer = new TextSizerWrapper(fn(this));
|
this.textSizer = new TextSizerWrapper(fn(this));
|
||||||
|
|
||||||
this.txt = this.txt.bind(this);
|
this.txt = this.txt.bind(this);
|
||||||
this.el = this.el.bind(this);
|
this.el = this.el.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetTextFilters() {
|
||||||
|
this.textFilters.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerTextFilter(name, id) {
|
||||||
|
this.textFilters.set(name, {id, used: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
getTextFilter(name) {
|
||||||
|
const filter = this.textFilters.get(name);
|
||||||
|
if(!filter) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
filter.used = true;
|
||||||
|
return 'url(#' + filter.id + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsedTextFilterNames() {
|
||||||
|
const used = [];
|
||||||
|
for(const [name, filter] of this.textFilters) {
|
||||||
|
if(filter.used) {
|
||||||
|
used.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return used;
|
||||||
|
}
|
||||||
|
|
||||||
linearGradient(attrs, stops) {
|
linearGradient(attrs, stops) {
|
||||||
return this.el('linearGradient')
|
return this.el('linearGradient')
|
||||||
.attrs(attrs)
|
.attrs(attrs)
|
||||||
|
@ -7508,6 +7600,7 @@
|
||||||
this.components = components || getComponents();
|
this.components = components || getComponents();
|
||||||
this.svg = new SVG(new DOMWrapper(document), textSizerFactory);
|
this.svg = new SVG(new DOMWrapper(document), textSizerFactory);
|
||||||
this.knownThemeDefs = new Set();
|
this.knownThemeDefs = new Set();
|
||||||
|
this.knownTextFilterDefs = new Map();
|
||||||
this.knownDefs = new Set();
|
this.knownDefs = new Set();
|
||||||
this.highlights = new Map();
|
this.highlights = new Map();
|
||||||
this.collapsed = new Set();
|
this.collapsed = new Set();
|
||||||
|
@ -7524,6 +7617,7 @@
|
||||||
this.prepareMeasurementsStage.bind(this);
|
this.prepareMeasurementsStage.bind(this);
|
||||||
this.renderStage = this.renderStage.bind(this);
|
this.renderStage = this.renderStage.bind(this);
|
||||||
this.addThemeDef = this.addThemeDef.bind(this);
|
this.addThemeDef = this.addThemeDef.bind(this);
|
||||||
|
this.addThemeTextDef = this.addThemeTextDef.bind(this);
|
||||||
this.addDef = this.addDef.bind(this);
|
this.addDef = this.addDef.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7582,6 +7676,15 @@
|
||||||
return namespacedName;
|
return namespacedName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addThemeTextDef(name, generator) {
|
||||||
|
const namespacedName = this.namespace + name;
|
||||||
|
if(!this.knownTextFilterDefs.has(name)) {
|
||||||
|
const def = generator().attr('id', namespacedName);
|
||||||
|
this.knownTextFilterDefs.set(name, def);
|
||||||
|
}
|
||||||
|
this.svg.registerTextFilter(name, namespacedName);
|
||||||
|
}
|
||||||
|
|
||||||
addDef(name, generator) {
|
addDef(name, generator) {
|
||||||
let nm = name;
|
let nm = name;
|
||||||
let gen = generator;
|
let gen = generator;
|
||||||
|
@ -7963,6 +8066,7 @@
|
||||||
_reset(theme) {
|
_reset(theme) {
|
||||||
if(theme) {
|
if(theme) {
|
||||||
this.knownThemeDefs.clear();
|
this.knownThemeDefs.clear();
|
||||||
|
this.knownTextFilterDefs.clear();
|
||||||
this.themeDefs.empty();
|
this.themeDefs.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8060,7 +8164,11 @@
|
||||||
this._reset(themeChanged);
|
this._reset(themeChanged);
|
||||||
|
|
||||||
this.metaCode.nodeValue = sequence.meta.code;
|
this.metaCode.nodeValue = sequence.meta.code;
|
||||||
this.theme.addDefs(this.addThemeDef);
|
this.svg.resetTextFilters();
|
||||||
|
this.theme.addDefs(this.addThemeDef, this.addThemeTextDef);
|
||||||
|
for(const def of this.knownTextFilterDefs.values()) {
|
||||||
|
def.detach();
|
||||||
|
}
|
||||||
|
|
||||||
this.title.set({
|
this.title.set({
|
||||||
attrs: Object.assign({
|
attrs: Object.assign({
|
||||||
|
@ -8094,6 +8202,10 @@
|
||||||
sequence.stages.forEach(this.renderStage);
|
sequence.stages.forEach(this.renderStage);
|
||||||
const bottomY = this.checkAgentRange(['[', ']'], this.currentY);
|
const bottomY = this.checkAgentRange(['[', ']'], this.currentY);
|
||||||
|
|
||||||
|
this.svg.getUsedTextFilterNames().forEach((name) => {
|
||||||
|
this.themeDefs.add(this.knownTextFilterDefs.get(name));
|
||||||
|
});
|
||||||
|
|
||||||
const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0);
|
const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0);
|
||||||
this.updateBounds(stagesHeight);
|
this.updateBounds(stagesHeight);
|
||||||
|
|
||||||
|
@ -8962,7 +9074,7 @@
|
||||||
this.random.reset();
|
this.random.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
addDefs(builder) {
|
addDefs(builder, textBuilder) {
|
||||||
builder('sketch_font', () => {
|
builder('sketch_font', () => {
|
||||||
const style = this.svg.el('style', null);
|
const style = this.svg.el('style', null);
|
||||||
// Font must be embedded for exporting as SVG / PNG
|
// Font must be embedded for exporting as SVG / PNG
|
||||||
|
@ -8974,6 +9086,8 @@
|
||||||
);
|
);
|
||||||
return style;
|
return style;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
super.addDefs(builder, textBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
vary(range, centre = 0) {
|
vary(range, centre = 0) {
|
||||||
|
|
|
@ -164,7 +164,9 @@ class WrappedElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
detach() {
|
detach() {
|
||||||
|
if(this.element.parentNode) {
|
||||||
this.element.parentNode.removeChild(this.element);
|
this.element.parentNode.removeChild(this.element);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,4 +171,11 @@ describe('SequenceDiagram', () => {
|
||||||
|
|
||||||
expect(content).toContain('<g class="region collapsed"');
|
expect(content).toContain('<g class="region collapsed"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('includes text filters if used', () => {
|
||||||
|
const diagram = makeDiagram('title <highlight>foo</highlight>');
|
||||||
|
const content = getSimplifiedContent(diagram);
|
||||||
|
|
||||||
|
expect(content).toContain('<filter id="highlight"');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,28 +1,36 @@
|
||||||
const STYLES = [
|
const STYLES = [
|
||||||
{
|
{
|
||||||
attrs: {'font-style': 'italic'},
|
attrs: {'font-style': 'italic'},
|
||||||
begin: /[\s_~`]\*(?=\S)/g,
|
begin: {matcher: /[\s_~`>]\*(?=\S)/g, skip: 1},
|
||||||
end: /\S\*(?=[\s_~`])/g,
|
end: {matcher: /\S\*(?=[\s_~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-style': 'italic'},
|
attrs: {'font-style': 'italic'},
|
||||||
begin: /[\s*~`]_(?=\S)/g,
|
begin: {matcher: /[\s*~`>]_(?=\S)/g, skip: 1},
|
||||||
end: /\S_(?=[\s*~`])/g,
|
end: {matcher: /\S_(?=[\s*~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-weight': 'bolder'},
|
attrs: {'font-weight': 'bolder'},
|
||||||
begin: /[\s_~`]\*\*(?=\S)/g,
|
begin: {matcher: /[\s_~`>]\*\*(?=\S)/g, skip: 1},
|
||||||
end: /\S\*\*(?=[\s_~`])/g,
|
end: {matcher: /\S\*\*(?=[\s_~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-weight': 'bolder'},
|
attrs: {'font-weight': 'bolder'},
|
||||||
begin: /[\s*~`]__(?=\S)/g,
|
begin: {matcher: /[\s*~`>]__(?=\S)/g, skip: 1},
|
||||||
end: /\S__(?=[\s*~`])/g,
|
end: {matcher: /\S__(?=[\s*~`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'text-decoration': 'line-through'},
|
attrs: {'text-decoration': 'line-through'},
|
||||||
begin: /[\s_*`]~(?=\S)/g,
|
begin: {matcher: /[\s_*`>]~(?=\S)/g, skip: 1},
|
||||||
end: /\S~(?=[\s_*`])/g,
|
end: {matcher: /\S~(?=[\s_*`<])/g, skip: 1},
|
||||||
}, {
|
}, {
|
||||||
attrs: {'font-family': 'Courier New,Liberation Mono,monospace'},
|
attrs: {'font-family': 'Courier New,Liberation Mono,monospace'},
|
||||||
begin: /[\s_*~.]`(?=\S)/g,
|
begin: {matcher: /[\s_*~.>]`(?=\S)/g, skip: 1},
|
||||||
end: /\S`(?=[\s_*~.])/g,
|
end: {matcher: /\S`(?=[\s_*~.<])/g, skip: 1},
|
||||||
|
}, {
|
||||||
|
attrs: {'fill': '#DD0000'},
|
||||||
|
begin: {matcher: /<red>/g, skip: 0},
|
||||||
|
end: {matcher: /<\/red>/g, skip: 0},
|
||||||
|
}, {
|
||||||
|
attrs: {'filter': 'highlight'},
|
||||||
|
begin: {matcher: /<highlight>/g, skip: 0},
|
||||||
|
end: {matcher: /<\/highlight>/g, skip: 0},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -34,19 +42,20 @@ function findNext(line, p, active) {
|
||||||
|
|
||||||
STYLES.forEach(({begin, end}, ind) => {
|
STYLES.forEach(({begin, end}, ind) => {
|
||||||
const search = active[ind] ? end : begin;
|
const search = active[ind] ? end : begin;
|
||||||
search.lastIndex = p;
|
search.matcher.lastIndex = p + 1 - search.skip;
|
||||||
const m = search.exec(virtLine);
|
const m = search.matcher.exec(virtLine);
|
||||||
if(m && (
|
const beginInd = m ? (m.index + search.skip) : Number.POSITIVE_INFINITY;
|
||||||
m.index < bestStart ||
|
if(
|
||||||
(m.index === bestStart && search.lastIndex > bestEnd)
|
beginInd < bestStart ||
|
||||||
)) {
|
(beginInd === bestStart && search.matcher.lastIndex > bestEnd)
|
||||||
|
) {
|
||||||
styleIndex = ind;
|
styleIndex = ind;
|
||||||
bestStart = m.index;
|
bestStart = beginInd;
|
||||||
bestEnd = search.lastIndex;
|
bestEnd = search.matcher.lastIndex;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {end: bestEnd - 1, start: bestStart, styleIndex};
|
return {end: bestEnd - 1, start: bestStart - 1, styleIndex};
|
||||||
}
|
}
|
||||||
|
|
||||||
function combineAttrs(activeCount, active) {
|
function combineAttrs(activeCount, active) {
|
||||||
|
|
|
@ -117,6 +117,26 @@ describe('Markdown Parser', () => {
|
||||||
]]);
|
]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recognises red styling', () => {
|
||||||
|
const formatted = parser('a <red>b</red> c');
|
||||||
|
|
||||||
|
expect(formatted).toEqual([[
|
||||||
|
{attrs: null, text: 'a '},
|
||||||
|
{attrs: {'fill': '#DD0000'}, text: 'b'},
|
||||||
|
{attrs: null, text: ' c'},
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recognises highlight styling', () => {
|
||||||
|
const formatted = parser('a <highlight>b</highlight> c');
|
||||||
|
|
||||||
|
expect(formatted).toEqual([[
|
||||||
|
{attrs: null, text: 'a '},
|
||||||
|
{attrs: {'filter': 'highlight'}, text: 'b'},
|
||||||
|
{attrs: null, text: ' c'},
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
it('allows dots around monospace styling', () => {
|
it('allows dots around monospace styling', () => {
|
||||||
const formatted = parser('a.`b`.c');
|
const formatted = parser('a.`b`.c');
|
||||||
|
|
||||||
|
@ -140,6 +160,18 @@ describe('Markdown Parser', () => {
|
||||||
]]);
|
]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recognises heavily combined styling', () => {
|
||||||
|
const formatted = parser('**_`abc`_**');
|
||||||
|
|
||||||
|
expect(formatted).toEqual([[
|
||||||
|
{attrs: {
|
||||||
|
'font-family': MONO_FONT,
|
||||||
|
'font-style': 'italic',
|
||||||
|
'font-weight': 'bolder',
|
||||||
|
}, text: 'abc'},
|
||||||
|
]]);
|
||||||
|
});
|
||||||
|
|
||||||
it('allows complex interactions between styles', () => {
|
it('allows complex interactions between styles', () => {
|
||||||
const formatted = parser('_a **b_ ~c~**');
|
const formatted = parser('_a **b_ ~c~**');
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ const TOKENS = [
|
||||||
start: /"/y,
|
start: /"/y,
|
||||||
},
|
},
|
||||||
{baseToken: {v: '...'}, start: /\.\.\./y},
|
{baseToken: {v: '...'}, start: /\.\.\./y},
|
||||||
{end: /(?=[ \t\r\n:+~\-*!<>,])|$/y, start: /(?=[^ \t\r\n:+~\-*!<>,])/y},
|
{end: /(?=[ \t\r\n:+~\-*!<,])|$/y, start: /(?=[^ \t\r\n:+~\-*!<,])/y},
|
||||||
{
|
{
|
||||||
end: /(?=[^~\-<>x])|[-~]x|[<>](?=x)|$/y,
|
end: /(?=[^~\-<>x])|[-~]x|[<>](?=x)|$/y,
|
||||||
includeEnd: true,
|
includeEnd: true,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable max-statements */
|
||||||
|
|
||||||
import Tokeniser from './Tokeniser.mjs';
|
import Tokeniser from './Tokeniser.mjs';
|
||||||
|
|
||||||
describe('Sequence Tokeniser', () => {
|
describe('Sequence Tokeniser', () => {
|
||||||
|
@ -89,6 +91,20 @@ describe('Sequence Tokeniser', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts XML tag-like syntax', () => {
|
||||||
|
const input = 'abc <def> >> ghi <<';
|
||||||
|
const tokens = tokeniser.tokenise(input);
|
||||||
|
|
||||||
|
expect(tokens).toEqual([
|
||||||
|
token({s: '', v: 'abc'}),
|
||||||
|
token({s: ' ', v: '<'}),
|
||||||
|
token({s: '', v: 'def>'}),
|
||||||
|
token({s: ' ', v: '>>'}),
|
||||||
|
token({s: ' ', v: 'ghi'}),
|
||||||
|
token({s: ' ', v: '<<'}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('stores line numbers', () => {
|
it('stores line numbers', () => {
|
||||||
const input = 'foo bar\nbaz';
|
const input = 'foo bar\nbaz';
|
||||||
const tokens = tokeniser.tokenise(input);
|
const tokens = tokeniser.tokenise(input);
|
||||||
|
|
|
@ -76,6 +76,7 @@ export default class Renderer extends EventObject {
|
||||||
this.components = components || getComponents();
|
this.components = components || getComponents();
|
||||||
this.svg = new SVG(new DOMWrapper(document), textSizerFactory);
|
this.svg = new SVG(new DOMWrapper(document), textSizerFactory);
|
||||||
this.knownThemeDefs = new Set();
|
this.knownThemeDefs = new Set();
|
||||||
|
this.knownTextFilterDefs = new Map();
|
||||||
this.knownDefs = new Set();
|
this.knownDefs = new Set();
|
||||||
this.highlights = new Map();
|
this.highlights = new Map();
|
||||||
this.collapsed = new Set();
|
this.collapsed = new Set();
|
||||||
|
@ -92,6 +93,7 @@ export default class Renderer extends EventObject {
|
||||||
this.prepareMeasurementsStage.bind(this);
|
this.prepareMeasurementsStage.bind(this);
|
||||||
this.renderStage = this.renderStage.bind(this);
|
this.renderStage = this.renderStage.bind(this);
|
||||||
this.addThemeDef = this.addThemeDef.bind(this);
|
this.addThemeDef = this.addThemeDef.bind(this);
|
||||||
|
this.addThemeTextDef = this.addThemeTextDef.bind(this);
|
||||||
this.addDef = this.addDef.bind(this);
|
this.addDef = this.addDef.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,6 +152,15 @@ export default class Renderer extends EventObject {
|
||||||
return namespacedName;
|
return namespacedName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addThemeTextDef(name, generator) {
|
||||||
|
const namespacedName = this.namespace + name;
|
||||||
|
if(!this.knownTextFilterDefs.has(name)) {
|
||||||
|
const def = generator().attr('id', namespacedName);
|
||||||
|
this.knownTextFilterDefs.set(name, def);
|
||||||
|
}
|
||||||
|
this.svg.registerTextFilter(name, namespacedName);
|
||||||
|
}
|
||||||
|
|
||||||
addDef(name, generator) {
|
addDef(name, generator) {
|
||||||
let nm = name;
|
let nm = name;
|
||||||
let gen = generator;
|
let gen = generator;
|
||||||
|
@ -531,6 +542,7 @@ export default class Renderer extends EventObject {
|
||||||
_reset(theme) {
|
_reset(theme) {
|
||||||
if(theme) {
|
if(theme) {
|
||||||
this.knownThemeDefs.clear();
|
this.knownThemeDefs.clear();
|
||||||
|
this.knownTextFilterDefs.clear();
|
||||||
this.themeDefs.empty();
|
this.themeDefs.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -628,7 +640,11 @@ export default class Renderer extends EventObject {
|
||||||
this._reset(themeChanged);
|
this._reset(themeChanged);
|
||||||
|
|
||||||
this.metaCode.nodeValue = sequence.meta.code;
|
this.metaCode.nodeValue = sequence.meta.code;
|
||||||
this.theme.addDefs(this.addThemeDef);
|
this.svg.resetTextFilters();
|
||||||
|
this.theme.addDefs(this.addThemeDef, this.addThemeTextDef);
|
||||||
|
for(const def of this.knownTextFilterDefs.values()) {
|
||||||
|
def.detach();
|
||||||
|
}
|
||||||
|
|
||||||
this.title.set({
|
this.title.set({
|
||||||
attrs: Object.assign({
|
attrs: Object.assign({
|
||||||
|
@ -662,6 +678,10 @@ export default class Renderer extends EventObject {
|
||||||
sequence.stages.forEach(this.renderStage);
|
sequence.stages.forEach(this.renderStage);
|
||||||
const bottomY = this.checkAgentRange(['[', ']'], this.currentY);
|
const bottomY = this.checkAgentRange(['[', ']'], this.currentY);
|
||||||
|
|
||||||
|
this.svg.getUsedTextFilterNames().forEach((name) => {
|
||||||
|
this.themeDefs.add(this.knownTextFilterDefs.get(name));
|
||||||
|
});
|
||||||
|
|
||||||
const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0);
|
const stagesHeight = Math.max(bottomY - this.theme.actionMargin, 0);
|
||||||
this.updateBounds(stagesHeight);
|
this.updateBounds(stagesHeight);
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,58 @@ export default class BaseTheme {
|
||||||
// No-op
|
// No-op
|
||||||
}
|
}
|
||||||
|
|
||||||
addDefs() {
|
addDefs(builder, textBuilder) {
|
||||||
// No-op
|
// Thanks, https://stackoverflow.com/a/12263962/1180785
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=603157
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=917766
|
||||||
|
textBuilder('highlight', () => this.svg.el('filter')
|
||||||
|
.add(
|
||||||
|
// Morph makes characters consistent
|
||||||
|
this.svg.el('feMorphology').attrs({
|
||||||
|
'in': 'SourceAlpha',
|
||||||
|
'operator': 'dilate',
|
||||||
|
'radius': '4',
|
||||||
|
}),
|
||||||
|
// Blur+thresh makes edges smooth
|
||||||
|
this.svg.el('feGaussianBlur').attrs({
|
||||||
|
'edgeMode': 'none',
|
||||||
|
'stdDeviation': '3, 1.5',
|
||||||
|
}),
|
||||||
|
this.svg.el('feComponentTransfer').add(
|
||||||
|
this.svg.el('feFuncA').attrs({
|
||||||
|
'intercept': -70,
|
||||||
|
'slope': 100,
|
||||||
|
'type': 'linear',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// Add colour
|
||||||
|
this.svg.el('feComponentTransfer').add(
|
||||||
|
this.svg.el('feFuncR').attrs({
|
||||||
|
'intercept': 1,
|
||||||
|
'slope': 0,
|
||||||
|
'type': 'linear',
|
||||||
|
}),
|
||||||
|
this.svg.el('feFuncG').attrs({
|
||||||
|
'intercept': 0.9375,
|
||||||
|
'slope': 0,
|
||||||
|
'type': 'linear',
|
||||||
|
}),
|
||||||
|
this.svg.el('feFuncB').attrs({
|
||||||
|
'intercept': 0.09375,
|
||||||
|
'slope': 0,
|
||||||
|
'type': 'linear',
|
||||||
|
}),
|
||||||
|
this.svg.el('feFuncA').attrs({
|
||||||
|
'slope': 0.7,
|
||||||
|
'type': 'linear',
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// Draw text on top
|
||||||
|
this.svg.el('feMerge').add(
|
||||||
|
this.svg.el('feMergeNode'),
|
||||||
|
this.svg.el('feMergeNode').attr('in', 'SourceGraphic')
|
||||||
|
)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitleAttrs() {
|
getTitleAttrs() {
|
||||||
|
|
|
@ -350,7 +350,7 @@ export default class SketchTheme extends BaseTheme {
|
||||||
this.random.reset();
|
this.random.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
addDefs(builder) {
|
addDefs(builder, textBuilder) {
|
||||||
builder('sketch_font', () => {
|
builder('sketch_font', () => {
|
||||||
const style = this.svg.el('style', null);
|
const style = this.svg.el('style', null);
|
||||||
// Font must be embedded for exporting as SVG / PNG
|
// Font must be embedded for exporting as SVG / PNG
|
||||||
|
@ -362,6 +362,8 @@ export default class SketchTheme extends BaseTheme {
|
||||||
);
|
);
|
||||||
return style;
|
return style;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
super.addDefs(builder, textBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
vary(range, centre = 0) {
|
vary(range, centre = 0) {
|
||||||
|
|
|
@ -189,12 +189,40 @@ export default class SVG {
|
||||||
this.dom = domWrapper;
|
this.dom = domWrapper;
|
||||||
this.body = this.el('svg').attr('xmlns', NS).attr('version', '1.1');
|
this.body = this.el('svg').attr('xmlns', NS).attr('version', '1.1');
|
||||||
const fn = (textSizerFactory || defaultTextSizerFactory);
|
const fn = (textSizerFactory || defaultTextSizerFactory);
|
||||||
|
this.textFilters = new Map();
|
||||||
this.textSizer = new TextSizerWrapper(fn(this));
|
this.textSizer = new TextSizerWrapper(fn(this));
|
||||||
|
|
||||||
this.txt = this.txt.bind(this);
|
this.txt = this.txt.bind(this);
|
||||||
this.el = this.el.bind(this);
|
this.el = this.el.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetTextFilters() {
|
||||||
|
this.textFilters.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerTextFilter(name, id) {
|
||||||
|
this.textFilters.set(name, {id, used: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
getTextFilter(name) {
|
||||||
|
const filter = this.textFilters.get(name);
|
||||||
|
if(!filter) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
filter.used = true;
|
||||||
|
return 'url(#' + filter.id + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsedTextFilterNames() {
|
||||||
|
const used = [];
|
||||||
|
for(const [name, filter] of this.textFilters) {
|
||||||
|
if(filter.used) {
|
||||||
|
used.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return used;
|
||||||
|
}
|
||||||
|
|
||||||
linearGradient(attrs, stops) {
|
linearGradient(attrs, stops) {
|
||||||
return this.el('linearGradient')
|
return this.el('linearGradient')
|
||||||
.attrs(attrs)
|
.attrs(attrs)
|
||||||
|
|
|
@ -15,11 +15,14 @@ function populateSvgTextLine(svg, node, formattedLine) {
|
||||||
throw new Error('Invalid formatted text line: ' + formattedLine);
|
throw new Error('Invalid formatted text line: ' + formattedLine);
|
||||||
}
|
}
|
||||||
formattedLine.forEach(({text, attrs}) => {
|
formattedLine.forEach(({text, attrs}) => {
|
||||||
|
let element = text;
|
||||||
if(attrs) {
|
if(attrs) {
|
||||||
node.add(svg.el('tspan').attrs(attrs).add(text));
|
element = svg.el('tspan').attrs(attrs).add(text);
|
||||||
} else {
|
if(attrs.filter) {
|
||||||
node.add(text);
|
element.attr('filter', svg.getTextFilter(attrs.filter));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
node.add(element);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,17 @@ describe('SVGTextBlock', () => {
|
||||||
.toEqual('foo<tspan zig="zag">bar</tspan>');
|
.toEqual('foo<tspan zig="zag">bar</tspan>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('converts filter attributes using the registered filters', () => {
|
||||||
|
svg.registerTextFilter('foo', 'local-foobar');
|
||||||
|
block.set({formatted: [[
|
||||||
|
{text: 'foo'},
|
||||||
|
{attrs: {'filter': 'foo'}, text: 'bar'},
|
||||||
|
]]});
|
||||||
|
|
||||||
|
expect(hold.childNodes[0].innerHTML)
|
||||||
|
.toEqual('foo<tspan filter="url(#local-foobar)">bar</tspan>');
|
||||||
|
});
|
||||||
|
|
||||||
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;
|
||||||
|
|
|
@ -461,9 +461,20 @@ ImageRegion.loadURL = function(url, size = {}) {
|
||||||
};
|
};
|
||||||
|
|
||||||
ImageRegion.loadSVG = function(svg, size = {}) {
|
ImageRegion.loadSVG = function(svg, size = {}) {
|
||||||
const blob = new Blob([svg], {type: 'image/svg+xml'});
|
// Safari workaround:
|
||||||
|
// Tweak SVG size directly rather than resizing image to keep quality high
|
||||||
|
const resolution = size.resolution || 1;
|
||||||
|
const svgCode = svg.replace(
|
||||||
|
/<svg width="([^"]+)" height="([^"]+)"/,
|
||||||
|
(m, w, h) => (
|
||||||
|
'<svg width="' + (w * resolution) +
|
||||||
|
'" height="' + (h * resolution) + '"'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const blob = new Blob([svgCode], {type: 'image/svg+xml'});
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
return ImageRegion.loadURL(url, size)
|
return ImageRegion.loadURL(url, Object.assign({}, size, {resolution: 1}))
|
||||||
.then((region) => {
|
.then((region) => {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
return region;
|
return region;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
<svg width="86.02510070800781" height="98" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-43.012550354003906 -93 86.02510070800781 98"><metadata>title "Foo **Bar**
|
<svg width="96.69921875" height="124" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-48.349609375 -119 96.69921875 124"><metadata>title "Foo **Bar**
|
||||||
_Baz_ `Zig`
|
_Baz_ `Zig`
|
||||||
**_`Zag`_**"</metadata><defs></defs><defs><mask id="R0FullMask" maskUnits="userSpaceOnUse"><rect fill="#FFFFFF" height="98" width="86.02510070800781" x="-43.012550354003906" y="-93"></rect></mask><mask id="R0LineMask" maskUnits="userSpaceOnUse"><rect fill="#FFFFFF" height="98" width="86.02510070800781" x="-43.012550354003906" y="-93"></rect></mask></defs><g></g><g><text x="0" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" class="title" y="-68">Foo <tspan font-weight="bolder">Bar</tspan></text><text x="0" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" class="title" y="-42"><tspan font-style="italic">Baz</tspan> <tspan font-family="Courier New,Liberation Mono,monospace">Zig</tspan></text><text x="0" font-family="Helvetica,Arial,Liberation Sans,sans-serif" font-size="20" line-height="1.3" text-anchor="middle" class="title" y="-16"><tspan font-style="italic" font-weight="bolder" font-family="Courier New,Liberation Mono,monospace">Zag</tspan></text></g><g></g><g mask="url(#R0FullMask)"><g mask="url(#R0LineMask)"></g><g></g><g></g></g></svg>
|
**_`Zag`_**
|
||||||
|
<highlight>Back</highlight> <red>Text</red>"
|
||||||
|
</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.9375" slope="0" type="linear"></feFuncG><feFuncB intercept="0.09375" slope="0" type="linear"></feFuncB><feFuncA slope="0.7" type="linear"></feFuncA></feComponentTransfer><feMerge><feMergeNode></feMergeNode><feMergeNode in="SourceGraphic"></feMergeNode></feMerge></filter></defs><defs><mask id="R0FullMask" maskUnits="userSpaceOnUse"><rect fill="#FFFFFF" height="124" width="96.69921875" x="-48.349609375" y="-119"></rect></mask><mask id="R0LineMask" maskUnits="userSpaceOnUse"><rect fill="#FFFFFF" height="124" width="96.69921875" x="-48.349609375" y="-119"></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="-94">Foo <tspan font-weight="bolder">Bar</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 font-style="italic">Baz</tspan> <tspan font-family="Courier New,Liberation Mono,monospace">Zig</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"><tspan font-style="italic" font-weight="bolder" font-family="Courier New,Liberation Mono,monospace">Zag</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="-16"><tspan filter="url(#R0highlight)">Back</tspan> <tspan fill="#DD0000">Text</tspan></text></g><g></g><g mask="url(#R0FullMask)"><g mask="url(#R0LineMask)"></g><g></g><g></g></g></svg>
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
@ -266,6 +266,16 @@
|
||||||
preview: 'A -> B: `mono`',
|
preview: 'A -> B: `mono`',
|
||||||
title: 'Monospace markdown',
|
title: 'Monospace markdown',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
code: '<red>{text}</red>',
|
||||||
|
preview: 'A -> B: <red>red</red>',
|
||||||
|
title: 'Red markdown',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<highlight>{text}</highlight>',
|
||||||
|
preview: 'A -> B: <highlight>highlight</highlight>',
|
||||||
|
title: 'Highlight markdown',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
code: '{Agent} is red',
|
code: '{Agent} is red',
|
||||||
preview: 'headers box\nA is red\nbegin A',
|
preview: 'headers box\nA is red\nbegin A',
|
||||||
|
@ -479,7 +489,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
detach() {
|
detach() {
|
||||||
|
if(this.element.parentNode) {
|
||||||
this.element.parentNode.removeChild(this.element);
|
this.element.parentNode.removeChild(this.element);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -263,6 +263,16 @@ export default [
|
||||||
preview: 'A -> B: `mono`',
|
preview: 'A -> B: `mono`',
|
||||||
title: 'Monospace markdown',
|
title: 'Monospace markdown',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
code: '<red>{text}</red>',
|
||||||
|
preview: 'A -> B: <red>red</red>',
|
||||||
|
title: 'Red markdown',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<highlight>{text}</highlight>',
|
||||||
|
preview: 'A -> B: <highlight>highlight</highlight>',
|
||||||
|
title: 'Highlight markdown',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
code: '{Agent} is red',
|
code: '{Agent} is red',
|
||||||
preview: 'headers box\nA is red\nbegin A',
|
preview: 'headers box\nA is red\nbegin A',
|
||||||
|
|
Loading…
Reference in New Issue