🔖

「TypeScript でプログラマブルに動く日本語組版処理システムの提案」をJSX対応する

2024/12/13に公開

はじめに

まずはこちらの素晴らしい記事をお読みください。
https://zenn.dev/inaniwaudon/articles/5d040f543c4c69

という訳で、TS (JS)のオブジェクトを用いて記述ができるとのことですが、コメント (Discussion)にある通り、JSXで書けても面白そうです。という訳で簡易的に一部実装しました。

こんな感じで動きます(コードを一部抜粋)。

// pillar関数などは元のコードのtest/src/helper.tsにあるユーティリティ関数
const Pillar = ({ children }: { children: string }) => pillar(children)
const H1 = ({ children }: { children: string }) => h1(children.split("\\n"));
const H2 = ({ children }: { children: string }) => h2(children);
const H3 = ({ children }: { children: string }) => h3(children);
const P = ({ children }: { children: string | string[] }) => p(Array.isArray(children) ? children : [children]);
const Author = ({ children }: { children: Inline[] }) => author(children);
const B = ({ children }: { children: string }) => b(children);
const URL = ({ children, href }: { children: string, href: string }) => url(children, href);
const Nombre = () => nombre();
const Fn = ({ children }: { children: string }) => fn(children);
const FootNote = ({ children, label }: { children: string, label: string }) => footnote(children, label);
const Li = ({ children }: { children: Inline[] }) => li(children);
const C = ({ children }: { children: string }) => c(children);
const Figure = ({ children }: { children: string }) => figure(children);
const Caption = ({ children }: { children: string }) => caption(children);


