djnote
v0.1.0
Published
djot ベースのメモ/講義資料処理系。setext 見出し・RFC822 メタデータ・祖先要素への属性付与に対応。
Readme
djnote
djot をベースにした、メモ・講義資料用の HTML 変換ツール。
djot がそのまま持っている機能(表・数式・任意要素への属性・見出しの自動 id・
見出しからの <section> 自動生成)に、次の 3 つだけを足している。
- setext 見出し(
====/----)を ATX 見出しに変換する - RFC822 形式のメタデータ(MultiMarkdown title block)を先頭から取り出す
- 祖先要素への属性付与(
[]{up=n key=value})
ほぼClaude Opus5に作ってもらった物です。
インストール
npm install -g djnote # コマンドとして使う
npm install djnote # ライブラリとして使う記法
メタデータ
先頭から最初の空行までが RFC822 形式のメタデータになる。
Title: 第3回 知識表現の基礎
Author: 健司
Keywords: djot, 知識表現,
トリプル記法
Css: style.css
本文はここから- 空白始まりの行は直前の値への継続行(folding)
- キーは小文字化し空白・アンダースコア・ハイフンを除去して正規化する
(
Key Words:→keywords) - 先頭行が
キー:の形でなければメタデータ無しと判定するので、 「注意: これは本文です」で始まる文書が誤って食われることはない 特別扱いされるキー:
| キー | 出力 |
|------|------|
| title | <title> |
| lang | <html lang="..."> |
| css | カンマ区切り → <link rel="stylesheet"> |
| javascript | カンマ区切り → <script src="..." defer> |
| turtle | <script type="text/turtle"> として埋め込み |
| その他 | <meta name="キー" content="値"> |
javascript / turtle
Css: base.css, print.css
Javascript: highlight.js, mermaid.js
Turtle: @prefix ed: <http://example.org/ed#> .
@prefix dcterms: <http://purl.org/dc/terms/> .
<> a ed:Lecture ;
dcterms:title "第3回 知識表現の基礎" ;
ed:code "IT101" .<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="print.css">
<script type="text/turtle">
@prefix ed: <http://example.org/ed#> .
@prefix dcterms: <http://purl.org/dc/terms/> .
<> a ed:Lecture ;
dcterms:title "第3回 知識表現の基礎" ;
ed:code "IT101" .
</script>
<script src="highlight.js" defer></script>
<script src="mermaid.js" defer></script>turtle だけは他のキーと違い、継続行を改行のまま保持する(他のキーは
RFC822 の folding に従って空白で連結される)。共通のインデントは取り除かれ、
相対的なインデントは残る。turtle の値は djot として解釈されないので、
引用符がスマート引用符に変換されることもない。
RFC822 形式の規則どおり、値の中に空行は置けない(空行はメタデータ全体の 終端になる)。Turtle は空行を必要としないので実用上は問題ない。
複数行キーは extractMetadata(src, { multilineKeys: [...] }) で変更できる。
見出し
第3回 知識表現の基礎
====================
用語の整理
----------これらは # / ## に変換される。コードフェンスの中は変換しない。
==== の行は空行に置き換えるので、警告に出る行番号は元ファイルと一致する。
祖先要素への属性付与
djot 標準のスパン記法のまま、up というキーで「何段外の要素か」を指定する。
段落全体に属性を付ける []{up=1 class=lead}<p class="lead">段落全体に属性を付ける</p>| 書き方 | 対象 |
|--------|------|
| []{abc=xyz} | スパン自身(djot 標準) |
| []{up=0 abc=xyz} | 同上(up を落とすだけ) |
| []{up=1 abc=xyz} | スパンを直接含む要素(<p>, <td>, <li> …) |
| []{up=2 abc=xyz} | そのさらに 1 つ外側 |
表のセルに書けば up=1 が <td>、up=2 が <tr>、up=3 が <table> になる。
- 段数の数え方は既定では自動生成される
<section>を飛ばす。--count-sections/countSections: trueで数に含める - スパンに内容がある場合(
[中身]{up=1 class=x})は、属性だけ祖先に移し 内容はその場に残す - 段数が文書の深さを超えたときは警告を出し、属性は捨てずにスパンに残す
コマンド
djnote note.dj # note.html を作る
djnote --overwrite note.dj # note.html が既にあっても上書き
djnote --stdout note.dj # 標準出力へ
djnote note.dj -o out.html # 出力先を明示
djnote src/*.dj -o build/ # まとめて変換
djnote --body note.dj # <body> の中身だけ
cat note.dj | djnote # パイプ(標準出力へ)-o を付けない場合、入力ファイルの拡張子を出力形式に置き換えたファイルを
同じ場所に作る(note.dj → note.html)。出力先が既に存在するときは
何も書かずに終了コード 1 で停止する。上書きしたいときは --overwrite。
-o で出力先を明示した場合はこの確認をしない(ビルド先の作り直しを
毎回止められると困るため)。--emit-djot のように出力先が入力ファイルと
同じになる場合は、上書きを避けて停止する。
主なオプション:
| オプション | 意味 |
|------------|------|
| --overwrite | 既存の出力ファイルを警告なしで上書き |
| --stdout | 標準出力に書き出す |
| --body | <body> の中身だけを出力 |
| --emit-djot | 前処理後の djot を出力(一括正規化用) |
| --count-sections | up= の段数に <section> を含める |
| --no-setext | setext 見出しの変換をしない |
| --no-fix-lists | 入れ子リスト前の空行補完をしない |
| --css <url> | スタイルシートを追加(複数可) |
| --js <url> | スクリプトを追加(複数可) |
| --no-defer | スクリプトに defer を付けない |
| --no-turtle | turtle を埋め込まない |
| --server | 簡易 Web サーバを起動 |
| --port <n> | 待ち受けポート(既定: 4000) |
| --host <addr> | 待ち受けアドレス(既定: 127.0.0.1) |
| --math <name> | mathjax(既定)/ katex / none |
数式は文書に数式が含まれるときだけ CDN のスクリプトを差し込む。
サーバ
djnote --server # http://127.0.0.1:4000/
djnote --server --port 4001
djnote --server --host 0.0.0.0 --port 8080カレントディレクトリをそのまま配信する。要求されたリソースの実体が無い ときは、それを生成できる元ファイルを探して変換して返す。
拡張子つきの URL
URL で表現が名指しされているので、ネゴシエーションはしない。
| 要求 | 動作 |
|------|------|
| /style.css | 実ファイルをそのまま返す |
| /abc.html | 無ければ abc.dj を探して変換して返す |
| /missing.html | 元ファイルも無ければ 404 |
拡張子なしの URL — コンテントネゴシエーション
拡張子の無い URL は、Accept ヘッダを見て表現を選ぶ(RFC 9110 §12)。
候補には実ファイルと生成できるものの両方が入る。
metadata.dj と metadata.ttl が置いてあるディレクトリで /metadata を
要求した場合:
| Accept | 応答 | Content-Location |
|--------|------|------------------|
| text/turtle | 200 text/turtle | /metadata.ttl |
| text/html | 200 text/html(metadata.dj から生成) | /metadata.html |
| text/html;q=0.5,text/turtle;q=0.9 | 200 text/turtle | /metadata.ttl |
| */*(curl の既定) | 200 text/html | /metadata.html |
| text/plain | 200 text/plain | /metadata.dj |
| image/png | 406 Not Acceptable(候補一覧を本文に出す) | — |
ネゴシエーションした応答には Vary: Accept と、実際に選ばれた表現を示す
Content-Location を付ける。
同じ media type に実ファイルと生成物の両方があるときは実ファイルを優先する。
Accept がワイルドカードだけのときの順序は DEFAULT_PREFERENCE
(HTML が先頭)で決まり、preference オプションで変更できる。
Generator の入力になっている拡張子(.dj)の実ファイルは優先度を下げて
あり、text/plain のように名指しで要求されたときだけ選ばれる。
ディレクトリ要求 /sub/ は sub/index として同じ仕組みにかかるので、
index.dj があれば HTML が、index.ttl があれば Turtle が返る。
生成結果はディスクに書かない。キャッシュもしないので、.dj を編集して
リロードすればそのまま反映される。root の外を指す要求は 403 で拒否する。
ブラウザが遮断するポート(6000、6665〜6669、10080 など)を指定した場合は、 起動時に警告を出す。
変換規則を足す
Generator を実装して渡すだけで、サーバの対応形式が増える。
Generator は「どの media type を作れるか」を軸に宣言する。拡張子は
/abc.ttl のような名指しの URL を解決するための別名として持つ。
import { startNoteServer, defaultGenerators } from "djnote";
import type { Generator } from "djnote";
const djotToTurtle: Generator = {
name: "djot → turtle",
mediaType: "text/turtle", // ネゴシエーションで使われる
extension: ".ttl", // /abc.ttl を解決するための別名
source: ".dj",
quality: 1, // Accept がワイルドカードのときの優先度
async build({ sourcePath, mediaType }) {
/* 文字列か Buffer を返す */
},
};
await startNoteServer({
port: 6001,
generators: [...defaultGenerators, djotToTurtle],
});charset は media type から自動で付く(text/* や application/json など)。
同じ拡張子に複数の Generator を登録した場合は、元ファイルが実在する
最初のものが使われる。
API
import { renderBody, renderDocument, parseNote } from "djnote";
// <body> の中身だけが欲しいとき(テンプレートエンジンや SSG に差し込む)
const { meta, html, warnings } = renderBody(source);
// 完全な HTML 文書
const { html } = renderDocument(source, {
css: ["/style.css"],
javascript: ["/app.js"],
lang: "ja",
});
// AST が欲しいとき
const { doc, djot } = parseNote(source, { keepSourcePositions: true });低レベルの部品も個別に使える。
| 関数 | 役割 |
|------|------|
| extractMetadata(src) | { meta, body, consumedLines } |
| preprocess(src, opts) | setext 変換・入れ子リスト補正 |
| applyAncestorAttributes(doc, opts) | up= の適用(doc を破壊的に変更) |
| renderBodyFromDoc(doc) | 加工済み doc → HTML 断片 |
| startNoteServer(opts) | サーバ起動。{ port, host, root, close() } を返す |
| findGeneration(path, gens) | 拡張子つきパスの生成元を探す |
| findRepresentations(base, gens) | 拡張子なしパスの表現をすべて集める |
| parseAccept(header) | Accept ヘッダを解釈する |
| selectVariant(variants, ranges) | 候補から最良の表現を選ぶ |
| contentTypeOf(mediaType) | charset 付きの Content-Type を作る |
Markdown からの移行メモ
djot は Markdown ではないので、次の違いに注意。
- 強調が逆。
_強調_が<em>、*強調*が<strong> - 入れ子リストの前に空行が必要。
* aの次行に+ bと書いても djot では入れ子にならない。 本ツールは既定でこれを検出して空行を補うが(--no-fix-listsで無効化)、 一度--emit-djotでファイル自体を正規化しておくのが確実 - インデントコードブロックが無い。4 スペース字下げは単なる段落
- 同じ階層のリストマーカーを
*→+→-と使い分ける流儀はそのまま使える
一括正規化はこうする。
for f in *.md; do djnote --emit-djot "$f" > "${f%.md}.dj"; doneライセンス
MIT
