diff --git a/packages/zenn-cli/src/server/__tests__/preview/app.test.ts b/packages/zenn-cli/src/server/__tests__/preview/app.test.ts index 30de9e01..4a3e6585 100644 --- a/packages/zenn-cli/src/server/__tests__/preview/app.test.ts +++ b/packages/zenn-cli/src/server/__tests__/preview/app.test.ts @@ -62,7 +62,7 @@ describe('/api/articles/:slug', () => { type: 'tech', topics: [], published: true, - bodyHtml: expect.stringContaining('

Hello!

'), + bodyHtml: expect.stringMatching(/Hello!<\/p>/), }) ); }); @@ -216,7 +216,7 @@ describe('/api/books/:book_slug/chapters/:chapter_filename', () => { title: 'title2', free: true, position: 0, - bodyHtml: expect.stringContaining('

Hello!

'), + bodyHtml: expect.stringMatching(/Hello!<\/p>/), }) ); }); diff --git a/packages/zenn-markdown-html/__tests__/basic.test.ts b/packages/zenn-markdown-html/__tests__/basic.test.ts index a809afb9..32c19e00 100644 --- a/packages/zenn-markdown-html/__tests__/basic.test.ts +++ b/packages/zenn-markdown-html/__tests__/basic.test.ts @@ -1,14 +1,23 @@ import { describe, test, expect } from 'vitest'; import markdownToHtml from '../src/index'; +import { parse } from 'node-html-parser'; describe('MarkdownからHTMLへの変換テスト', () => { test('markdownからhtmlへ変換する', () => { const html = markdownToHtml('Hello\n## hey\n\n- first\n- second\n'); - expect(html).toContain(`

Hello

`); - expect(html).toContain( - `

hey

` + const p = parse(html).querySelector('p'); + const h2 = parse(html).querySelector('h2'); + const ul = parse(html).querySelector('ul'); + const liElms = parse(html).querySelectorAll('li'); + + expect(p?.innerHTML).toBe('Hello'); + expect(h2?.innerHTML).toBe( + ' hey' ); - expect(html).toContain(`\n`); + expect(ul).not.toBeNull(); + expect(liElms?.length).toBe(2); + expect(liElms[0].innerHTML).toBe('first'); + expect(liElms[1].innerHTML).toBe('second'); }); test('インラインコメントはhtmlに変換しない', () => { diff --git a/packages/zenn-markdown-html/__tests__/br.test.ts b/packages/zenn-markdown-html/__tests__/br.test.ts index ef0e44cb..b9058dad 100644 --- a/packages/zenn-markdown-html/__tests__/br.test.ts +++ b/packages/zenn-markdown-html/__tests__/br.test.ts @@ -6,7 +6,7 @@ describe('
のテスト', () => { const patterns = ['foo
bar', 'foo
bar', 'foo
bar']; patterns.forEach((pattern) => { const html = markdownToHtml(pattern); - expect(html).toMatch(/

foo
bar<\/p>/); + expect(html).toContain('foo
bar'); }); }); test('テーブル内の
は保持する', () => { @@ -20,7 +20,7 @@ describe('
のテスト', () => { }); test('インラインコード内の
はエスケープする', () => { const html = markdownToHtml('foo`
`bar'); - expect(html).toMatch(/

foo<br><\/code>bar<\/p>/); + expect(html).toContain('foo<br>bar'); }); test('コードブロック内の
はエスケープする', () => { const html = markdownToHtml('```\n
\n```'); diff --git a/packages/zenn-markdown-html/__tests__/dollar.test.ts b/packages/zenn-markdown-html/__tests__/dollar.test.ts index 083d141c..98d97b51 100644 --- a/packages/zenn-markdown-html/__tests__/dollar.test.ts +++ b/packages/zenn-markdown-html/__tests__/dollar.test.ts @@ -4,22 +4,22 @@ import markdownToHtml from '../src/index'; describe('$ マークのテスト', () => { test('リンクと同じ行にある $ は katex に変換される', () => { const html = markdownToHtml('$a,b,c$foo[foo](https://foo.bar)bar'); - expect(html).toEqual( - '

a,b,cfoofoobar

\n' + expect(html).toContain( + 'a,b,cfoofoobar' ); }); test('リンクの後に無効な $ が続く場合はそのままにする', () => { const html = markdownToHtml('$a,b,c$foo[foo](http://foo.bar)$bar'); - expect(html).toEqual( - '

a,b,cfoofoo$bar

\n' + expect(html).toContain( + 'a,b,cfoofoo$bar' ); }); test('リンク名に $ が含まれる場合はそのままにする', () => { const html = markdownToHtml('$a,b,c$foo[$bar](http://foo.bar)bar'); - expect(html).toEqual( - '

a,b,cfoo$barbar

\n' + expect(html).toContain( + 'a,b,cfoo$barbar' ); }); test('リンクのhrefに $ が含まれる場合はそのままにする', () => { @@ -35,8 +35,8 @@ describe('$ マークのテスト', () => { describe('HTMLタグにエスケープするテスト', () => { test('katex内のをエスケープする', () => { const html = markdownToHtml('$a,,c$'); - expect(html).toEqual( - `

a,<script>alert("XSS")</script>,c

\n` + expect(html).toContain( + `a,<script>alert("XSS")</script>,c` ); }); }); @@ -44,36 +44,36 @@ describe('HTMLタグにエスケープするテスト', () => { describe('$ のペアのテスト', () => { test('リンクの前後にある一文字だけを含む$のペアをkatexに変換する', () => { const html = markdownToHtml('$a$foo[foo](https://foo.bar)bar,refs:$(2)$'); - expect(html).toEqual( - '

afoofoobar,refs:(2)

\n' + expect(html).toContain( + 'afoofoobar,refs:(2)' ); }); test('リンク前後にある$のペアをkatexに変換する', () => { const html = markdownToHtml( '$a,b,c$foo[foo](https://foo.bar)bar,refs:$(2)$' ); - expect(html).toEqual( - '

a,b,cfoofoobar,refs:(2)

\n' + expect(html).toContain( + 'a,b,cfoofoobar,refs:(2)' ); }); test('リンク前後にある三つの$のペアをkatexに変換する', () => { const html = markdownToHtml( '$a,b,c$foo[foo](https://foo.bar)bar,refs:$(2)$,and:$(3)$' ); - expect(html).toEqual( - '

a,b,cfoofoobar,refs:(2),and:(3)

\n' + expect(html).toContain( + 'a,b,cfoofoobar,refs:(2),and:(3)' ); }); test('リンク周りにある$のペアをkatexに変換する', () => { const html = markdownToHtml('$a,b,c$foo[foo](https://foo.bar)bar,refs:$2$'); - expect(html).toEqual( - '

a,b,cfoofoobar,refs:2

\n' + expect(html).toContain( + 'a,b,cfoofoobar,refs:2' ); }); test('二つの$のペアをkatexに変換する', () => { const html = markdownToHtml('$a,b,c$foobar,refs:$(2)$'); - expect(html).toEqual( - '

a,b,cfoobar,refs:(2)

\n' + expect(html).toContain( + 'a,b,cfoobar,refs:(2)' ); }); }); diff --git a/packages/zenn-markdown-html/__tests__/highlight.test.ts b/packages/zenn-markdown-html/__tests__/highlight.test.ts index 6d738de7..82f7ad7c 100644 --- a/packages/zenn-markdown-html/__tests__/highlight.test.ts +++ b/packages/zenn-markdown-html/__tests__/highlight.test.ts @@ -2,6 +2,7 @@ import loadLanguages from 'prismjs/components/index'; import { describe, test, expect } from 'vitest'; import markdownToHtml from '../src/index'; +import parse from 'node-html-parser'; // markdownToHtml で diff を使っているので、あらかじめ読み込んでおく loadLanguages('diff'); @@ -11,7 +12,10 @@ describe('コードハイライトのテスト', () => { const html = markdownToHtml( `\`\`\`js:foo.js\nconsole.log("hello")\n\`\`\`` ); - expect(html).toContain(''); + // が取得できないので
で取得する
+    const pre: any = parse(html).querySelector('pre');
+    const code = parse(pre?.innerHTML).querySelector('code.language-js');
+    expect(code).toBeTruthy();
     expect(html).toContain('foo.js');
   });
 
