記事の見た目をいじる
いろいろきれいに見えるようにする†
メモ: 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 { decolatorsInfo } from './codeDecolators/codeDecolatorInfo'; + + // 各デコレータが追加する css をまとめる + const codeRendererCss = decolatorsInfo.map(info=>info.css).join("\n"); ... // シンタックスハイライト + marked.use(codeRenderer); ... </script> + <svelte:head> + {@html '<style>' + codeRendererCss + '</style>'} + </svelte:head>
decolatorsInfo には孫ライブラリが必要な css を個別に追加するので、 それを繋げてヘッダーに入れている。
LANG: html <style>{@html codeRendererCss}</style>
としたのでは動かなかった。<style> タグ自体を文字列として生成したらうまくいった。
codeRenderer は marked からコードブロックごとに呼び出されることになる。
デコレータ的なアルゴリズムで
- rowsDecorator rows(50) などとして表示行数を変更できる
- numDecorator 行番号付与
- diffDecorator diff 的な +/- 行に色を付ける
- splitterDecorator 行ごとに処理できるよう改行単位でタグを閉じる
- highlightDecorator シンタックスハイライト
などを各 Decorator に分業させてコードブロックを処理する。
src/lib/components/codeRenderer.ts
LANG: ts import { rowsDecorator } from './codeDecolators/rowsDecorator'; import { numDecorator } from './codeDecolators/numDecorator'; import { diffDecorator } from './codeDecolators/diffDecorator'; import { splitterDecorator } from './codeDecolators/splitterDecorator'; 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[], maxRows?: number, rawHtml?: string, // ここに代入されればそのままそれを出力する }; // デコレータ鎖は先頭から実行される export declare type Decolator = (chain: Decolator[], block: CodeBlock) => CodeBlock; const decolators: Decolator[] = [ rowsDecorator, numDecorator, diffDecorator, splitterDecorator, // 行ごとに分けられるようにするため改行の前後でタグを閉じる・開く highlightDecorator, // highlight.js を呼び出す ]; 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 block.rawHtml ? block.rawHtml : `<pre class="marked-code">` + (block.tags.length ? `<span class="tags">${block.tags.join(' ')}</span>` : '') + `<code class="${block.codeClasses.join(' ')}" style="max-height:${block.maxRows || 30}lh">${block.code}</code>` + `</pre>`; } }};
CodeBlock に新しいオプションを追加したり、
Decorator を増やしたりすることで、
ブロックの内容を画像にして表示するなど、ブロック要素プラグインのような機能をどんどん追加できるはず。
ここでは個別のコードは載せないが、かなりいろいろ表示できるようになった。
LANG: console $ git add . && git commit -m "コードブロックの表示を改善した" modified: .gitignore modified: package.json modified: pnpm-lock.yaml new file: src/lib/components/ArticleElement.svelte new file: src/lib/components/codeDecolators/codeDecolatorInfo.ts new file: src/lib/components/codeDecolators/diffDecorator.ts new file: src/lib/components/codeDecolators/highlightDecorator.ts new file: src/lib/components/codeDecolators/numDecorator.ts new file: src/lib/components/codeDecolators/rowsDecorator.ts new file: src/lib/components/codeDecolators/splitterDecorator.ts new file: src/lib/components/codeRenderer.ts new file: src/node_modules/@types/highlightjs-svelte.d.ts modified: src/routes/articles/[...titleOrId]/+page.svelte
この時のコミットに対して後に不具合修正を入れた
コードブロック表示の不具合修正 e57b415177da49da10b41cf8b4e5f1d935faea89