anti scroll

ブラウザと小説の新しい関係を模索する

アプリケーションにTypeNovelのコンパイラを組み込む

この記事は、TypeNovelコンパイラをアプリケーションから利用したい開発者向けのものです。

導入

npm install --save typenovel

コンパイラの呼び出し

ソーステキストからコンパイルするときは、Tnc.fromStringです。

import { Tnc } from 'typenovel';

const result = Tnc.fromString('@scene(){ foo }', {
  format: 'html', // もしくは 'text'
  minify: false // trueだと圧縮表示
});

console.error(result.errors); // エラー
console.log(result.output); // コンパイル結果

ソースファイルからコンパイルするときは、Tnc.fromFileです。

import { Tnc } from 'typenovel';

const result = Tnc.fromFile('sample.tn', {
  format: 'html',
  minify: false
});

console.error(result.errors); // エラー
console.log(result.output); // コンパイル結果

コンパイラを拡張する

Compileクラスを使うと、コンパイルの各段階におけるVisitorを自前のものに置き換えることができます。

例えば、TypeNovelのノードツリー(TnNode)から出力文字列に変換するVisitor(NodeFormatter)を自前のものにするときは、こんな風にします。

import { Compile } from 'typenovel';

const myFormatter = new MyNodeFormatter(); // 自前で実装したFormatter
const result = Compile.fromString('@scene(){ foo }', {
  nodeFormatter: myFormatter // 自前のFormatterを設定
});

console.log(result.output); // コンパイル結果

ここで、MyNodeFormatterは、NodeFormatter interfaceを実装したクラスです。

NodeFormatter interfaceはnode-formatter.tsにて、次のように定義されています。

export interface NodeFormatter {
  visitTextNode: (args: {
    content: string;
    isWhiteSpacePre: boolean;
    isFirstChild: boolean;
    isLastChild: boolean;
    prev?: TnNode;
    next?: TnNode;
    indent: number;
  }) => string;

  visitAnnotNode: (args: {
    name: string;
    tagName: string;
    id: string;
    className: string;
    attrs: any;
    content: string;
    selfClosing: boolean;
    prev?: TnNode;
    next?: TnNode;
    indent: number;
  }) => string;

  visitBlockNode: (args: {
    name: string;
    tagName: string;
    id: string;
    className: string;
    attrs: any;
    content?: string;
    children: TnNode[];
    prev?: TnNode;
    next?: TnNode;
    indent: number;
  }) => string;
}

それぞれの関数の引数に、各ノードの情報が入っています。

ようするに、これらの情報を元にして、テキストノード(TextNode)、注釈タグ(AnnotNode)、制約ブロック(BlockNode)のそれぞれを、なんらかのテキストに変換して返せばよいわけです。

例えば注釈タグではクラス属性やid属性など無視して、タグ名だけあれば十分!などと思うなら、次のようにvisitAnnotNodeを実装すればいいわけです。

class MyNodeFormatter implements NodeFormatter {

  (省略)

  visitAnnotNode: (args: {
    name: string;
    tagName: string;
    id: string;
    className: string;
    attrs: any;
    content: string;
    selfClosing: boolean;
    prev?: TnNode;
    next?: TnNode;
    indent: number;
  }){
    return `<${args.tagName}>${args.content}</${args.tagName}>`;
  }
}

拡張できるinterfaceはノードから出力テキストの変換処理だけではありません。

次の部分をユーザーが自由に拡張できます。

TypeNovelParser(String -> Ast)

TypeNovelParserは、TypeNovelのソースからAstを作成するインターフェイスです。

AstMapper(Ast -> Ast')

AstMapperは、Astを別のAstに入れ替えるインターフェイスです。

AstConverter(Ast -> TnNode)

AstConverterは、AstTnNodeに変換するインターフェイスです。

NodeMapper(TnNode -> TnNode')

NodeMapperは、TnNodeを別のTnNodeに入れ替えるインターフェイスです。

NodeValidator(TnNode -> ValidationError[])

NodeValidatorは、TnNodeを検査してValidationErrorを出力するインターフェイスです。

NodeFormatter(TnNode -> String)

NodeFormatterは、TnNodeから出力テキストを生成するインターフェイスです。

拡張例

例えばTypeNovelにおいて、別のTypeNovelソースを展開する構文である

$include('別ソース')

などは、Ast -> Ast'な処理なので、IncludeExpanderクラスとしてAstMapperインターフェイスで実装されています。

ast-mapper.ts

あるいは、ノードから無駄なホワイトスペースを削除するNodeWhitespaceCleanerクラスは、TnNode -> TnNode' な処理なので、NodeMapperインターフェイスで実装されています。

node-mapper.ts

こうやって、コンパイラの各段階を自分好みのものに置き換えることで、各自でコンパイラが拡張できるような作りになっています。