export const body = (
  <div>
    <Pillar>TypeScript でプログラマブルに動く日本語組版処理システムの提案</Pillar>
    <H1>
      TypeScript でプログラマブルに\n動く日本語組版処理システムの提案
    </H1>
    <Author>
      <B>いなにわうどん @kyoto_inaniwa</B>
      <URL href="https://zenn.dev/inaniwaudon/articles/5d040f543c4c69">
        https://zenn.dev/inaniwaudon/articles/5d040f543c4c69
      </URL>
    </Author>
    <Nombre />
    <H2>はじめに</H2>
    <P>X(旧 Twitter)のタイムラインが組版の話で盛り上がっていたため、自分も軽く参画したところ思いのほかアツくなってしまい、今なお組版への熱が失われていなかったことを再確認した</P>
    <P>
      さて、春先に TypeScript 上にてプログラマブルに作動する日本語組版処理システム(以下、仮称として minitype を用います)を構想し、数週間掛けてプロトタイプの実装を行っていました。ところが、今年度になって個人開発にリソースを割く余裕がなくなり、宙ぶらりんな状態のまま年末を迎えてしまいました。まだ開発途中ではありますが
      <Fn>software</Fn>
      、折角なので「日本語組版処理システムの夢
      <Fn>yume</Fn>
      」としてアイデアを供養するとともに、具体的なプロトタイプの実装を示したいと思います
      <Fn>system</Fn></P>
    <P>
      実装についてはソースコードを見ていただくこととして、本記事では特に「
      <B>ユーザがどのような記述をするとどのような出力が得られるのか</B>
      」という点に焦点を絞って紹介を進めていきます。
    </P>
    <FootNote label="software">ソフトウェアは出す時期も大事で、完璧を目指すと一生日の目を見ることはない</FootNote>
    <FootNote label="yume">ヨドバシの福袋?</FootNote>
    <FootNote label="system">概念実証のような段階であり、実用は目的としていません</FootNote>
    <H2>概念</H2>
    <P>minitype は TypeScript を用いて記述され、Node.js 上で作動します。プロトタイプの実装を以下の GitHub レポジトリに公開しています。開発途中であるため機能は限定的です(バグもあります)。</P>
    <Li>
      inaniwaudon/minitype-test
      <URL href="https://github.com/inaniwaudon/minitype-test">
        https://github.com/inaniwaudon/minitype-test
      </URL>
    </Li>
    <P>
      詳細を話す前に、まずはシステムの動作例をご覧ください。以下のソースコードは、本記事と同様の文書を記述した ts ファイルになります。minitype のパッケージを
      <C>npm i</C>
      した後、
      <C>npx article.ts</C>
      を実行することにより、図 1 に示す PDF 文書を出力することができます。
    </P>
    <P>(ソースコードは省略)</P>
    <Figure>thumbnail.png</Figure>
    <Caption>minitype を用いて作成した組版例</Caption>
    <P>
      実装としては、OpenType の読み込みに
      <URL href="https://github.com/opentypejs">opentype.js</URL>
      を、PDF の描画に
      <URL href="https://pdfkit.org/">pdfkit</URL>
      を使用しています。現状の実装ではテキストをアウトライン化した PDF を生成しているため、フォントの埋め込みは今後の課題になります。加えて、シンタックスハイライトに lowlight を、フォントのキャッシュ周りには sqlite3 を使用しています。
    </P>
    <H3>マークアップ</H3>
  </div>
);

  (async () => {
    minitype(
      render(body) as Block[],
      // 以下省略


(モリサワフォントを持っていないのでトガリテ Regularにしています)

実装

こちらの素晴らしいBookを読みました。
https://zenn.dev/uhyo/books/your-own-jsx-implementation
JSXのランタイムを実装したい方はマストで読むと良いです。

その上で実装しましたが、残念ながら私のTS力の欠如により、型がだいぶ終わっています。

// jsx-runtime.ts
import { Block } from "../lib/block.js";
import { Command, Inline } from "../lib/inline.js";

type FuncTag = ((props: Record<string, unknown>) => MinitypeJSXElement)
  | ((props: Record<string, unknown>) => Block)
  | ((props: Record<string, unknown>) => Inline)

export interface MinitypeJSXElement {
    tag: string | FuncTag;
    props: Record<string, unknown>;
}

export function jsx(
    tag: string | FuncTag,
    props: Record<string, unknown>,
): MinitypeJSXElement {
    return { tag, props };
}

export type Renderable =
  | Block
  | Inline
  | MinitypeJSXElement
  | Renderable[]
  | null
  | undefined;

function isCommand(arg: MinitypeJSXElement | Block | Command): arg is Command {
    return arg.hasOwnProperty("body");
}

function isBlock(arg: MinitypeJSXElement | Block): arg is Block {
    return arg.hasOwnProperty("type");
}

export function render(renderable: Renderable): Block | Block[] | Inline {
    if (Array.isArray(renderable)) {
        return renderable.map(render) as Block[];
      }
    if (renderable === undefined || renderable === null) {
        return "";
    }
    if (typeof renderable === "string" || isCommand(renderable) || isBlock(renderable)) {
        return renderable;
    }
    const { tag, props } = renderable;
    if (typeof tag === "function") {
        const renderedProps = render(props.children as any);
        return render(tag({...props, children: renderedProps}));
    }
    const { children, ...rest } = props;
    if (tag === "div") {
        return Array.isArray(children) ? render(children as Block[]) : render(children as Block);
    }
    return Array.isArray(children) ? children as Block[] : children as Block;
}
  
export { jsx as jsxs };

// types.d.ts
import { MinitypeJSXElement, Renderable } from "./jsx-runtime.ts";


interface HasChildren {
    children?: Renderable;
  }
  
  declare global 
  {
  namespace JSX {
    interface IntrinsicElements {
      div: HasChildren;
    }
    type Element = MinitypeJSXElement | Inline | Block;
    interface ElementChildrenAttribute {
      children: unknown;
    }
  }
}

また上記のBookにある「この本ではプロジェクト内にJSXのランタイムを用意したので〜を含めましょう。」の部分がよくわからずライブラリとしてJSXに対応させることができなかったため、minitype-test/package/srcにpdfを生成させるスクリプトを置いて(つまり、ライブラリをtestディレクトリで読み込んで動かすのではなくアプリケーションとして)動かしています。

コード全体のリポジトリはこちらです。
https://github.com/Catminusminus/minitype-test/tree/feature/jsx

cd packageしてSourceHanCodeJP-Normal.otfとTogalite-Regular.otfを置いて、npm installしてnpm run buildしてnode dist/a-article.jsするとa.pdfが生成されます。

コードの解説

一応最後に実装したJSXランタイムの解説をします。
minitype-testにおけるマークアップで出てくる「オブジェクト」は、

  • 文字列
  • Command
  • Block (ParagraphとかHeadingとか)

の3種類に大別できます(type Inline = string | Block)。なのでrenderの返り値の型(そしてJSX.Element)がこうなってるんですね。

で、bodydivで括られていますが、現在の実装では必ずdivで全体をくくってください。
divは特別扱いしていて、間に挟んでいるものをrenderに入れたものを返します。
今回は間に複数のJSXタグが(PillarH1タグが同じレベルで)入っているため、配列としてrenderに渡ります。
で、配列が入った場合renderable.map(render) as Block[]なので、それぞれrenderに入れられた配列が返ってきます。
まずPillarタグを見てみましょう。これはrenderableとしては、tagPillar関数で、props{ children: "TypeScript でプログラマブルに動く日本語組版処理システムの提案" }です。なので

    if (typeof tag === "function") {
        const renderedProps = render(props.children as any);
        return render(tag({...props, children: renderedProps}));
    }

の部分に入ります。文字列はrenderしてもそのままの文字列なので( if (typeof renderable === "string"のあたり)、render(Pillar({ children: "TypeScript でプログラマブルに動く日本語組版処理システムの提案" }))となります。ここで、文字列はrenderしようがしまいが変化はありませんが、一般にタグが間に挟まっているような場合は先にrenderしないとタグのままになってしまうので注意です。
ここでPillar({ children: "TypeScript でプログラマブルに動く日本語組版処理システムの提案" })pillar("TypeScript でプログラマブルに動く日本語組版処理システムの提案")なのでBlockです。そのためrenderはこれをそのまま返します。よって、<Pillar>A</Pillar>pillar("a")と書くのと同じ、という訳です。
あとは他のタグでもほとんど同じです。

おわりに

という訳で簡易的にですがJSXが使えるように実装してみました。型とかもうちょっとどうにかしたいですが力尽きたので読者の課題とします。

Discussion