作品に登場人物を登録した場合、先頭ページに「登場人物の一覧」が表示されるようになりました。
市販の書籍でも、だいたいこんな感じで冒頭に表示されてますよね。
登場人物の挿絵については、小説本文の下側に「登場人物」のタブメニューがあって、そこをクリックすると挿絵付きで一覧が表示されます。
人物をクリックすると、こんな感じで詳細情報が表示されます。
作品に登場人物を登録した場合、先頭ページに「登場人物の一覧」が表示されるようになりました。
市販の書籍でも、だいたいこんな感じで冒頭に表示されてますよね。
登場人物の挿絵については、小説本文の下側に「登場人物」のタブメニューがあって、そこをクリックすると挿絵付きで一覧が表示されます。
人物をクリックすると、こんな感じで詳細情報が表示されます。
jingoo 1.4.0をリリースしました。
Jg_template
モジュールを部分的に改良したJg_template2
というモジュールが追加されました。これまでJg_template
モジュールは、次のようにmodels
に連想リストを与えていました。
open Jingoo open Jg_types let result = Jg_template.from_string "{{ msg }}" ~models:[ ("msg", Tstr "hello, world!"); ]
Jg_template2
モジュールからは、models
に名前を与えたら値を返す関数を渡します。
open Jingoo open Jg_types let alist = [("msg", Tstr "hello, world!")] let result = Jg_template2.from_string "{{ msg }}" ~models:(fun key -> try List.assoc key alist with Not_found -> Tnull )
これの何が良いのかというと、string -> tvalue
という型の関数にさえなっていれば、データソースは何でも良くなることです。
例えばデータソースを(連想配列より高速な)Hashtbl
にすることもできます。
open Jingoo open Jg_types let htbl = Hashtbl.create 1 in let () = Hashtbl.add htbl "msg" (Tstr "hello, world!") in let result = Jg_template2.from_string "{{ msg }}" ~models:(fun key -> try Hashtbl.find htbl key with Not_found -> Tnull )
データソースなしで、単なる関数にしてしまうと、さらに高速です。
open Jingoo open Jg_types let result = Jg_template2.from_string "{{ msg }}" ~models:(function | "msg" -> Tstr "hello, world!" | _ -> Tnull )
関数にすることで、乱数値みたいなものを扱うこともできます。
open Jingoo open Jg_types let result = Jg_template2.from_string "{{ msg }}, {{ rand }}" ~models:(function | "msg" -> Tstr "hello, world!" | "rand" -> Tint (Random.int 100) (* 0 ~ 100 の値をランダムに返す *) | _ -> Tnull )
将来的にはこっちをメインのAPIとして使用したいのですが、旧版のAPIで既に広く使用されてしまっているので、今回はモジュールを別名で分けることで対応しました。
Togetterの次の記事に「探す作業が嫌だ」みたいなことが書かれていて「そりゃごもっとも…」と思ったので、表題の件を実装しました。
少年ジャンプ+副編集長が大学1年生から「漫画アプリのUIについて物申したい!」というDMが来て実際に会って考えが整理されて意義深い時間になったという話 - Togetter
これまでは、シリーズを設定している作品について、続きを読む際に、いったんシリーズ作品のリンクへ飛んでから、自分で続きを探さないといけませんでした。
これからは続きがある場合は、本文の最後にリンクが表示されます。
当たり前にそうあるべきことが、これまで出来ていなかったことが、ちょっと恥ずかしいです。
nehan6からnehan7にバージョンアップしました。
npm install --save nehan
nehan.css
が不要になりました。vertical-align: middle
が効くようになりました。PageReader
は廃止されました。今後はPagedHtmlDocument
を使用してください(後述)。縦書き文庫のビューアーも、新バージョンのエンジンに差し替えました。
nehanのコードサイズが減ったぶん、ページの読み込みも(少しだけ)速くなったと思います。
500KByteほど削減されているので、モバイル環境なんかでは、そこそこ効果が大きいのではないでしょうか。
UIについても、ちょっとだけ刷新しました。
これまでは「本文」と「目次・登場人物」の2列で表示していたのですが、今後は本文を一列(ワンカラム)で表示するようにして、「目次・登場人物」は本文下のメニューに移動させました。
これにより、本文は表示領域が広くなり見やすくなった一方、目次や登場人物に対する操作性は悪くなってしまったので、これに関しては今後の課題としておきます。
(本文の横に目次やら人物やらのタブを表示したらいいのかなあ、と考えています)
だいたいこんな感じで使います。
import { PagedHtmlDocument, CssStyleSheet } from 'nehan'; const src = "<h1>hello, nehan!</h1>"; const doc = new PagedHtmlDocument(src, { styleSheets: [ new CssStyleSheet({ "body": { "writing-mode": "horizontal-tb", // or "vertical-rl" // インライン方向のサイズ(横書きならwidth、縦書きならheightに相当) "measure": "450px", // ブロック方向のサイズ(横書きならheight、縦書きならwidthに相当) "extent": "500px", "font-size": "16px", "padding": "1em" } }) ] }); const $dst = document.querySelector("#dst"); // 結果を格納するDOM // 描画を開始(非同期処理) doc.render({ onPage: (ctx){ const page = ctx.caller.getPage(ctx.page.index); // ページを評価 $dst.appendChild(page.dom); // 評価したページのDOMを注入 $dst.appendChild(document.createElement("hr")); // 一応、分割線 }, onComplete: (ctx){ console.log(`終わりました(${ctx.time}msec)`); } });
nehan
は論理組版エンジンなので、領域サイズの指定にwidth
とかheight
などは使えません。
代わりにmeasure
とかextent
という論理プロパティで指定します。
measure
は横書き(writing-mode:"horizontal-tb")ではwidth
のことです。
縦書き(writing-mode:"vertical-rl")ならheight
になります。
extent
は横書き(writing-mode:"horizontal-tb")ではheight
のことです。
縦書き(writing-mode:"vertical-rl")ならwidth
になります。
同じく、top
, right
, bottom
, left
といった名前は、nehanではそれぞれbefore
, end
, after
, start
と指定します。
例えば横書きのmargin-left
は、nehanではmargin-start
です。
before/afterはブロック方向の前後で、start/endはインライン方向の前後、と覚えてください。
上のサンプルでは、appendChild
を使って、組版結果をどんどん追記する「段組み方式」で表示していますが、ページ送りで表示したいときは、自前で現在表示するページやページ番号などを管理する必要があります。
その場合、起動時には先頭ページだけを表示し、それ以降のページはページ送りをしたタイミングでdoc.getPage
を使って動的に対象ページを取得し、画面上のDOMを差し替える、という実装になるでしょう。
ページ送りの場合の実装については、サンプルのbook.tsなどが参考になるかもしれません。
結論だけ先に述べておきます。
以下に理由を書きますが、非常にしょうもないことです。
nehan(<=6.0.38)においては、package.jsonのtypes
という属性の値が./dist/indx.d.ts
となっていました。
しかし実はこれ正しくは./dist/index.d.ts
と書かねばならなかったのです(eの字が抜けていた)。
- "types": "./dist/indx.d.ts", // version 6.0.38 + "types": "./dist/index.d.ts", // version 6.0.40
先に「Angular8以下では、nehan6.0.38を使ってください」と書きましたが、つまりそれは、このバグったpackage.json
を使ったversion6.0.38を使ってください、ということです。なぜか?
理由は「そのバグがあるために、Typescript3.5以下でもエラーが出ないから」です。つまりバグのある古いバージョンを使うことで、Typescript3.5以下しか使えないAngular(<=8)でバグがでない、ということです。
実はnehanは各所にgetterと言われる機能を使っているのですが、実はこれ、Typescript3.6.3以降じゃないと次のようなエラーが出て、正しく使えなかった機能だったのです。
error TS1086: An accessor cannot be declared in an ambient context.
でもnehan6.0.38は正しいindex.d.ts
が読み込めていない状態だったので、幸運にも? Typescript(<=3.5)でもこのエラーが出なかったのです!
というわけで、Typescript3.5.3までしか使えないAngular8では、nehan(>=6.0.40)を使用できません…
ちなみにAngular9になると、Typescript3.6系列がサポートされるらしいので、そこからはnehanを6.0.40にアップデートできます。
VSCodeのマーケットプレースにvscode-typenovel
というTypeNovel用のVisual Studio Code拡張を公開しました。
インストール後は、*.tn
ファイルを編集するときに有効になります。
実際に動かすと、こんな感じです。
@
で始まる)と注釈($
で始まる)のインテリセンスちなみに$
で起動するインテリセンスは、もちろん組み込みタグだけではなく、ブロックで定義した制約の一覧も表示します。
バグや機能要望はGithubにて受け付けています。
vsce package
コマンドで拡張機能をパッケージ化するときに、.vscodeignore
ファイルでパッケージに含めないファイルを定義できるのですが、このファイルに.gitignore
を入れると、.gitignore
の中身を勝手に見て、その中に記述されたファイルもパッケージの対象外にしてしまうみたいです。
つまり.gitignore
の中に「gitの管理は不要だが、vs拡張のパッケージには必要」なファイルを記述してしまうと、それらがvs拡張にパッケージされず、拡張機能が動かないことがあります。
これを回避するのは簡単で、ようするに.vscodeignore
に.gitignore
を記述しなければよいのです(幸い.gitignore
は、.vscodeignore
に記述しなくてもパッケージ対象にはならないので)。
ちなみにパッケージされるファイルを事前に確認したい場合は、vsce package
をする前にvsce ls
とすると、パッケージされるファイル一覧を事前に確認することができます。
ローカルでインストールした.vsix
ファイルを、VSCodeのUIからアンインストールした場合、.vscode/extensions/<拡張機能のフォルダ>
が削除されないことがあります。
で、わかりにくいことに、この状態でVSCodeのUIから新しい.vsix
ファイルを読み込ませて上書きインストールさせようとすると、インストールは成功と表示されるのですが、実際には何も新しいファイルが展開されず、古いディレクトリだけが残っている状態になってしまいます。
つまりバグを修正して再インストールしても、修正されていない古い拡張が残り続けるので、まったく修正されていない状態になっているわけです。
この状態について、最初は「修正が正しくない」と判断して、ハマってしまいました。そこから「実は再インストール自体がなされておらず、古い拡張が残り続けているだけ」と気付くのに小一時間ぐらいかかりました。
ようするに「ローカルでインストール・アンインストールのテストするときは、UIを介さずに手動で拡張機能のディレクトリを削除する必要がある」ということになります。
久しぶりになりますが、jingooのv1.3.1をリリースしました。
Release v1.3.1 · tategakibunko/jingoo · GitHub
+=
, -=
,*=
, /=
,%=
がサポートされました。elseif
だけではなく、else if
を使うこともできるようになりました。macro
~ endmacro
構文にて、endmacroの後に、macro名を付け足すことができるようになりました。匿名関数はこんなふうに使います。
{# 2と表示される #} {{ ((i) => i + 1)(1) }}
最後のmacro
~endmacro
については、こんな風に使います。
{% macro li (content) %}<li>{{content}}</li>{% endmacro %} {# v1.3.1からは、こういうふうにも書ける #} {% macro li (content) %}<li>{{content}}</li>{% endmacro li %}
マクロの中身が長くなったときに「あれ、これってなんのマクロの宣言だっけ?」とかならないように、endmacro
の部分で、メモ代わりみたいな感覚でマクロ名を付け足すこともできるようになった、ということです。
ただし次のように、宣言したマクロ名と違う名前を付け足すと、構文エラーになります。
{# エラー(マクロ名はliであって、fooではない) #} {% macro li (content) %}<li>{{content}}</li>{% endmacro foo %}
この記事は、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
は、TypeNovelのソースからAstを作成するインターフェイスです。
AstMapper
は、Ast
を別のAst
に入れ替えるインターフェイスです。
AstConverter
は、Ast
をTnNode
に変換するインターフェイスです。
NodeMapper
は、TnNode
を別のTnNode
に入れ替えるインターフェイスです。
NodeValidator
は、TnNode
を検査してValidationError
を出力するインターフェイスです。
NodeFormatter
は、TnNode
から出力テキストを生成するインターフェイスです。
例えばTypeNovelにおいて、別のTypeNovelソースを展開する構文である
$include('別ソース')
などは、Ast -> Ast'
な処理なので、IncludeExpander
クラスとしてAstMapper
インターフェイスで実装されています。
あるいは、ノードから無駄なホワイトスペースを削除するNodeWhitespaceCleaner
クラスは、TnNode -> TnNode'
な処理なので、NodeMapper
インターフェイスで実装されています。
こうやって、コンパイラの各段階を自分好みのものに置き換えることで、各自でコンパイラが拡張できるような作りになっています。