記事の見た目をいじる のバックアップの現在との差分(No.3)

更新


  • 追加された行はこの色です。
  • 削除された行はこの色です。
[[プログラミング/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 { css } from './codeRendererCss';
 +   import { decolatorsInfo } from './codeDecolators/codeDecolatorInfo';
 +
 +   // 追加の CSS
 +   const codeRendererCss = css.join("\n");
 +   // 各デコレータが追加する css をまとめる
 +   const codeRendererCss = decolatorsInfo.map(info=>info.css).join("\n");
 
     ...

 
     // シンタックスハイライト
 +   marked.use(codeRenderer);
     ...
    
   </script>
 
 + <svelte:head>
 +   {@html '<style>' + codeRendererCss + '</style>'}
 + </svelte:head>

'./codeRendererCss' の css には孫ライブラリが必要な css を個別に追加するので、
decolatorsInfo には孫ライブラリが必要な css を個別に追加するので、
それを繋げてヘッダーに入れている。

 LANG: html
 <style>{@html codeRendererCss}</style>

としたのでは動かないので、<style> タグ自体を文字列として生成する必要がある。
としたのでは動かなかった。<style> タグ自体を文字列として生成したらうまくいった。

codeRenderer は marked からコードブロックごとに呼び出されることになる。

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[],
   removeBreak?: boolean,
   maxRows?: number,
   rawHtml?: string,   // ここに代入されればそのままそれを出力する
 };
 
 // デコレータ鎖は先頭から実行される
 export declare type Decolator = (chain: Decolator[], block: CodeBlock) => CodeBlock;
 
 const decolators: Decolator[] = [
   rowsDecorator,
   numDecorator,
   diffDecorator,
   highlightDecorator,
   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 `<pre class="marked-code">`
          +  (block.tags.length ? `<span class="tags">${block.tags.join(' ')}</span>` : '')
          + `<code class="${block.codeClasses.join(' ')}">${block.code}</code>`
          + `</pre>`;
 
     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>`;
   }
 }};

Decorator を増やすことで
ブロックの内容を画像にして表示するなど、
ブロック要素プラグインのような機能をどんどん追加できるはず。
CodeBlock に新しいオプションを追加したり、~
Decorator を増やしたりすることで、~
ブロックの内容を画像にして表示するなど、ブロック要素プラグインのような機能をどんどん追加できるはず。

** diffDecorator [#bdc081ae]
ここでは個別のコードは載せないが、かなりいろいろ表示できるようになった。

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);
 }
 `);
 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

- info から "diff" の要素を取り除く
- 各行の先頭の1文字をマーカーとして取り除く
- 後続のデコレータを適用(ここでハイライト処理される)
- 行単位で <span> で囲み、マーカーがあれば data-mark 属性に入れる
- すべて繋げて返す
- 各行は親に display:grid を指定することで横いっぱいに広がる
- 引数で与えられた行番号をハイライト表示可能
この時のコミットに対して後に不具合修正を入れた

 コードブロック表示の不具合修正 e57b415177da49da10b41cf8b4e5f1d935faea89


Counter: 383 (from 2010/06/03), today: 3, yesterday: 0