Open40

alosaur-lite + deno deploy で markdown の表示

nikogolinikogoli

準備の準備

↓これの markdown の example をベースにする
https://github.com/alosaur/alosaur-lite

nikogolinikogoli

とりあえずやること

  1. clone するなりして、alosaur-lite の example/markdown 一式を取得する
  2. (別にする必要はないが、main.ts を server.ts にリネームする)
  3. このままではdeployctlでは動くものの実際の deploy には失敗するので、server.ts を修正する
  4. 特に理由はないが、markdown パーサーを deno 版のものに変更する
nikogolinikogoli

server.ts (main.ts) の修正

server.ts
 // 33行あたり
 async function getMarkDownPage(path: string) {
   if (globalRenderCache.has(path)) {
     return globalRenderCache.get(path);
   }
   let result = null;

-  let rootPath = import.meta.url;
-  rootPath = rootPath.substring(0, rootPath.lastIndexOf("/") + 1);
-
-  const request = new Request(rootPath + "views/" + path);
-  const response = await fetch(request);
-
-  if (response && response.status !== 404) {
-    const text = await response.text();
-    result = getHtmlPage(Marked.parse(text));
-  }
+  const text = await Deno.readTextFile(`${Deno.cwd()}/views/${path}`);
+  result = getHtmlPage(Marked.parse(text));

  globalRenderCache.set(path, result);

  return result;
}
 // ...
nikogolinikogoli

deno 版 markdown へのパーサーの変更

server.ts
-import { Marked } from "https://jspm.dev/@ts-stack/markdown";
+import { Marked } from 'https://deno.land/x/markdown@v2.0.0/mod.ts';

 // ...

 // 上のスクラップで書き換えた部分の末尾
-    result = getHtmlPage(Marked.parse(text));
+    result = getHtmlPage(Marked.parse(text).content);

 // ...

markdown のパーサー

少なくとも @ts-stack/markdownと deno 版のmarkdownには特に大きな違いはないっぽい[1]