diff --git a/packages/zenn-markdown-html/__tests__/link.test.ts b/packages/zenn-markdown-html/__tests__/link.test.ts
index 93ca3b4f..f2724140 100644
--- a/packages/zenn-markdown-html/__tests__/link.test.ts
+++ b/packages/zenn-markdown-html/__tests__/link.test.ts
@@ -51,29 +51,24 @@ describe('Linkifyのテスト', () => {
 
     test('URLの前にテキストが存在する場合はリンクをリンクカードに変換しない', () => {
       const html = renderLink('foo https://example.com');
-      expect(html).toEqual(
-        '

foo https://example.com

\n' + expect(html).toContain( + 'foo https://example.com' ); }); test('意図的にリンクしているURLはリンクカードに変換しない', () => { const html = renderLink('[https://example.com](https://example.com)'); - expect(html).toEqual( - '

https://example.com

\n' - ); - }); - - test('リンク内のリンクを変換しない', () => { - const html = renderLink('- https://example.com\n- second'); - expect(html).toEqual( - '\n' + expect(html).toContain( + 'https://example.com' ); }); test('
内のリンクはリンクカードに変換しない', () => { const html = renderLink(':::message alert\nhttps://example.com\n:::'); - expect(html).toEqual( - '\n' + const iframe = parse(html).querySelector('aside iframe'); + expect(iframe).toBeNull(); + expect(html).toContain( + 'https://example.com' ); }); @@ -81,43 +76,48 @@ describe('Linkifyのテスト', () => { const html = renderLink( ':::message alert\nhello\n\nhttps://example.com\n:::' ); + const iframes = parse(html).querySelectorAll('aside iframe'); + expect(iframes.length).toBe(0); + console.log(html); expect(html).toContain( - '' + 'https://example.com' ); }); test('リスト内のリンクをリンクカードに変換しない', () => { const html = renderLink('- https://example.com\n- second'); - expect(html).toEqual( - '\n' + const iframe = parse(html).querySelector('aside iframe'); + expect(iframe).toBeNull(); + expect(html).toContain( + 'https://example.com' ); }); test('URLにテキストが続く場合はリンクカードに変換しない', () => { const html = renderLink('https://example.com foo'); - expect(html).toEqual( - '

https://example.com foo

\n' + expect(html).toContain( + 'https://example.com foo' ); }); test('同じ段落内のテキストを含むリンクをリンクカードに変換しない', () => { const html = renderLink(`a: https://example.com\nb: https://example.com`); - expect(html).toEqual( - '

a: https://example.com
\nb: https://example.com

\n' + expect(html).toContain( + 'a: https://example.com
\nb: https://example.com' ); }); test('URLにテキストが続くならリンクが先頭であってもリンクカードに変換しない', () => { const html = renderLink('\n\nhttps://example.com text'); - expect(html).toEqual( - '

https://example.com text

\n' + expect(html).toContain( + 'https://example.com text' ); }); test('URLの前にテキストがあるならリンクが行末でもリンクカードに変換しない', () => { const html = renderLink('text https://example.com\n\n'); - expect(html).toEqual( - '

text https://example.com

\n' + expect(html).toContain( + 'text https://example.com' ); }); }); diff --git a/packages/zenn-markdown-html/__tests__/source-map.test.ts b/packages/zenn-markdown-html/__tests__/source-map.test.ts new file mode 100644 index 00000000..26be12d5 --- /dev/null +++ b/packages/zenn-markdown-html/__tests__/source-map.test.ts @@ -0,0 +1,127 @@ +import { describe, test, expect } from "vitest"; +import markdownToHtml from "../src/index"; +import parse from "node-html-parser"; + +describe("ソースマップ(data-line属性)のテスト", () => { + test("Header", () => { + const html = markdownToHtml(`# Header1 +## Header2 +### Header3 +#### Header4 +##### Header5 +###### Header6`); + const h1 = parse(html).querySelector("h1"); + const h2 = parse(html).querySelector("h2"); + const h3 = parse(html).querySelector("h3"); + const h4 = parse(html).querySelector("h4"); + const h5 = parse(html).querySelector("h5"); + const h6 = parse(html).querySelector("h6"); + expect(h1?.getAttribute("data-line")).toEqual("0"); + expect(h1?.classList.contains("code-line")).toBe(true); + expect(h2?.getAttribute("data-line")).toEqual("1"); + expect(h2?.classList.contains("code-line")).toBe(true); + expect(h3?.getAttribute("data-line")).toEqual("2"); + expect(h3?.classList.contains("code-line")).toBe(true); + expect(h4?.getAttribute("data-line")).toEqual("3"); + expect(h4?.classList.contains("code-line")).toBe(true); + expect(h5?.getAttribute("data-line")).toEqual("4"); + expect(h5?.classList.contains("code-line")).toBe(true); + expect(h6?.getAttribute("data-line")).toEqual("5"); + expect(h6?.classList.contains("code-line")).toBe(true); + }); + + test("Paragraph", () => { + const html = markdownToHtml(`Paragraph1\n\nhttps://example.com`); + const p = parse(html).querySelectorAll("p"); + expect(p?.[0].getAttribute("data-line")).toEqual("0"); + expect(p?.[0].classList.contains("code-line")).toBe(true); + expect(p?.[1].getAttribute("data-line")).toEqual("2"); + expect(p?.[1].classList.contains("code-line")).toBe(true); + }); + + test("List(unordered)", () => { + const html = markdownToHtml(`- item1\n- item2`); + const ul = parse(html).querySelector("ul"); + const li = parse(html).querySelectorAll("li"); + expect(ul?.getAttribute("data-line")).toEqual("0"); + expect(ul?.classList.contains("code-line")).toBe(true); + expect(li?.[0].getAttribute("data-line")).toEqual("0"); + expect(li?.[0].classList.contains("code-line")).toBe(true); + expect(li?.[1].getAttribute("data-line")).toEqual("1"); + expect(li?.[1].classList.contains("code-line")).toBe(true); + }); + + test("List(ordered)", () => { + const html = markdownToHtml(`1. item1\n2. item2`); + const ol = parse(html).querySelector("ol"); + const li = parse(html).querySelectorAll("li"); + expect(ol?.getAttribute("data-line")).toEqual("0"); + expect(ol?.classList.contains("code-line")).toBe(true); + expect(li?.[0].getAttribute("data-line")).toEqual("0"); + expect(li?.[0].classList.contains("code-line")).toBe(true); + expect(li?.[1].getAttribute("data-line")).toEqual("1"); + expect(li?.[1].classList.contains("code-line")).toBe(true); + }); + + test("Table", () => { + const html = markdownToHtml(`| a | b |\n| --- | --- |\n| c | d |`); + const table = parse(html).querySelector("table"); + const thead = parse(html).querySelector("thead"); + const tbody = parse(html).querySelector("tbody"); + const tr = parse(html).querySelectorAll("tr"); + expect(table?.getAttribute("data-line")).toEqual("0"); + expect(table?.classList.contains("code-line")).toBe(true); + expect(thead?.getAttribute("data-line")).toEqual("0"); + expect(thead?.classList.contains("code-line")).toBe(true); + expect(tbody?.getAttribute("data-line")).toEqual("2"); + expect(tbody?.classList.contains("code-line")).toBe(true); + expect(tr?.[0].getAttribute("data-line")).toEqual("0"); + expect(tr?.[0].classList.contains("code-line")).toBe(true); + expect(tr?.[1].getAttribute("data-line")).toEqual("2"); + expect(tr?.[1].classList.contains("code-line")).toBe(true); + }); + + test("Code Block", () => { + const html = markdownToHtml("```\ncode\n```"); + // が取得できないので
で取得する
+    const innerHTML: any = parse(html).querySelector("pre")?.innerHTML;
+    const code = parse(innerHTML).querySelector("code");
+    expect(code?.getAttribute("data-line")).toEqual("0"); // フェンス開始時の行番号
+    expect(code?.classList.contains("code-line")).toBe(true);
+  });
+
+  test("Katex", () => {
+    const html = markdownToHtml(`$$\na\n$$`);
+    const katex = parse(html).querySelector("section");
+    expect(katex?.getAttribute("data-line")).toEqual("0");
+    expect(katex?.classList.contains("code-line")).toBe(true);
+  });
+
+  test("Blockquote", () => {
+    const html = markdownToHtml("> quote");
+    const blockquote = parse(html).querySelector("blockquote");
+    expect(blockquote?.getAttribute("data-line")).toEqual("0");
+    expect(blockquote?.classList.contains("code-line")).toBe(true);
+  });
+
+  test("Horizontal Rule", () => {
+    const html = markdownToHtml(`---`);
+    const hr = parse(html).querySelector("hr");
+    expect(hr?.getAttribute("data-line")).toEqual("0");
+    expect(hr?.classList.contains("code-line")).toBe(true);
+  });
+
+  test("Alert", () => {
+    const html = markdownToHtml(":::message\nhello\n:::");
+    const p = parse(html).querySelector("p");
+    expect(p?.getAttribute("data-line")).toEqual("1");
+    expect(p?.classList.contains("code-line")).toBe(true);
+  });
+
+  test("Details/Summary", () => {
+    const html = markdownToHtml(`:::details タイトル\nhello\n:::`);
+    const p = parse(html).querySelector("p");
+    expect(p?.getAttribute("data-line")).toEqual("1");
+    expect(p?.classList.contains("code-line")).toBe(true);
+  });
+});
diff --git a/packages/zenn-markdown-html/__tests__/xss.test.ts b/packages/zenn-markdown-html/__tests__/xss.test.ts
index ef773a7b..99b8c003 100644
--- a/packages/zenn-markdown-html/__tests__/xss.test.ts
+++ b/packages/zenn-markdown-html/__tests__/xss.test.ts
@@ -4,12 +4,12 @@ import markdownToHtml from '../src/index';
 describe('XSS脆弱性のテスト', () => {
   test('');
-    expect(html).toMatch('

<script>alert("XSS!")</script>

'); + expect(html).toMatch('<script>alert("XSS!")</script>'); }); test('"javascript:"構文をリンクにせずにそのままにする', () => { const html = markdownToHtml('javascript:alert(1)'); - expect(html).toMatch('

javascript:alert(1)

'); + expect(html).toMatch('javascript:alert(1)'); }); test('katex内のをエスケープする', () => { @@ -30,8 +30,8 @@ describe('XSS脆弱性のテスト', () => { const html = markdownToHtml( `\`\`\`">\nany\n\`\`\`` ); - expect(html).toContain( - '
any\n
' + expect(html).toBe( + '
any\n
' ); }); test('コードブロックのファイル名に仕込まれた\nany\n\`\`\`` ); expect(html).toContain( - '
<script>alert("XSS")</script>
any\n
' + '<script>alert("XSS")</script>' ); }); test('コードブロック内の