Improve consistency of SVGs generated, and fix non-BMP unicode exports from VirtualDocument
This commit is contained in:
parent
2a7d9e76ed
commit
816206ed33
|
@ -7,7 +7,7 @@
|
|||
script-src 'self' https://cdnjs.cloudflare.com https://unpkg.com;
|
||||
style-src 'self'
|
||||
https://cdnjs.cloudflare.com
|
||||
'sha256-ru2GY2rXeOf7PQX5LzK3ckNo21FCDUoRc2f3i0QcD1g='
|
||||
'sha256-s7UPtBgvov5WNF9C1DlTZDpqwLgEmfiWha5a5p/Zn7E='
|
||||
;
|
||||
font-src 'self' data:;
|
||||
img-src 'self' blob:;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
script-src 'self' https://cdnjs.cloudflare.com https://unpkg.com;
|
||||
style-src 'self'
|
||||
https://cdnjs.cloudflare.com
|
||||
'sha256-ru2GY2rXeOf7PQX5LzK3ckNo21FCDUoRc2f3i0QcD1g='
|
||||
'sha256-s7UPtBgvov5WNF9C1DlTZDpqwLgEmfiWha5a5p/Zn7E='
|
||||
;
|
||||
font-src 'self' data:;
|
||||
img-src 'self' blob:;
|
||||
|
|
|
@ -4022,6 +4022,14 @@
|
|||
return attrs;
|
||||
}
|
||||
|
||||
function shrinkWhitespace(text) {
|
||||
return text.replace(/[\f\n\r\t\v ]+/g, ' ');
|
||||
}
|
||||
|
||||
function trimCollapsible(text) {
|
||||
return text.replace(/^[\f\n\r\t\v ]+|[\f\n\r\t\v ]+$/g, '');
|
||||
}
|
||||
|
||||
function parseMarkdown(text) {
|
||||
if(!text) {
|
||||
return [];
|
||||
|
@ -4030,13 +4038,14 @@
|
|||
const active = STYLES.map(() => false);
|
||||
let activeCount = 0;
|
||||
let attrs = null;
|
||||
const lines = text.split('\n');
|
||||
const lines = trimCollapsible(text).split('\n');
|
||||
const result = [];
|
||||
lines.forEach((line) => {
|
||||
const ln = shrinkWhitespace(trimCollapsible(line));
|
||||
const parts = [];
|
||||
let p = 0;
|
||||
for(;;) {
|
||||
const {styleIndex, start, end} = findNext(line, p, active);
|
||||
const {styleIndex, start, end} = findNext(ln, p, active);
|
||||
if(styleIndex === -1) {
|
||||
break;
|
||||
}
|
||||
|
@ -4048,13 +4057,13 @@
|
|||
++ activeCount;
|
||||
}
|
||||
if(start > p) {
|
||||
parts.push({attrs, text: line.substring(p, start)});
|
||||
parts.push({attrs, text: ln.substring(p, start)});
|
||||
}
|
||||
attrs = combineAttrs(activeCount, active);
|
||||
p = end;
|
||||
}
|
||||
if(p < line.length) {
|
||||
parts.push({attrs, text: line.substr(p)});
|
||||
if(p < ln.length) {
|
||||
parts.push({attrs, text: ln.substr(p)});
|
||||
}
|
||||
result.push(parts);
|
||||
});
|
||||
|
@ -8456,7 +8465,7 @@
|
|||
/* eslint-disable max-lines */
|
||||
|
||||
const FONT$3 = Handlee.name;
|
||||
const FONT_FAMILY = '\'' + FONT$3 + '\',cursive';
|
||||
const FONT_FAMILY = FONT$3 + ',cursive';
|
||||
const LINE_HEIGHT$3 = 1.5;
|
||||
const MAX_CHAOS = 5;
|
||||
|
||||
|
@ -8824,7 +8833,7 @@
|
|||
// Font must be embedded for exporting as SVG / PNG
|
||||
style.text(
|
||||
'@font-face{' +
|
||||
'font-family:"' + Handlee.name + '";' +
|
||||
'font-family:' + Handlee.name + ';' +
|
||||
'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' +
|
||||
'}'
|
||||
);
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -4022,6 +4022,14 @@
|
|||
return attrs;
|
||||
}
|
||||
|
||||
function shrinkWhitespace(text) {
|
||||
return text.replace(/[\f\n\r\t\v ]+/g, ' ');
|
||||
}
|
||||
|
||||
function trimCollapsible(text) {
|
||||
return text.replace(/^[\f\n\r\t\v ]+|[\f\n\r\t\v ]+$/g, '');
|
||||
}
|
||||
|
||||
function parseMarkdown(text) {
|
||||
if(!text) {
|
||||
return [];
|
||||
|
@ -4030,13 +4038,14 @@
|
|||
const active = STYLES.map(() => false);
|
||||
let activeCount = 0;
|
||||
let attrs = null;
|
||||
const lines = text.split('\n');
|
||||
const lines = trimCollapsible(text).split('\n');
|
||||
const result = [];
|
||||
lines.forEach((line) => {
|
||||
const ln = shrinkWhitespace(trimCollapsible(line));
|
||||
const parts = [];
|
||||
let p = 0;
|
||||
for(;;) {
|
||||
const {styleIndex, start, end} = findNext(line, p, active);
|
||||
const {styleIndex, start, end} = findNext(ln, p, active);
|
||||
if(styleIndex === -1) {
|
||||
break;
|
||||
}
|
||||
|
@ -4048,13 +4057,13 @@
|
|||
++ activeCount;
|
||||
}
|
||||
if(start > p) {
|
||||
parts.push({attrs, text: line.substring(p, start)});
|
||||
parts.push({attrs, text: ln.substring(p, start)});
|
||||
}
|
||||
attrs = combineAttrs(activeCount, active);
|
||||
p = end;
|
||||
}
|
||||
if(p < line.length) {
|
||||
parts.push({attrs, text: line.substr(p)});
|
||||
if(p < ln.length) {
|
||||
parts.push({attrs, text: ln.substr(p)});
|
||||
}
|
||||
result.push(parts);
|
||||
});
|
||||
|
@ -8456,7 +8465,7 @@
|
|||
/* eslint-disable max-lines */
|
||||
|
||||
const FONT$3 = Handlee.name;
|
||||
const FONT_FAMILY = '\'' + FONT$3 + '\',cursive';
|
||||
const FONT_FAMILY = FONT$3 + ',cursive';
|
||||
const LINE_HEIGHT$3 = 1.5;
|
||||
const MAX_CHAOS = 5;
|
||||
|
||||
|
@ -8824,7 +8833,7 @@
|
|||
// Font must be embedded for exporting as SVG / PNG
|
||||
style.text(
|
||||
'@font-face{' +
|
||||
'font-family:"' + Handlee.name + '";' +
|
||||
'font-family:' + Handlee.name + ';' +
|
||||
'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' +
|
||||
'}'
|
||||
);
|
||||
|
@ -9981,15 +9990,21 @@
|
|||
});
|
||||
|
||||
function encodeChar(c) {
|
||||
return '&#' + c.charCodeAt(0).toString(10) + ';';
|
||||
return '&#' + c.codePointAt(0).toString(10) + ';';
|
||||
}
|
||||
|
||||
function escapeHTML(text) {
|
||||
return text.replace(/[^\r\n\t -%'-;=?-~]/g, encodeChar);
|
||||
return text.replace(
|
||||
/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\r\n\t -%'-;=?-~]/g,
|
||||
encodeChar
|
||||
);
|
||||
}
|
||||
|
||||
function escapeQuoted(text) {
|
||||
return text.replace(/[^\r\n\t !#$%(-;=?-~]/g, encodeChar);
|
||||
return text.replace(
|
||||
/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\r\n\t !#$%'-;=?-~]/g,
|
||||
encodeChar
|
||||
);
|
||||
}
|
||||
|
||||
class TextNode {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
style-src 'self'
|
||||
https://cdnjs.cloudflare.com
|
||||
https://fonts.googleapis.com
|
||||
'sha256-ru2GY2rXeOf7PQX5LzK3ckNo21FCDUoRc2f3i0QcD1g='
|
||||
'sha256-s7UPtBgvov5WNF9C1DlTZDpqwLgEmfiWha5a5p/Zn7E='
|
||||
;
|
||||
font-src 'self' data: https://fonts.gstatic.com;
|
||||
img-src 'self';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
script-src 'self';
|
||||
connect-src 'self';
|
||||
style-src 'self'
|
||||
'sha256-ru2GY2rXeOf7PQX5LzK3ckNo21FCDUoRc2f3i0QcD1g='
|
||||
'sha256-s7UPtBgvov5WNF9C1DlTZDpqwLgEmfiWha5a5p/Zn7E='
|
||||
;
|
||||
font-src 'self' data:;
|
||||
img-src 'self' blob:;
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
function encodeChar(c) {
|
||||
return '&#' + c.charCodeAt(0).toString(10) + ';';
|
||||
return '&#' + c.codePointAt(0).toString(10) + ';';
|
||||
}
|
||||
|
||||
function escapeHTML(text) {
|
||||
return text.replace(/[^\r\n\t -%'-;=?-~]/g, encodeChar);
|
||||
return text.replace(
|
||||
/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\r\n\t -%'-;=?-~]/g,
|
||||
encodeChar
|
||||
);
|
||||
}
|
||||
|
||||
function escapeQuoted(text) {
|
||||
return text.replace(/[^\r\n\t !#$%(-;=?-~]/g, encodeChar);
|
||||
return text.replace(
|
||||
/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\r\n\t !#$%'-;=?-~]/g,
|
||||
encodeChar
|
||||
);
|
||||
}
|
||||
|
||||
class TextNode {
|
||||
|
|
|
@ -236,9 +236,9 @@ describe('VirtualDocument', () => {
|
|||
|
||||
it('escapes attributes', () => {
|
||||
const o = doc.createElement('div');
|
||||
o.setAttribute('foo', 'b&a"r');
|
||||
o.setAttribute('foo', 'b&a"\'r');
|
||||
|
||||
expect(o.outerHTML).toEqual('<div foo="b&a"r"></div>');
|
||||
expect(o.outerHTML).toEqual('<div foo="b&a"\'r"></div>');
|
||||
});
|
||||
|
||||
it('includes all children', () => {
|
||||
|
@ -258,5 +258,12 @@ describe('VirtualDocument', () => {
|
|||
|
||||
expect(o.outerHTML).toEqual('<div>a<b>c</div>');
|
||||
});
|
||||
|
||||
it('escapes non-BMP unicode characters', () => {
|
||||
const o = doc.createElement('div');
|
||||
o.appendChild(doc.createTextNode('\uD83D\uDE02'));
|
||||
|
||||
expect(o.outerHTML).toEqual('<div>😂</div>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -62,6 +62,14 @@ function combineAttrs(activeCount, active) {
|
|||
return attrs;
|
||||
}
|
||||
|
||||
function shrinkWhitespace(text) {
|
||||
return text.replace(/[\f\n\r\t\v ]+/g, ' ');
|
||||
}
|
||||
|
||||
function trimCollapsible(text) {
|
||||
return text.replace(/^[\f\n\r\t\v ]+|[\f\n\r\t\v ]+$/g, '');
|
||||
}
|
||||
|
||||
export default function parseMarkdown(text) {
|
||||
if(!text) {
|
||||
return [];
|
||||
|
@ -70,13 +78,14 @@ export default function parseMarkdown(text) {
|
|||
const active = STYLES.map(() => false);
|
||||
let activeCount = 0;
|
||||
let attrs = null;
|
||||
const lines = text.split('\n');
|
||||
const lines = trimCollapsible(text).split('\n');
|
||||
const result = [];
|
||||
lines.forEach((line) => {
|
||||
const ln = shrinkWhitespace(trimCollapsible(line));
|
||||
const parts = [];
|
||||
let p = 0;
|
||||
for(;;) {
|
||||
const {styleIndex, start, end} = findNext(line, p, active);
|
||||
const {styleIndex, start, end} = findNext(ln, p, active);
|
||||
if(styleIndex === -1) {
|
||||
break;
|
||||
}
|
||||
|
@ -88,13 +97,13 @@ export default function parseMarkdown(text) {
|
|||
++ activeCount;
|
||||
}
|
||||
if(start > p) {
|
||||
parts.push({attrs, text: line.substring(p, start)});
|
||||
parts.push({attrs, text: ln.substring(p, start)});
|
||||
}
|
||||
attrs = combineAttrs(activeCount, active);
|
||||
p = end;
|
||||
}
|
||||
if(p < line.length) {
|
||||
parts.push({attrs, text: line.substr(p)});
|
||||
if(p < ln.length) {
|
||||
parts.push({attrs, text: ln.substr(p)});
|
||||
}
|
||||
result.push(parts);
|
||||
});
|
||||
|
|
|
@ -24,6 +24,33 @@ describe('Markdown Parser', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('trims leading and trailing whitespace', () => {
|
||||
const formatted = parser(' a \n \u00A0b \n ');
|
||||
|
||||
expect(formatted).toEqual([
|
||||
[{attrs: null, text: 'a'}],
|
||||
[{attrs: null, text: '\u00A0b'}],
|
||||
]);
|
||||
});
|
||||
|
||||
it('replaces sequences of whitespace with a single space', () => {
|
||||
const formatted = parser('abc \t \v def');
|
||||
|
||||
expect(formatted).toEqual([
|
||||
[{attrs: null, text: 'abc def'}],
|
||||
]);
|
||||
});
|
||||
|
||||
it('maintains internal blank lines', () => {
|
||||
const formatted = parser('abc\n\ndef');
|
||||
|
||||
expect(formatted).toEqual([
|
||||
[{attrs: null, text: 'abc'}],
|
||||
[],
|
||||
[{attrs: null, text: 'def'}],
|
||||
]);
|
||||
});
|
||||
|
||||
it('recognises bold styling', () => {
|
||||
const formatted = parser('a **b** c __d__ e');
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import Handlee from '../../fonts/HandleeFontData.mjs';
|
|||
import Random from '../../core/Random.mjs';
|
||||
|
||||
const FONT = Handlee.name;
|
||||
const FONT_FAMILY = '\'' + FONT + '\',cursive';
|
||||
const FONT_FAMILY = FONT + ',cursive';
|
||||
const LINE_HEIGHT = 1.5;
|
||||
const MAX_CHAOS = 5;
|
||||
|
||||
|
@ -374,7 +374,7 @@ export default class SketchTheme extends BaseTheme {
|
|||
// Font must be embedded for exporting as SVG / PNG
|
||||
style.text(
|
||||
'@font-face{' +
|
||||
'font-family:"' + Handlee.name + '";' +
|
||||
'font-family:' + Handlee.name + ';' +
|
||||
'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' +
|
||||
'}'
|
||||
);
|
||||
|
|
|
@ -66,6 +66,16 @@ describe('SVGTextBlock', () => {
|
|||
expect(hold.childNodes[1].innerHTML).toEqual('bar');
|
||||
});
|
||||
|
||||
it('renders with tspans if the formatting changes', () => {
|
||||
block.set({formatted: [[
|
||||
{text: 'foo'},
|
||||
{attrs: {zig: 'zag'}, text: 'bar'},
|
||||
]]});
|
||||
|
||||
expect(hold.childNodes[0].innerHTML)
|
||||
.toEqual('foo<tspan zig="zag">bar</tspan>');
|
||||
});
|
||||
|
||||
it('re-uses text nodes when possible, adding more if needed', () => {
|
||||
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
|
||||
const [line0, line1] = hold.childNodes;
|
||||
|
|
Loading…
Reference in New Issue