例
Cosense Web の /help-jp/その他の書き方 を変換したものです。実装はそのまま動くコードで、この画面もそれで作っています。
そのまま変換する
parse して toHtml に渡すだけの、いちばん短い書きかたです。
import { parse } from "@cosense-toolbox/parser"
import { toHtml } from "@cosense-toolbox/parser/html"
import style from "@cosense-toolbox/style/style.css?raw"
const source = [
"その他の書き方",
"基本的な[ブラケティング]の他にも、色々な[記法]があります",
"",
"外部リンク (一般のWebページへのリンク)",
" URLを書くとリンクになる",
" https://yahoo.co.jp",
" `[URL タイトル]`",
" [https://yahoo.com Yahoo!]",
" `[タイトル URL]`逆順でもok",
" [Yahoo! https://yahoo.com]",
"",
"画像",
" `[画像URL]`",
" [https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]",
" `[[画像URL]]`",
" 横幅いっぱい、高さ制限無しで大きな画像を表示",
"",
"リンク付き画像",
" `[リンク先URL 画像URL]`",
" [http://yahoo.co.jp https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]",
"",
"別プロジェクトへのリンク",
" `/プロジェクト名/ページ名`",
" [/icons]",
" [/icons/すごい]",
"",
"アイコン",
" [/qaynam/qaynam.icon]",
" [/Icons2/trainSymbol-M.icon]",
"",
"[数式]",
" [$ \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}]",
"",
"[HashTag]",
" #HashTag",
"",
"引用",
" > 行頭に`>`を書くと引用になります",
"",
"コード",
" バッククオート`\\``で囲む",
" `function() { return \\`hello\\` }`",
"",
"[コードブロック]",
" code:hello.js",
" function () {",
" alert(document.location.href)",
' console.log("hello")',
" // コメントも書けるぞ",
" }",
"",
"[テーブル]",
" `table:テーブル名` の後、インデントしてtab区切り",
" table:テーブルの例",
" abc def",
" 12345 6789",
" 長い長い文字列 短い文字列",
"",
"コマンドライン",
" 行頭に`$`もしくは`%`",
" $ git reset --hard HEAD^",
" % cp a.txt b.txt",
"",
"[[強調]]と[*** 大きな文字]",
" `[[強調]]` ⇒ [[強調]]",
" `[* 強調]` ⇒ [* 強調]",
" `[**** もっと大きな文字]` => [**** もっと大きな文字]",
"",
"[/ 斜体]",
" `[/ 斜体文字]` ⇒ [/ 斜体文字]",
" `[/* 太字斜体文字]` => [/* 太字斜体文字]",
"",
"[- 打ち消し線]",
" `[- 打ち消し]` ⇒ [- 打ち消し]",
].join("\n")
const body = toHtml(parse(source))
export const html = `<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<style>${style}</style>
</head>
<body>
${body}
</body>
</html>`ただし、アイコンが画像になりません。アイコンの画像は Cosense の/api/pages/{project}/{user}/icon にありますが、[user.icon]のように記法にプロジェクト名が書かれていないことがあるので、パーサーは既定では URL を組み立てないからです。 リンク先が /タイトル になっているのも同じ理由です。
リンク先とアイコンを渡す
そこで pageUrl と iconImageUrl を渡します。プロジェクト名を知っているのは呼び出し側なので、URL の組み立ても呼び出し側が行います。
import { parse } from "@cosense-toolbox/parser"
import { toHtml } from "@cosense-toolbox/parser/html"
import { collect } from "@cosense-toolbox/parser/utils"
import style from "@cosense-toolbox/style/style.css?raw"
const project = "help-jp"
const source = [
"リンクとアイコン",
" 同じプロジェクトのページ",
" [ブラケティング]",
" #HashTag",
" 別のプロジェクトのページ",
" [/icons/すごい]",
" アイコン",
" [/qaynam/qaynam.icon]",
" [/Icons2/trainSymbol-M.icon]",
].join("\n")
const encodePath = (path: string) => path.split("/").map(encodeURIComponent).join("/")
/** `/project/page` はそのまま、`page` は `project` のページとして、Cosense のパスにする。 */
const pagePath = (title: string) =>
title.startsWith("/")
? encodePath(title)
: `/${encodeURIComponent(project)}/${encodeURIComponent(title)}`
/**
* アイコンの画像を取ってきて、`data:` URL にする。取れなければ null (ユーザー名のリンクになる)。
*
* API の URL を `<img>` にそのまま入れることはできない。API は `Cross-Origin-Resource-Policy: same-origin`
* を返すので、ほかのサイトからは読めない。転送先の URL も、期限付き (数分で切れる) のことがある。
*/
async function fetchIconDataUrl(user: string): Promise<string | null> {
try {
const res = await fetch(`https://scrapbox.io/api/pages${pagePath(user)}/icon`, {
signal: AbortSignal.timeout(4000),
})
if (!res.ok) return null
const type = res.headers.get("content-type") ?? "image/png"
const bytes = Buffer.from(await res.arrayBuffer()).toString("base64")
return `data:${type};base64,${bytes}`
} catch {
return null
}
}
const page = parse(source)
const iconByUser = new Map(
await Promise.all(
collect(page, "icon").map(
async (icon) => [icon.user, await fetchIconDataUrl(icon.user)] as const,
),
),
)
const body = toHtml(page, {
pageUrl: (title) => `https://scrapbox.io${pagePath(title)}`,
iconImageUrl: (icon) => iconByUser.get(icon.user) ?? null,
})
export const html = `<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<style>${style}</style>
</head>
<body>
${body}
</body>
</html>`Markdown から読む
逆向きに、ChatGPT の回答のような Markdown を@cosense-toolbox/markdown で読むと、Cosense のページに貼れる記法のテキストや、Cosense の AST になります。
import { markdownToCosenseText, parseFromMarkdown } from "@cosense-toolbox/markdown"
import { toHtml } from "@cosense-toolbox/parser/html"
import style from "@cosense-toolbox/style/style.css?raw"
// ChatGPT の回答のような Markdown
const markdown = `## React の useEffect
**useEffect** は、副作用を扱うための *フック* です。詳しくは[公式ドキュメント](https://react.dev/reference/react/useEffect)にあります。
### 主なポイント
1. **依存配列**を指定する
- 空配列なら初回だけ
- 省略すると毎回
2. クリーンアップ関数を返せる
\`\`\`tsx
useEffect(() => {
const id = setInterval(tick, 1000)
return () => clearInterval(id)
}, [])
\`\`\`
> **注意:** 開発中の Strict Mode では 2 回呼ばれます。
| 書き方 | 実行のタイミング |
| --- | --- |
| \`useEffect(fn)\` | 毎回 |
| \`useEffect(fn, [])\` | 初回だけ |
- [x] 依存配列を確かめた
- [ ] テストを書く
`
// Cosense のページに貼るなら、記法のテキストにする
export const text = markdownToCosenseText(markdown)
// 描画するなら、AST を toHtml に渡す
const body = toHtml(parseFromMarkdown(markdown))
export const html = `<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<style>${style}</style>
</head>
<body>
${body}
</body>
</html>`
[**** React の useEffect]
[* useEffect] は、副作用を扱うための [/ フック] です。詳しくは[公式ドキュメント https://react.dev/reference/react/useEffect]にあります。
[*** 主なポイント]
1. [* 依存配列]を指定する
空配列なら初回だけ
省略すると毎回
2. クリーンアップ関数を返せる
code:tsx
useEffect(() => {
const id = setInterval(tick, 1000)
return () => clearInterval(id)
}, [])
> [* 注意:] 開発中の Strict Mode では 2 回呼ばれます。
table:table
書き方 実行のタイミング
`useEffect(fn)` 毎回
`useEffect(fn, [])` 初回だけ
☑ 依存配列を確かめた
☐ テストを書く