@wdprlib/parser 5.2.0 → 5.4.0
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/dist/index.cjs +2211 -1182
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +2211 -1182
- package/package.json +3 -2
- package/src/build-info.generated.ts +2 -2
- package/src/lexer/lexer.ts +34 -5
- package/src/lexer/quoted-string.ts +4 -4
- package/src/lexer/syntax-actions.ts +6 -1
- package/src/lexer/text-actions.ts +15 -1
- package/src/lexer/url-schemes.ts +78 -0
- package/src/parser/constants.ts +4 -0
- package/src/parser/parse/block.ts +1 -1
- package/src/parser/postprocess/divAdjacentParagraph.ts +23 -21
- package/src/parser/postprocess/spanStrip/merge.ts +8 -2
- package/src/parser/preprocess/typography.ts +25 -3
- package/src/parser/preprocess/utils/raw-regions.ts +59 -14
- package/src/parser/preprocess/whitespace/index.ts +8 -1
- package/src/parser/rules/block/bibliography/entry-content.ts +1 -1
- package/src/parser/rules/block/block-list/bare-content.ts +3 -1
- package/src/parser/rules/block/block-list/bare-paragraph.ts +7 -2
- package/src/parser/rules/block/block-list/item-content.ts +7 -3
- package/src/parser/rules/block/block-list/li-content.ts +8 -3
- package/src/parser/rules/block/block-list/li-item.ts +1 -1
- package/src/parser/rules/block/blockquote/build.ts +1 -1
- package/src/parser/rules/block/code/boundary.ts +76 -0
- package/src/parser/rules/block/code/content.ts +11 -40
- package/src/parser/rules/block/code/index.ts +8 -31
- package/src/parser/rules/block/code/open.ts +46 -0
- package/src/parser/rules/block/definition-list/item-key.ts +1 -1
- package/src/parser/rules/block/definition-list/item-value.ts +1 -1
- package/src/parser/rules/block/div/failed.ts +2 -0
- package/src/parser/rules/block/index.ts +3 -0
- package/src/parser/rules/block/list/line.ts +6 -3
- package/src/parser/rules/block/math/boundary.ts +104 -0
- package/src/parser/rules/block/math/index.ts +17 -57
- package/src/parser/rules/block/module/listpages/resolution/items.ts +2 -2
- package/src/parser/rules/block/module/listpages/resolution/pager.ts +43 -0
- package/src/parser/rules/block/module/listpages/resolution/wrapper.ts +6 -1
- package/src/parser/rules/block/module/listpages/resolve.ts +1 -1
- package/src/parser/rules/block/module/listpages/types/external-data.ts +13 -0
- package/src/parser/rules/block/module/listpages/url-resolution/params.ts +5 -4
- package/src/parser/rules/block/module/resolution/data-maps.ts +75 -3
- package/src/parser/rules/block/module/resolution/resolve-async.ts +12 -3
- package/src/parser/rules/block/note/boundary.ts +88 -0
- package/src/parser/rules/block/note/index.ts +37 -0
- package/src/parser/rules/block/paragraph/index.ts +12 -4
- package/src/parser/rules/block/paragraph/normalize.ts +6 -1
- package/src/parser/rules/block/parsing/block-item.ts +1 -1
- package/src/parser/rules/block/parsing/content.ts +1 -1
- package/src/parser/rules/block/parsing/inline-content.ts +6 -3
- package/src/parser/rules/block/table/pipe/cell.ts +3 -1
- package/src/parser/rules/block/table-block/cell-content/index.ts +1 -1
- package/src/parser/rules/block/table-block/cell-content/segments.ts +5 -2
- package/src/parser/rules/block/table-block/cell-newline.ts +2 -1
- package/src/parser/rules/contracts/rule.ts +4 -2
- package/src/parser/rules/contracts/scope.ts +2 -0
- package/src/parser/rules/inline/anchor/child.ts +8 -2
- package/src/parser/rules/inline/anchor/content.ts +3 -1
- package/src/parser/rules/inline/anchor/index.ts +4 -1
- package/src/parser/rules/inline/anchor/newline.ts +2 -1
- package/src/parser/rules/inline/autolink.ts +153 -0
- package/src/parser/rules/inline/button/attributes.ts +17 -0
- package/src/parser/rules/inline/button/index.ts +17 -0
- package/src/parser/rules/inline/button/syntax.ts +56 -0
- package/src/parser/rules/inline/date/index.ts +17 -0
- package/src/parser/rules/inline/date/syntax.ts +46 -0
- package/src/parser/rules/inline/email/candidates.ts +134 -0
- package/src/parser/rules/inline/email/index.ts +36 -0
- package/src/parser/rules/inline/email/scan.ts +76 -0
- package/src/parser/rules/inline/expr/branch.ts +3 -1
- package/src/parser/rules/inline/footnote/boundary.ts +56 -0
- package/src/parser/rules/inline/footnote/content.ts +29 -41
- package/src/parser/rules/inline/footnote/elements.ts +9 -34
- package/src/parser/rules/inline/footnote/index.ts +4 -1
- package/src/parser/rules/inline/formatting/close.ts +12 -0
- package/src/parser/rules/inline/index.ts +14 -0
- package/src/parser/rules/inline/line-break/newline.ts +8 -1
- package/src/parser/rules/inline/link-bracket/direct-url.ts +11 -3
- package/src/parser/rules/inline/link-bracket/parsed.ts +9 -4
- package/src/parser/rules/inline/link-bracket/parts.ts +14 -36
- package/src/parser/rules/inline/link-bracket/special-target.ts +9 -0
- package/src/parser/rules/inline/link-single.ts +9 -7
- package/src/parser/rules/inline/link-triple/index.ts +1 -0
- package/src/parser/rules/inline/link-triple/label.ts +7 -1
- package/src/parser/rules/inline/parsing/automatic-line-break.ts +35 -0
- package/src/parser/rules/inline/parsing/block-boundary.ts +2 -0
- package/src/parser/rules/inline/parsing/block-start-predicates.ts +10 -0
- package/src/parser/rules/inline/parsing/inline-content.ts +36 -6
- package/src/parser/rules/inline/parsing/plain-text.ts +7 -2
- package/src/parser/rules/inline/parsing/preserved-line-break.ts +13 -0
- package/src/parser/rules/inline/parsing/raw-tag.ts +19 -0
- package/src/parser/rules/inline/raw/angle.ts +2 -1
- package/src/parser/rules/inline/raw/end.ts +21 -1
- package/src/parser/rules/inline/size/content.ts +1 -1
- package/src/parser/rules/inline/size/value.ts +11 -0
- package/src/parser/rules/inline/social/index.ts +17 -0
- package/src/parser/rules/inline/social/syntax.ts +40 -0
- package/src/parser/rules/inline/span/content.ts +3 -1
- package/src/parser/rules/inline/span/newline.ts +2 -1
- package/src/parser/rules/opaque-probe.ts +58 -0
- package/src/pipeline/process.ts +8 -1
- package/src/parser/rules/block/math/content.ts +0 -54
- package/src/parser/rules/block/math/name.ts +0 -35
- package/src/parser/rules/inline/footnote/child.ts +0 -22
- package/src/parser/rules/inline/footnote/newline.ts +0 -27
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
2
|
import { getModuleParseAst, type ParseFunction } from "../../types";
|
|
3
3
|
import type { ListPagesModuleData } from "../resolve";
|
|
4
|
+
import type { ListPagesExternalData } from "../types";
|
|
5
|
+
import { createListPagesPager } from "./pager";
|
|
4
6
|
|
|
5
7
|
export function wrapListPagesResult(
|
|
6
8
|
module: ListPagesModuleData,
|
|
7
9
|
items: Element[],
|
|
8
10
|
parse: ParseFunction,
|
|
11
|
+
data?: ListPagesExternalData,
|
|
9
12
|
): Element[] {
|
|
10
|
-
if (items.length === 0) {
|
|
13
|
+
if (items.length === 0 && (!data || data.pages.length === 0)) {
|
|
11
14
|
return [];
|
|
12
15
|
}
|
|
13
16
|
|
|
@@ -25,6 +28,8 @@ export function wrapListPagesResult(
|
|
|
25
28
|
result.push(...appendAst.elements);
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
if (data) result.push(...createListPagesPager(data));
|
|
32
|
+
|
|
28
33
|
if (module.wrapper) {
|
|
29
34
|
return [
|
|
30
35
|
{
|
|
@@ -56,5 +56,5 @@ export function resolveListPages(
|
|
|
56
56
|
parse: ParseFunction,
|
|
57
57
|
): Element[] {
|
|
58
58
|
const items = renderListPagesItems(module, data, compiledTemplate, parse);
|
|
59
|
-
return wrapListPagesResult(module, items, parse);
|
|
59
|
+
return wrapListPagesResult(module, items, parse, data);
|
|
60
60
|
}
|
|
@@ -94,6 +94,19 @@ export interface SiteContext {
|
|
|
94
94
|
*/
|
|
95
95
|
export interface ListPagesExternalData {
|
|
96
96
|
pages: PageData[];
|
|
97
|
+
/** Matching items before offset and limit; use the same visibility filters as pages. */
|
|
97
98
|
totalCount: number;
|
|
98
99
|
site: SiteContext;
|
|
100
|
+
/** Filled by module resolution when a page URL is available. */
|
|
101
|
+
pagination?: {
|
|
102
|
+
currentPage: number;
|
|
103
|
+
perPage: number;
|
|
104
|
+
totalPages: number;
|
|
105
|
+
/** Actual fetch offset, including the module's starting offset. */
|
|
106
|
+
offset: number;
|
|
107
|
+
/** Total item limit after URL resolution, not the fetch batch size. */
|
|
108
|
+
limit?: number;
|
|
109
|
+
urlPath: string;
|
|
110
|
+
parameter: string;
|
|
111
|
+
};
|
|
99
112
|
}
|
|
@@ -2,12 +2,13 @@ import { URL_RESOLVABLE_FIELDS } from "./fields";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Parse URL path parameters like /offset/1/page2_limit/1.
|
|
5
|
-
* Returns a map of parameter name -> value.
|
|
5
|
+
* Returns a map of parameter name -> value. Set hasPageName for a full page path;
|
|
6
|
+
* otherwise the legacy parameter-only-path heuristic is used.
|
|
6
7
|
*/
|
|
7
|
-
export function parseUrlParams(url: string): Map<string, string> {
|
|
8
|
+
export function parseUrlParams(url: string, hasPageName = false): Map<string, string> {
|
|
8
9
|
const params = new Map<string, string>();
|
|
9
|
-
const parts = url.split("/").filter(Boolean);
|
|
10
|
-
const pairStart = isUrlParameter(parts[0]) ? 0 : 1;
|
|
10
|
+
const parts = url.split(/[?#]/, 1)[0]!.split("/").filter(Boolean);
|
|
11
|
+
const pairStart = !hasPageName && isUrlParameter(parts[0]) ? 0 : 1;
|
|
11
12
|
|
|
12
13
|
// Skip the page name (first part), parse key/value pairs.
|
|
13
14
|
for (let i = pairStart; i < parts.length - 1; i += 2) {
|
|
@@ -4,25 +4,97 @@ import type { ListUsersDataRequirement, ListUsersExternalData } from "../listuse
|
|
|
4
4
|
import type { TagCloudDataRequirement, TagCloudExternalData } from "../tagcloud/types";
|
|
5
5
|
import { parseUrlParams, resolveAndNormalizeQuery } from "../listpages/url-resolver";
|
|
6
6
|
|
|
7
|
+
export interface ListPagesPaginationState {
|
|
8
|
+
nextUnprefixed: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
export async function buildListPagesDataMap(
|
|
8
12
|
dataProvider: DataProvider,
|
|
9
13
|
requirements: ListPagesDataRequirement[],
|
|
10
14
|
urlPath: string | undefined,
|
|
15
|
+
paginationState: ListPagesPaginationState = { nextUnprefixed: 1 },
|
|
11
16
|
): Promise<Map<number, ListPagesExternalData>> {
|
|
12
17
|
const dataMap = new Map<number, ListPagesExternalData>();
|
|
13
|
-
const urlParams = parseUrlParams(urlPath ?? "");
|
|
18
|
+
const urlParams = parseUrlParams(urlPath ?? "", true);
|
|
14
19
|
|
|
15
20
|
for (const req of requirements) {
|
|
16
21
|
const normalizedQuery = resolveAndNormalizeQuery(req, urlParams);
|
|
17
|
-
|
|
22
|
+
if (!urlPath || !urlPath.startsWith("/") || urlPath.startsWith("//")) {
|
|
23
|
+
const data = await dataProvider.fetchListPages?.(normalizedQuery, req);
|
|
24
|
+
if (data) dataMap.set(req.id, data);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const ordinal = req.urlAttrPrefix ? 0 : paginationState.nextUnprefixed++;
|
|
28
|
+
const parameter = req.urlAttrPrefix
|
|
29
|
+
? `${encodeURIComponent(req.urlAttrPrefix)}_p`
|
|
30
|
+
: ordinal === 1
|
|
31
|
+
? "p"
|
|
32
|
+
: `p${ordinal}`;
|
|
33
|
+
const perPage = Math.min(250, positiveInteger(normalizedQuery.perPage, 20));
|
|
34
|
+
const baseOffset = positiveInteger(normalizedQuery.offset, 0);
|
|
35
|
+
const limit =
|
|
36
|
+
normalizedQuery.limit !== undefined && normalizedQuery.limit >= 0
|
|
37
|
+
? Math.floor(normalizedQuery.limit)
|
|
38
|
+
: undefined;
|
|
39
|
+
const rawPage = urlParams.get(parameter) ?? "1";
|
|
40
|
+
let currentPage = /^\d+$/.test(rawPage) ? positiveInteger(Number(rawPage), 1) : 1;
|
|
41
|
+
currentPage = Math.min(
|
|
42
|
+
currentPage,
|
|
43
|
+
Math.floor((Number.MAX_SAFE_INTEGER - baseOffset) / perPage) + 1,
|
|
44
|
+
);
|
|
45
|
+
if (limit !== undefined)
|
|
46
|
+
currentPage = Math.min(currentPage, Math.max(1, Math.ceil(limit / perPage)));
|
|
47
|
+
const fetchPage = () => {
|
|
48
|
+
const relativeOffset = (currentPage - 1) * perPage;
|
|
49
|
+
return dataProvider.fetchListPages?.(
|
|
50
|
+
{
|
|
51
|
+
...normalizedQuery,
|
|
52
|
+
offset: baseOffset + relativeOffset,
|
|
53
|
+
limit: Math.min(
|
|
54
|
+
perPage,
|
|
55
|
+
limit === undefined ? perPage : Math.max(0, limit - relativeOffset),
|
|
56
|
+
),
|
|
57
|
+
perPage,
|
|
58
|
+
},
|
|
59
|
+
req,
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
let data = await fetchPage();
|
|
18
63
|
if (data) {
|
|
19
|
-
|
|
64
|
+
const count = Math.min(Math.max(0, data.totalCount - baseOffset), limit ?? Infinity);
|
|
65
|
+
const totalPages = Math.ceil(count / perPage);
|
|
66
|
+
const lastPage = Math.max(1, totalPages);
|
|
67
|
+
if (currentPage > lastPage) {
|
|
68
|
+
currentPage = lastPage;
|
|
69
|
+
data = await fetchPage();
|
|
70
|
+
}
|
|
71
|
+
if (data)
|
|
72
|
+
dataMap.set(req.id, {
|
|
73
|
+
...data,
|
|
74
|
+
pages: data.pages.slice(
|
|
75
|
+
0,
|
|
76
|
+
Math.min(perPage, Math.max(0, count - (currentPage - 1) * perPage)),
|
|
77
|
+
),
|
|
78
|
+
pagination: {
|
|
79
|
+
currentPage,
|
|
80
|
+
perPage,
|
|
81
|
+
totalPages,
|
|
82
|
+
offset: baseOffset + (currentPage - 1) * perPage,
|
|
83
|
+
limit,
|
|
84
|
+
urlPath,
|
|
85
|
+
parameter,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
20
88
|
}
|
|
21
89
|
}
|
|
22
90
|
|
|
23
91
|
return dataMap;
|
|
24
92
|
}
|
|
25
93
|
|
|
94
|
+
function positiveInteger(value: number | undefined, fallback: number): number {
|
|
95
|
+
return value !== undefined && Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
96
|
+
}
|
|
97
|
+
|
|
26
98
|
export async function buildListUsersDataMap(
|
|
27
99
|
dataProvider: DataProvider,
|
|
28
100
|
requirements: ListUsersDataRequirement[],
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
import { buildListPagesDataMap, buildListUsersDataMap, buildTagCloudDataMap } from "./data-maps";
|
|
27
27
|
import { ModuleDocumentRegistry } from "./document";
|
|
28
28
|
import { collectStyles, mergeCollectedStyles } from "./styles";
|
|
29
|
+
import { createListPagesPager } from "../listpages/resolution/pager";
|
|
30
|
+
import type { ListPagesPaginationState } from "./data-maps";
|
|
29
31
|
|
|
30
32
|
export type AsyncModuleParseFunction = (source: string) => Promise<ModuleParseResult>;
|
|
31
33
|
|
|
@@ -40,6 +42,7 @@ export interface ResolveModulesWithAsyncParseOptions {
|
|
|
40
42
|
};
|
|
41
43
|
urlPath?: string;
|
|
42
44
|
pageTags: string[];
|
|
45
|
+
paginationState?: ListPagesPaginationState;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export interface AsyncModuleResolutionResult {
|
|
@@ -73,7 +76,12 @@ export async function resolveModulesWithAsyncParse(
|
|
|
73
76
|
registry.register(await options.parse(source));
|
|
74
77
|
|
|
75
78
|
const [listPagesData, listUsersData, tagCloudData] = await Promise.all([
|
|
76
|
-
buildListPagesDataMap(
|
|
79
|
+
buildListPagesDataMap(
|
|
80
|
+
dataProvider,
|
|
81
|
+
options.requirements.listPages ?? [],
|
|
82
|
+
options.urlPath,
|
|
83
|
+
options.paginationState,
|
|
84
|
+
),
|
|
77
85
|
buildListUsersDataMap(dataProvider, options.requirements.listUsers ?? []),
|
|
78
86
|
buildTagCloudDataMap(dataProvider, options.requirements.tagCloud ?? []),
|
|
79
87
|
]);
|
|
@@ -175,9 +183,9 @@ async function resolveListPagesAsync(
|
|
|
175
183
|
if (!page) continue;
|
|
176
184
|
const variableContext: VariableContext = {
|
|
177
185
|
page,
|
|
178
|
-
index: i + 1,
|
|
186
|
+
index: (data.pagination?.offset ?? 0) + i + 1,
|
|
179
187
|
total: data.totalCount,
|
|
180
|
-
limit: module.limit,
|
|
188
|
+
limit: data.pagination ? data.pagination.limit : module.limit,
|
|
181
189
|
site: data.site,
|
|
182
190
|
};
|
|
183
191
|
const parsed = await parse(template(variableContext));
|
|
@@ -197,6 +205,7 @@ async function resolveListPagesAsync(
|
|
|
197
205
|
if (module["append-line"] && !module.separate) {
|
|
198
206
|
result.push(...(await parse(module["append-line"])).elements);
|
|
199
207
|
}
|
|
208
|
+
result.push(...createListPagesPager(data));
|
|
200
209
|
|
|
201
210
|
return module.wrapper
|
|
202
211
|
? [
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Token } from "../../../../lexer";
|
|
2
|
+
import type { ParseContext } from "../../types";
|
|
3
|
+
import { createOpaqueProbe, opaqueRuleEnd } from "../../opaque-probe";
|
|
4
|
+
import { codeBlockRule } from "../code";
|
|
5
|
+
import { mathBlockRule } from "../math";
|
|
6
|
+
import { linkTripleRule } from "../../inline/link-triple";
|
|
7
|
+
import { protectedInlineRegionEnd } from "../../inline/raw/end";
|
|
8
|
+
|
|
9
|
+
// These constructs are replaced before Note in Wikidot's processing order.
|
|
10
|
+
const opaqueRules = [codeBlockRule, mathBlockRule, linkTripleRule];
|
|
11
|
+
interface NoteBounds {
|
|
12
|
+
bodyStart: number;
|
|
13
|
+
close: number;
|
|
14
|
+
end: number;
|
|
15
|
+
}
|
|
16
|
+
const boundsCache = new WeakMap<readonly Token[], Map<number, NoteBounds | null>>();
|
|
17
|
+
const closeCache = new WeakMap<readonly Token[], Map<string, Map<number, number>>>();
|
|
18
|
+
|
|
19
|
+
function tagEnd(tokens: readonly Token[], pos: number, name: string, close = false): number {
|
|
20
|
+
return tokens[pos]?.type === (close ? "BLOCK_END_OPEN" : "BLOCK_OPEN") &&
|
|
21
|
+
tokens[pos + 1]?.value.toLowerCase() === name &&
|
|
22
|
+
tokens[pos + 2]?.type === "BLOCK_CLOSE"
|
|
23
|
+
? pos + 3
|
|
24
|
+
: pos;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findClose(probe: ParseContext, start: number, name: string): number {
|
|
28
|
+
let byName = closeCache.get(probe.tokens);
|
|
29
|
+
if (!byName) {
|
|
30
|
+
byName = new Map();
|
|
31
|
+
closeCache.set(probe.tokens, byName);
|
|
32
|
+
}
|
|
33
|
+
let cache = byName.get(name);
|
|
34
|
+
if (!cache) {
|
|
35
|
+
cache = new Map();
|
|
36
|
+
byName.set(name, cache);
|
|
37
|
+
}
|
|
38
|
+
if (cache.has(start)) return cache.get(start)!;
|
|
39
|
+
const visited: number[] = [];
|
|
40
|
+
let result = -1;
|
|
41
|
+
for (let pos = start; pos < probe.tokens.length; pos++) {
|
|
42
|
+
if (cache.has(pos)) {
|
|
43
|
+
result = cache.get(pos)!;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
visited.push(pos);
|
|
47
|
+
const protectedEnd = protectedInlineRegionEnd(probe.tokens, pos, probe.tokens.length);
|
|
48
|
+
if (protectedEnd > pos) {
|
|
49
|
+
pos = protectedEnd - 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (tagEnd(probe.tokens, pos, name, true) > pos) {
|
|
53
|
+
result = pos;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const opaqueEnd = opaqueRuleEnd(probe, pos, opaqueRules, true);
|
|
57
|
+
if (opaqueEnd > pos) {
|
|
58
|
+
pos = opaqueEnd - 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Footnote precedes Note and uses its first complete close, without nesting.
|
|
62
|
+
if (name === "note") {
|
|
63
|
+
const footnoteStart = tagEnd(probe.tokens, pos, "footnote");
|
|
64
|
+
if (footnoteStart > pos) {
|
|
65
|
+
const end = findClose(probe, footnoteStart, "footnote");
|
|
66
|
+
if (end >= 0) pos = end + 2;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Only visited positions are reusable: opaque interiors have different boundaries.
|
|
71
|
+
for (const pos of visited) cache.set(pos, result);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function findNoteBounds(ctx: ParseContext, start: number = ctx.pos): NoteBounds | null {
|
|
76
|
+
const openEnd = tagEnd(ctx.tokens, start, "note");
|
|
77
|
+
if (openEnd === start || ctx.tokens[openEnd]?.type !== "NEWLINE") return null;
|
|
78
|
+
let cache = boundsCache.get(ctx.tokens);
|
|
79
|
+
if (!cache) {
|
|
80
|
+
cache = new Map();
|
|
81
|
+
boundsCache.set(ctx.tokens, cache);
|
|
82
|
+
}
|
|
83
|
+
if (cache.has(start)) return cache.get(start)!;
|
|
84
|
+
const close = findClose(createOpaqueProbe(ctx), openEnd + 1, "note");
|
|
85
|
+
const result = close < 0 ? null : { bodyStart: openEnd + 1, close, end: close + 3 };
|
|
86
|
+
cache.set(start, result);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BlockRule } from "../../types";
|
|
2
|
+
import { parseBlocksUntil } from "../parsing/content";
|
|
3
|
+
import { findNoteBounds } from "./boundary";
|
|
4
|
+
|
|
5
|
+
const excludedBlockNames = new Set(["note"]);
|
|
6
|
+
|
|
7
|
+
export const noteRule: BlockRule = {
|
|
8
|
+
name: "note",
|
|
9
|
+
startTokens: ["BLOCK_OPEN"],
|
|
10
|
+
requiresLineStart: false,
|
|
11
|
+
parse(ctx) {
|
|
12
|
+
const bounds = findNoteBounds(ctx);
|
|
13
|
+
if (!bounds) return { success: false };
|
|
14
|
+
const body = parseBlocksUntil(
|
|
15
|
+
{
|
|
16
|
+
...ctx,
|
|
17
|
+
tokens: ctx.tokens.slice(bounds.bodyStart, bounds.close),
|
|
18
|
+
pos: 0,
|
|
19
|
+
scope: {
|
|
20
|
+
...ctx.scope,
|
|
21
|
+
inlineEnd: undefined,
|
|
22
|
+
tableFormatting: undefined,
|
|
23
|
+
blockCloseCondition: undefined,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
() => false,
|
|
27
|
+
{ excludedBlockNames },
|
|
28
|
+
);
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
consumed: bounds.end - ctx.pos,
|
|
32
|
+
elements: [
|
|
33
|
+
{ element: "container", data: { type: "note", attributes: {}, elements: body.elements } },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { markUnparsedDivStart } from "../../../postprocess/divAdjacentParagraph";
|
|
2
|
+
import { isDivClose } from "../div/close";
|
|
1
3
|
/**
|
|
2
4
|
* Paragraph rule
|
|
3
5
|
*
|
|
@@ -8,6 +10,7 @@ import type { Element } from "@wdprlib/ast";
|
|
|
8
10
|
import type { BlockRule, ParseContext, RuleResult } from "../../types";
|
|
9
11
|
import { parseInlineContent } from "./content";
|
|
10
12
|
import { normalizeParagraphElements } from "./normalize";
|
|
13
|
+
import { isPreservedLeadingLineBreak } from "../../inline/parsing/preserved-line-break";
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Paragraph is the fallback block rule.
|
|
@@ -23,18 +26,19 @@ export const paragraphRule: BlockRule = {
|
|
|
23
26
|
|
|
24
27
|
parse(ctx: ParseContext): RuleResult<Element> {
|
|
25
28
|
const result = parseInlineContent(ctx);
|
|
26
|
-
if (result.
|
|
29
|
+
if (result.consumed === 0) {
|
|
27
30
|
return { success: false };
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
const elements = normalizeParagraphElements(result.elements);
|
|
31
34
|
if (elements.length === 0) {
|
|
32
|
-
return { success:
|
|
35
|
+
return { success: true, elements: [], consumed: result.consumed };
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
const nextPos = ctx.pos + result.consumed;
|
|
36
39
|
const nextToken = ctx.tokens[nextPos];
|
|
37
40
|
if (nextToken?.type === "COLON" && nextToken.lineStart) {
|
|
41
|
+
if (isPreservedLeadingLineBreak(elements[0])) elements[0] = { element: "line-break" };
|
|
38
42
|
return {
|
|
39
43
|
success: true,
|
|
40
44
|
elements: [...elements, { element: "line-break" }],
|
|
@@ -42,9 +46,11 @@ export const paragraphRule: BlockRule = {
|
|
|
42
46
|
};
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
const wrapped = wrapParagraphElements(elements);
|
|
50
|
+
if (isDivClose(ctx)) markUnparsedDivStart(wrapped);
|
|
45
51
|
return {
|
|
46
52
|
success: true,
|
|
47
|
-
elements:
|
|
53
|
+
elements: wrapped,
|
|
48
54
|
consumed: result.consumed,
|
|
49
55
|
};
|
|
50
56
|
},
|
|
@@ -57,7 +63,8 @@ export function wrapParagraphElements(elements: Element[]): Element[] {
|
|
|
57
63
|
let bare = false;
|
|
58
64
|
const flush = (trimBreaks = false) => {
|
|
59
65
|
const content = trimBreaks ? normalizeParagraphElements(group) : group;
|
|
60
|
-
while (content[0]?.element === "line-break"
|
|
66
|
+
while (content[0]?.element === "line-break" && !isPreservedLeadingLineBreak(content[0]))
|
|
67
|
+
content.shift();
|
|
61
68
|
while (content.length) {
|
|
62
69
|
const last = content.at(-1)!;
|
|
63
70
|
if (last.element !== "text" || last.data.trim() !== "") break;
|
|
@@ -66,6 +73,7 @@ export function wrapParagraphElements(elements: Element[]): Element[] {
|
|
|
66
73
|
while (content[0]?.element === "text" && content[0].data.trim() === "") content.shift();
|
|
67
74
|
if (content[0]?.element === "text")
|
|
68
75
|
content[0] = { element: "text", data: content[0].data.trimStart() };
|
|
76
|
+
if (isPreservedLeadingLineBreak(content[0])) content[0] = { element: "line-break" };
|
|
69
77
|
if (content.length)
|
|
70
78
|
output.push(
|
|
71
79
|
...(bare || content.some((el) => el.element === "image")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
2
|
import { processCloseSpanMarkers } from "./span-markers";
|
|
3
|
+
import { isPreservedLeadingLineBreak } from "../../inline/parsing/preserved-line-break";
|
|
3
4
|
|
|
4
5
|
type PreservedLineBreak = Extract<Element, { element: "line-break" }> & {
|
|
5
6
|
_preservedTrailingBreak?: boolean;
|
|
@@ -44,7 +45,11 @@ function removeTrailingWhitespaceText(elements: Element[]): void {
|
|
|
44
45
|
|
|
45
46
|
function removeLeadingLineBreaks(elements: Element[]): Element[] {
|
|
46
47
|
let first = 0;
|
|
47
|
-
while (
|
|
48
|
+
while (
|
|
49
|
+
first < elements.length &&
|
|
50
|
+
elements[first]?.element === "line-break" &&
|
|
51
|
+
!isPreservedLeadingLineBreak(elements[first])
|
|
52
|
+
) {
|
|
48
53
|
first++;
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -21,7 +21,7 @@ export function parseBlockItem(ctx: ParseContext): BlockItemResult {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const fallback = ctx.blockFallbackRule.parse(ctx);
|
|
24
|
-
if (fallback.success
|
|
24
|
+
if (fallback.success) {
|
|
25
25
|
return { elements: fallback.elements, consumed: fallback.consumed };
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -101,7 +101,7 @@ export function parseBlocksUntil(
|
|
|
101
101
|
blockCtx.scope = blockScope;
|
|
102
102
|
|
|
103
103
|
const result = parseBlockItem(blockCtx);
|
|
104
|
-
|
|
104
|
+
for (const element of result.elements) elements.push(element);
|
|
105
105
|
consumed += result.consumed;
|
|
106
106
|
pos += result.consumed;
|
|
107
107
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { stripAutomaticLineBreak } from "../../inline/parsing/automatic-line-break";
|
|
2
|
+
import { createAutomaticLineBreak } from "../../inline/parsing/automatic-line-break";
|
|
1
3
|
import type { Element } from "@wdprlib/ast";
|
|
2
4
|
import type { ParseContext } from "../../types";
|
|
3
5
|
import { getCandidateInlineRules } from "../../inline/utils";
|
|
@@ -58,7 +60,7 @@ export function parseInlineContentUntil(
|
|
|
58
60
|
consumed += newlineResult.consumed;
|
|
59
61
|
pos += newlineResult.consumed;
|
|
60
62
|
if (newlineResult.addLineBreak) {
|
|
61
|
-
elements.push(
|
|
63
|
+
elements.push(createAutomaticLineBreak(token));
|
|
62
64
|
}
|
|
63
65
|
continue;
|
|
64
66
|
}
|
|
@@ -69,7 +71,7 @@ export function parseInlineContentUntil(
|
|
|
69
71
|
for (const rule of getCandidateBlockRules(blockRules, token)) {
|
|
70
72
|
const result = rule.parse(blockCtx);
|
|
71
73
|
if (result.success) {
|
|
72
|
-
|
|
74
|
+
for (const element of result.elements) elements.push(element);
|
|
73
75
|
consumed += result.consumed;
|
|
74
76
|
pos += result.consumed;
|
|
75
77
|
matched = true;
|
|
@@ -84,7 +86,8 @@ export function parseInlineContentUntil(
|
|
|
84
86
|
for (const rule of getCandidateInlineRules(inlineRules, token.type)) {
|
|
85
87
|
const result = rule.parse(inlineCtx);
|
|
86
88
|
if (result.success) {
|
|
87
|
-
elements
|
|
89
|
+
stripAutomaticLineBreak(elements, result.stripLeadingLineBreak);
|
|
90
|
+
for (const element of result.elements) elements.push(element);
|
|
88
91
|
consumed += result.consumed;
|
|
89
92
|
pos += result.consumed;
|
|
90
93
|
matched = true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripAutomaticLineBreak } from "../../../inline/parsing/automatic-line-break";
|
|
1
2
|
import { protectedInlineRegionEnd } from "../../../inline/raw/end";
|
|
2
3
|
import type { Element, TableCell } from "@wdprlib/ast";
|
|
3
4
|
import type { ParseContext } from "../../../types";
|
|
@@ -85,7 +86,8 @@ export function parseTableCell(
|
|
|
85
86
|
for (const rule of getCandidateInlineRules(inlineRules, token.type)) {
|
|
86
87
|
const result = rule.parse(inlineCtx);
|
|
87
88
|
if (result.success) {
|
|
88
|
-
children
|
|
89
|
+
stripAutomaticLineBreak(children, result.stripLeadingLineBreak);
|
|
90
|
+
for (const element of result.elements) children.push(element);
|
|
89
91
|
consumed += result.consumed;
|
|
90
92
|
pos += result.consumed;
|
|
91
93
|
matched = true;
|
|
@@ -69,7 +69,7 @@ export function parseCellContent(
|
|
|
69
69
|
for (const rule of getCandidateInlineRules(ctx.inlineRules, token.type)) {
|
|
70
70
|
const result = rule.parse(inlineCtx);
|
|
71
71
|
if (result.success) {
|
|
72
|
-
content.addInlineElements(result.elements);
|
|
72
|
+
content.addInlineElements(result.elements, result.stripLeadingLineBreak);
|
|
73
73
|
consumed += result.consumed;
|
|
74
74
|
pos += result.consumed;
|
|
75
75
|
matched = true;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { Token } from "../../../../../lexer";
|
|
2
|
+
import { stripAutomaticLineBreak } from "../../../inline/parsing/automatic-line-break";
|
|
1
3
|
import type { Element } from "@wdprlib/ast";
|
|
2
4
|
|
|
3
5
|
export interface CellContentAccumulator {
|
|
4
6
|
addInline(element: Element): void;
|
|
5
|
-
addInlineElements(elements: Element[]): void;
|
|
7
|
+
addInlineElements(elements: Element[], stripLeadingLineBreak?: Token): void;
|
|
6
8
|
addBlockElements(elements: Element[]): void;
|
|
7
9
|
addParagraphBreak(): void;
|
|
8
10
|
closeInlineSegmentBeforeBlock(): void;
|
|
@@ -40,7 +42,8 @@ export function createCellContentAccumulator(): CellContentAccumulator {
|
|
|
40
42
|
addInline(element: Element) {
|
|
41
43
|
currentSegment.push(element);
|
|
42
44
|
},
|
|
43
|
-
addInlineElements(nextElements: Element[]) {
|
|
45
|
+
addInlineElements(nextElements: Element[], stripLeadingLineBreak?: Token) {
|
|
46
|
+
stripAutomaticLineBreak(currentSegment, stripLeadingLineBreak);
|
|
44
47
|
currentSegment.push(...nextElements);
|
|
45
48
|
},
|
|
46
49
|
addBlockElements(blockElements: Element[]) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAutomaticLineBreak } from "../../inline/parsing/automatic-line-break";
|
|
1
2
|
import type { ParseContext } from "../../types";
|
|
2
3
|
import type { CellContentAccumulator } from "./cell-content/segments";
|
|
3
4
|
|
|
@@ -40,7 +41,7 @@ export function consumeCellContentNewline(
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
if (!content.isEmpty()) {
|
|
43
|
-
content.addInline(
|
|
44
|
+
content.addInline(createAutomaticLineBreak(ctx.tokens[startPos]!));
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
return { consumed };
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
|
-
import type { TokenType } from "../../../lexer";
|
|
2
|
+
import type { Token, TokenType } from "../../../lexer";
|
|
3
3
|
import type { ParseContext } from "./parse-context";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Result of a rule attempt.
|
|
7
7
|
*/
|
|
8
|
-
export type RuleResult<T> =
|
|
8
|
+
export type RuleResult<T> =
|
|
9
|
+
| { success: true; elements: T[]; consumed: number; stripLeadingLineBreak?: Token }
|
|
10
|
+
| { success: false };
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Block rule interface.
|
|
@@ -10,6 +10,8 @@ import type { ParseContext } from "./parse-context";
|
|
|
10
10
|
export interface ScopeContext {
|
|
11
11
|
/** Exclusive token boundary inherited by nested inline rules. */
|
|
12
12
|
readonly inlineEnd?: number;
|
|
13
|
+
/** Keep bare addresses as text inside an existing anchor. */
|
|
14
|
+
readonly suppressEmailLinks?: boolean;
|
|
13
15
|
/** Closing delimiters paired across cells of the current pipe table. */
|
|
14
16
|
readonly tableFormatting?: { end: number; suppressedClosers: Set<number> };
|
|
15
17
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Token } from "../../../../lexer";
|
|
1
2
|
import type { Element } from "@wdprlib/ast";
|
|
2
3
|
import type { ParseContext } from "../../types";
|
|
3
4
|
import { inlineRules } from "../index";
|
|
@@ -6,6 +7,7 @@ import { getCandidateInlineRules } from "../utils";
|
|
|
6
7
|
export interface AnchorChildResult {
|
|
7
8
|
elements: Element[];
|
|
8
9
|
consumed: number;
|
|
10
|
+
stripLeadingLineBreak?: Token;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function parseAnchorChild(ctx: ParseContext, pos: number): AnchorChildResult {
|
|
@@ -14,11 +16,15 @@ export function parseAnchorChild(ctx: ParseContext, pos: number): AnchorChildRes
|
|
|
14
16
|
return { elements: [], consumed: 0 };
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
const inlineCtx: ParseContext = {
|
|
19
|
+
const inlineCtx: ParseContext = {
|
|
20
|
+
...ctx,
|
|
21
|
+
pos,
|
|
22
|
+
scope: { ...ctx.scope, suppressEmailLinks: true },
|
|
23
|
+
};
|
|
18
24
|
for (const rule of getCandidateInlineRules(inlineRules, token.type)) {
|
|
19
25
|
const result = rule.parse(inlineCtx);
|
|
20
26
|
if (result.success) {
|
|
21
|
-
return
|
|
27
|
+
return result;
|
|
22
28
|
}
|
|
23
29
|
}
|
|
24
30
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripAutomaticLineBreak } from "../parsing/automatic-line-break";
|
|
1
2
|
import type { Element } from "@wdprlib/ast";
|
|
2
3
|
import type { ParseContext } from "../../types";
|
|
3
4
|
import { parseAnchorChild } from "./child";
|
|
@@ -50,7 +51,8 @@ export function parseAnchorContent(
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
const child = parseAnchorChild(ctx, pos);
|
|
53
|
-
children
|
|
54
|
+
stripAutomaticLineBreak(children, child.stripLeadingLineBreak);
|
|
55
|
+
for (const element of child.elements) children.push(element);
|
|
54
56
|
pos += child.consumed;
|
|
55
57
|
consumed += child.consumed;
|
|
56
58
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { precedingSingleNewline } from "../parsing/automatic-line-break";
|
|
1
2
|
/**
|
|
2
3
|
*
|
|
3
4
|
* Parses the Wikidot anchor inline block syntax: `[[a]]...[[/a]]`.
|
|
@@ -10,7 +11,8 @@
|
|
|
10
11
|
* - `[[a_ href="url"]]text[[/a]]` -- paragraph strip mode (trailing underscore)
|
|
11
12
|
*
|
|
12
13
|
* Paragraph strip mode (`[[a_]]`) suppresses newlines within the anchor
|
|
13
|
-
* body and strips at most one
|
|
14
|
+
* body and strips at most one automatic newline before the opening tag
|
|
15
|
+
* and one trailing newline after the closing tag
|
|
14
16
|
* (preserving double newlines as paragraph breaks). This prevents
|
|
15
17
|
* unwanted `<br>` elements when consecutive anchor blocks are placed on
|
|
16
18
|
* separate lines.
|
|
@@ -98,6 +100,7 @@ export const anchorRule: InlineRule = {
|
|
|
98
100
|
},
|
|
99
101
|
],
|
|
100
102
|
consumed,
|
|
103
|
+
stripLeadingLineBreak: openResult.paragraphStrip ? precedingSingleNewline(ctx) : undefined,
|
|
101
104
|
};
|
|
102
105
|
},
|
|
103
106
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAutomaticLineBreak } from "../parsing/automatic-line-break";
|
|
1
2
|
import type { Element } from "@wdprlib/ast";
|
|
2
3
|
import type { ParseContext } from "../../types";
|
|
3
4
|
|
|
@@ -15,7 +16,7 @@ export function consumeAnchorNewline(
|
|
|
15
16
|
let nextPos = pos + 1;
|
|
16
17
|
|
|
17
18
|
if (!paragraphStrip) {
|
|
18
|
-
children.push(
|
|
19
|
+
children.push(createAutomaticLineBreak(ctx.tokens[pos]!));
|
|
19
20
|
while (ctx.tokens[nextPos]?.type === "WHITESPACE" && ctx.tokens[nextPos]?.lineStart) {
|
|
20
21
|
nextPos++;
|
|
21
22
|
consumed++;
|