Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [zenn-markdown-html] VS Code 拡張機能でのスクロール同期のためのソースマップ追加 #504

Merged
merged 3 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/zenn-cli/src/server/__tests__/preview/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('/api/articles/:slug', () => {
type: 'tech',
topics: [],
published: true,
bodyHtml: expect.stringContaining('<p>Hello!</p>'),
bodyHtml: expect.stringMatching(/<p.*>Hello!<\/p>/),
})
);
});
Expand Down Expand Up @@ -216,7 +216,7 @@ describe('/api/books/:book_slug/chapters/:chapter_filename', () => {
title: 'title2',
free: true,
position: 0,
bodyHtml: expect.stringContaining('<p>Hello!</p>'),
bodyHtml: expect.stringMatching(/<p.*>Hello!<\/p>/),
})
);
});
Expand Down
17 changes: 13 additions & 4 deletions packages/zenn-markdown-html/__tests__/basic.test.ts
Original file line number Diff line number Diff line change
@@ -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(`<p>Hello</p>`);
expect(html).toContain(
`<h2 id="hey"><a class="header-anchor-link" href="#hey" aria-hidden="true"></a> hey</h2>`
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(
'<a class="header-anchor-link" href="#hey" aria-hidden="true"></a> hey'
);
expect(html).toContain(`<ul>\n<li>first</li>\n<li>second</li>\n</ul>\n`);
expect(ul).not.toBeNull();
expect(liElms?.length).toBe(2);
expect(liElms[0].innerHTML).toBe('first');
expect(liElms[1].innerHTML).toBe('second');
});

