記事の見た目をいじる の変更点

更新


[[プログラミング/svelte]]

* いろいろきれいに見えるようにする [#k9f0334b]

#contents

* メモ: style の :global について [#xfd2fae0]

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 コンポーネントを作成 [#q2fec75a]

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)}
 />

* 見出しのレベルを調整 [#s2a8d1d1]

ブログ記事ではタイトルが 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 [#db9a96e3]

 LANG: ts 
 import { baseUrl } from 'marked-base-url';
 import { PUBLIC_URL_ROOT } from '$env/static/public';
   ...
 
   // ベースアドレス
   marked.use(baseUrl(PUBLIC_URL_ROOT));

* シンタックスハイライト [#fea7e75c]

痒い所に手が届くよう、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 を作る [#z7d15cbe]

コンポーネントからはこのように使える。

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: 815 (from 2010/06/03), today: 3, yesterday: 2