@link-assistant/web-capture 1.7.17 → 1.7.18
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 +6 -0
- package/package.json +1 -1
- package/src/gdocs.js +147 -19
- package/tests/integration/gdocs-public-doc.test.js +63 -4
- package/tests/unit/gdocs.test.js +108 -3
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/gdocs.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import fetch from 'node-fetch';
|
|
11
11
|
import he from 'he';
|
|
12
|
+
import * as cheerio from 'cheerio';
|
|
12
13
|
import { convertHtmlToMarkdown } from './lib.js';
|
|
13
14
|
import { createBrowser as defaultCreateBrowser } from './browser.js';
|
|
14
15
|
import {
|
|
@@ -200,11 +201,10 @@ export async function fetchGoogleDoc(url, options = {}) {
|
|
|
200
201
|
bytes: Buffer.byteLength(rawContent),
|
|
201
202
|
}));
|
|
202
203
|
|
|
203
|
-
//
|
|
204
|
+
// Keep HTML markup escaped so literal examples such as `<ol>` do not
|
|
205
|
+
// become real tags before the HTML parser sees the document.
|
|
204
206
|
const content =
|
|
205
|
-
format === '
|
|
206
|
-
? he.decode(rawContent)
|
|
207
|
-
: rawContent;
|
|
207
|
+
format === 'txt' || format === 'md' ? he.decode(rawContent) : rawContent;
|
|
208
208
|
|
|
209
209
|
return {
|
|
210
210
|
content,
|
|
@@ -430,11 +430,16 @@ export async function captureGoogleDocWithBrowser(url, options = {}) {
|
|
|
430
430
|
documentId,
|
|
431
431
|
log,
|
|
432
432
|
});
|
|
433
|
+
const exportHtml = await fetchGoogleDocsExportHtmlFromPage(
|
|
434
|
+
page,
|
|
435
|
+
documentId,
|
|
436
|
+
log
|
|
437
|
+
);
|
|
433
438
|
|
|
434
439
|
const capture = parseGoogleDocsModelChunks(
|
|
435
440
|
modelData.chunks,
|
|
436
441
|
modelData.cidUrlMap,
|
|
437
|
-
{ log }
|
|
442
|
+
{ log, exportHtml }
|
|
438
443
|
);
|
|
439
444
|
log?.debug?.(() => ({
|
|
440
445
|
event: 'gdocs.browser-model.parsed',
|
|
@@ -567,6 +572,7 @@ export function extractCidUrlMapFromHtml(html) {
|
|
|
567
572
|
* @param {Object<string,string>} [cidUrlMap={}] - CID-to-docs-images-rt URL map
|
|
568
573
|
* @param {Object} [options] - Parse options
|
|
569
574
|
* @param {Object} [options.log] - Optional verbose logger
|
|
575
|
+
* @param {string} [options.exportHtml] - Optional Google Docs export HTML used for list and quote semantics
|
|
570
576
|
* @returns {{blocks: Array, tables: Array, images: Array, text: string}}
|
|
571
577
|
*/
|
|
572
578
|
export function parseGoogleDocsModelChunks(
|
|
@@ -813,12 +819,17 @@ export function parseGoogleDocsModelChunks(
|
|
|
813
819
|
}
|
|
814
820
|
}
|
|
815
821
|
|
|
816
|
-
|
|
822
|
+
const capture = {
|
|
817
823
|
blocks,
|
|
818
824
|
tables,
|
|
819
825
|
images,
|
|
820
826
|
text: contentBlocksToText(blocks),
|
|
821
827
|
};
|
|
828
|
+
if (options.exportHtml) {
|
|
829
|
+
applyGoogleDocsExportSemanticHints(capture, options.exportHtml);
|
|
830
|
+
capture.text = contentBlocksToText(capture.blocks);
|
|
831
|
+
}
|
|
832
|
+
return capture;
|
|
822
833
|
}
|
|
823
834
|
|
|
824
835
|
function cellIsEmpty(cell) {
|
|
@@ -889,13 +900,13 @@ function applyInlineTextStyle(inlineStyles, start, end, patch) {
|
|
|
889
900
|
|
|
890
901
|
function textStyleFromModel(item) {
|
|
891
902
|
const style = {};
|
|
892
|
-
if (item.sm?.ts_bd) {
|
|
903
|
+
if (item.sm?.ts_bd && item.sm?.ts_bd_i !== true) {
|
|
893
904
|
style.bold = true;
|
|
894
905
|
}
|
|
895
|
-
if (item.sm?.ts_it) {
|
|
906
|
+
if (item.sm?.ts_it && item.sm?.ts_it_i !== true) {
|
|
896
907
|
style.italic = true;
|
|
897
908
|
}
|
|
898
|
-
if (item.sm?.ts_st) {
|
|
909
|
+
if (item.sm?.ts_st && item.sm?.ts_st_i !== true) {
|
|
899
910
|
style.strike = true;
|
|
900
911
|
}
|
|
901
912
|
return style;
|
|
@@ -944,19 +955,136 @@ function paragraphMetaForEndPosition(styleMaps, endPos, text) {
|
|
|
944
955
|
}
|
|
945
956
|
|
|
946
957
|
function inferOrderedList(list, text) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
// item labels that are demonstrably numbered in the reference document.
|
|
950
|
-
if (
|
|
951
|
-
/ordered|^\d+(?:\.|\))|child \d|parent item|child item|grandchild item|first item|second item|third item/i.test(
|
|
952
|
-
text
|
|
953
|
-
)
|
|
954
|
-
) {
|
|
955
|
-
return /^kix\.list\.(7|8|9|10|11|13)$/u.test(list.id);
|
|
956
|
-
}
|
|
958
|
+
void list;
|
|
959
|
+
void text;
|
|
957
960
|
return false;
|
|
958
961
|
}
|
|
959
962
|
|
|
963
|
+
async function fetchGoogleDocsExportHtmlFromPage(page, documentId, log) {
|
|
964
|
+
const exportUrl = buildExportUrl(documentId, 'html');
|
|
965
|
+
try {
|
|
966
|
+
const result = await page.evaluate(async (url) => {
|
|
967
|
+
const response = await fetch(url, {
|
|
968
|
+
credentials: 'include',
|
|
969
|
+
redirect: 'follow',
|
|
970
|
+
});
|
|
971
|
+
return {
|
|
972
|
+
ok: response.ok,
|
|
973
|
+
status: response.status,
|
|
974
|
+
statusText: response.statusText,
|
|
975
|
+
contentType: response.headers.get('content-type'),
|
|
976
|
+
body: await response.text(),
|
|
977
|
+
};
|
|
978
|
+
}, exportUrl);
|
|
979
|
+
log?.debug?.(() => ({
|
|
980
|
+
event: 'gdocs.browser-model.export-hints.response',
|
|
981
|
+
documentId,
|
|
982
|
+
exportUrl,
|
|
983
|
+
status: result.status,
|
|
984
|
+
ok: result.ok,
|
|
985
|
+
contentType: result.contentType,
|
|
986
|
+
bytes: Buffer.byteLength(result.body || ''),
|
|
987
|
+
}));
|
|
988
|
+
return result.ok ? result.body || '' : null;
|
|
989
|
+
} catch (error) {
|
|
990
|
+
log?.debug?.(() => ({
|
|
991
|
+
event: 'gdocs.browser-model.export-hints.failed',
|
|
992
|
+
documentId,
|
|
993
|
+
exportUrl,
|
|
994
|
+
error: error?.message || String(error),
|
|
995
|
+
}));
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function applyGoogleDocsExportSemanticHints(capture, exportHtml) {
|
|
1001
|
+
const hints = extractGoogleDocsExportSemanticHints(exportHtml);
|
|
1002
|
+
let cursor = 0;
|
|
1003
|
+
for (const block of capture.blocks || []) {
|
|
1004
|
+
if (block.type !== 'paragraph') {
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const text = normalizeSemanticText(contentToText(block.content));
|
|
1008
|
+
if (!text) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
const match = findNextSemanticHint(
|
|
1012
|
+
hints,
|
|
1013
|
+
text,
|
|
1014
|
+
cursor,
|
|
1015
|
+
Boolean(block.list)
|
|
1016
|
+
);
|
|
1017
|
+
if (!match) {
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
cursor = match.index + 1;
|
|
1021
|
+
if (block.list && match.hint.listOrdered !== null) {
|
|
1022
|
+
block.list.ordered = match.hint.listOrdered;
|
|
1023
|
+
} else if (!block.list) {
|
|
1024
|
+
block.quote = match.hint.quote;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function findNextSemanticHint(hints, text, cursor, needsListHint) {
|
|
1030
|
+
for (let index = cursor; index < hints.length; index++) {
|
|
1031
|
+
const hint = hints[index];
|
|
1032
|
+
if (hint.text !== text) {
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
if (needsListHint && hint.listOrdered === null) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (!needsListHint && hint.listOrdered !== null) {
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
return { index, hint };
|
|
1042
|
+
}
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function extractGoogleDocsExportSemanticHints(exportHtml) {
|
|
1047
|
+
const preprocessed = preprocessGoogleDocsExportHtml(exportHtml).html;
|
|
1048
|
+
const $ = cheerio.load(preprocessed);
|
|
1049
|
+
const hints = [];
|
|
1050
|
+
$('body')
|
|
1051
|
+
.find('h1,h2,h3,h4,h5,h6,p,li')
|
|
1052
|
+
.each((_, element) => {
|
|
1053
|
+
const tag = element.tagName?.toLowerCase();
|
|
1054
|
+
const text = getExportElementSemanticText($, element, tag);
|
|
1055
|
+
if (!text) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const parentList = $(element).parents('ol,ul').first();
|
|
1059
|
+
const listOrdered =
|
|
1060
|
+
tag === 'li' && parentList.length
|
|
1061
|
+
? parentList[0].tagName?.toLowerCase() === 'ol'
|
|
1062
|
+
: null;
|
|
1063
|
+
hints.push({
|
|
1064
|
+
text,
|
|
1065
|
+
listOrdered,
|
|
1066
|
+
quote: tag !== 'li' && $(element).parents('blockquote').length > 0,
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
return hints;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function getExportElementSemanticText($, element, tag) {
|
|
1073
|
+
if (tag !== 'li') {
|
|
1074
|
+
return normalizeSemanticText($(element).text());
|
|
1075
|
+
}
|
|
1076
|
+
const clone = $(element).clone();
|
|
1077
|
+
clone.find('ol,ul').remove();
|
|
1078
|
+
return normalizeSemanticText(clone.text());
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function normalizeSemanticText(text) {
|
|
1082
|
+
return String(text || '')
|
|
1083
|
+
.replace(/\u00a0/g, ' ')
|
|
1084
|
+
.replace(/\s+/g, ' ')
|
|
1085
|
+
.trim();
|
|
1086
|
+
}
|
|
1087
|
+
|
|
960
1088
|
function collectModelItems(chunks) {
|
|
961
1089
|
const items = [];
|
|
962
1090
|
for (const chunk of chunks || []) {
|
|
@@ -86,6 +86,33 @@ export const PUBLIC_TEST_DOCUMENT = {
|
|
|
86
86
|
],
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
// The issue #108 v2 document extends the public reference fixture with Section
|
|
90
|
+
// 15, covering browser-model list type and continuation-paragraph regressions.
|
|
91
|
+
export const PUBLIC_TEST_DOCUMENT_V2 = {
|
|
92
|
+
id: '1Rvaod_u2wgkAUNdXG-e29yQem-P36KAQ',
|
|
93
|
+
canonicalUrl:
|
|
94
|
+
'https://docs.google.com/document/d/1Rvaod_u2wgkAUNdXG-e29yQem-P36KAQ/edit',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function expectIssue108Markdown(markdown) {
|
|
98
|
+
expect(markdown).toContain(
|
|
99
|
+
'15. List & Blockquote Edge Cases (browser-mode reproducers)'
|
|
100
|
+
);
|
|
101
|
+
expect(markdown).toMatch(/1\.\s+Apple/u);
|
|
102
|
+
expect(markdown).toMatch(/2\.\s+Banana/u);
|
|
103
|
+
expect(markdown).toMatch(/3\.\s+Cherry/u);
|
|
104
|
+
expect(markdown).not.toMatch(/^-\s+Apple$/mu);
|
|
105
|
+
expect(markdown).toContain(
|
|
106
|
+
'Continuation paragraph that is not a list item. Same indent as Step one.'
|
|
107
|
+
);
|
|
108
|
+
expect(markdown).not.toContain(
|
|
109
|
+
'> Continuation paragraph that is not a list item.'
|
|
110
|
+
);
|
|
111
|
+
expect(markdown).toMatch(/^-\s+Red$/mu);
|
|
112
|
+
expect(markdown).toMatch(/^-\s+Green$/mu);
|
|
113
|
+
expect(markdown).toMatch(/^-\s+Blue$/mu);
|
|
114
|
+
}
|
|
115
|
+
|
|
89
116
|
describe('Google Docs public test document (issue #90)', () => {
|
|
90
117
|
describe('URL variations', () => {
|
|
91
118
|
it.each(PUBLIC_TEST_DOCUMENT.urlVariations)(
|
|
@@ -181,11 +208,19 @@ describe('Google Docs public test document (issue #90)', () => {
|
|
|
181
208
|
baseDelay: 2000,
|
|
182
209
|
});
|
|
183
210
|
|
|
211
|
+
const browserEngine =
|
|
212
|
+
process.env.GDOCS_BROWSER_ENGINE || process.env.BROWSER_ENGINE;
|
|
184
213
|
const captureBrowserWithRetry = (url) =>
|
|
185
|
-
retry(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
214
|
+
retry(
|
|
215
|
+
() =>
|
|
216
|
+
captureGoogleDocWithBrowser(url, {
|
|
217
|
+
...(browserEngine ? { engine: browserEngine } : {}),
|
|
218
|
+
}),
|
|
219
|
+
{
|
|
220
|
+
retries: 2,
|
|
221
|
+
baseDelay: 2000,
|
|
222
|
+
}
|
|
223
|
+
);
|
|
189
224
|
|
|
190
225
|
it('fetches the public document via --capture api and returns markdown', async () => {
|
|
191
226
|
const { markdown, documentId, exportUrl } = await fetchWithRetry(
|
|
@@ -252,5 +287,29 @@ describe('Google Docs public test document (issue #90)', () => {
|
|
|
252
287
|
expect(markdown).toContain('docs-images-rt/');
|
|
253
288
|
expect(markdown).toContain('---');
|
|
254
289
|
});
|
|
290
|
+
|
|
291
|
+
it('fetches the issue #108 v2 document via --capture api and preserves Section 15 semantics', async () => {
|
|
292
|
+
const { markdown, documentId, exportUrl } = await fetchWithRetry(
|
|
293
|
+
PUBLIC_TEST_DOCUMENT_V2.canonicalUrl
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(documentId).toBe(PUBLIC_TEST_DOCUMENT_V2.id);
|
|
297
|
+
expect(exportUrl).toBe(
|
|
298
|
+
buildExportUrl(PUBLIC_TEST_DOCUMENT_V2.id, 'html')
|
|
299
|
+
);
|
|
300
|
+
expectIssue108Markdown(markdown);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('captures the issue #108 v2 document via --capture browser and preserves Section 15 semantics', async () => {
|
|
304
|
+
const { markdown, documentId, exportUrl, capture } =
|
|
305
|
+
await captureBrowserWithRetry(PUBLIC_TEST_DOCUMENT_V2.canonicalUrl);
|
|
306
|
+
|
|
307
|
+
expect(documentId).toBe(PUBLIC_TEST_DOCUMENT_V2.id);
|
|
308
|
+
expect(exportUrl).toBe(buildEditUrl(PUBLIC_TEST_DOCUMENT_V2.id));
|
|
309
|
+
expect(capture.blocks.length).toBeGreaterThan(30);
|
|
310
|
+
expectIssue108Markdown(markdown);
|
|
311
|
+
expect(markdown).not.toContain('*Apple*');
|
|
312
|
+
expect(markdown).not.toContain('*Red*');
|
|
313
|
+
});
|
|
255
314
|
});
|
|
256
315
|
});
|
package/tests/unit/gdocs.test.js
CHANGED
|
@@ -431,7 +431,16 @@ describe('gdocs', () => {
|
|
|
431
431
|
],
|
|
432
432
|
},
|
|
433
433
|
];
|
|
434
|
-
const
|
|
434
|
+
const exportHtml = `
|
|
435
|
+
<html><body>
|
|
436
|
+
<ol>
|
|
437
|
+
<li>First item</li>
|
|
438
|
+
<li>Second item</li>
|
|
439
|
+
<li>Third item</li>
|
|
440
|
+
</ol>
|
|
441
|
+
</body></html>
|
|
442
|
+
`;
|
|
443
|
+
const capture = parseGoogleDocsModelChunks(chunks, {}, { exportHtml });
|
|
435
444
|
const markdown = renderGoogleDocsCapture(capture, 'markdown');
|
|
436
445
|
const lines = markdown.split('\n');
|
|
437
446
|
|
|
@@ -465,7 +474,15 @@ describe('gdocs', () => {
|
|
|
465
474
|
],
|
|
466
475
|
},
|
|
467
476
|
];
|
|
468
|
-
const
|
|
477
|
+
const exportHtml = `
|
|
478
|
+
<html><body>
|
|
479
|
+
<ol>
|
|
480
|
+
<li>First item</li>
|
|
481
|
+
<li>Second item</li>
|
|
482
|
+
</ol>
|
|
483
|
+
</body></html>
|
|
484
|
+
`;
|
|
485
|
+
const capture = parseGoogleDocsModelChunks(chunks, {}, { exportHtml });
|
|
469
486
|
const markdown = renderGoogleDocsCapture(capture, 'markdown');
|
|
470
487
|
|
|
471
488
|
expect(markdown).not.toMatch(/First item\n\n2\. Second item/u);
|
|
@@ -540,7 +557,10 @@ describe('gdocs', () => {
|
|
|
540
557
|
],
|
|
541
558
|
},
|
|
542
559
|
];
|
|
543
|
-
const
|
|
560
|
+
const exportHtml = `
|
|
561
|
+
<html><body><ol><li>Parent item 1<ol><li>Child item 1.1</li><li>Child item 1.2<ol><li>Grandchild item 1.2.1</li><li>Grandchild item 1.2.2</li></ol></li><li>Child item 1.3</li></ol></li><li>Parent item 2</li></ol></body></html>
|
|
562
|
+
`;
|
|
563
|
+
const capture = parseGoogleDocsModelChunks(chunks, {}, { exportHtml });
|
|
544
564
|
const markdown = renderGoogleDocsCapture(capture, 'markdown');
|
|
545
565
|
|
|
546
566
|
expect(markdown).toContain(
|
|
@@ -558,6 +578,91 @@ describe('gdocs', () => {
|
|
|
558
578
|
expect(markdown).not.toContain('- Child item 1.1');
|
|
559
579
|
});
|
|
560
580
|
|
|
581
|
+
it('uses export semantics for ambiguous browser-model lists and continuation paragraphs (issue #108)', () => {
|
|
582
|
+
const text = [
|
|
583
|
+
'Apple',
|
|
584
|
+
'Banana',
|
|
585
|
+
'Cherry',
|
|
586
|
+
'Step one',
|
|
587
|
+
'Continuation paragraph that is not a list item. Same indent as Step one.',
|
|
588
|
+
'Step two',
|
|
589
|
+
'Red',
|
|
590
|
+
'Green',
|
|
591
|
+
'Blue',
|
|
592
|
+
'',
|
|
593
|
+
].join('\n');
|
|
594
|
+
const lineEnd = (needle) => text.indexOf('\n', text.indexOf(needle)) + 1;
|
|
595
|
+
const listStyle = (needle, id) => ({
|
|
596
|
+
ty: 'as',
|
|
597
|
+
st: 'list',
|
|
598
|
+
si: lineEnd(needle),
|
|
599
|
+
ei: lineEnd(needle),
|
|
600
|
+
sm: { ls_id: id },
|
|
601
|
+
});
|
|
602
|
+
const paragraphStyle = (needle, indentStart, indentFirstLine) => ({
|
|
603
|
+
ty: 'as',
|
|
604
|
+
st: 'paragraph',
|
|
605
|
+
si: lineEnd(needle),
|
|
606
|
+
ei: lineEnd(needle),
|
|
607
|
+
sm: { ps_il: indentStart, ps_ifl: indentFirstLine },
|
|
608
|
+
});
|
|
609
|
+
const chunks = [
|
|
610
|
+
{
|
|
611
|
+
chunk: [
|
|
612
|
+
{ ty: 'is', s: text },
|
|
613
|
+
listStyle('Apple', 'kix.ordered'),
|
|
614
|
+
listStyle('Banana', 'kix.ordered'),
|
|
615
|
+
listStyle('Cherry', 'kix.ordered'),
|
|
616
|
+
listStyle('Step one', 'kix.step-one'),
|
|
617
|
+
paragraphStyle(
|
|
618
|
+
'Continuation paragraph that is not a list item',
|
|
619
|
+
36,
|
|
620
|
+
36
|
|
621
|
+
),
|
|
622
|
+
listStyle('Step two', 'kix.step-two'),
|
|
623
|
+
listStyle('Red', 'kix.bullets'),
|
|
624
|
+
listStyle('Green', 'kix.bullets'),
|
|
625
|
+
listStyle('Blue', 'kix.bullets'),
|
|
626
|
+
{
|
|
627
|
+
ty: 'as',
|
|
628
|
+
st: 'text',
|
|
629
|
+
si: 1,
|
|
630
|
+
ei: text.length - 1,
|
|
631
|
+
sm: { ts_it: true, ts_it_i: true },
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
},
|
|
635
|
+
];
|
|
636
|
+
const exportHtml = `
|
|
637
|
+
<html><body>
|
|
638
|
+
<ol><li>Apple</li><li>Banana</li><li>Cherry</li></ol>
|
|
639
|
+
<ol><li>Step one</li></ol>
|
|
640
|
+
<p>Continuation paragraph that is not a list item. Same indent as Step one.</p>
|
|
641
|
+
<ol><li>Step two</li></ol>
|
|
642
|
+
<ul><li>Red</li><li>Green</li><li>Blue</li></ul>
|
|
643
|
+
</body></html>
|
|
644
|
+
`;
|
|
645
|
+
const capture = parseGoogleDocsModelChunks(chunks, {}, { exportHtml });
|
|
646
|
+
const markdown = renderGoogleDocsCapture(capture, 'markdown');
|
|
647
|
+
const html = renderGoogleDocsCapture(capture, 'html');
|
|
648
|
+
|
|
649
|
+
expect(markdown).toContain('1. Apple\n2. Banana\n3. Cherry');
|
|
650
|
+
expect(markdown).not.toContain('- Apple');
|
|
651
|
+
expect(markdown).toContain(
|
|
652
|
+
'Continuation paragraph that is not a list item. Same indent as Step one.'
|
|
653
|
+
);
|
|
654
|
+
expect(markdown).not.toContain(
|
|
655
|
+
'> Continuation paragraph that is not a list item.'
|
|
656
|
+
);
|
|
657
|
+
expect(markdown).toContain('- Red\n- Green\n- Blue');
|
|
658
|
+
expect(markdown).not.toContain('*Apple*');
|
|
659
|
+
expect(html).toContain('<ol><li>Apple</li></ol>');
|
|
660
|
+
expect(html).toContain('<ul><li>Red</li></ul>');
|
|
661
|
+
expect(html).not.toContain(
|
|
662
|
+
'<blockquote>Continuation paragraph that is not a list item.'
|
|
663
|
+
);
|
|
664
|
+
});
|
|
665
|
+
|
|
561
666
|
it('renders model style records for headings, inline formatting, links, lists, blockquotes, rules, and images', () => {
|
|
562
667
|
const text = [
|
|
563
668
|
'Title',
|