test('インラインコメントはhtmlに変換しない', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/zenn-markdown-html/__tests__/br.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('<br /> のテスト', () => {
const patterns = ['foo<br>bar', 'foo<br/>bar', 'foo<br />bar'];
patterns.forEach((pattern) => {
const html = markdownToHtml(pattern);
expect(html).toMatch(/<p>foo<br \/>bar<\/p>/);
expect(html).toContain('foo<br />bar');
});
});
test('テーブル内の<br />は保持する', () => {
Expand All @@ -20,7 +20,7 @@ describe('<br /> のテスト', () => {
});
test('インラインコード内の<br />はエスケープする', () => {
const html = markdownToHtml('foo`<br>`bar');
expect(html).toMatch(/<p>foo<code>&lt;br&gt;<\/code>bar<\/p>/);
expect(html).toContain('foo<code>&lt;br&gt;</code>bar');
});
test('コードブロック内の<br />はエスケープする', () => {
const html = markdownToHtml('```\n<br>\n```');
Expand Down
36 changes: 18 additions & 18 deletions packages/zenn-markdown-html/__tests__/dollar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<p><embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="https://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>bar</p>\n'
expect(html).toContain(
'<embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="https://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>bar'
);
});

test('リンクの後に無効な $ が続く場合はそのままにする', () => {
const html = markdownToHtml('$a,b,c$foo[foo](http://foo.bar)$bar');
expect(html).toEqual(
'<p><embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="http://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>$bar</p>\n'
expect(html).toContain(
'<embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="http://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>$bar'
);
});

test('リンク名に $ が含まれる場合はそのままにする', () => {
const html = markdownToHtml('$a,b,c$foo[$bar](http://foo.bar)bar');
expect(html).toEqual(
'<p><embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="http://foo.bar" target="_blank" rel="nofollow noopener noreferrer">$bar</a>bar</p>\n'
expect(html).toContain(
'<embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="http://foo.bar" target="_blank" rel="nofollow noopener noreferrer">$bar</a>bar'
);
});
test('リンクのhrefに $ が含まれる場合はそのままにする', () => {
Expand All @@ -35,45 +35,45 @@ describe('$ マークのテスト', () => {
describe('HTMLタグにエスケープするテスト', () => {
test('katex内の<sscript />をエスケープする', () => {
const html = markdownToHtml('$a,<script>alert("XSS")</script>,c$');
expect(html).toEqual(
`<p><embed-katex><eq class="zenn-katex">a,&lt;script&gt;alert("XSS")&lt;/script&gt;,c</eq></embed-katex></p>\n`
expect(html).toContain(
`<embed-katex><eq class="zenn-katex">a,&lt;script&gt;alert("XSS")&lt;/script&gt;,c</eq></embed-katex>`
);
});
});

describe('$ のペアのテスト', () => {
test('リンクの前後にある一文字だけを含む$のペアをkatexに変換する', () => {
const html = markdownToHtml('$a$foo[foo](https://foo.bar)bar,refs:$(2)$');
expect(html).toEqual(
'<p><embed-katex><eq class="zenn-katex">a</eq></embed-katex>foo<a href="https://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>bar,refs:<embed-katex><eq class="zenn-katex">(2)</eq></embed-katex></p>\n'
expect(html).toContain(
'<embed-katex><eq class="zenn-katex">a</eq></embed-katex>foo<a href="https://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>bar,refs:<embed-katex><eq class="zenn-katex">(2)</eq></embed-katex>'
);
});
test('リンク前後にある$のペアをkatexに変換する', () => {
const html = markdownToHtml(
'$a,b,c$foo[foo](https://foo.bar)bar,refs:$(2)$'
);
expect(html).toEqual(
'<p><embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="https://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>bar,refs:<embed-katex><eq class="zenn-katex">(2)</eq></embed-katex></p>\n'
expect(html).toContain(
'<embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="https://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>bar,refs:<embed-katex><eq class="zenn-katex">(2)</eq></embed-katex>'
);
});
test('リンク前後にある三つの$のペアをkatexに変換する', () => {
const html = markdownToHtml(
'$a,b,c$foo[foo](https://foo.bar)bar,refs:$(2)$,and:$(3)$'
);
expect(html).toEqual(
'<p><embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="https://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>bar,refs:<embed-katex><eq class="zenn-katex">(2)</eq></embed-katex>,and:<embed-katex><eq class="zenn-katex">(3)</eq></embed-katex></p>\n'
expect(html).toContain(
'<embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="https://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>bar,refs:<embed-katex><eq class="zenn-katex">(2)</eq></embed-katex>,and:<embed-katex><eq class="zenn-katex">(3)</eq></embed-katex>'
);
});
test('リンク周りにある$のペアをkatexに変換する', () => {
const html = markdownToHtml('$a,b,c$foo[foo](https://foo.bar)bar,refs:$2$');
expect(html).toEqual(
'<p><embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="https://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>bar,refs:<embed-katex><eq class="zenn-katex">2</eq></embed-katex></p>\n'
expect(html).toContain(
'<embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foo<a href="https://foo.bar" target="_blank" rel="nofollow noopener noreferrer">foo</a>bar,refs:<embed-katex><eq class="zenn-katex">2</eq></embed-katex>'
);
});
test('二つの$のペアをkatexに変換する', () => {
const html = markdownToHtml('$a,b,c$foobar,refs:$(2)$');
expect(html).toEqual(
'<p><embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foobar,refs:<embed-katex><eq class="zenn-katex">(2)</eq></embed-katex></p>\n'
expect(html).toContain(
'<embed-katex><eq class="zenn-katex">a,b,c</eq></embed-katex>foobar,refs:<embed-katex><eq class="zenn-katex">(2)</eq></embed-katex>'
);
});
});
6 changes: 5 additions & 1 deletion packages/zenn-markdown-html/__tests__/highlight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -11,7 +12,10 @@ describe('コードハイライトのテスト', () => {
const html = markdownToHtml(
`\`\`\`js:foo.js\nconsole.log("hello")\n\`\`\``
);
expect(html).toContain('<code class="language-js">');
// <code />が取得できないので<pre />で取得する
const pre: any = parse(html).querySelector('pre');
const code = parse(pre?.innerHTML).querySelector('code.language-js');
expect(code).toBeTruthy();
expect(html).toContain('<span class="code-block-filename">foo.js</span>');
});

Expand Down
46 changes: 18 additions & 28 deletions packages/zenn-markdown-html/__tests__/link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,73 +51,63 @@ describe('Linkifyのテスト', () => {

test('URLの前にテキストが存在する場合はリンクをリンクカードに変換しない', () => {
const html = renderLink('foo https://example.com');
expect(html).toEqual(
'<p>foo <a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a></p>\n'
expect(html).toContain(
'foo <a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a>'
);
});

test('意図的にリンクしているURLはリンクカードに変換しない', () => {
const html = renderLink('[https://example.com](https://example.com)');
expect(html).toEqual(
'<p><a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a></p>\n'
);
});

test('リンク内のリンクを変換しない', () => {
const html = renderLink('- https://example.com\n- second');
expect(html).toEqual(
'<ul>\n<li><a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a></li>\n<li>second</li>\n</ul>\n'
Comment on lines -66 to -69

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[memo] テストケースがL89と重複しているため削除。

expect(html).toContain(
'<a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a>'
);
});

test('<details />内のリンクはリンクカードに変換しない', () => {
const html = renderLink(':::message alert\nhttps://example.com\n:::');
expect(html).toEqual(
'<aside class="msg alert"><span class="msg-symbol">!</span><div class="msg-content"><p><a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a></p>\n</div></aside>\n'
);
const iframe = parse(html).querySelector('span.zenn-embedded iframe');
expect(iframe).toBeNull();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iframeが存在しないことを確認するのはいい作戦と思いましたが、ここのテストケースの意図としては「details内のリンクが、リンクカードには変換されずリンクになる」ということを確認したいのだと思いました。

なので期待値としては以下を確認するようにしていただけますでしょうか?

  • aside iframe が存在しない
  • htmlに <a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a> が含まれる

});

test('<details />内の2段落空いたリンクをリンクカードに変換しない', () => {
const html = renderLink(
':::message alert\nhello\n\nhttps://example.com\n:::'
);
expect(html).toContain(
'<aside class="msg alert"><span class="msg-symbol">!</span><div class="msg-content"><p>hello</p>\n<p><a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a></p>\n</div></aside>'
);
const iframes = parse(html).querySelectorAll('span.zenn-embedded iframe');
expect(iframes.length).toBe(0);
});

test('リスト内のリンクをリンクカードに変換しない', () => {
const html = renderLink('- https://example.com\n- second');
expect(html).toEqual(
'<ul>\n<li><a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a></li>\n<li>second</li>\n</ul>\n'
);
const iframe = parse(html).querySelector('span.zenn-embedded iframe');
expect(iframe).toBeNull();
});

test('URLにテキストが続く場合はリンクカードに変換しない', () => {
const html = renderLink('https://example.com foo');
expect(html).toEqual(
'<p><a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a> foo</p>\n'
expect(html).toContain(
'<a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a> foo'
);
});

test('同じ段落内のテキストを含むリンクをリンクカードに変換しない', () => {
const html = renderLink(`a: https://example.com\nb: https://example.com`);
expect(html).toEqual(
'<p>a: <a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a><br />\nb: <a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a></p>\n'
expect(html).toContain(
'a: <a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a><br />\nb: <a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a>'
);
});

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

test('URLの前にテキストがあるならリンクが行末でもリンクカードに変換しない', () => {
const html = renderLink('text https://example.com\n\n');
expect(html).toEqual(
'<p>text <a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a></p>\n'
expect(html).toContain(
'text <a href="https://example.com" target="_blank" rel="nofollow noopener noreferrer">https://example.com</a>'
);
});
});
Expand Down
127 changes: 127 additions & 0 deletions packages/zenn-markdown-html/__tests__/source-map.test.ts
Original file line number Diff line number Diff line change
@@ -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```");
// <code />が取得できないので<pre />で取得する
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);
});
});
Loading
Loading