記事の見た目をいじる の履歴(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 を指定することで横いっぱいに広がる
- 引数で与えられた行番号をハイライト表示可能