Improve consistency of SVGs generated, and fix non-BMP unicode exports from VirtualDocument

This commit is contained in:
David Evans 2018-04-22 19:17:31 +01:00
parent 2a7d9e76ed
commit 816206ed33
13 changed files with 117 additions and 34 deletions

View File

@ -7,7 +7,7 @@
script-src 'self' https://cdnjs.cloudflare.com https://unpkg.com; script-src 'self' https://cdnjs.cloudflare.com https://unpkg.com;
style-src 'self' style-src 'self'
https://cdnjs.cloudflare.com https://cdnjs.cloudflare.com
'sha256-ru2GY2rXeOf7PQX5LzK3ckNo21FCDUoRc2f3i0QcD1g=' 'sha256-s7UPtBgvov5WNF9C1DlTZDpqwLgEmfiWha5a5p/Zn7E='
; ;
font-src 'self' data:; font-src 'self' data:;
img-src 'self' blob:; img-src 'self' blob:;

View File

@ -7,7 +7,7 @@
script-src 'self' https://cdnjs.cloudflare.com https://unpkg.com; script-src 'self' https://cdnjs.cloudflare.com https://unpkg.com;
style-src 'self' style-src 'self'
https://cdnjs.cloudflare.com https://cdnjs.cloudflare.com
'sha256-ru2GY2rXeOf7PQX5LzK3ckNo21FCDUoRc2f3i0QcD1g=' 'sha256-s7UPtBgvov5WNF9C1DlTZDpqwLgEmfiWha5a5p/Zn7E='
; ;
font-src 'self' data:; font-src 'self' data:;
img-src 'self' blob:; img-src 'self' blob:;

View File

@ -4022,6 +4022,14 @@
return attrs; 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) { function parseMarkdown(text) {
if(!text) { if(!text) {
return []; return [];
@ -4030,13 +4038,14 @@
const active = STYLES.map(() => false); const active = STYLES.map(() => false);
let activeCount = 0; let activeCount = 0;
let attrs = null; let attrs = null;
const lines = text.split('\n'); const lines = trimCollapsible(text).split('\n');
const result = []; const result = [];
lines.forEach((line) => { lines.forEach((line) => {
const ln = shrinkWhitespace(trimCollapsible(line));
const parts = []; const parts = [];
let p = 0; let p = 0;
for(;;) { for(;;) {
const {styleIndex, start, end} = findNext(line, p, active); const {styleIndex, start, end} = findNext(ln, p, active);
if(styleIndex === -1) { if(styleIndex === -1) {
break; break;
} }
@ -4048,13 +4057,13 @@
++ activeCount; ++ activeCount;
} }
if(start > p) { if(start > p) {
parts.push({attrs, text: line.substring(p, start)}); parts.push({attrs, text: ln.substring(p, start)});
} }
attrs = combineAttrs(activeCount, active); attrs = combineAttrs(activeCount, active);
p = end; p = end;
} }
if(p < line.length) { if(p < ln.length) {
parts.push({attrs, text: line.substr(p)}); parts.push({attrs, text: ln.substr(p)});
} }
result.push(parts); result.push(parts);
}); });
@ -8456,7 +8465,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const FONT$3 = Handlee.name; const FONT$3 = Handlee.name;
const FONT_FAMILY = '\'' + FONT$3 + '\',cursive'; const FONT_FAMILY = FONT$3 + ',cursive';
const LINE_HEIGHT$3 = 1.5; const LINE_HEIGHT$3 = 1.5;
const MAX_CHAOS = 5; const MAX_CHAOS = 5;
@ -8824,7 +8833,7 @@
// Font must be embedded for exporting as SVG / PNG // Font must be embedded for exporting as SVG / PNG
style.text( style.text(
'@font-face{' + '@font-face{' +
'font-family:"' + Handlee.name + '";' + 'font-family:' + Handlee.name + ';' +
'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' + 'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' +
'}' '}'
); );

File diff suppressed because one or more lines are too long

View File

@ -4022,6 +4022,14 @@
return attrs; 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) { function parseMarkdown(text) {
if(!text) { if(!text) {
return []; return [];
@ -4030,13 +4038,14 @@
const active = STYLES.map(() => false); const active = STYLES.map(() => false);
let activeCount = 0; let activeCount = 0;
let attrs = null; let attrs = null;
const lines = text.split('\n'); const lines = trimCollapsible(text).split('\n');
const result = []; const result = [];
lines.forEach((line) => { lines.forEach((line) => {
const ln = shrinkWhitespace(trimCollapsible(line));
const parts = []; const parts = [];
let p = 0; let p = 0;
for(;;) { for(;;) {
const {styleIndex, start, end} = findNext(line, p, active); const {styleIndex, start, end} = findNext(ln, p, active);
if(styleIndex === -1) { if(styleIndex === -1) {
break; break;
} }
@ -4048,13 +4057,13 @@
++ activeCount; ++ activeCount;
} }
if(start > p) { if(start > p) {
parts.push({attrs, text: line.substring(p, start)}); parts.push({attrs, text: ln.substring(p, start)});
} }
attrs = combineAttrs(activeCount, active); attrs = combineAttrs(activeCount, active);
p = end; p = end;
} }
if(p < line.length) { if(p < ln.length) {
parts.push({attrs, text: line.substr(p)}); parts.push({attrs, text: ln.substr(p)});
} }
result.push(parts); result.push(parts);
}); });
@ -8456,7 +8465,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const FONT$3 = Handlee.name; const FONT$3 = Handlee.name;
const FONT_FAMILY = '\'' + FONT$3 + '\',cursive'; const FONT_FAMILY = FONT$3 + ',cursive';
const LINE_HEIGHT$3 = 1.5; const LINE_HEIGHT$3 = 1.5;
const MAX_CHAOS = 5; const MAX_CHAOS = 5;
@ -8824,7 +8833,7 @@
// Font must be embedded for exporting as SVG / PNG // Font must be embedded for exporting as SVG / PNG
style.text( style.text(
'@font-face{' + '@font-face{' +
'font-family:"' + Handlee.name + '";' + 'font-family:' + Handlee.name + ';' +
'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' + 'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' +
'}' '}'
); );
@ -9981,15 +9990,21 @@
}); });
function encodeChar(c) { function encodeChar(c) {
return '&#' + c.charCodeAt(0).toString(10) + ';'; return '&#' + c.codePointAt(0).toString(10) + ';';
} }
function escapeHTML(text) { 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) { 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 { class TextNode {

View File

@ -8,7 +8,7 @@
style-src 'self' style-src 'self'
https://cdnjs.cloudflare.com https://cdnjs.cloudflare.com
https://fonts.googleapis.com https://fonts.googleapis.com
'sha256-ru2GY2rXeOf7PQX5LzK3ckNo21FCDUoRc2f3i0QcD1g=' 'sha256-s7UPtBgvov5WNF9C1DlTZDpqwLgEmfiWha5a5p/Zn7E='
; ;
font-src 'self' data: https://fonts.gstatic.com; font-src 'self' data: https://fonts.gstatic.com;
img-src 'self'; img-src 'self';

View File

@ -7,7 +7,7 @@
script-src 'self'; script-src 'self';
connect-src 'self'; connect-src 'self';
style-src 'self' style-src 'self'
'sha256-ru2GY2rXeOf7PQX5LzK3ckNo21FCDUoRc2f3i0QcD1g=' 'sha256-s7UPtBgvov5WNF9C1DlTZDpqwLgEmfiWha5a5p/Zn7E='
; ;
font-src 'self' data:; font-src 'self' data:;
img-src 'self' blob:; img-src 'self' blob:;

View File

@ -1,13 +1,19 @@
function encodeChar(c) { function encodeChar(c) {
return '&#' + c.charCodeAt(0).toString(10) + ';'; return '&#' + c.codePointAt(0).toString(10) + ';';
} }
function escapeHTML(text) { 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) { 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 { class TextNode {

View File

@ -236,9 +236,9 @@ describe('VirtualDocument', () => {
it('escapes attributes', () => { it('escapes attributes', () => {
const o = doc.createElement('div'); const o = doc.createElement('div');
o.setAttribute('foo', 'b&a"r'); o.setAttribute('foo', 'b&a"\'r');
expect(o.outerHTML).toEqual('<div foo="b&#38;a&#34;r"></div>'); expect(o.outerHTML).toEqual('<div foo="b&#38;a&#34;\'r"></div>');
}); });
it('includes all children', () => { it('includes all children', () => {
@ -258,5 +258,12 @@ describe('VirtualDocument', () => {
expect(o.outerHTML).toEqual('<div>a&#60;b&#62;c</div>'); expect(o.outerHTML).toEqual('<div>a&#60;b&#62;c</div>');
}); });
it('escapes non-BMP unicode characters', () => {
const o = doc.createElement('div');
o.appendChild(doc.createTextNode('\uD83D\uDE02'));
expect(o.outerHTML).toEqual('<div>&#128514;</div>');
});
}); });
}); });

View File

@ -62,6 +62,14 @@ function combineAttrs(activeCount, active) {
return attrs; 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) { export default function parseMarkdown(text) {
if(!text) { if(!text) {
return []; return [];
@ -70,13 +78,14 @@ export default function parseMarkdown(text) {
const active = STYLES.map(() => false); const active = STYLES.map(() => false);
let activeCount = 0; let activeCount = 0;
let attrs = null; let attrs = null;
const lines = text.split('\n'); const lines = trimCollapsible(text).split('\n');
const result = []; const result = [];
lines.forEach((line) => { lines.forEach((line) => {
const ln = shrinkWhitespace(trimCollapsible(line));
const parts = []; const parts = [];
let p = 0; let p = 0;
for(;;) { for(;;) {
const {styleIndex, start, end} = findNext(line, p, active); const {styleIndex, start, end} = findNext(ln, p, active);
if(styleIndex === -1) { if(styleIndex === -1) {
break; break;
} }
@ -88,13 +97,13 @@ export default function parseMarkdown(text) {
++ activeCount; ++ activeCount;
} }
if(start > p) { if(start > p) {
parts.push({attrs, text: line.substring(p, start)}); parts.push({attrs, text: ln.substring(p, start)});
} }
attrs = combineAttrs(activeCount, active); attrs = combineAttrs(activeCount, active);
p = end; p = end;
} }
if(p < line.length) { if(p < ln.length) {
parts.push({attrs, text: line.substr(p)}); parts.push({attrs, text: ln.substr(p)});
} }
result.push(parts); result.push(parts);
}); });

View File

@ -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', () => { it('recognises bold styling', () => {
const formatted = parser('a **b** c __d__ e'); const formatted = parser('a **b** c __d__ e');

View File

@ -6,7 +6,7 @@ import Handlee from '../../fonts/HandleeFontData.mjs';
import Random from '../../core/Random.mjs'; import Random from '../../core/Random.mjs';
const FONT = Handlee.name; const FONT = Handlee.name;
const FONT_FAMILY = '\'' + FONT + '\',cursive'; const FONT_FAMILY = FONT + ',cursive';
const LINE_HEIGHT = 1.5; const LINE_HEIGHT = 1.5;
const MAX_CHAOS = 5; const MAX_CHAOS = 5;
@ -374,7 +374,7 @@ export default class SketchTheme extends BaseTheme {
// Font must be embedded for exporting as SVG / PNG // Font must be embedded for exporting as SVG / PNG
style.text( style.text(
'@font-face{' + '@font-face{' +
'font-family:"' + Handlee.name + '";' + 'font-family:' + Handlee.name + ';' +
'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' + 'src:url("data:font/woff2;base64,' + Handlee.woff2 + '");' +
'}' '}'
); );

View File

@ -66,6 +66,16 @@ describe('SVGTextBlock', () => {
expect(hold.childNodes[1].innerHTML).toEqual('bar'); 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', () => { 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;