記事の見た目をいじる

(145d) 更新


プログラミング/svelte

いろいろきれいに見えるようにする

メモ: 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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
  return text.replace(/[&<>"']/g, (c) => map[c as keyof typeof map]);
}

function unescapeHtml(text: string) {
  const map = { '&amp;':'&', '&lt;':'<', '&gt;':'>', '&quot;':'"', '&#039;':"'" };
  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

Counter: 341 (from 2010/06/03), today: 1, yesterday: 2