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;
|
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:;
|
||||||
|
|
|
@ -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:;
|
||||||
|
|
|
@ -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
|
@ -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 {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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:;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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&a"r"></div>');
|
expect(o.outerHTML).toEqual('<div foo="b&a"\'r"></div>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('includes all children', () => {
|
it('includes all children', () => {
|
||||||
|
@ -258,5 +258,12 @@ describe('VirtualDocument', () => {
|
||||||
|
|
||||||
expect(o.outerHTML).toEqual('<div>a<b>c</div>');
|
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;
|
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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
@ -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 + '");' +
|
||||||
'}'
|
'}'
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue