securemark 0.296.0 → 0.296.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -1
- package/dist/index.js +63 -83
- package/package.json +1 -1
- package/src/combinator/control/constraint/block.ts +2 -2
- package/src/combinator/control/constraint/line.ts +5 -5
- package/src/combinator/control/manipulation/fence.ts +4 -4
- package/src/parser/api/normalize.ts +5 -1
- package/src/parser/block/reply/cite.ts +1 -1
- package/src/parser/block/reply/quote.ts +1 -1
- package/src/parser/block/reply.ts +1 -1
- package/src/parser/inline/html.ts +3 -3
- package/src/parser/inline/htmlentity.ts +2 -2
- package/src/parser/inline/link.test.ts +0 -1
- package/src/parser/inline/media.test.ts +0 -1
- package/src/parser/inline/media.ts +1 -1
- package/src/parser/inline/ruby.test.ts +5 -0
- package/src/parser/inline/ruby.ts +2 -2
- package/src/parser/node.ts +4 -4
- package/src/parser/source/escapable.ts +2 -1
- package/src/parser/source/text.ts +1 -1
- package/src/parser/source/unescapable.ts +2 -1
- package/src/parser/visibility.ts +32 -65
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.296.1
|
|
4
|
+
|
|
5
|
+
- Fix parsers to disallow leading blank characters.
|
|
6
|
+
|
|
3
7
|
## 0.296.0
|
|
4
8
|
|
|
5
|
-
- Change link, media, annotation, and reference parsers to disallow invisible
|
|
9
|
+
- Change link, media, annotation, and reference parsers to disallow leading invisible characters.
|
|
6
10
|
|
|
7
11
|
## 0.295.9
|
|
8
12
|
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! securemark v0.296.
|
|
1
|
+
/*! securemark v0.296.1 https://github.com/falsandtru/securemark | (c) 2017, falsandtru | UNLICENSED License */
|
|
2
2
|
(function webpackUniversalModuleDefinition(root, factory) {
|
|
3
3
|
if(typeof exports === 'object' && typeof module === 'object')
|
|
4
4
|
module.exports = factory(require("Prism"), require("DOMPurify"));
|
|
@@ -2585,7 +2585,7 @@ function block(parser, separation = true) {
|
|
|
2585
2585
|
if (position === source.length) return;
|
|
2586
2586
|
const result = parser(input);
|
|
2587
2587
|
if (result === undefined) return;
|
|
2588
|
-
if (separation && !(0, line_1.
|
|
2588
|
+
if (separation && !(0, line_1.isEmptyline)(source, context.position)) return;
|
|
2589
2589
|
return context.position === source.length || source[context.position - 1] === '\n' ? result : undefined;
|
|
2590
2590
|
});
|
|
2591
2591
|
}
|
|
@@ -2629,7 +2629,7 @@ exports.verify = verify;
|
|
|
2629
2629
|
Object.defineProperty(exports, "__esModule", ({
|
|
2630
2630
|
value: true
|
|
2631
2631
|
}));
|
|
2632
|
-
exports.
|
|
2632
|
+
exports.isEmptyline = exports.firstline = exports.line = void 0;
|
|
2633
2633
|
const parser_1 = __webpack_require__(605);
|
|
2634
2634
|
function line(parser) {
|
|
2635
2635
|
return (0, parser_1.failsafe)(({
|
|
@@ -2648,7 +2648,7 @@ function line(parser) {
|
|
|
2648
2648
|
context.source = source;
|
|
2649
2649
|
context.offset -= position;
|
|
2650
2650
|
if (result === undefined) return;
|
|
2651
|
-
if (context.position < position + line.length && !
|
|
2651
|
+
if (context.position < position + line.length && !isEmptyline(source, context.position)) return;
|
|
2652
2652
|
context.position = position + line.length;
|
|
2653
2653
|
return result;
|
|
2654
2654
|
});
|
|
@@ -2659,12 +2659,12 @@ function firstline(source, position) {
|
|
|
2659
2659
|
return i === -1 ? source.slice(position) : source.slice(position, i + 1);
|
|
2660
2660
|
}
|
|
2661
2661
|
exports.firstline = firstline;
|
|
2662
|
-
const
|
|
2663
|
-
function
|
|
2664
|
-
|
|
2665
|
-
return source.length === position || source[position] === '\n' ||
|
|
2662
|
+
const emptyline = /[^\S\n]*(?:$|\n)/y;
|
|
2663
|
+
function isEmptyline(source, position) {
|
|
2664
|
+
emptyline.lastIndex = position;
|
|
2665
|
+
return source.length === position || source[position] === '\n' || emptyline.test(source);
|
|
2666
2666
|
}
|
|
2667
|
-
exports.
|
|
2667
|
+
exports.isEmptyline = isEmptyline;
|
|
2668
2668
|
|
|
2669
2669
|
/***/ },
|
|
2670
2670
|
|
|
@@ -2796,20 +2796,20 @@ function fence(opener, limit, separation = true) {
|
|
|
2796
2796
|
context.position += matches[0].length;
|
|
2797
2797
|
// Prevent annoying parsing in editing.
|
|
2798
2798
|
const secondline = (0, line_1.firstline)(source, context.position);
|
|
2799
|
-
if ((0, line_1.
|
|
2799
|
+
if ((0, line_1.isEmptyline)(secondline, 0) && (0, line_1.firstline)(source, context.position + secondline.length).trimEnd() !== delim) return;
|
|
2800
2800
|
let block = '';
|
|
2801
2801
|
let closer = '';
|
|
2802
2802
|
let overflow = '';
|
|
2803
2803
|
for (let count = 1;; ++count) {
|
|
2804
2804
|
if (context.position === source.length) break;
|
|
2805
2805
|
const line = (0, line_1.firstline)(source, context.position);
|
|
2806
|
-
if ((closer || count > limit + 1) && (0, line_1.
|
|
2806
|
+
if ((closer || count > limit + 1) && (0, line_1.isEmptyline)(line, 0)) break;
|
|
2807
2807
|
if (closer) {
|
|
2808
2808
|
overflow += line;
|
|
2809
2809
|
}
|
|
2810
2810
|
if (!closer && count <= limit + 1 && line.slice(0, delim.length) === delim && line.trimEnd() === delim) {
|
|
2811
2811
|
closer = line;
|
|
2812
|
-
if ((0, line_1.
|
|
2812
|
+
if ((0, line_1.isEmptyline)(source, context.position + line.length)) {
|
|
2813
2813
|
context.position += line.length;
|
|
2814
2814
|
break;
|
|
2815
2815
|
}
|
|
@@ -4386,7 +4386,7 @@ function parse(source) {
|
|
|
4386
4386
|
Object.defineProperty(exports, "__esModule", ({
|
|
4387
4387
|
value: true
|
|
4388
4388
|
}));
|
|
4389
|
-
exports.escape = exports.
|
|
4389
|
+
exports.escape = exports.invisibleGraphHTMLEntityNames = exports.invisibleBlankHTMLEntityNames = exports.normalize = void 0;
|
|
4390
4390
|
const dom_1 = __webpack_require__(394);
|
|
4391
4391
|
const UNICODE_REPLACEMENT_CHARACTER = '\uFFFD';
|
|
4392
4392
|
function normalize(source) {
|
|
@@ -4406,13 +4406,15 @@ function sanitize(source) {
|
|
|
4406
4406
|
}
|
|
4407
4407
|
// https://dev.w3.org/html5/html-author/charref
|
|
4408
4408
|
// https://en.wikipedia.org/wiki/Whitespace_character
|
|
4409
|
-
|
|
4409
|
+
const invisibleHTMLEntityNames = ['Tab', 'NewLine', 'NonBreakingSpace', 'nbsp', 'shy', 'ensp', 'emsp', 'emsp13', 'emsp14', 'numsp', 'puncsp', 'ThinSpace', 'thinsp', 'VeryThinSpace', 'hairsp', 'ZeroWidthSpace', 'NegativeVeryThinSpace', 'NegativeThinSpace', 'NegativeMediumSpace', 'NegativeThickSpace', 'zwj', 'zwnj', 'lrm', 'rlm', 'MediumSpace', 'NoBreak', 'ApplyFunction', 'af', 'InvisibleTimes', 'it', 'InvisibleComma', 'ic'];
|
|
4410
4410
|
const parser = (el => entity => {
|
|
4411
4411
|
if (entity === '
') return entity;
|
|
4412
4412
|
el.innerHTML = entity;
|
|
4413
4413
|
return el.textContent;
|
|
4414
4414
|
})((0, dom_1.html)('span'));
|
|
4415
|
-
|
|
4415
|
+
exports.invisibleBlankHTMLEntityNames = invisibleHTMLEntityNames.filter(name => parser(`&${name};`).trimStart() === '');
|
|
4416
|
+
exports.invisibleGraphHTMLEntityNames = invisibleHTMLEntityNames.filter(name => parser(`&${name};`).trimStart() !== '');
|
|
4417
|
+
const unreadableEscapeHTMLEntityNames = invisibleHTMLEntityNames.filter(name => !['Tab', 'NewLine', 'NonBreakingSpace', 'nbsp', 'zwj', 'zwnj'].includes(name));
|
|
4416
4418
|
const unreadableEscapeCharacters = unreadableEscapeHTMLEntityNames.map(name => parser(`&${name};`));
|
|
4417
4419
|
const unreadableEscapeCharacter = new RegExp(`[${unreadableEscapeCharacters.join('')}]`, 'g');
|
|
4418
4420
|
// https://www.pandanoir.info/entry/2018/03/11/193000
|
|
@@ -5759,7 +5761,7 @@ const delimiter = new RegExp(`${cite_1.syntax.source}|${quote_1.syntax.source}`,
|
|
|
5759
5761
|
exports.reply = (0, combinator_1.block)((0, combinator_1.validate)(cite_1.syntax, (0, combinator_1.fmap)((0, combinator_1.some)((0, combinator_1.union)([cite_1.cite, quote_1.quote, (0, combinator_1.rewrite)((0, combinator_1.some)(source_1.anyline, delimiter), (0, visibility_1.visualize)((0, combinator_1.fmap)((0, combinator_1.some)(inline_1.inline), (ns, {
|
|
5760
5762
|
source,
|
|
5761
5763
|
position
|
|
5762
|
-
}) => source[position - 1] === '\n' ? ns : ns.push(new parser_1.Node((0, dom_1.html)('br'), 1 /* Flag.
|
|
5764
|
+
}) => source[position - 1] === '\n' ? ns : ns.push(new parser_1.Node((0, dom_1.html)('br'), 1 /* Flag.blank */)) && ns)))])), ns => new parser_1.List([new parser_1.Node((0, dom_1.html)('p', (0, dom_1.defrag)((0, util_1.unwrap)((0, visibility_1.trimBlankNodeEnd)(ns)))))]))));
|
|
5763
5765
|
|
|
5764
5766
|
/***/ },
|
|
5765
5767
|
|
|
@@ -5811,7 +5813,7 @@ exports.cite = (0, combinator_1.line)((0, combinator_1.fmap)((0, combinator_1.op
|
|
|
5811
5813
|
...(0, util_1.invalid)('cite', 'syntax', 'Invalid syntax')
|
|
5812
5814
|
}, (0, dom_1.defrag)([`${quotes}>`, typeof node === 'object' ? (0, dom_1.define)(node, {
|
|
5813
5815
|
'data-depth': `${quotes.length + 1}`
|
|
5814
|
-
}, node.innerText.slice(1)) : node.slice(1)]))), new parser_1.Node((0, dom_1.html)('br'), 1 /* Flag.
|
|
5816
|
+
}, node.innerText.slice(1)) : node.slice(1)]))), new parser_1.Node((0, dom_1.html)('br'), 1 /* Flag.blank */)]);
|
|
5815
5817
|
}));
|
|
5816
5818
|
|
|
5817
5819
|
/***/ },
|
|
@@ -5839,7 +5841,7 @@ exports.quote = (0, combinator_1.lazy)(() => (0, combinator_1.block)((0, combina
|
|
|
5839
5841
|
math_1.math, autolink_1.autolink, source_1.unescsource])))), (ns, {
|
|
5840
5842
|
source,
|
|
5841
5843
|
position
|
|
5842
|
-
}) => new parser_1.List([new parser_1.Node(source[position - 1] === '\n' ? ns.pop().value : (0, dom_1.html)('br'), 1 /* Flag.
|
|
5844
|
+
}) => new parser_1.List([new parser_1.Node(source[position - 1] === '\n' ? ns.pop().value : (0, dom_1.html)('br'), 1 /* Flag.blank */), new parser_1.Node((0, dom_1.html)('span', {
|
|
5843
5845
|
class: 'quote'
|
|
5844
5846
|
}, (0, dom_1.defrag)((0, util_1.unwrap)(ns))))].reverse())), false));
|
|
5845
5847
|
|
|
@@ -7189,7 +7191,7 @@ Object.setPrototypeOf(attrspecs, null);
|
|
|
7189
7191
|
Object.values(attrspecs).forEach(o => Object.setPrototypeOf(o, null));
|
|
7190
7192
|
exports.html = (0, combinator_1.lazy)(() => (0, combinator_1.validate)(/<[a-z]+(?=[ >])/yi, (0, combinator_1.union)([(0, combinator_1.surround)(
|
|
7191
7193
|
// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
|
|
7192
|
-
(0, source_1.str)(/<(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)(?=[ >])/y), (0, combinator_1.precedence)(9, (0, combinator_1.some)((0, combinator_1.union)([exports.attribute]))), (0, combinator_1.open)((0, source_1.str)(/ ?/y), (0, source_1.str)('>'), true), true, [], ([as, bs = new parser_1.List(), cs], context) => new parser_1.List([new parser_1.Node(elem(as.head.value.slice(1), false, [...(0, util_1.unwrap)(as.import(bs).import(cs))], new parser_1.List(), new parser_1.List(), context), as.head.value === '<wbr' ? 1 /* Flag.
|
|
7194
|
+
(0, source_1.str)(/<(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)(?=[ >])/y), (0, combinator_1.precedence)(9, (0, combinator_1.some)((0, combinator_1.union)([exports.attribute]))), (0, combinator_1.open)((0, source_1.str)(/ ?/y), (0, source_1.str)('>'), true), true, [], ([as, bs = new parser_1.List(), cs], context) => new parser_1.List([new parser_1.Node(elem(as.head.value.slice(1), false, [...(0, util_1.unwrap)(as.import(bs).import(cs))], new parser_1.List(), new parser_1.List(), context), as.head.value === '<wbr' ? 1 /* Flag.blank */ : 0 /* Flag.none */)]), ([as, bs = new parser_1.List()], context) => new parser_1.List([new parser_1.Node(elem(as.head.value.slice(1), false, [...(0, util_1.unwrap)(as.import(bs))], new parser_1.List(), new parser_1.List(), context))])), (0, combinator_1.match)(new RegExp(String.raw`<(${TAGS.join('|')})(?=[ >])`, 'y'), (0, memoize_1.memoize)(([, tag]) => (0, combinator_1.surround)((0, combinator_1.surround)((0, source_1.str)(`<${tag}`), (0, combinator_1.precedence)(9, (0, combinator_1.some)(exports.attribute)), (0, combinator_1.open)((0, source_1.str)(/ ?/y), (0, source_1.str)('>'), true), true, [], ([as, bs = new parser_1.List(), cs]) => as.import(bs).import(cs), ([as, bs = new parser_1.List()]) => as.import(bs)),
|
|
7193
7195
|
// 不可視のHTML構造が可視構造を変化させるべきでない。
|
|
7194
7196
|
// 可視のHTMLは優先度変更を検討する。
|
|
7195
7197
|
// このため`<>`記号は将来的に共通構造を変化させる可能性があり
|
|
@@ -7203,7 +7205,7 @@ function elem(tag, content, as, bs, cs, context) {
|
|
|
7203
7205
|
if (content) {
|
|
7204
7206
|
if (cs.length === 0) return ielem('tag', `Missing the closing HTML tag "</${tag}>"`, context);
|
|
7205
7207
|
if (bs.length === 0) return ielem('content', `Missing the content`, context);
|
|
7206
|
-
if (!(0, visibility_1.
|
|
7208
|
+
if (!(0, visibility_1.isNonblankFirstLine)(bs)) return ielem('content', `Missing the visible content in the same line`, context);
|
|
7207
7209
|
}
|
|
7208
7210
|
const [attrs] = attributes('html', attrspecs[tag], as.slice(1, as.at(-1) === '>' ? -1 : as.length));
|
|
7209
7211
|
if (/(?<!\S)invalid(?!\S)/.test(attrs['class'] ?? '')) return ielem('attribute', 'Invalid HTML attribute', context);
|
|
@@ -7268,7 +7270,7 @@ const combinator_1 = __webpack_require__(3484);
|
|
|
7268
7270
|
const source_1 = __webpack_require__(8745);
|
|
7269
7271
|
const util_1 = __webpack_require__(4992);
|
|
7270
7272
|
const dom_1 = __webpack_require__(394);
|
|
7271
|
-
exports.unsafehtmlentity = (0, combinator_1.surround)((0, source_1.str)('&'), (0, source_1.str)(/[0-9A-Za-z]+/y), (0, source_1.str)(';'), false, [3 | 8 /* Backtrack.unescapable */], ([as, bs, cs]) => new parser_1.List([new parser_1.Node(parser(as.head.value + bs.head.value + cs.head.value), (0, node_1.
|
|
7273
|
+
exports.unsafehtmlentity = (0, combinator_1.surround)((0, source_1.str)('&'), (0, source_1.str)(/[0-9A-Za-z]+/y), (0, source_1.str)(';'), false, [3 | 8 /* Backtrack.unescapable */], ([as, bs, cs]) => new parser_1.List([new parser_1.Node(parser(as.head.value + bs.head.value + cs.head.value), (0, node_1.isBlankHTMLEntityName)(bs.head.value) ? 1 /* Flag.blank */ : 0 /* Flag.none */)]), ([as, bs]) => new parser_1.List([new parser_1.Node(as.head.value + (bs?.head?.value ?? ''))]));
|
|
7272
7274
|
exports.htmlentity = (0, combinator_1.fmap)((0, combinator_1.union)([exports.unsafehtmlentity]), ([{
|
|
7273
7275
|
value,
|
|
7274
7276
|
flags
|
|
@@ -7606,7 +7608,7 @@ exports.media = (0, combinator_1.lazy)(() => (0, combinator_1.constraint)(4 /* S
|
|
|
7606
7608
|
}, {
|
|
7607
7609
|
value: params
|
|
7608
7610
|
}], context) => {
|
|
7609
|
-
if (flags & 1 /* Flag.
|
|
7611
|
+
if (flags & 1 /* Flag.blank */) return;
|
|
7610
7612
|
if (text) {
|
|
7611
7613
|
const tmp = text;
|
|
7612
7614
|
text = text.trim();
|
|
@@ -7825,7 +7827,7 @@ const util_1 = __webpack_require__(4992);
|
|
|
7825
7827
|
const dom_1 = __webpack_require__(394);
|
|
7826
7828
|
exports.ruby = (0, combinator_1.lazy)(() => (0, combinator_1.bind)((0, combinator_1.inits)([(0, combinator_1.dup)((0, combinator_1.surround)('[', text, ']', false, [1 | 4 /* Backtrack.common */, 3 | 32 /* Backtrack.ruby */], ([, ns]) => {
|
|
7827
7829
|
ns && ns.last?.value === '' && ns.pop();
|
|
7828
|
-
return (0, visibility_1.
|
|
7830
|
+
return (0, visibility_1.isNonblankNodeStart)(ns) ? ns : undefined;
|
|
7829
7831
|
})), (0, combinator_1.dup)((0, combinator_1.surround)('(', text, ')', false, [3 | 32 /* Backtrack.ruby */]))]), ([{
|
|
7830
7832
|
value: texts
|
|
7831
7833
|
}, {
|
|
@@ -7984,9 +7986,9 @@ const bracket = (0, combinator_1.lazy)(() => (0, combinator_1.union)([(0, combin
|
|
|
7984
7986
|
Object.defineProperty(exports, "__esModule", ({
|
|
7985
7987
|
value: true
|
|
7986
7988
|
}));
|
|
7987
|
-
exports.
|
|
7989
|
+
exports.isBlankHTMLEntityName = void 0;
|
|
7988
7990
|
const normalize_1 = __webpack_require__(4490);
|
|
7989
|
-
exports.
|
|
7991
|
+
exports.isBlankHTMLEntityName = eval(['name => {', 'switch(name){', normalize_1.invisibleBlankHTMLEntityNames.map(name => `case '${name}':`).join(''), 'return true;', 'default:', 'return false;', '}', '}'].join(''));
|
|
7990
7992
|
|
|
7991
7993
|
/***/ },
|
|
7992
7994
|
|
|
@@ -8491,7 +8493,7 @@ const escsource = ({
|
|
|
8491
8493
|
return new parser_1.List();
|
|
8492
8494
|
case '\n':
|
|
8493
8495
|
context.linebreak ||= source.length - position;
|
|
8494
|
-
return new parser_1.List([new parser_1.Node((0, dom_1.html)('br'))]);
|
|
8496
|
+
return new parser_1.List([new parser_1.Node((0, dom_1.html)('br'), 1 /* Flag.blank */)]);
|
|
8495
8497
|
default:
|
|
8496
8498
|
if (context.sequential) return new parser_1.List([new parser_1.Node(char)]);
|
|
8497
8499
|
let i = (0, text_1.next)(source, position, state, delimiter);
|
|
@@ -8650,7 +8652,7 @@ const text = input => {
|
|
|
8650
8652
|
return new parser_1.List();
|
|
8651
8653
|
case '\n':
|
|
8652
8654
|
context.linebreak ||= source.length - position;
|
|
8653
|
-
return new parser_1.List([new parser_1.Node((0, dom_1.html)('br'), 1 /* Flag.
|
|
8655
|
+
return new parser_1.List([new parser_1.Node((0, dom_1.html)('br'), 1 /* Flag.blank */)]);
|
|
8654
8656
|
default:
|
|
8655
8657
|
if (context.sequential) return new parser_1.List([new parser_1.Node(char)]);
|
|
8656
8658
|
exports.nonWhitespace.lastIndex = position + 1;
|
|
@@ -8880,7 +8882,7 @@ const unescsource = ({
|
|
|
8880
8882
|
return new parser_1.List();
|
|
8881
8883
|
case '\n':
|
|
8882
8884
|
context.linebreak ||= source.length - position;
|
|
8883
|
-
return new parser_1.List([new parser_1.Node((0, dom_1.html)('br'))]);
|
|
8885
|
+
return new parser_1.List([new parser_1.Node((0, dom_1.html)('br'), 1 /* Flag.blank */)]);
|
|
8884
8886
|
default:
|
|
8885
8887
|
if (context.sequential) return new parser_1.List([new parser_1.Node(char)]);
|
|
8886
8888
|
text_1.nonWhitespace.lastIndex = position + 1;
|
|
@@ -9036,18 +9038,18 @@ exports.stringify = stringify;
|
|
|
9036
9038
|
Object.defineProperty(exports, "__esModule", ({
|
|
9037
9039
|
value: true
|
|
9038
9040
|
}));
|
|
9039
|
-
exports.trimBlankNodeEnd = exports.trimBlankEnd = exports.trimBlank = exports.
|
|
9041
|
+
exports.trimBlankNodeEnd = exports.trimBlankEnd = exports.trimBlank = exports.isNonblankNodeStart = exports.isNonblankFirstLine = exports.beforeNonblank = exports.blankWith = exports.afterNonblank = exports.visualize = void 0;
|
|
9040
9042
|
const parser_1 = __webpack_require__(605);
|
|
9041
9043
|
const combinator_1 = __webpack_require__(3484);
|
|
9042
9044
|
const normalize_1 = __webpack_require__(4490);
|
|
9043
|
-
var
|
|
9044
|
-
(function (
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
})(
|
|
9045
|
+
var blank;
|
|
9046
|
+
(function (blank) {
|
|
9047
|
+
blank.line = new RegExp(/((?:^|\n)[^\S\n]*(?=\S))((?:[^\S\n]|\\(?=$|\s)|&IBHN;|<wbr ?>)+(?=$|\n))/g.source.replace('IBHN', `(?:${normalize_1.invisibleBlankHTMLEntityNames.join('|')})`), 'g');
|
|
9048
|
+
blank.start = new RegExp(/(?:[^\S\n]|\\(?=$|\s)|&IBHN;|<wbr ?>)+/y.source.replace('IBHN', `(?:${normalize_1.invisibleBlankHTMLEntityNames.join('|')})`), 'y');
|
|
9049
|
+
blank.unit = new RegExp(/(?:[^\S\n]|\\(?=$|\s)|&IBHN;|<wbr ?>)/y.source.replace('IBHN', `(?:${normalize_1.invisibleBlankHTMLEntityNames.join('|')})`), 'y');
|
|
9050
|
+
})(blank || (blank = {}));
|
|
9049
9051
|
function visualize(parser) {
|
|
9050
|
-
return (0, combinator_1.convert)(source => source.replace(
|
|
9052
|
+
return (0, combinator_1.convert)(source => source.replace(blank.line, `$1${"\u001B" /* Command.Escape */}$2`), parser);
|
|
9051
9053
|
}
|
|
9052
9054
|
exports.visualize = visualize;
|
|
9053
9055
|
exports.afterNonblank = nonblankWith('');
|
|
@@ -9055,17 +9057,17 @@ function blankWith(starts, delimiter) {
|
|
|
9055
9057
|
return new RegExp([
|
|
9056
9058
|
// 空行除去
|
|
9057
9059
|
// 完全な空行はエスケープ済みなので再帰的バックトラックにはならない。
|
|
9058
|
-
String.raw`(?:${starts}(?:\\?\s|&(?:${normalize_1.
|
|
9060
|
+
String.raw`(?:${starts}(?:\\?\s|&(?:${normalize_1.invisibleBlankHTMLEntityNames.join('|')});|<wbr ?>)*)?`, typeof delimiter === 'string' ? delimiter.replace(/[*+()\[\]]/g, '\\$&') : delimiter.source].join(''), 'y');
|
|
9059
9061
|
}
|
|
9060
9062
|
exports.blankWith = blankWith;
|
|
9061
9063
|
function nonblankWith(delimiter) {
|
|
9062
|
-
return new RegExp([String.raw`(?<!\s|&(?:${normalize_1.
|
|
9064
|
+
return new RegExp([String.raw`(?<!\s|&(?:${normalize_1.invisibleBlankHTMLEntityNames.join('|')});|<wbr ?>)`, typeof delimiter === 'string' ? delimiter.replace(/[*+()\[\]]/g, '\\$&') : delimiter.source].join(''), 'y');
|
|
9063
9065
|
}
|
|
9064
9066
|
function beforeNonblank(parser) {
|
|
9065
|
-
return input =>
|
|
9067
|
+
return input => isNonblankStart(input) ? parser(input) : undefined;
|
|
9066
9068
|
}
|
|
9067
9069
|
exports.beforeNonblank = beforeNonblank;
|
|
9068
|
-
function
|
|
9070
|
+
function isNonblankStart(input) {
|
|
9069
9071
|
const {
|
|
9070
9072
|
context
|
|
9071
9073
|
} = input;
|
|
@@ -9081,34 +9083,30 @@ function isTightStart(input) {
|
|
|
9081
9083
|
case '\n':
|
|
9082
9084
|
return false;
|
|
9083
9085
|
default:
|
|
9084
|
-
const reg =
|
|
9086
|
+
const reg = blank.unit;
|
|
9085
9087
|
reg.lastIndex = position;
|
|
9086
9088
|
return !reg.test(source);
|
|
9087
9089
|
}
|
|
9088
9090
|
}
|
|
9089
|
-
function
|
|
9091
|
+
function isNonblankFirstLine(nodes) {
|
|
9090
9092
|
if (nodes.length === 0) return true;
|
|
9091
9093
|
for (const node of nodes) {
|
|
9092
|
-
if (
|
|
9093
|
-
if (typeof node.value === 'object' && node.value.tagName === 'BR') break;
|
|
9094
|
+
if (isNonblank(node)) return true;
|
|
9095
|
+
if (node.flags & 1 /* Flag.blank */ && typeof node.value === 'object' && node.value.tagName === 'BR') break;
|
|
9094
9096
|
}
|
|
9095
9097
|
return false;
|
|
9096
9098
|
}
|
|
9097
|
-
exports.
|
|
9098
|
-
function
|
|
9099
|
+
exports.isNonblankFirstLine = isNonblankFirstLine;
|
|
9100
|
+
function isNonblankNodeStart(nodes) {
|
|
9099
9101
|
if (nodes.length === 0) return true;
|
|
9100
|
-
return
|
|
9101
|
-
}
|
|
9102
|
-
exports.
|
|
9103
|
-
|
|
9104
|
-
// if (nodes.length === 0) return true;
|
|
9105
|
-
// return isVisible(nodes.at(-1)!, -1);
|
|
9106
|
-
//}
|
|
9107
|
-
function isVisible({
|
|
9102
|
+
return isNonblank(nodes.head, 0);
|
|
9103
|
+
}
|
|
9104
|
+
exports.isNonblankNodeStart = isNonblankNodeStart;
|
|
9105
|
+
function isNonblank({
|
|
9108
9106
|
value: node,
|
|
9109
9107
|
flags
|
|
9110
9108
|
}, strpos) {
|
|
9111
|
-
if (flags & 1 /* Flag.
|
|
9109
|
+
if (flags & 1 /* Flag.blank */) return false;
|
|
9112
9110
|
if (typeof node !== 'string') return true;
|
|
9113
9111
|
const str = node && strpos !== undefined ? node[strpos >= 0 ? strpos : node.length + strpos] : node;
|
|
9114
9112
|
switch (str) {
|
|
@@ -9135,7 +9133,7 @@ function trimBlankStart(parser) {
|
|
|
9135
9133
|
position
|
|
9136
9134
|
} = context;
|
|
9137
9135
|
if (position === source.length) return;
|
|
9138
|
-
const reg =
|
|
9136
|
+
const reg = blank.start;
|
|
9139
9137
|
reg.lastIndex = position;
|
|
9140
9138
|
reg.test(source);
|
|
9141
9139
|
context.position = reg.lastIndex || position;
|
|
@@ -9146,37 +9144,19 @@ function trimBlankEnd(parser) {
|
|
|
9146
9144
|
return (0, combinator_1.fmap)(parser, trimBlankNodeEnd);
|
|
9147
9145
|
}
|
|
9148
9146
|
exports.trimBlankEnd = trimBlankEnd;
|
|
9149
|
-
//export function trimBlankNode<N extends HTMLElement | string>(nodes: N[]): N[] {
|
|
9150
|
-
// return trimBlankNodeStart(trimBlankNodeEnd(nodes));
|
|
9151
|
-
//}
|
|
9152
|
-
//function trimBlankNodeStart<N extends HTMLElement | string>(nodes: N[]): N[] {
|
|
9153
|
-
// for (let node = nodes[0]; nodes.length > 0 && !isVisible(node = nodes[0], 0);) {
|
|
9154
|
-
// if (typeof node === 'string') {
|
|
9155
|
-
// const pos = node.trimStart().length;
|
|
9156
|
-
// if (pos > 0) {
|
|
9157
|
-
// nodes[0] = node.slice(-pos) as N;
|
|
9158
|
-
// break;
|
|
9159
|
-
// }
|
|
9160
|
-
// }
|
|
9161
|
-
// else if (nodes.length === 1 && node.className === 'indexer') {
|
|
9162
|
-
// break;
|
|
9163
|
-
// }
|
|
9164
|
-
// nodes.shift();
|
|
9165
|
-
// }
|
|
9166
|
-
// return nodes;
|
|
9167
|
-
//}
|
|
9168
9147
|
function trimBlankNodeEnd(nodes) {
|
|
9169
|
-
const skip = nodes.last && ~nodes.last.flags & 1 /* Flag.
|
|
9148
|
+
const skip = nodes.last && ~nodes.last.flags & 1 /* Flag.blank */ && typeof nodes.last.value === 'object' ? nodes.last.value.className === 'indexer' : false;
|
|
9170
9149
|
for (let node = skip ? nodes.last?.prev : nodes.last; node;) {
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9150
|
+
if (~node.flags & 1 /* Flag.blank */) {
|
|
9151
|
+
if (typeof node.value === 'string') {
|
|
9152
|
+
const str = node.value.trimEnd();
|
|
9153
|
+
if (str.length > 0) {
|
|
9154
|
+
node.value = str;
|
|
9155
|
+
break;
|
|
9156
|
+
}
|
|
9157
|
+
} else {
|
|
9176
9158
|
break;
|
|
9177
9159
|
}
|
|
9178
|
-
} else if (visible) {
|
|
9179
|
-
break;
|
|
9180
9160
|
}
|
|
9181
9161
|
const target = node;
|
|
9182
9162
|
node = node.prev;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Parser, failsafe } from '../../data/parser';
|
|
2
|
-
import {
|
|
2
|
+
import { isEmptyline } from './line';
|
|
3
3
|
|
|
4
4
|
export function block<P extends Parser>(parser: P, separation?: boolean): P;
|
|
5
5
|
export function block<N>(parser: Parser<N>, separation = true): Parser<N> {
|
|
@@ -10,7 +10,7 @@ export function block<N>(parser: Parser<N>, separation = true): Parser<N> {
|
|
|
10
10
|
if (position === source.length) return;
|
|
11
11
|
const result = parser(input);
|
|
12
12
|
if (result === undefined) return;
|
|
13
|
-
if (separation && !
|
|
13
|
+
if (separation && !isEmptyline(source, context.position)) return;
|
|
14
14
|
assert(context.position === source.length || source[context.position - 1] === '\n');
|
|
15
15
|
return context.position === source.length || source[context.position - 1] === '\n'
|
|
16
16
|
? result
|
|
@@ -14,7 +14,7 @@ export function line<N>(parser: Parser<N>): Parser<N> {
|
|
|
14
14
|
context.source = source;
|
|
15
15
|
context.offset -= position;
|
|
16
16
|
if (result === undefined) return;
|
|
17
|
-
if (context.position < position + line.length && !
|
|
17
|
+
if (context.position < position + line.length && !isEmptyline(source, context.position)) return;
|
|
18
18
|
context.position = position + line.length;
|
|
19
19
|
return result;
|
|
20
20
|
});
|
|
@@ -27,10 +27,10 @@ export function firstline(source: string, position: number): string {
|
|
|
27
27
|
: source.slice(position, i + 1);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
export function
|
|
32
|
-
|
|
30
|
+
const emptyline = /[^\S\n]*(?:$|\n)/y;
|
|
31
|
+
export function isEmptyline(source: string, position: number): boolean {
|
|
32
|
+
emptyline.lastIndex = position;
|
|
33
33
|
return source.length === position
|
|
34
34
|
|| source[position] === '\n'
|
|
35
|
-
||
|
|
35
|
+
|| emptyline.test(source);
|
|
36
36
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Parser, List, Node, Context, failsafe } from '../../data/parser';
|
|
2
2
|
import { consume } from '../../../combinator';
|
|
3
|
-
import { firstline,
|
|
3
|
+
import { firstline, isEmptyline } from '../constraint/line';
|
|
4
4
|
import { push } from 'spica/array';
|
|
5
5
|
|
|
6
6
|
export function fence<C extends Context, D extends Parser<unknown, C>[]>(opener: RegExp, limit: number, separation = true): Parser<string, C, D> {
|
|
@@ -20,20 +20,20 @@ export function fence<C extends Context, D extends Parser<unknown, C>[]>(opener:
|
|
|
20
20
|
context.position += matches[0].length;
|
|
21
21
|
// Prevent annoying parsing in editing.
|
|
22
22
|
const secondline = firstline(source, context.position);
|
|
23
|
-
if (
|
|
23
|
+
if (isEmptyline(secondline, 0) && firstline(source, context.position + secondline.length).trimEnd() !== delim) return;
|
|
24
24
|
let block = '';
|
|
25
25
|
let closer = '';
|
|
26
26
|
let overflow = '';
|
|
27
27
|
for (let count = 1; ; ++count) {
|
|
28
28
|
if (context.position === source.length) break;
|
|
29
29
|
const line = firstline(source, context.position);
|
|
30
|
-
if ((closer || count > limit + 1) &&
|
|
30
|
+
if ((closer || count > limit + 1) && isEmptyline(line, 0)) break;
|
|
31
31
|
if(closer) {
|
|
32
32
|
overflow += line;
|
|
33
33
|
}
|
|
34
34
|
if (!closer && count <= limit + 1 && line.slice(0, delim.length) === delim && line.trimEnd() === delim) {
|
|
35
35
|
closer = line;
|
|
36
|
-
if (
|
|
36
|
+
if (isEmptyline(source, context.position + line.length)) {
|
|
37
37
|
context.position += line.length;
|
|
38
38
|
break;
|
|
39
39
|
}
|
|
@@ -24,7 +24,7 @@ function sanitize(source: string): string {
|
|
|
24
24
|
|
|
25
25
|
// https://dev.w3.org/html5/html-author/charref
|
|
26
26
|
// https://en.wikipedia.org/wiki/Whitespace_character
|
|
27
|
-
|
|
27
|
+
const invisibleHTMLEntityNames = [
|
|
28
28
|
'Tab',
|
|
29
29
|
'NewLine',
|
|
30
30
|
'NonBreakingSpace',
|
|
@@ -63,6 +63,10 @@ const parser = (el => (entity: string): string => {
|
|
|
63
63
|
el.innerHTML = entity;
|
|
64
64
|
return el.textContent!;
|
|
65
65
|
})(html('span'));
|
|
66
|
+
export const invisibleBlankHTMLEntityNames: readonly string[] = invisibleHTMLEntityNames
|
|
67
|
+
.filter(name => parser(`&${name};`).trimStart() === '');
|
|
68
|
+
export const invisibleGraphHTMLEntityNames: readonly string[] = invisibleHTMLEntityNames
|
|
69
|
+
.filter(name => parser(`&${name};`).trimStart() !== '');
|
|
66
70
|
const unreadableEscapeHTMLEntityNames = invisibleHTMLEntityNames.filter(name => ![
|
|
67
71
|
'Tab',
|
|
68
72
|
'NewLine',
|
|
@@ -22,7 +22,7 @@ export const quote: ReplyParser.QuoteParser = lazy(() => block(fmap(
|
|
|
22
22
|
unescsource,
|
|
23
23
|
])))),
|
|
24
24
|
(ns, { source, position }) => new List([
|
|
25
|
-
new Node(source[position - 1] === '\n' ? ns.pop()!.value as HTMLBRElement : html('br'), Flag.
|
|
25
|
+
new Node(source[position - 1] === '\n' ? ns.pop()!.value as HTMLBRElement : html('br'), Flag.blank),
|
|
26
26
|
new Node(html('span', { class: 'quote' }, defrag(unwrap(ns)))),
|
|
27
27
|
].reverse())),
|
|
28
28
|
false));
|
|
@@ -21,6 +21,6 @@ export const reply: ReplyParser = block(validate(csyntax, fmap(
|
|
|
21
21
|
visualize(fmap(some(inline), (ns, { source, position }) =>
|
|
22
22
|
source[position - 1] === '\n'
|
|
23
23
|
? ns
|
|
24
|
-
: ns.push(new Node(html('br'), Flag.
|
|
24
|
+
: ns.push(new Node(html('br'), Flag.blank)) && ns)))
|
|
25
25
|
])),
|
|
26
26
|
ns => new List([new Node(html('p', defrag(unwrap(trimBlankNodeEnd(ns)))))]))));
|
|
@@ -5,7 +5,7 @@ import { Flag } from '../node';
|
|
|
5
5
|
import { union, some, recursion, precedence, validate, surround, open, match, lazy } from '../../combinator';
|
|
6
6
|
import { inline } from '../inline';
|
|
7
7
|
import { str } from '../source';
|
|
8
|
-
import {
|
|
8
|
+
import { isNonblankFirstLine, blankWith } from '../visibility';
|
|
9
9
|
import { invalid, unwrap } from '../util';
|
|
10
10
|
import { memoize } from 'spica/memoize';
|
|
11
11
|
import { html as h, defrag } from 'typed-dom/dom';
|
|
@@ -29,7 +29,7 @@ export const html: HTMLParser = lazy(() => validate(/<[a-z]+(?=[ >])/yi,
|
|
|
29
29
|
open(str(/ ?/y), str('>'), true),
|
|
30
30
|
true, [],
|
|
31
31
|
([as, bs = new List(), cs], context) =>
|
|
32
|
-
new List([new Node(elem(as.head!.value.slice(1), false, [...unwrap(as.import(bs).import(cs))], new List(), new List(), context), as.head!.value === '<wbr' ? Flag.
|
|
32
|
+
new List([new Node(elem(as.head!.value.slice(1), false, [...unwrap(as.import(bs).import(cs))], new List(), new List(), context), as.head!.value === '<wbr' ? Flag.blank : Flag.none)]),
|
|
33
33
|
([as, bs = new List()], context) =>
|
|
34
34
|
new List([new Node(elem(as.head!.value.slice(1), false, [...unwrap(as.import(bs))], new List(), new List(), context))])),
|
|
35
35
|
match(
|
|
@@ -85,7 +85,7 @@ function elem(tag: string, content: boolean, as: readonly string[], bs: List<Nod
|
|
|
85
85
|
if (content) {
|
|
86
86
|
if (cs.length === 0) return ielem('tag', `Missing the closing HTML tag "</${tag}>"`, context);
|
|
87
87
|
if (bs.length === 0) return ielem('content', `Missing the content`, context);
|
|
88
|
-
if (!
|
|
88
|
+
if (!isNonblankFirstLine(bs)) return ielem('content', `Missing the visible content in the same line`, context);
|
|
89
89
|
}
|
|
90
90
|
const [attrs] = attributes('html', attrspecs[tag], as.slice(1, as.at(-1) === '>' ? -1 : as.length));
|
|
91
91
|
if (/(?<!\S)invalid(?!\S)/.test(attrs['class'] ?? '')) return ielem('attribute', 'Invalid HTML attribute', context)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HTMLEntityParser, UnsafeHTMLEntityParser } from '../inline';
|
|
2
2
|
import { Backtrack } from '../context';
|
|
3
3
|
import { List, Node } from '../../combinator/data/parser';
|
|
4
|
-
import { Flag,
|
|
4
|
+
import { Flag, isBlankHTMLEntityName } from '../node';
|
|
5
5
|
import { union, surround, fmap } from '../../combinator';
|
|
6
6
|
import { str } from '../source';
|
|
7
7
|
import { invalid } from '../util';
|
|
@@ -15,7 +15,7 @@ export const unsafehtmlentity: UnsafeHTMLEntityParser = surround(
|
|
|
15
15
|
new List([
|
|
16
16
|
new Node(
|
|
17
17
|
parser(as.head!.value + bs.head!.value + cs.head!.value),
|
|
18
|
-
|
|
18
|
+
isBlankHTMLEntityName(bs.head!.value) ? Flag.blank : Flag.none)
|
|
19
19
|
]),
|
|
20
20
|
([as, bs]) =>
|
|
21
21
|
new List([new Node(as.head!.value + (bs?.head?.value ?? ''))]));
|
|
@@ -84,7 +84,6 @@ describe('Unit: parser/inline/link', () => {
|
|
|
84
84
|
assert.deepStrictEqual(inspect(parser, input('[\\ ]{b}', new Context())), undefined);
|
|
85
85
|
assert.deepStrictEqual(inspect(parser, input('[\\\n]{b}', new Context())), undefined);
|
|
86
86
|
assert.deepStrictEqual(inspect(parser, input('[	]{b}', new Context())), undefined);
|
|
87
|
-
assert.deepStrictEqual(inspect(parser, input('[‍]{b}', new Context())), undefined);
|
|
88
87
|
assert.deepStrictEqual(inspect(parser, input('[[]{b}', new Context())), undefined);
|
|
89
88
|
assert.deepStrictEqual(inspect(parser, input('[]]{b}', new Context())), undefined);
|
|
90
89
|
assert.deepStrictEqual(inspect(parser, input('[a]{}', new Context())), undefined);
|
|
@@ -49,7 +49,6 @@ describe('Unit: parser/inline/media', () => {
|
|
|
49
49
|
assert.deepStrictEqual(inspect(parser, input('![\\ ]{b}', new Context())), undefined);
|
|
50
50
|
assert.deepStrictEqual(inspect(parser, input('![\\\n]{b}', new Context())), undefined);
|
|
51
51
|
assert.deepStrictEqual(inspect(parser, input('![	]{b}', new Context())), undefined);
|
|
52
|
-
assert.deepStrictEqual(inspect(parser, input('![‍]{b}', new Context())), undefined);
|
|
53
52
|
assert.deepStrictEqual(inspect(parser, input('![&a;]{b}', new Context())), [['<a href="b" target="_blank"><img class="media" data-src="b" alt="&a;"></a>'], '']);
|
|
54
53
|
assert.deepStrictEqual(inspect(parser, input('![[]{b}', new Context())), undefined);
|
|
55
54
|
assert.deepStrictEqual(inspect(parser, input('![]]{b}', new Context())), undefined);
|
|
@@ -53,7 +53,7 @@ export const media: MediaParser = lazy(() => constraint(State.media, open(
|
|
|
53
53
|
? new List<Node<List<Node<string>>>>([new Node(new List([new Node('')])), nodes.delete(nodes.head!)])
|
|
54
54
|
: new List<Node<List<Node<string>>>>([new Node(new List([new Node(nodes.head!.value.foldl((acc, { value }) => acc + value, ''), nodes.head!.value.head?.flags)])), nodes.delete(nodes.last!)])),
|
|
55
55
|
([{ value: [{ value: text, flags }] }, { value: params }], context) => {
|
|
56
|
-
if (flags & Flag.
|
|
56
|
+
if (flags & Flag.blank) return;
|
|
57
57
|
if (text) {
|
|
58
58
|
const tmp = text;
|
|
59
59
|
text = text.trim();
|
|
@@ -46,7 +46,12 @@ describe('Unit: parser/inline/ruby', () => {
|
|
|
46
46
|
assert.deepStrictEqual(inspect(parser, input('[AB](a b c)', new Context())), [['<ruby>AB<rp>(</rp><rt>a b c</rt><rp>)</rp></ruby>'], '']);
|
|
47
47
|
assert.deepStrictEqual(inspect(parser, input('[A B](ab)', new Context())), [['<ruby>A<rp>(</rp><rt>ab</rt><rp>)</rp>B<rt></rt></ruby>'], '']);
|
|
48
48
|
assert.deepStrictEqual(inspect(parser, input('[A B](a b)', new Context())), [['<ruby>A<rp>(</rp><rt>a</rt><rp>)</rp>B<rp>(</rp><rt>b</rt><rp>)</rp></ruby>'], '']);
|
|
49
|
+
assert.deepStrictEqual(inspect(parser, input('[A B](a b )', new Context())), [['<ruby>A B<rp>(</rp><rt>a b</rt><rp>)</rp></ruby>'], '']);
|
|
50
|
+
assert.deepStrictEqual(inspect(parser, input('[ABC](a )', new Context())), [['<ruby>A<rp>(</rp><rt>a</rt><rp>)</rp>B<rt></rt>C<rt></rt></ruby>'], '']);
|
|
51
|
+
assert.deepStrictEqual(inspect(parser, input('[ABC](a )', new Context())), [['<ruby>A<rp>(</rp><rt>a</rt><rp>)</rp>B<rt></rt>C<rt></rt></ruby>'], '']);
|
|
52
|
+
assert.deepStrictEqual(inspect(parser, input('[ABC]( b)', new Context())), [['<ruby>A<rt></rt>B<rp>(</rp><rt>b</rt><rp>)</rp>C<rt></rt></ruby>'], '']);
|
|
49
53
|
assert.deepStrictEqual(inspect(parser, input('[ABC]( b )', new Context())), [['<ruby>A<rt></rt>B<rp>(</rp><rt>b</rt><rp>)</rp>C<rt></rt></ruby>'], '']);
|
|
54
|
+
assert.deepStrictEqual(inspect(parser, input('[ABC]( c)', new Context())), [['<ruby>A<rt></rt>B<rt></rt>C<rp>(</rp><rt>c</rt><rp>)</rp></ruby>'], '']);
|
|
50
55
|
assert.deepStrictEqual(inspect(parser, input('[ABC](a c)', new Context())), [['<ruby>A<rp>(</rp><rt>a</rt><rp>)</rp>B<rt></rt>C<rp>(</rp><rt>c</rt><rp>)</rp></ruby>'], '']);
|
|
51
56
|
assert.deepStrictEqual(inspect(parser, input('[東方](とう ほう)', new Context())), [['<ruby>東<rp>(</rp><rt>とう</rt><rp>)</rp>方<rp>(</rp><rt>ほう</rt><rp>)</rp></ruby>'], '']);
|
|
52
57
|
assert.deepStrictEqual(inspect(parser, input('[秦 \\ こころ](はた の こころ)', new Context())), [['<ruby>秦<rp>(</rp><rt>はた</rt><rp>)</rp> <rp>(</rp><rt>の</rt><rp>)</rp>こころ<rp>(</rp><rt>こころ</rt><rp>)</rp></ruby>'], '']);
|
|
@@ -4,7 +4,7 @@ import { List, Node } from '../../combinator/data/parser';
|
|
|
4
4
|
import { inits, surround, setBacktrack, dup, lazy, bind } from '../../combinator';
|
|
5
5
|
import { unsafehtmlentity } from './htmlentity';
|
|
6
6
|
import { txt } from '../source';
|
|
7
|
-
import {
|
|
7
|
+
import { isNonblankNodeStart } from '../visibility';
|
|
8
8
|
import { unwrap } from '../util';
|
|
9
9
|
import { html, defrag } from 'typed-dom/dom';
|
|
10
10
|
|
|
@@ -16,7 +16,7 @@ export const ruby: RubyParser = lazy(() => bind(
|
|
|
16
16
|
[1 | Backtrack.common, 3 | Backtrack.ruby],
|
|
17
17
|
([, ns]) => {
|
|
18
18
|
ns && ns.last?.value === '' && ns.pop();
|
|
19
|
-
return
|
|
19
|
+
return isNonblankNodeStart(ns) ? ns : undefined;
|
|
20
20
|
})),
|
|
21
21
|
dup(surround(
|
|
22
22
|
'(', text, ')',
|
package/src/parser/node.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { invisibleBlankHTMLEntityNames } from './api/normalize';
|
|
2
2
|
|
|
3
3
|
export const enum Flag {
|
|
4
4
|
none,
|
|
5
|
-
|
|
5
|
+
blank,
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export const
|
|
8
|
+
export const isBlankHTMLEntityName: (name: string) => boolean = eval([
|
|
9
9
|
'name => {',
|
|
10
10
|
'switch(name){',
|
|
11
|
-
|
|
11
|
+
invisibleBlankHTMLEntityNames.map(name => `case '${name}':`).join(''),
|
|
12
12
|
'return true;',
|
|
13
13
|
'default:',
|
|
14
14
|
'return false;',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EscapableSourceParser } from '../source';
|
|
2
2
|
import { Command } from '../context';
|
|
3
|
+
import { Flag } from '../node';
|
|
3
4
|
import { List, Node } from '../../combinator/data/parser';
|
|
4
5
|
import { consume } from '../../combinator';
|
|
5
6
|
import { next } from './text';
|
|
@@ -34,7 +35,7 @@ export const escsource: EscapableSourceParser = ({ context }) => {
|
|
|
34
35
|
return new List();
|
|
35
36
|
case '\n':
|
|
36
37
|
context.linebreak ||= source.length - position;
|
|
37
|
-
return new List([new Node(html('br'))]);
|
|
38
|
+
return new List([new Node(html('br'), Flag.blank)]);
|
|
38
39
|
default:
|
|
39
40
|
assert(char !== '\n');
|
|
40
41
|
if (context.sequential) return new List([new Node(char)]);
|
|
@@ -33,7 +33,7 @@ export const text: TextParser = input => {
|
|
|
33
33
|
return new List();
|
|
34
34
|
case '\n':
|
|
35
35
|
context.linebreak ||= source.length - position;
|
|
36
|
-
return new List([new Node(html('br'), Flag.
|
|
36
|
+
return new List([new Node(html('br'), Flag.blank)]);
|
|
37
37
|
default:
|
|
38
38
|
assert(char !== '\n');
|
|
39
39
|
if (context.sequential) return new List([new Node(char)]);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { UnescapableSourceParser } from '../source';
|
|
2
2
|
import { Command } from '../context';
|
|
3
|
+
import { Flag } from '../node';
|
|
3
4
|
import { List, Node } from '../../combinator/data/parser';
|
|
4
5
|
import { consume } from '../../combinator';
|
|
5
6
|
import { nonWhitespace, canSkip, next } from './text';
|
|
@@ -22,7 +23,7 @@ export const unescsource: UnescapableSourceParser = ({ context }) => {
|
|
|
22
23
|
return new List();
|
|
23
24
|
case '\n':
|
|
24
25
|
context.linebreak ||= source.length - position;
|
|
25
|
-
return new List([new Node(html('br'))]);
|
|
26
|
+
return new List([new Node(html('br'), Flag.blank)]);
|
|
26
27
|
default:
|
|
27
28
|
assert(char !== '\n');
|
|
28
29
|
if (context.sequential) return new List([new Node(char)]);
|
package/src/parser/visibility.ts
CHANGED
|
@@ -2,26 +2,26 @@ import { Parser, Input, List, Node, failsafe } from '../combinator/data/parser';
|
|
|
2
2
|
import { Context, Command } from './context';
|
|
3
3
|
import { Flag } from './node';
|
|
4
4
|
import { convert, fmap } from '../combinator';
|
|
5
|
-
import {
|
|
5
|
+
import { invisibleBlankHTMLEntityNames } from './api/normalize';
|
|
6
6
|
|
|
7
|
-
namespace
|
|
7
|
+
namespace blank {
|
|
8
8
|
export const line = new RegExp(
|
|
9
|
-
/((?:^|\n)[^\S\n]*(?=\S))((?:[^\S\n]|\\(?=$|\s)|&
|
|
10
|
-
.replace('
|
|
9
|
+
/((?:^|\n)[^\S\n]*(?=\S))((?:[^\S\n]|\\(?=$|\s)|&IBHN;|<wbr ?>)+(?=$|\n))/g.source
|
|
10
|
+
.replace('IBHN', `(?:${invisibleBlankHTMLEntityNames.join('|')})`),
|
|
11
11
|
'g');
|
|
12
12
|
export const start = new RegExp(
|
|
13
|
-
/(?:[^\S\n]|\\(?=$|\s)|&
|
|
14
|
-
.replace('
|
|
13
|
+
/(?:[^\S\n]|\\(?=$|\s)|&IBHN;|<wbr ?>)+/y.source
|
|
14
|
+
.replace('IBHN', `(?:${invisibleBlankHTMLEntityNames.join('|')})`),
|
|
15
15
|
'y');
|
|
16
16
|
export const unit = new RegExp(
|
|
17
|
-
/(?:[^\S\n]|\\(?=$|\s)|&
|
|
18
|
-
.replace('
|
|
17
|
+
/(?:[^\S\n]|\\(?=$|\s)|&IBHN;|<wbr ?>)/y.source
|
|
18
|
+
.replace('IBHN', `(?:${invisibleBlankHTMLEntityNames.join('|')})`),
|
|
19
19
|
'y');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function visualize<P extends Parser>(parser: P): P {
|
|
23
23
|
return convert(
|
|
24
|
-
source => source.replace(
|
|
24
|
+
source => source.replace(blank.line, `$1${Command.Escape}$2`),
|
|
25
25
|
parser);
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -30,7 +30,7 @@ export function blankWith(starts: '\n', delimiter: string | RegExp): RegExp {
|
|
|
30
30
|
return new RegExp([
|
|
31
31
|
// 空行除去
|
|
32
32
|
// 完全な空行はエスケープ済みなので再帰的バックトラックにはならない。
|
|
33
|
-
String.raw`(?:${starts}(?:\\?\s|&(?:${
|
|
33
|
+
String.raw`(?:${starts}(?:\\?\s|&(?:${invisibleBlankHTMLEntityNames.join('|')});|<wbr ?>)*)?`,
|
|
34
34
|
typeof delimiter === 'string'
|
|
35
35
|
? delimiter.replace(/[*+()\[\]]/g, '\\$&')
|
|
36
36
|
: delimiter.source,
|
|
@@ -38,32 +38,21 @@ export function blankWith(starts: '\n', delimiter: string | RegExp): RegExp {
|
|
|
38
38
|
}
|
|
39
39
|
function nonblankWith(delimiter: string | RegExp): RegExp {
|
|
40
40
|
return new RegExp([
|
|
41
|
-
String.raw`(?<!\s|&(?:${
|
|
41
|
+
String.raw`(?<!\s|&(?:${invisibleBlankHTMLEntityNames.join('|')});|<wbr ?>)`,
|
|
42
42
|
typeof delimiter === 'string'
|
|
43
43
|
? delimiter.replace(/[*+()\[\]]/g, '\\$&')
|
|
44
44
|
: delimiter.source,
|
|
45
45
|
].join(''), 'y');
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
//export function looseStart<P extends Parser<HTMLElement | string>>(parser: P): P;
|
|
49
|
-
//export function looseStart<N extends HTMLElement | string>(parser: Parser<N>): Parser<N> {
|
|
50
|
-
// return input =>
|
|
51
|
-
// isLooseStart(input)
|
|
52
|
-
// ? parser(input)
|
|
53
|
-
// : undefined;
|
|
54
|
-
//}
|
|
55
|
-
//const isLooseStart = reduce(({ source, context }: Input<Context>): boolean => {
|
|
56
|
-
// return isTightStart({ source: source.replace(invisible.start, ''), context });
|
|
57
|
-
//}, ({ source }) => `${source}${Command.Separator}`);
|
|
58
|
-
|
|
59
48
|
export function beforeNonblank<P extends Parser>(parser: P): P;
|
|
60
49
|
export function beforeNonblank<N>(parser: Parser<N>): Parser<N, Context> {
|
|
61
50
|
return input =>
|
|
62
|
-
|
|
51
|
+
isNonblankStart(input)
|
|
63
52
|
? parser(input)
|
|
64
53
|
: undefined;
|
|
65
54
|
}
|
|
66
|
-
function
|
|
55
|
+
function isNonblankStart(input: Input<Context>): boolean {
|
|
67
56
|
const { context } = input;
|
|
68
57
|
const { source, position } = context;
|
|
69
58
|
if (position === source.length) return true;
|
|
@@ -74,30 +63,26 @@ function isTightStart(input: Input<Context>): boolean {
|
|
|
74
63
|
case '\n':
|
|
75
64
|
return false;
|
|
76
65
|
default:
|
|
77
|
-
const reg =
|
|
66
|
+
const reg = blank.unit;
|
|
78
67
|
reg.lastIndex = position;
|
|
79
68
|
return !reg.test(source);
|
|
80
69
|
}
|
|
81
70
|
}
|
|
82
71
|
|
|
83
|
-
export function
|
|
72
|
+
export function isNonblankFirstLine(nodes: List<Node<HTMLElement | string>>): boolean {
|
|
84
73
|
if (nodes.length === 0) return true;
|
|
85
74
|
for (const node of nodes) {
|
|
86
|
-
if (
|
|
87
|
-
if (typeof node.value === 'object' && node.value.tagName === 'BR') break;
|
|
75
|
+
if (isNonblank(node)) return true;
|
|
76
|
+
if (node.flags & Flag.blank && typeof node.value === 'object' && node.value.tagName === 'BR') break;
|
|
88
77
|
}
|
|
89
78
|
return false;
|
|
90
79
|
}
|
|
91
|
-
export function
|
|
80
|
+
export function isNonblankNodeStart(nodes: List<Node<HTMLElement | string>>): boolean {
|
|
92
81
|
if (nodes.length === 0) return true;
|
|
93
|
-
return
|
|
82
|
+
return isNonblank(nodes.head!, 0);
|
|
94
83
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// return isVisible(nodes.at(-1)!, -1);
|
|
98
|
-
//}
|
|
99
|
-
function isVisible({ value: node, flags }: Node<HTMLElement | string>, strpos?: number): boolean {
|
|
100
|
-
if (flags & Flag.invisible) return false;
|
|
84
|
+
function isNonblank({ value: node, flags }: Node<HTMLElement | string>, strpos?: number): boolean {
|
|
85
|
+
if (flags & Flag.blank) return false;
|
|
101
86
|
if (typeof node !== 'string') return true;
|
|
102
87
|
const str = node && strpos !== undefined
|
|
103
88
|
? node[strpos >= 0 ? strpos : node.length + strpos]
|
|
@@ -124,7 +109,7 @@ function trimBlankStart<N>(parser: Parser<N>): Parser<N> {
|
|
|
124
109
|
const { context } = input;
|
|
125
110
|
const { source, position } = context;
|
|
126
111
|
if (position === source.length) return;
|
|
127
|
-
const reg =
|
|
112
|
+
const reg = blank.start;
|
|
128
113
|
reg.lastIndex = position;
|
|
129
114
|
reg.test(source);
|
|
130
115
|
context.position = reg.lastIndex || position;
|
|
@@ -137,41 +122,23 @@ export function trimBlankEnd<P extends Parser<HTMLElement | string>>(parser: P):
|
|
|
137
122
|
export function trimBlankEnd<N extends HTMLElement>(parser: Parser<N>): Parser<string | N> {
|
|
138
123
|
return fmap(parser, trimBlankNodeEnd);
|
|
139
124
|
}
|
|
140
|
-
//export function trimBlankNode<N extends HTMLElement | string>(nodes: N[]): N[] {
|
|
141
|
-
// return trimBlankNodeStart(trimBlankNodeEnd(nodes));
|
|
142
|
-
//}
|
|
143
|
-
//function trimBlankNodeStart<N extends HTMLElement | string>(nodes: N[]): N[] {
|
|
144
|
-
// for (let node = nodes[0]; nodes.length > 0 && !isVisible(node = nodes[0], 0);) {
|
|
145
|
-
// if (typeof node === 'string') {
|
|
146
|
-
// const pos = node.trimStart().length;
|
|
147
|
-
// if (pos > 0) {
|
|
148
|
-
// nodes[0] = node.slice(-pos) as N;
|
|
149
|
-
// break;
|
|
150
|
-
// }
|
|
151
|
-
// }
|
|
152
|
-
// else if (nodes.length === 1 && node.className === 'indexer') {
|
|
153
|
-
// break;
|
|
154
|
-
// }
|
|
155
|
-
// nodes.shift();
|
|
156
|
-
// }
|
|
157
|
-
// return nodes;
|
|
158
|
-
//}
|
|
159
125
|
export function trimBlankNodeEnd<N extends HTMLElement>(nodes: List<Node<string | N>>): List<Node<string | N>> {
|
|
160
|
-
const skip = nodes.last && ~nodes.last.flags & Flag.
|
|
126
|
+
const skip = nodes.last && ~nodes.last.flags & Flag.blank && typeof nodes.last.value === 'object'
|
|
161
127
|
? nodes.last.value.className === 'indexer'
|
|
162
128
|
: false;
|
|
163
129
|
for (let node = skip ? nodes.last?.prev : nodes.last; node;) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
130
|
+
if (~node.flags & Flag.blank) {
|
|
131
|
+
if (typeof node.value === 'string') {
|
|
132
|
+
const str = node.value.trimEnd();
|
|
133
|
+
if (str.length > 0) {
|
|
134
|
+
node.value = str;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
169
139
|
break;
|
|
170
140
|
}
|
|
171
141
|
}
|
|
172
|
-
else if (visible) {
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
142
|
const target = node;
|
|
176
143
|
node = node.prev;
|
|
177
144
|
nodes.delete(target);
|