記事の見た目をいじる の履歴(No.3)
更新いろいろきれいに見えるようにする†
メモ: style の :global について†
svelte の style 定義には自動的にコンポーネントインスタンスの id が追加されるため、 そのコンポーネントに直接含まれる(子コンポーネントは除外)エレメントにしか効果を及ぼさない。
コンポーネントを超えて効果を期待する場合には :global を付ける。
https://svelte.jp/docs/svelte-components#style
LANG: html <style> :global(strong) { /* これはすべての <strong> に適用されます */ margin: 0; } div :global(strong) { /* これは「このコンポーネント内の <div> 要素」の中にある 「すべての <strong> 要素」に 適用されます */ color: goldenrod; } </style>
上記の例では div に :global がかかっていないため、div にはコンポーネントインスタンスの ID が付き、その下にあることが条件になっている。
「このコンポーネントの子供」に適用したい場合にはこのテクニックが役に立ちそう。
ArticleElement コンポーネントを作成†
db の Article と同名だとややこしいことになるようなので ArticleElement とした。
src/routes/articles/ArticleElement.svelte に置くことも考えたのだけれど、 関連コードが増えてフォルダーを掘りたくなる気もしたので
src/lib/components/ArticleElement.svelte にした
LANG: html <script lang="ts" context="module"> import { marked } from 'marked'; import { baseUrl } from 'marked-base-url'; import markedLinkifyIt from 'marked-linkify-it'; import { markedHighlight } from 'marked-highlight'; import hljs from 'highlight.js'; import markedKatex from 'marked-katex-extension'; import { page } from '$app/stores'; import 'highlight.js/styles/stackoverflow-dark.min.css'; marked.use(baseUrl('https://example.com/folder/')); const linkifyItSchemas = {}; const linkifyItOptions = {}; marked.use(markedLinkifyIt(linkifyItSchemas, linkifyItOptions)); marked.use( markedHighlight({ langPrefix: 'hljs language-', highlight(code, lang) { const language = hljs.getLanguage(lang) ? lang : 'plaintext'; return hljs.highlight(code, { language }).value; }, }) ); marked.use( markedKatex({ throwOnError: false, }) ); </script> <script lang="ts"> export let title: string; export let author: string; export let date: Date; export let body: string; export let permLink: string; </script> <article class="prose max-w-full p-4"> <h1>{title}</h1> <div> <span>{author}</span> <span>{date.toLocaleString()}</span> <span><a href={permLink + '/edit'}>編集</a></span> </div> <content> {@html marked.parse(body)} </content> </article>
src/routes/articles/[...titleOrId]/+page.svelte
LANG: html <script lang="ts"> import type { PageData } from './$types'; import ArticleElement from '$lib/components/ArticleElement.svelte'; import { encodeTitle } from '../lib'; export let data: PageData; </script> <ArticleElement title={data.article.title} body={data.article.body} author={data.article.author.name} date={data.article.createdAt} permLink={data.urlRoot + '/articles/' + encodeTitle(data.article.title)} />
見出しのレベルを調整†
ブログ記事ではタイトルが h1 なので本文中では一番大きいのが h2 になる。
LANG: ts // デフォルトのレンダラ const vanillaRenderer = new marked.Renderer(); // 見出しの階層を1つ下げる marked.use({ renderer: { heading(text, level, raw) { return vanillaRenderer.heading(text, level + 1, raw); } } });
ベース URL†
LANG: ts import { baseUrl } from 'marked-base-url'; import { PUBLIC_URL_ROOT } from '$env/static/public'; ... // ベースアドレス marked.use(baseUrl(PUBLIC_URL_ROOT));
シンタックスハイライト†
痒い所に手が届くよう、marked-highlight を使わず自分でやろう。
- highlightjs-svelte
- highlightjs-cobol
- highlightjs-iptables
- highlightjs-vba
あたりが気になるけど、さしあたりは svelte だけ入れとく。
LANG: console $ pnpm rm marked-highlight $ pnpm i highlightjs-svelte $ pnpm add --save-dev @types/highlightjs-svelte
ERR_PNPM_FETCH_404 GET https://registry.npmjs.org/@types%2Fhighlightjs-svelte: Not Found - 404
This error happened while installing a direct dependency of C:\Users\osamu\Desktop\svelte\authtest @types/highlightjs-svelte is not in the npm registry, or you have no permission to fetch it. No authorization header was set for the request. Progress: resolved 42, reused 42, downloaded 0, added 0
あーと、highlightjs-svelte にはタイプ定義が提供されていない。
仕方がないので、
src/node_modules/@types/highlightjs-svelte.d.ts
LANG: ts import type { HLJSApi } from 'highlight.js'; declare function hljs_svelte(hljs: HLJSApi): void; export default hljs_svelte;
を作成した。どうやら src/@types ではだめで src/node_module/@types じゃないと読み込んでくれないらしい。
で、これだと .gitignore の node_module というルールに引っ掛かってしまうので、
.gitignore
- node_modules + node_modules/* + !/src/node_modules/@types/*
として、このフォルダの中身だけ特別にコミットするようにした。
marked のエクステンション codeRenderer を作る†
コンポーネントからはこのように使える。
src/lib/components/ArticleElement.svelte
LANG: html <style context="module"> + import { codeRenderer } from './codeRenderer'; + import { css } from './codeRendererCss'; + + // 追加の CSS + const codeRendererCss = css.join("\n"); ...
// シンタックスハイライト + marked.use(codeRenderer); ... </script> + <svelte:head> + {@html '<style>' + codeRendererCss + '</style>'} + </svelte:head>
'./codeRendererCss' の css には孫ライブラリが必要な css を個別に追加するので、 それを繋げてヘッダーに入れている。
LANG: html <style>{@html codeRendererCss}</style>
としたのでは動かないので、<style> タグ自体を文字列として生成する必要がある。
codeRenderer は marked からコードブロックごとに呼び出され、 デコレータ的なアルゴリズムで
- numDecorator 行番号付与
- diffDecorator diff 的な +/- 行に色を付ける
- highlightDecorator シンタックスハイライト
などを個別に処理する。
src/lib/components/codeRenderer.ts
LANG: ts import { numDecorator } from './codeDecolators/numDecorator'; import { diffDecorator } from './codeDecolators/diffDecorator'; import { highlightDecorator } from './codeDecolators/highlightDecorator'; // HTML の特殊文字をエスケープ // https://stackoverflow.com/questions/1787322/what-is-the-htmlspecialchars-equivalent-in-javascript export function escapeHtml(text: string) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, (c) => map[c as keyof typeof map]); } function unescapeHtml(text: string) { const map = { '&':'&', '<':'<', '>':'>', '"':'"', ''':"'" }; return text.replace(/&(?:amp|lt|gt|quot|#039);/g, (code) => map[code as keyof typeof map]); } //==================== // CodeBlockDecolator export declare type CodeBlock = { code: string, info: string[], params: string[], tags: string[], codeClasses: string[], removeBreak?: boolean, }; // デコレータ鎖は先頭から実行される export declare type Decolator = (chain: Decolator[], block: CodeBlock) => CodeBlock; const decolators: Decolator[] = [ numDecorator, diffDecorator, highlightDecorator, ]; export function callDecolatorChain(chain: Decolator[], block: CodeBlock) { const first = chain[0]; return first ? first(chain.slice(1), block) : block; } //==================== // code ブロックをレンダリングする export const codeRenderer = {renderer:{ code: (code: string, infoString = '', escaped: boolean) => { if(escaped) { code = unescapeHtml(code); } // infoString は info param0 param1 param2 // info は some:some:some const params = infoString.split(' '); const info = params.length ? params.shift()!.split(':') : []; const block = callDecolatorChain(decolators, {code, info, params, tags: [], codeClasses: []}); return `<pre class="marked-code">` + (block.tags.length ? `<span class="tags">${block.tags.join(' ')}</span>` : '') + `<code class="${block.codeClasses.join(' ')}">${block.code}</code>` + `</pre>`; } }};
Decorator を増やすことで ブロックの内容を画像にして表示するなど、 ブロック要素プラグインのような機能をどんどん追加できるはず。
diffDecorator†
src/lib/components/codeDecolators/diffDecorator.ts
LANG: ts import { type Decolator, type CodeBlock, callDecolatorChain, escapeHtml } from '../codeRenderer'; import { css } from '../codeRendererCss'; export function diffDecorator(chain: Decolator[], block: CodeBlock) { if (!block.info.includes('diff')) { return callDecolatorChain(chain, block); } // remove "diff" const i = block.info.indexOf('diff'); block.info.splice(i, 1); // 先頭文字を marks に切り出す const lines = block.code.split(/\r\n?|\n/); const marks = lines.map(line => line.substring(0, 1)); block.code = lines.map(line => line.substring(1)).join("\n"); // 後続のデコレータがあれば呼び出す block = callDecolatorChain(chain, block); // マークを付ける block.codeClasses.push('diff-lines'); block.code = block.code .split("\n") .map((line, i) => `<span class="diff-line" data-mark="${escapeHtml(marks[i])}">${line}</span>`) .join("\n"); return block; } css.push(` .article code.line-numbered { display: grid; grid-template-columns: auto; grid-template-rows: repeat(1fr); } .article span.diff-line::before { content: attr(data-mark); position: absolute; margin-left: -2em; text-weight: bold; } .article span.diff-line { padding-left: 3em; margin-left: -1em; margin-right: -1em; display: block; min-height: 1lh; } .article span.diff-line:not([data-mark=" "])::before { color: #ff0; } .article span.diff-line[data-mark="+"]::before { color: #0f0; } .article span.diff-line[data-mark="-"]::before { color: #f00; } .article span.diff-line:not([data-mark=" "]) { background-color: rgba(100,100,0,0.2); } .article span.diff-line[data-mark="+"] { background-color: rgba(0,100,0,0.2); } .article span.diff-line[data-mark="-"] { background-color: rgba(100,0,0,0.2); } `);
- info から "diff" の要素を取り除く
- 各行の先頭の1文字をマーカーとして取り除く
- 後続のデコレータを適用(ここでハイライト処理される)
- 行単位で <span> で囲み、マーカーがあれば data-mark 属性に入れる
- すべて繋げて返す
- 各行は親に display:grid を指定することで横いっぱいに広がる
- 引数で与えられた行番号をハイライト表示可能