脚注
  1. でも deno 版のmarkdownには document がないので、結局 @ts-stack/markdown(https://github.com/ts-stack/markdown) の doc を読むことになる ↩︎

nikogolinikogoli

Zenn のスクラップを対象にしてやってみる

スクラップにおいて『JSONで内容を出力』を行い、blob.jsonとして保存
   ↓
markdown 部分を適当に切り出す 確認のため、とりあえず5個目以降は無視

const json_text = await Deno.readTextFile(`blob.json`);
const json_obj = JSON.parse(json_text);

const page_title = json_obj.title;
const sep_post = "\n\n<br>\n\n--------\n\n--------\n\n<br>\n\n"

let md_text_list: string[] = [];
json_obj.comments.forEach((elem: any, idx: number) => {
	let base_md: string = elem.body_markdown;
	if ("children" in elem) {
		elem.children.forEach((child: any) => {
			base_md += "\n\n<br><br>\n\n" + child.body_markdown;
		})
	}
        md_text_list.push( (idx>0)? sep_post  + base_md : base_md)
})

const out = md_text_list.slice(0,4).join()
await Deno.writeTextFileSync("views/scrap.md", out);
console.log("end!!")
nikogolinikogoli

HTML 側の調整

フォントを Kosugi Maru に変える あと、最近増えたらしい Kaisei Decol も使ってみる

html-page.ts
 <head>
   <meta charset="utf-8"/>
   <title>Alosaur-Lite + Deno Deploy で Markdown をあれこれする</title>
   <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/4.0.0/github-markdown.min.css">

+  <link rel="preconnect" href="https://fonts.googleapis.com">
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+  <link href="https://fonts.googleapis.com/css2?family=Kosugi+Maru&display=swap" rel="stylesheet">
+  <link href="https://fonts.googleapis.com/css2?family=Kaisei+Decol:wght@500&display=swap" rel="stylesheet">

ヘッダーメニューを追加する CSSよくわからないので調べて出てきたものを適当に調整

html-page.ts
<style>
  header {
    width: 100vw;
    padding: 10px;
    padding-left: 50px;
    background-color: #f0f8ff;
    position: absolute;
    display: flex;
    top: 0;
    left: 0;
    align-items: center;
    font-family: 'Kaisei Decol';
  }
</style>
<!-- ...  -->
<header>
  <h3>Alosaur-Lite + Deno Deploy で Markdown をあれこれする</h3>
  <nav class="pc-nav">
    <ul style="list-style:none; display:flex;">
      <li style="margin: 0 0 0 15px;"><a href="#1">特に</a></li>
      <li style="margin: 0 0 0 15px;"><a href="#2">どこにも</a></li>
      <li style="margin: 0 0 0 15px;"><a href="#3">飛ばない</a></li>
    </ul>
  </nav>
</header>
nikogolinikogoli

Kosugi Maru は本文だけに適用したいので直書き
あとスクロールについてくるサイドバーも追加

html-page.ts
<style>
  .container {
  display: -ms-flexbox;
  display: -webkit-box;
  display: -webkit-flex;
  display: flex;
  margin: 0 auto;
  }
  .markdown-body {
    max-width: 780px;
  }
  .sidebar {
    width: 200px;
    margin:100px 0px 0px 50px;
    font-family: 'Kaisei Decol';
  }
  .sidebar-fixed {
    position: sticky;
    top: 100px;
  }
  </style>
<!--  ...  -->
<body>
  <div class="container">
    <div class="markdown-body" style="margin-top:100px;font-family:'Kosugi Maru'">
      ${content}
    </div>
    <aside class="sidebar">
      <div class="sidebar-fixed"><br><br><br><br><br><br><br>バー</div>
    </aside>
  </div>
</body>
nikogolinikogoli

初期状態

server.ts の中身

読み込む markdown のファイル名を scrap.mdに変えているので注意

server.ts
import { Marked } from 'https://deno.land/x/markdown@v2.0.0/mod.ts';

import { App, Controller, Get, View } from "https://deno.land/x/alosaur_lite/dist/mod.js";
import { getHtmlPage } from "./html-page.ts";

@Controller()
export class MainController {
  @Get()
  indexPage() {
    return View("scrap.md", {});
  }
}

const app = new App({
  controllers: [MainController],
});

app.useViewRender({
  type: "markdown",
  basePath: `/views/`,
  getBody: async (path: string, model: Object, config: any) => {
    return await getMarkDownPage(path);
  },
});

const globalRenderCache = new Map();

async function getMarkDownPage(path: string) {
  if (globalRenderCache.has(path)) {
    return globalRenderCache.get(path);
  }
  let result = null;

  const text = await Deno.readTextFile(`${Deno.cwd()}/views/${path}`);
  result = getHtmlPage(Marked.parse(text).content);

  globalRenderCache.set(path, result);

  return result;
}

addEventListener("fetch", (event: FetchEvent) => {
  event.respondWith(app.handleRequest(event.request));
});

nikogolinikogoli

シンタックスハイライト

highlight.js を使ってシンタックスハイライトを適用する[1]

Marked の Doc に記載されている方法が利用できたので、server.tsに以下の部分を追加

server.ts
import highlightJs from 'https://cdn.skypack.dev/highlight.js?dts';

// ...

marked.setOptions({
  highlight: function(code, lang) {
    const language = highlightJs.getLanguage(lang) ? lang : 'plaintext';
    return highlightJs.highlight(code, { language }).value;
  },
  // langPrefix: 'hljs language-', // これは無くても問題ない(と思う)
});

HTML に 使用する highlight.js テーマを追加

html-page.ts
<head>
<!--  ...  -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/styles/xcode.min.css">
脚注
  1. prism.js も試してみたが、とりあえず skypack から持ってくる方法ではだめだった。知識なさすぎて何がだめなのかもわからない・・・ ↩︎

nikogolinikogoli

結果[1]

脚注
  1. "=" がハイライトされないの、仕様なんだっけ? 正直 highlight.js の python ハイライトは手抜き感があって好きじゃない ↩︎

nikogolinikogoli

インラインのコードにもハイライトを適用する

パーサーの HTML タグ設定に介入する処理は、markdown の Renderer を override して実現する[1]
今回なら、codespan(text: string): string { ... }を書き直すことになる

やること

「インラインコードもコードブロックと同じ処理にする → ハイライトされる」と考えて、superを使って override 前のcode()の処理を適用させ、不要な<pre>タグを取り除く

server.ts
// Renderer も import 対象に追加する
import { Marked, Renderer } from 'https://deno.land/x/markdown@v2.0.0/mod.ts';

// ...

class MyRenderer extends Renderer{

  // インラインコードの処理
  codespan(text: string): string {
    const lang = "python"; // 手動で設定...

    //コードブロックと同じ処理を行うことで、ハイライト処理を適用させる
    const code_out = super.code(text.replace(/\&quot\;/g, '"'), lang)
    const matched = code_out.match(/<pre>([\S\s]+)<\/pre>/)
    if (matched !== null) {
      return matched[1].replace(/\n<\/code>/, "</code>");  
    } else {
      return code_out
    }
  }
}

Marked.setOptions({
  renderer: new MyRenderer, // oberride した Renderer を使う
  highlight: function(code, lang) {
    // ...
  },
})
脚注
  1. @ts-stack/markdow の Doc が参考になる + その下にある API から各処理の定義に飛べる ↩︎

nikogolinikogoli

行番号を表示する

highlight.js には行番号を表示させるプラグイン highlightjs-line-numbers.js があるが、DOMを操作する?想定になっているようで Marked の処理に組み込む感じでは動かなかった
  ↓
実装を見ると、(少なくともこの部分は)複雑な処理をしていないので、コピペして調整する

nikogolinikogoli

要するに、highlight.js がタグをつけたコードブロックの各行の文字列データを

<tr>
  <td data-line-number="${行番号}">${行番号}</td>
  <td data-line-number="${行番号}">${コード}</td>
</tr>

に変換し、最後に<table>で囲むという処理になる

ただし、そのままでは CSS のテーブル設定が適用されてしまうので、適当に上書きする

<tr style="border-top:0px">
  <td style="border:0px; border-right:1px solid #b9b9b9; padding:0px 7px 0px 0px" data-line-number="${行番号}">${行番号}</td>
  <td style="border:0px;padding:0px 0px 0px 7px" data-line-number="${行番号}"> ${コード}</td>
</tr>
nikogolinikogoli

というわけで以下のような関数を作り

function addLineNumbers (inputHtml: string, use_singleLine=false, startFrom=1): string{
  var lines = inputHtml.split(/\r\n|\r|\n/g)

  if (lines[lines.length-1].trim() === '') {
      lines.pop();
  }

  if (lines.length > 1 || use_singleLine) {
    var html = '';

    lines.forEach((li, idx) => {
      html += `<tr style="border-top:0px;">`
        + `<td style="border:0px; border-right:1px solid #b9b9b9; padding:0px 7px 0px 0px" data-line-number="${idx + startFrom}">${idx + startFrom}</td>`
        + `<td style="border:0px;padding:0px 0px 0px 7px" data-line-number="${idx + startFrom}"> ${li.length > 0 ? li : ' '}</td></tr>`;
    })
  return `<table style="margin:0px;">${html}</table>`;
  }

  return inputHtml;
}

行番号の表示の有無を変更できるような雰囲気を出しつつ、ハイライト処理に追加する

server.ts
 // ...
+const ADD_LINE_NUM = true

 //...

 Marked.setOptions({
   renderer: new MyRenderer,
   highlight: function(code, lang) {
     const language = highlightJs.getLanguage(lang) ? lang : 'plaintext';
-     return highlightJs.highlight(code, { language }).value;
+     const out = highlightJs.highlight(code, { language }).value;
+     return ADD_LINE_NUM? addLineNumbers(out) : out
   },
})

nikogolinikogoli

これで行番号の追加は実現できるが、末端に謎の空白が入るという現象が起こる

これは、Renderer が "... ${タグがついたコード}\n</code></pre>\n"返す処理になっているために、"...</table>\n</code></pre>"となって表の下に改行が入ってしまうため
   ↓
codespan() と同様に code() も override して "\n" を入れないようにする

server.ts
class MyRenderer extends Renderer{

  code(code: string, lang?: string, escaped?: boolean, meta?: string): string {
    const [c, l, e] = [...arguments] // なぜか引数は3つしか入っていない meta はどこへ?
    const code_out = super.code(c, l, e);
    return code_out.replace(/\n<\/code>/, "</code>");
  }

  codespan(text: string): string {
  // ...
  }
}
nikogolinikogoli

diff の表示

「行番号の処理 = コード各行を逐次処理 → diff も判定できる!」ということで diff も表示

addLineNumbers 関数に以下の処理を追加する[1]

  1. 行頭の "+" と "-" の有無をチェックする
  2. 結果に応じて "+" と "-" を番号の方に移す
  3. 結果に応じて <tr> の背景色を変える
server.ts
 function addLineNumbers (inputHtml: string, use_singleLine=false, startFrom=1): string{
  // ...
  if (lines.length > 1 || use_singleLine) {
  //...
  lines.forEach((li, idx) => {
+      let diff_prefix = "";
+      let bc_color = "#f6f8fa";
+      if (li.length) {
+        if (li[0]=="-") {
+          diff_prefix = " -";
+          li = li.replace(/^-/g, "");
+          bc_color = "#ffe6e6";
+        } else if (li[0]=="+") {
+          diff_prefix =" +";
+          li = li.replace(/^\+/g, "");
+          bc_color = "#c8f5d0";
+      }
+      const num_text = `${idx + startFrom}${diff_prefix}`;
+      html += `<tr style="border-top:0px; background-color:${bc_color}">`
+          + `<td style="border:0px; border-right:1px solid #b9b9b9; padding:0px 7px 0px 0px" data-line-number="${idx + startFrom}">${num_text}</td>`
       //....
       
脚注
  1. つまり行番号の表示が無いときは別の diff 表示関数が必要なわけで、この辺の面倒さが highlight.js が行番号の表示を導入しない宣言を出している理由の1つなのかなと思った ↩︎

nikogolinikogoli

ここまでの結果

行番号の有無でメイン表示部の幅が変わるなど、意図しない副作用が発生している感がすごい

nikogolinikogoli

サイドメニュー作成

自分の見出しの振り方が下手すぎてあれなのだが、適宜調整してサイドメニューに各<h1>要素へのリンクを置いてみる

方法

  1. まず、<h>要素をパースする際に、内容と id 要素から <a>タグを作り、リストに格納する
  2. パースが完了したあとで<a>タグリストの中身を結合し、サイドメニューの html を作る
  3. getHtmlPage関数を変更し、サイドメニューに渡した html を流し込むようにする
nikogolinikogoli

https://zenn.dev/link/comments/4ff5dbb369376f
↑を参考に、heading()を override する

server.ts
//...
let HEADER_STORE: string[] = [] // こういうグローバル変数の使い方はやっぱよくないんだろうか?

// ...

class MyRenderer extends Renderer{

  heading(text: string, level: number, raw: string): string {
    const id: string = this.options.headerPrefix + String(text).trim().toLowerCase().replace(/\s+/g, '-');
    if (level==1) {
      HEADER_STORE.push(`<a href="#${id}">${text}</a>`);
    }
    return `<h${level} id="${id}">${text}</h${level}>\n`;
  }
  //...
nikogolinikogoli

リストの中身を結合して getHtmlPage()にわたす

server.ts
async function getMarkDownPage(path: string, config: any) {
  // ...
   const text = await Deno.readTextFile(`${Deno.cwd()}/views/${path}`);
+  const parsed = Marked.parse(text).content;
+  const side_texts = HEADER_STORE.join("<br><br>");
+  HEADER_STORE = [];
-  result = getHtmlPage(Marked.parse(text).content);
+  result = getHtmlPage(parsed, side_texts);

getHtmlPgae()の引数を増やし、サイドメニューの中に流すようにする

html-page.ts
export function getHtmlPage(content: string, side_content: string) {
  return `
  /...
      <aside class="sidebar">
        <div class="sidebar-fixed">${side_content}</div>
      </aside>
  / ...
  `
nikogolinikogoli

注釈の処理

未調整の場合、こんな感じになる (スクラップのセル?の切れ目のEND_OF_SCRAPについては後述)

  • 問題1:注釈番号がリンクされているのに注釈の内容が表示されていない
  • 問題2:注釈番号の表示が適切でない (記号^が残っている)
  • 問題3:そもそもリンクになっていない注釈がある
nikogolinikogoli

リンク化された注釈の調整

リンク化された注釈でやるべきこと

  1. 表示を ^NUM から (NUM)に直す + <sup>タグを使って上付き文字にする
  2. リンク先として href="#fn:NUM"を設定する
  3. リンク先から戻ってこれるように、id="#fn_ref:NUM"を設定する

というわけで、注釈番号の部分で使う html タグを返してくれる関数を追加し

server.ts
function get_refnumber_html (ft_count: number, content: string): string{
  const ref_text = `
    <sup id="fn_ref:${ft_count}">
      <a href="#fn:${ft_count}">(${ft_count})</a>
    </sup>`
  return ref_text
}

Renderer の リンクの処理 link()を override する

server.ts
// ...
class MyRenderer extends Renderer{
  // ...
  link(href: string, title: string, text: string): string {
    if (text.match(/\^(\d+)/) !== null) {
      let footnote_count = Number(text.match(/\^(\d+)/)![1])
      return get_refnumber_html(footnote_count, href)
    }
    return super.link(href, title, text)
  }
  // ...
nikogolinikogoli

ただ、これでは画像の ^1 の部分が (1) になるものの注釈が表示されていないのは変わらない

というわけで、以下の方法で注釈を表示させることにする

  1. リンク化された注釈を処理する際に、注釈の内容を変数FOOTNOTE_STOREに格納する
  2. 1まとまりの『親スクラップ+子コメントスクラップ』のパースが終了した時点でFOOTNOTE_STOREを確認し、中身があれば注釈部分の html を差し込む
  3. FOOTNOTE_STOREを空にし、次の『親スクラップ+子コメントスクラップ』のパースへ
nikogolinikogoli

まず準備として、scrap の markdown 切り出し処理を変更し、区切り線ではなくEND_OF_SCRAPを差し込むようにして、markdown を取得し直す

で、FOOTNOTE_STOREなのだが、せっかくなので interface を使ってみる

server.ts
// ...
interface FtnoteInfo {
  index : number; // 注釈番号
  text : string; // 注釈の内容
  is_tip_set : boolean; // 後で使う
}

// 注釈番号でソートしたいので、配列ではなく Map にする
let FOOTNOTE_STORE = new Map<number, FtnoteInfo>();

上でいじった link() を、これを使う形式に変更

server.ts
 // ...
 class MyRenderer extends Renderer{
  // ...
   link(href: string, title: string, text: string): string {
     if (text.match(/\^(\d+)/) !== null) {
       let footnote_count = Number(text.match(/\^(\d+)/)![1])
+      const ft_info: FtnoteInfo = {
+        index: footnote_count,
+        text: href,
+        is_tip_set: true
+      }
+      FOOTNOTE_STORE.set(footnote_count, ft_info);
       return get_refnumber_html(footnote_count, href);
     }
     return super.link(href, title, text);
   }
   // ...
nikogolinikogoli

さらに注釈部分の html を返してくれる関数を追加し

server.ts
function make_footnote(): string{
  const keys = [...FOOTNOTE_STORE.keys()].sort(); // 注釈番号順に並べ替える
  const start_num = keys[0];                     //一番若い番号を取得
  // .map を使って<li><a> で囲んだ注釈のリストを作る
  const L = keys.map((idx:number) =>{      
    const ft_info = FOOTNOTE_STORE.get(idx)
    if (ft_info !== undefined){
      // 注釈番号のid: fn_Ref:NUM、注釈のid: fn:NUM
      const ft_text = `
        <li id="fn:${ft_info.index}" >
          <font size="1">${ft_info.text}</font>
          <a href="#fn_ref:${ft_info.index}">  ↩</a>    
        </li>`;
      return ft_text;
    } else {
      return ""
    }
  })
 FOOTNOTE_STORE = new Map<number, FtnoteInfo>();  // FOOTNOTE_STORE のクリア
  if (L.length) {
    // .map で得た注釈のリストを <ol> で囲む
    const ft_htmls = `
      <div style="border-bottom:1px solid #b9b9b9;font-size:90%">脚注</div>
      <ol start="${start_num}" style="color:dimgray;">
        ${L.join("\n")}
      </ol><br>`;
    return ft_htmls;
  } else {
    return ""
  }
}

END_OF_SCRAPがあったときに注釈を差し込むように、Renderer の paragraph を override

server.ts
///...
 class MyRenderer extends Renderer{
  // ....
  paragraph(text: string): string {
    if (text.includes(SCRAP_SEPARATION)) {
      let ft_html = ""
      if ([...FOOTNOTE_STORE.keys()].length) {
        ft_html = make_footnote()
      }
      // 注釈の有無によらず、END_... は 区切り線に置き換える
      return `${ft_html}<hr><hr>\n`
    }
    return super.paragraph(text);
  }
  // ...

さらに、末尾のスクラップのために、ファイル全体のパースが終わったあとに未処理の注釈を差し込む処理も追加

server.ts
 async function getMarkDownPage(path: string, config: any) {
   // ...
   const text = await Deno.readTextFile(`${Deno.cwd()}/views/${path}`);
   let parsed = Marked.parse(text).content;
+  if ([...FOOTNOTE_STORE.keys()].length) {
+    parsed =  parsed + make_footnote()
+  }
   //...
nikogolinikogoli

注釈のツールチップ化

以前から試してみたかった、『注釈内容のツールチップ表示』をやってみる

と言っても、↓で説明されている CSS を注釈番号の部分に追加するだけでできてしまう[1]
https://zenn.dev/catnose99/articles/26bd8dac9ea5268486c8

ただし、ツールチップは1行で表示されるが注釈が長文だと読みづらくなるので、適宜 style を上書きする

というわけで、tooltipやらの設定を html-page.ts に追加した上で、 get_refnumber_html()が返す html を少し変更する

server.ts
 function get_refnumber_html (ft_count: number, content: string): string{
+  let style_setting = ""
+  if (content.length > 30) {
+    style_setting = ' style="min_width=450px; white-space: break-spaces;"'
+  }
   const ref_text = `
-   <sup id="fn_ref:${ft_count}">
+   <sup class="tooltip" id="fn_ref:${ft_count}">
       <a href="#fn:${ft_count}">(${ft_count})</a>
+     <span class="tooltip-text"${style_setting}>${content}</span>
     </sup>`;
   return ref_text
 }


マウスを乗せると注釈の内容を見ることができる (表示位置は少し調整している)

脚注
  1. これだけで実現するのめっちゃ感動した。CSS いじるの本当に苦手なので、こういう情報はとても助かる ↩︎

nikogolinikogoli

リンク化されていない注釈の調整

文中の [^NUM] を注釈番号の表示に変え、注釈へのリンクを張る

基本的には、文章中に[^NUM]があった場合にリンクと同じ処理をすれば良いので

  1. .matchAll(/\[\^(\d+)\]/g)[^NUM] の有無を調べる (ただし[^NUM]:は避ける)
  2. ヒットした部分の数字をもとに注釈番号としての html を取得する
  3. 取得した html で [^NUM] を置き換える

というわけで、paragraph()の overrider をさらに追加する

server.ts
class MyRenderer extends Renderer{
  // ...
  paragraph(text: string): string {
    // 注釈内容の html を追加する処理
    if (text.includes(SCRAP_SEPARATION)) {
      //...
    }

    // 文中の注釈内容 ( [^1]: ) の処理:今はなにもしない
    if ([...text.matchAll(/^\[\^(\d+)\]:/g)].length) {
      return super.paragraph(text)
    }
    // 文中の注釈番号 ( [^1] ) の処理
    if ([...text.matchAll(/\[\^(\d+)\]/g)].length) { // if の前に変数定義を置きたくないけど、汚い・・・
      [...text.matchAll(/\[\^(\d+)\]/g)].forEach( (matched: string[]) => {
          // 基本は link の場合と同じだが、注釈の内容を取得できないので仮のもので処理
          const footnote_count = Number(matched[1]);
          const ft_info: FtnoteInfo = {
            index: footnote_count,
            text: `TEMP_TOOLTIP_${footnote_count}`, // 仮の注釈の内容
            is_tip_set: false // 注釈が仮であるフラグ (注釈が仮 = ツールチップの内容が不正確)
          };
          FOOTNOTE_STORE.set(footnote_count, ft_info);
          const new_html = get_refnumber_html(footnote_count, `TEMP_TOOLTIP_${footnote_count}`);
          text = text.replace(matched[0], new_html); // 取得した html で置き換え
      })
      return `<p>${text}</p>\n`
    }
    return super.paragraph(text);
  }
nikogolinikogoli

文中の [^NUM]: を注釈の表示に変え、注釈番号へのリンクを張る

見た目は1文の中に複数の [^NUM]: があるように見えるが、内部的にはちゃんと(markdown で入力している)\nが間に入っている

というわけで、

  1. \nで切り分ける
  2. それぞれの部分から注釈番号と注釈内容を取り出す
  3. 注釈番号を使って FOOTNOTE_STORE からオブジェクトを取り出し、.textの内容をTEMP_TOOLTIP_*から正しい注釈内容に差し替える
server.ts
class MyRenderer extends Renderer{
  // ...
  paragraph(text: string): string {
    // 注釈内容の html を追加する処理
    if (text.includes(SCRAP_SEPARATION)) {
      //...
    }

    // 文中の注釈内容 ( [^1]: ) の処理
    if ([...text.matchAll(/^\[\^(\d+)\]:/g)].length) {
      const text_lis =  text.split(/\n/g);
      text_lis.forEach((tx:string) => {
        const [num, ref_text] = [...tx.matchAll(/^\[\^(\d+)\]: ([\s\S]+)/g)][0].slice(1);
        const footnote_count = Number(num);
        if (FOOTNOTE_STORE.has(footnote_count)) {
          FOOTNOTE_STORE.get(footnote_count)!.text = ref_text
        }  
      })
      return ""
    }
    // 文中の注釈番号 ( [^1] ) の処理
    if ([...text.matchAll(/\[\^(\d+)\]/g)].length) { 
   // ...
    }
    return super.paragraph(text);
  }
nikogolinikogoli

ツールチップの差し替え

  1. make_footnote()での注釈作成時に .is_tip_setフラグをチェックし、差し替えが必要な注釈、つまり<span>TEMP_TOOLTIP_*</span>になっているものを判定する
  2. 差し替えが必要な注釈は、変数TOOLTIP_STOREに格納する
  3. 全ての markdown のパースが終わったあとで TOOLTIP_STORE を確認し、中身があれば、それぞれについて 「TEMP_TOOLTIP_* → 適当な内容」の差し替えを行う
  4. 差し替える際には注釈の長さを確認し、長いものは<span>タグに style を差し込む形で差し替えを行う
server.ts
+let TOOLTIP_STORE= new Map<string, string>();
 
 // ...

 function make_footnote(): string{
  // ...
   const L = keys.map((idx:number) =>{
     const ft_info = FOOTNOTE_STORE.get(idx)
     // ...
+      if (!ft_info.is_tip_set) {
+        TOOLTIP_STORE.set(`TEMP_TOOLTIP_${idx}`,ft_info.text);
+      }
        return ft_text;
     } else {
       return ""
     }
   })
   // ...
 }

// ...

 async function getMarkDownPage(path: string, config: any) {
   // ...
   const text = await Deno.readTextFile(`${Deno.cwd()}/views/${path}`);
   let parsed = Marked.parse(text).content;
   if ([...FOOTNOTE_STORE.keys()].length) {
     parsed =  parsed + make_footnote()
   }
+  if ([...TOOLTIP_STORE.keys()].length) {
+    TOOLTIP_STORE.forEach((tooltip_text: string, target_txt: string) => {
+      if (tooltip_text.length > 30) {
+        const syle_setting = ' style="min-width:450px; white-space: break-spaces;"';
+        parsed = parsed.replace(`>${target_txt}`, `${syle_setting}}>${tooltip_text}`);
+      } else {
+        parsed = parsed.replace(target_txt, tooltip_text);
+      }
+    })
+  }
+   TOOLTIP_STORE = new Map<string, string>();
   const side_texts = HEADER_STORE.join("<br><br>");
   HEADER_STORE = [];
   // ...

nikogolinikogoli

連番の処理

悩み中・・・

markdown 内で注釈番号に重複がある場合、Renderer に来る前の時点でパーサーによるリンク張り自体が失敗するっぽい

失敗するというか『同名のリンクならば同名のとこに飛ぶ』という当たり前の法則にしたがっているわけで、単一で完結する想定でできているスクラップをまとめて処理しているのが良くないな、これ

json からの markdown の切り出しとパースをセットでやるように変える必要がある

debug()使えば BlockLexer.parser() の結果が見れるけど、リンク張りここで介入できないかな

nikogolinikogoli

ミニページ(アコーディオン・トグル)周り

ts_stack/markdown の説明にあるように、Marked.setBlockRuleを使えば対処できる

これ系の独自記法は大体コードブロックとしてパースされるので、code()内でフラグに応じて処理を変えるようにして力技で実現させることもできる

その際、Renderer 内で Marked.parse(code.replace(/\t/, ""))とかするとミニページ内の文章をパースさせることができる