ropegeo-common 1.24.0 → 1.24.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/dist/models/betaSections/betaSection.d.ts +6 -4
- package/dist/models/betaSections/betaSection.d.ts.map +1 -1
- package/dist/models/betaSections/betaSection.js +20 -9
- package/dist/models/betaSections/formatExcerptHtml.d.ts +8 -0
- package/dist/models/betaSections/formatExcerptHtml.d.ts.map +1 -1
- package/dist/models/betaSections/formatExcerptHtml.js +42 -7
- package/dist/models/betaSections/normalizeExcerptHtml.d.ts +10 -13
- package/dist/models/betaSections/normalizeExcerptHtml.d.ts.map +1 -1
- package/dist/models/betaSections/normalizeExcerptHtml.js +21 -39
- package/package.json +1 -1
|
@@ -3,13 +3,15 @@ import { BetaSectionExcerpt } from './betaSectionExcerpt';
|
|
|
3
3
|
import { BetaSectionImage } from './betaSectionImage';
|
|
4
4
|
/**
|
|
5
5
|
* An excerpt split into its verbatim `body` and the surrounding section
|
|
6
|
-
* context.
|
|
7
|
-
*
|
|
6
|
+
* context. Each of `before`, `body`, and `after` is independently well-formed
|
|
7
|
+
* HTML, so consumers can render them as separate (e.g. stacked) blocks and
|
|
8
|
+
* de-emphasize the context relative to the body. `before`/`after` are omitted
|
|
9
|
+
* when there is no surrounding context (or it would be empty/whitespace).
|
|
8
10
|
*/
|
|
9
11
|
export type ExcerptHtml = {
|
|
10
|
-
before
|
|
12
|
+
before?: string;
|
|
11
13
|
body: string;
|
|
12
|
-
after
|
|
14
|
+
after?: string;
|
|
13
15
|
};
|
|
14
16
|
export declare function registerBetaSectionParser(fetchType: FetchType, parse: (body: unknown) => BetaSection): void;
|
|
15
17
|
export declare abstract class BetaSection {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"betaSection.d.ts","sourceRoot":"","sources":["../../../src/models/betaSections/betaSection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAUtD
|
|
1
|
+
{"version":3,"file":"betaSection.d.ts","sourceRoot":"","sources":["../../../src/models/betaSections/betaSection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAUtD;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAIF,wBAAgB,yBAAyB,CACrC,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,WAAW,GACtC,IAAI,CAEN;AAED,8BAAsB,WAAW;IAC7B,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB,EAAE,IAAI,CAAC;IACzB,mEAAmE;IACnE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAElB,SAAS,aACL,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,kBAAkB,EAAE,IAAI,EACxB,EAAE,GAAE,MAAM,GAAG,IAAW;IAS5B;;;OAGG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,kBAAkB;IAWtE;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW;IA8CvD,MAAM,CAAC,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,SAAS,GAAG,WAAW;IA0B1E,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAS9E,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAS9E,SAAS,CAAC,MAAM,CAAC,uBAAuB,CACpC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,GAAG,EAAE,MAAM,GACZ,IAAI;IAeP,SAAS,CAAC,MAAM,CAAC,qBAAqB,CAClC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,OAAO,EAAE,MAAM,EACf,iBAAiB,EAAE,SAAS,GAC7B,IAAI;IAcP,OAAO,CAAC,MAAM,CAAC,4BAA4B;IAgB3C,SAAS,CAAC,MAAM,CAAC,gBAAgB,CAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,GAAG,EAAE,MAAM,EACX,iBAAiB,EAAE,SAAS,GAC7B,gBAAgB,EAAE;IAYrB,SAAS,CAAC,MAAM,CAAC,oBAAoB,CACjC,MAAM,EAAE,gBAAgB,EAAE,EAC1B,SAAS,EAAE,OAAO,GACnB,IAAI;CAeV"}
|
|
@@ -47,20 +47,31 @@ class BetaSection {
|
|
|
47
47
|
(0, formatExcerptHtml_1.isSafeTextBoundary)(this.text, excerpt.start) &&
|
|
48
48
|
(0, formatExcerptHtml_1.isSafeTextBoundary)(this.text, excerpt.end);
|
|
49
49
|
if (!validOffsets)
|
|
50
|
-
return {
|
|
50
|
+
return { body: excerpt.toHtml() };
|
|
51
51
|
const excerptStart = excerpt.start;
|
|
52
52
|
const excerptEnd = excerpt.end;
|
|
53
53
|
const characters = (0, formatExcerptHtml_1.visibleCharacters)(this.text);
|
|
54
54
|
const structuredContext = (0, formatExcerptHtml_1.listContext)(this.text, excerptStart, excerptEnd, characters);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return (0, normalizeExcerptHtml_1.normalizeExcerptListHtmlParts)({
|
|
60
|
-
before,
|
|
55
|
+
const parts = structuredContext ?? {
|
|
56
|
+
before: this.text
|
|
57
|
+
.slice((0, formatExcerptHtml_1.previousSentenceStart)(this.text, excerptStart, characters), excerptStart)
|
|
58
|
+
.trim(),
|
|
61
59
|
body: excerpt.text,
|
|
62
|
-
after
|
|
63
|
-
|
|
60
|
+
after: this.text
|
|
61
|
+
.slice(excerptEnd, (0, formatExcerptHtml_1.nextSentenceEnd)(this.text, excerptEnd, characters))
|
|
62
|
+
.trim(),
|
|
63
|
+
};
|
|
64
|
+
// We have the source list here, so keep ordered lists ordered; their
|
|
65
|
+
// `start` ordinals are already aligned to the source by `listContext`.
|
|
66
|
+
const options = { preserveOrderedLists: true };
|
|
67
|
+
const result = { body: (0, normalizeExcerptHtml_1.normalizeExcerptListHtml)(parts.body, options) };
|
|
68
|
+
const before = (0, normalizeExcerptHtml_1.normalizeExcerptListHtml)(parts.before, options);
|
|
69
|
+
if (before.trim().length > 0)
|
|
70
|
+
result.before = before;
|
|
71
|
+
const after = (0, normalizeExcerptHtml_1.normalizeExcerptListHtml)(parts.after, options);
|
|
72
|
+
if (after.trim().length > 0)
|
|
73
|
+
result.after = after;
|
|
74
|
+
return result;
|
|
64
75
|
}
|
|
65
76
|
static fromResponseBody(body, fetchType) {
|
|
66
77
|
if (body == null || typeof body !== 'object') {
|
|
@@ -7,8 +7,16 @@ export declare function visibleCharacters(html: string): VisibleCharacter[];
|
|
|
7
7
|
export declare function isSafeTextBoundary(html: string, offset: number): boolean;
|
|
8
8
|
export declare function previousSentenceStart(html: string, anchor: number, characters: VisibleCharacter[]): number;
|
|
9
9
|
export declare function nextSentenceEnd(html: string, anchor: number, characters: VisibleCharacter[]): number;
|
|
10
|
+
/**
|
|
11
|
+
* When an excerpt is a run of list items, returns the excerpt and its adjacent
|
|
12
|
+
* context split into three independently well-formed HTML fragments. Each
|
|
13
|
+
* fragment stands on its own: the surrounding list items (or the sentence
|
|
14
|
+
* before/after the list) become complete lists/prose so they can be rendered as
|
|
15
|
+
* separate stacked blocks.
|
|
16
|
+
*/
|
|
10
17
|
export declare function listContext(html: string, excerptStart: number, excerptEnd: number, characters: VisibleCharacter[]): {
|
|
11
18
|
before: string;
|
|
19
|
+
body: string;
|
|
12
20
|
after: string;
|
|
13
21
|
} | null;
|
|
14
22
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatExcerptHtml.d.ts","sourceRoot":"","sources":["../../../src/models/betaSections/formatExcerptHtml.ts"],"names":[],"mappings":"AAAA,KAAK,gBAAgB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAsBzD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAuBlE;AAYD,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAsCxE;AAuCD,wBAAgB,qBAAqB,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,gBAAgB,EAAE,GAC/B,MAAM,CAkBR;AAED,wBAAgB,eAAe,CAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,gBAAgB,EAAE,GAC/B,MAAM,CAuBR;
|
|
1
|
+
{"version":3,"file":"formatExcerptHtml.d.ts","sourceRoot":"","sources":["../../../src/models/betaSections/formatExcerptHtml.ts"],"names":[],"mappings":"AAAA,KAAK,gBAAgB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAsBzD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAuBlE;AAYD,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAsCxE;AAuCD,wBAAgB,qBAAqB,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,gBAAgB,EAAE,GAC/B,MAAM,CAkBR;AAED,wBAAgB,eAAe,CAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,gBAAgB,EAAE,GAC/B,MAAM,CAuBR;AAoED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACvB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,gBAAgB,EAAE,GAC/B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAwDxD"}
|
|
@@ -183,6 +183,33 @@ function findContainingList(html, excerptStart, excerptEnd) {
|
|
|
183
183
|
}
|
|
184
184
|
return (ranges.sort((a, b) => b.openStart - a.openStart || a.closeEnd - b.closeEnd)[0] ?? null);
|
|
185
185
|
}
|
|
186
|
+
/** `ul` or `ol`, parsed from a list's opening tag. */
|
|
187
|
+
function listContainerTag(openTag) {
|
|
188
|
+
return /^<\s*ol\b/i.test(openTag) ? 'ol' : 'ul';
|
|
189
|
+
}
|
|
190
|
+
/** The 1-based ordinal an ordered list's first item renders as (`start`, default 1). */
|
|
191
|
+
function listStartIndex(openTag) {
|
|
192
|
+
const match = openTag.match(/\bstart\s*=\s*["']?\s*(-?\d+)/i);
|
|
193
|
+
const parsed = match != null ? Number.parseInt(match[1], 10) : Number.NaN;
|
|
194
|
+
return Number.isFinite(parsed) ? parsed : 1;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Wraps list items in a standalone `<ul>`/`<ol>`. Ordered lists carry a `start`
|
|
198
|
+
* attribute (whenever the first item is not ordinal 1) so numbering lines up
|
|
199
|
+
* with the source list even when the excerpt parts are rendered as separate
|
|
200
|
+
* blocks.
|
|
201
|
+
*/
|
|
202
|
+
function wrapListItems(tag, itemsHtml, startOrdinal) {
|
|
203
|
+
const startAttr = tag === 'ol' && startOrdinal !== 1 ? ` start="${startOrdinal}"` : '';
|
|
204
|
+
return `<${tag}${startAttr}>${itemsHtml}</${tag}>`;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* When an excerpt is a run of list items, returns the excerpt and its adjacent
|
|
208
|
+
* context split into three independently well-formed HTML fragments. Each
|
|
209
|
+
* fragment stands on its own: the surrounding list items (or the sentence
|
|
210
|
+
* before/after the list) become complete lists/prose so they can be rendered as
|
|
211
|
+
* separate stacked blocks.
|
|
212
|
+
*/
|
|
186
213
|
function listContext(html, excerptStart, excerptEnd, characters) {
|
|
187
214
|
const excerpt = html.slice(excerptStart, excerptEnd).trim();
|
|
188
215
|
if (!/^<li\b/i.test(excerpt) || !/<\/li\s*>$/i.test(excerpt)) {
|
|
@@ -191,6 +218,10 @@ function listContext(html, excerptStart, excerptEnd, characters) {
|
|
|
191
218
|
const range = findContainingList(html, excerptStart, excerptEnd);
|
|
192
219
|
if (range == null)
|
|
193
220
|
return null;
|
|
221
|
+
const openTag = html.slice(range.openStart, range.openEnd);
|
|
222
|
+
const tag = listContainerTag(openTag);
|
|
223
|
+
// Ordinal of the list's first item so ordered excerpts line up with the source.
|
|
224
|
+
const baseOrdinal = tag === 'ol' ? listStartIndex(openTag) : 1;
|
|
194
225
|
const listBody = html.slice(range.openEnd, range.closeStart);
|
|
195
226
|
const items = [...listBody.matchAll(/<li\b[^>]*>[\s\S]*?<\/li\s*>/gi)].map((match) => ({
|
|
196
227
|
start: range.openEnd + match.index,
|
|
@@ -198,13 +229,17 @@ function listContext(html, excerptStart, excerptEnd, characters) {
|
|
|
198
229
|
}));
|
|
199
230
|
const previous = items.filter(({ end }) => end <= excerptStart).at(-1);
|
|
200
231
|
const next = items.find(({ start }) => start >= excerptEnd);
|
|
232
|
+
const firstBodyIndex = items.findIndex(({ start }) => start >= excerptStart);
|
|
233
|
+
const body = wrapListItems(tag, html.slice(excerptStart, excerptEnd), baseOrdinal + firstBodyIndex);
|
|
201
234
|
const before = previous != null
|
|
202
|
-
? html.slice(
|
|
203
|
-
|
|
204
|
-
|
|
235
|
+
? wrapListItems(tag, html.slice(previous.start, previous.end), baseOrdinal + items.indexOf(previous))
|
|
236
|
+
: html
|
|
237
|
+
.slice(previousSentenceStart(html, range.openStart, characters), range.openStart)
|
|
238
|
+
.trim();
|
|
205
239
|
const after = next != null
|
|
206
|
-
? html.slice(next.start, next.end) +
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
240
|
+
? wrapListItems(tag, html.slice(next.start, next.end), baseOrdinal + items.indexOf(next))
|
|
241
|
+
: html
|
|
242
|
+
.slice(range.closeEnd, nextSentenceEnd(html, range.closeEnd, characters))
|
|
243
|
+
.trim();
|
|
244
|
+
return { before, body, after };
|
|
210
245
|
}
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export type NormalizeExcerptListOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* When true, an unpaired closing `</ol>` is repaired by prepending an
|
|
4
|
+
* opening `<ol>` (preserving the ordered list). When false (the default,
|
|
5
|
+
* used for excerpt-only text with no known source order), it is converted
|
|
6
|
+
* to `</ul>` and an opening `<ul>` is prepended instead.
|
|
7
|
+
*/
|
|
8
|
+
preserveOrderedLists?: boolean;
|
|
6
9
|
};
|
|
7
|
-
/**
|
|
8
|
-
* Repairs the list fragments Ropewiki excerpts commonly produce, distributing
|
|
9
|
-
* any added opening tags to `before` and closing tags to `after` so the three
|
|
10
|
-
* parts remain a well-formed list when concatenated as `before + body + after`.
|
|
11
|
-
*/
|
|
12
|
-
export declare function normalizeExcerptListHtmlParts(parts: ExcerptHtmlParts): ExcerptHtmlParts;
|
|
13
10
|
/**
|
|
14
11
|
* Repairs the list fragments Ropewiki excerpts commonly produce without
|
|
15
|
-
* otherwise rewriting the source HTML.
|
|
12
|
+
* otherwise rewriting the source HTML, so the result is well-formed on its own.
|
|
16
13
|
*/
|
|
17
|
-
export declare function normalizeExcerptListHtml(html: string): string;
|
|
14
|
+
export declare function normalizeExcerptListHtml(html: string, { preserveOrderedLists }?: NormalizeExcerptListOptions): string;
|
|
18
15
|
//# sourceMappingURL=normalizeExcerptHtml.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalizeExcerptHtml.d.ts","sourceRoot":"","sources":["../../../src/models/betaSections/normalizeExcerptHtml.ts"],"names":[],"mappings":"AAIA,
|
|
1
|
+
{"version":3,"file":"normalizeExcerptHtml.d.ts","sourceRoot":"","sources":["../../../src/models/betaSections/normalizeExcerptHtml.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,2BAA2B,GAAG;IACtC;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAClC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,wBAAwB,CACpC,IAAI,EAAE,MAAM,EACZ,EAAE,oBAA4B,EAAE,GAAE,2BAAgC,GACnE,MAAM,CA+BR"}
|
|
@@ -1,59 +1,41 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeExcerptListHtmlParts = normalizeExcerptListHtmlParts;
|
|
4
3
|
exports.normalizeExcerptListHtml = normalizeExcerptListHtml;
|
|
5
4
|
function countTags(html, pattern) {
|
|
6
5
|
return html.match(pattern)?.length ?? 0;
|
|
7
6
|
}
|
|
8
7
|
/**
|
|
9
|
-
* Repairs the list fragments Ropewiki excerpts commonly produce
|
|
10
|
-
*
|
|
11
|
-
* parts remain a well-formed list when concatenated as `before + body + after`.
|
|
8
|
+
* Repairs the list fragments Ropewiki excerpts commonly produce without
|
|
9
|
+
* otherwise rewriting the source HTML, so the result is well-formed on its own.
|
|
12
10
|
*/
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
before = '<ul>'.repeat(olCloseCount) + before;
|
|
11
|
+
function normalizeExcerptListHtml(html, { preserveOrderedLists = false } = {}) {
|
|
12
|
+
if (html.length === 0)
|
|
13
|
+
return html;
|
|
14
|
+
let normalized = html;
|
|
15
|
+
const olOpenCount = countTags(normalized, /<ol\b[^>]*>/gi);
|
|
16
|
+
const olCloseCount = countTags(normalized, /<\/ol\s*>/gi);
|
|
17
|
+
if (!preserveOrderedLists && olOpenCount === 0 && olCloseCount > 0) {
|
|
18
|
+
normalized = normalized.replace(/<\/ol\s*>/gi, '</ul>');
|
|
19
|
+
normalized = '<ul>'.repeat(olCloseCount) + normalized;
|
|
23
20
|
}
|
|
24
21
|
else if (olOpenCount > olCloseCount) {
|
|
25
|
-
|
|
22
|
+
normalized += '</ol>'.repeat(olOpenCount - olCloseCount);
|
|
26
23
|
}
|
|
27
24
|
else if (olCloseCount > olOpenCount) {
|
|
28
|
-
|
|
25
|
+
normalized = '<ol>'.repeat(olCloseCount - olOpenCount) + normalized;
|
|
29
26
|
}
|
|
30
|
-
const ulOpenCount = countTags(
|
|
31
|
-
const ulCloseCount = countTags(
|
|
27
|
+
const ulOpenCount = countTags(normalized, /<ul\b[^>]*>/gi);
|
|
28
|
+
const ulCloseCount = countTags(normalized, /<\/ul\s*>/gi);
|
|
32
29
|
if (ulOpenCount > ulCloseCount) {
|
|
33
|
-
|
|
30
|
+
normalized += '</ul>'.repeat(ulOpenCount - ulCloseCount);
|
|
34
31
|
}
|
|
35
32
|
else if (ulCloseCount > ulOpenCount) {
|
|
36
|
-
|
|
33
|
+
normalized = '<ul>'.repeat(ulCloseCount - ulOpenCount) + normalized;
|
|
37
34
|
}
|
|
38
|
-
const hasListItem = /<li\b[^>]*>/i.test(
|
|
39
|
-
const hasListContainer = /<\/?(?:ul|ol)\b[^>]*>/i.test(
|
|
35
|
+
const hasListItem = /<li\b[^>]*>/i.test(normalized);
|
|
36
|
+
const hasListContainer = /<\/?(?:ul|ol)\b[^>]*>/i.test(normalized);
|
|
40
37
|
if (hasListItem && !hasListContainer) {
|
|
41
|
-
|
|
42
|
-
after = `${after}</ul>`;
|
|
38
|
+
normalized = `<ul>${normalized}</ul>`;
|
|
43
39
|
}
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Repairs the list fragments Ropewiki excerpts commonly produce without
|
|
48
|
-
* otherwise rewriting the source HTML.
|
|
49
|
-
*/
|
|
50
|
-
function normalizeExcerptListHtml(html) {
|
|
51
|
-
if (html.length === 0)
|
|
52
|
-
return html;
|
|
53
|
-
const { before, body, after } = normalizeExcerptListHtmlParts({
|
|
54
|
-
before: '',
|
|
55
|
-
body: html,
|
|
56
|
-
after: '',
|
|
57
|
-
});
|
|
58
|
-
return before + body + after;
|
|
40
|
+
return normalized;
|
|
59
41
|
}
|