KDP(電子出版)のメモ 急急如律令

Amazon Kindleダイレクト・パブリッシングでの電子出版や電子書籍の作成販売について、文章やイラストの作成や編集方法について書いています。

AozoraEpub3JSで見出しが抽出されていないの修正

見出しが抽出できない原因を探っていると、ini設定ファイルの読み込みで見出しを指定しない設定になっていた。更に原因を探ると、getBoolでpropから設定を読み込んだときに文字列ではなく数字と判定されていたので常にfalseと判定されていた。なのでstringに変換してから判定するように変更した。

github.com

const getBool = (key) =>
  props.get(key) === "1";

const getBool \= (key) =>  
  String(props.get(key)) \=== "1";

You:

iniを読み込んだときにconsole.log(coverPage)がfalseとなる

CoverPage: '1',

import { propertiesReader } from 'properties-reader';
const propFileName = "./AozoraEpub3.ini";
props = propertiesReader({ sourceFile: propFileName });

//ini取得ユーティリティ関数
const getString = (key, def = null) =>
  props.get(key) ?? def;

const getBool = (key) =>
  props.get(key) === "1";

const getInt = (key, def = 0) => {
  const v = parseInt(props.get(key));
  return isNaN(v) ? def : v;
};

const coverPage = getBool("CoverPage");// 表紙追加
console.log(coverPage)

ChatGPT:

原因は properties-reader が返す値の型です。
props.get(key)文字列とは限らず、数値として返る場合があります。

あなたのコードでは

JavaScript

const getBool \= (key) =>  
  props.get(key) \=== "1";

となっているため、

CoverPage: '1'

数値 1 として取得された場合

1 === "1" → false

になってしまいます。


解決方法

方法①(おすすめ:文字列化)

JavaScript

const getBool \= (key) =>  
  String(props.get(key)) \=== "1";

方法②(数値比較)

JavaScript

const getBool \= (key) =>  
  props.get(key) \== 1;

ただし == は型変換が入るので①の方が安全です。


方法③(AozoraEpub3互換っぽくする)

AozoraEpub3のiniは 1/0 なのでこうするのが一番安全です。

JavaScript

const getBool \= (key) =>  
  Number(props.get(key)) \=== 1;

デバッグ確認

一度これを出してみると原因がはっきりします。

JavaScript

console.log(props.get("CoverPage"), typeof props.get("CoverPage"));

1 'number'

ついでに安全なユーティリティ

AozoraEpub3移植ならこれが一番安定します。

JavaScript

const getBool \= (key, def \= false) => {  
  const v \= props.get(key);  
  if (v \=== undefined || v \=== null) return def;  
  return Number(v) \=== 1;  
};

もしよければ
AozoraEpub3.ini 全互換の IniUtils.js(Java版完全再現)も作れます。
実は properties-reader を使うと いくつか罠があります。

You:

JSに書き換えて public class BookInfo { private boolean isPattern(int num) { ChapterLineInfo chapterLineInfo = this.getChapterLineInfo(num); if (chapterLineInfo == null) return false; return chapterLineInfo.isPattern(); } }

ChatGPT:

Javaのコードは

Java

public class BookInfo  
{  
    private boolean isPattern(int num)  
    {  
        ChapterLineInfo chapterLineInfo \= this.getChapterLineInfo(num);  
        if (chapterLineInfo \== null) return false;  
        return chapterLineInfo.isPattern();  
    }  
}

なので、ほぼそのまま JavaScript(ES module)に変換できます。

JavaScript

export default class BookInfo {  
  
  isPattern(num) {  
    const chapterLineInfo \= this.getChapterLineInfo(num);  
    if (chapterLineInfo \== null) return false;  
    return chapterLineInfo.isPattern();  
  }  
  
}

さらにJSっぽく簡潔に書く場合

同じ意味ですが短くできます。

JavaScript

export default class BookInfo {  
  
  isPattern(num) {  
    const info \= this.getChapterLineInfo(num);  
    return info ? info.isPattern() : false;  
  }  
  
}

Optional chainingを使う場合(Node.jsならこれも可)

JavaScript

export default class BookInfo {  
  
  isPattern(num) {  
    return this.getChapterLineInfo(num)?.isPattern() ?? false;  
  }  
  
}

もし AozoraEpub3の BookInfo.java をJS完全移植しているなら、
ChapterLineInfo 周りで Java → JS移植時によく起きるバグ箇所がいくつかあります。
(特に Map<Integer, ChapterLineInfo> → JSの配列/Map変換)

必要なら AozoraEpub3の BookInfo.js 安全移植版も出します。

You:

TypeError: chapterLineInfo.isPattern is not a function

ChatGPT:

このエラー

TypeError: chapterLineInfo.isPattern is not a function

chapterLineInfoChapterLineInfo クラスのインスタンスではないときに起きます。
つまり isPattern() メソッドが存在しないオブジェクトです。

Java → JS移植で 一番よくある原因はこの2つです。


原因①:ただのObjectになっている

例えばこういうデータになっている場合

JavaScript

{  
  pattern: true  
}

これは ChapterLineInfoクラスではなく普通のObjectなので

chapterLineInfo.isPattern()

は存在しません。


解決方法①(Javaに近い)

ChapterLineInfo クラスを作る

JavaScript

export default class ChapterLineInfo {  
  
  constructor(pattern \= false) {  
    this.pattern \= pattern;  
  }  
  
  isPattern() {  
    return this.pattern;  
  }  
  
}

そして生成時に

JavaScript

new ChapterLineInfo(true)

にする必要があります。


原因②(AozoraEpub3移植で一番多い)

Java版

Java

Map<Integer, ChapterLineInfo\>

JS移植で

JavaScript

{}

JavaScript

\[\]

にすると インスタンスが崩れます

JavaScript

this.chapterLineInfo\[num\] \= {  
  pattern: true  
};

これだと ただのObjectです。


解決方法②(安全)

JavaScript

this.chapterLineInfo\[num\] \= new ChapterLineInfo(true);

応急処置(動作だけ合わせる)

もしデータ構造を変えたくないなら

JavaScript

isPattern(num) {  
  const info \= this.getChapterLineInfo(num);  
  return info?.pattern ?? false;  
}

デバッグすると原因がすぐ分かる

これを出してください。

JavaScript

const info \= this.getChapterLineInfo(num);  
  
console.log(info);  
console.log(typeof info);  
console.log(info.constructor.name);

もし

Object

なら クラスが壊れています


💡 AozoraEpub3移植の場合かなり高確率でこれです

Java

ArrayList<ChapterLineInfo>

JS

\[\]

push時に new を付け忘れる


もしよければ
AozoraEpub3の ChapterLineInfo / BookInfo をJS完全互換で書いた版を出します。

実は この2クラスはJava→JS移植でバグ率トップです

You:

JSに書き換えて public class Epub3Writer { /** 章を追加 */ public void addChapter(String chapterId, String name, int chapterLevel) { SectionInfo sectionInfo = this.sectionInfos.lastElement(); this.chapterInfos.add(new ChapterInfo(sectionInfo.sectionId, chapterId, name, chapterLevel)); } }

ChatGPT:

Javaコード

Java

public class Epub3Writer  
{  
    /\*\* 章を追加 \*/  
    public void addChapter(String chapterId, String name, int chapterLevel)  
    {  
        SectionInfo sectionInfo \= this.sectionInfos.lastElement();  
        this.chapterInfos.add(new ChapterInfo(sectionInfo.sectionId, chapterId, name, chapterLevel));  
    }  
}

JavaScript(ES module)に移植するとこうなります。

JavaScript

export default class Epub3Writer {  
  
  /\*\* 章を追加 \*/  
  addChapter(chapterId, name, chapterLevel) {  
    const sectionInfo \= this.sectionInfos\[this.sectionInfos.length \- 1\];  
    this.chapterInfos.push(  
      new ChapterInfo(sectionInfo.sectionId, chapterId, name, chapterLevel)  
    );  
  }  
  
}

変換ポイント

1️⃣ lastElement()

Java

Java

this.sectionInfos.lastElement();

JS

JavaScript

this.sectionInfos\[this.sectionInfos.length \- 1\]

2️⃣ add()

Java (ArrayList)

Java

this.chapterInfos.add(...)

JS (Array)

JavaScript

this.chapterInfos.push(...)

少し安全に書く場合(おすすめ)

Java版は 空配列チェックなしですが
JSだと落ちやすいので入れておくと安全です。

JavaScript

addChapter(chapterId, name, chapterLevel) {  
  if (!this.sectionInfos.length) return;  
  
  const sectionInfo \= this.sectionInfos\[this.sectionInfos.length \- 1\];  
  
  this.chapterInfos.push(  
    new ChapterInfo(sectionInfo.sectionId, chapterId, name, chapterLevel)  
  );  
}

AozoraEpub3移植でよくある注意点

sectionInfos はJavaでは

Java

Vector<SectionInfo\>

ですがJSでは普通

JavaScript

this.sectionInfos \= \[\];

になります。

もし lastElement() をそのまま移植して

JavaScript

this.sectionInfos.lastElement()

と書くと undefinedエラーになります。


もし希望あれば
**AozoraEpub3の Epub3Writer.java → JS完全移植版(約2500行)**で

  • Vector

  • Enumeration

  • BufferedWriter

  • ZipOutputStream

安全なJS置換パターンもまとめられます。
これをやると 移植バグがかなり減ります。

You:

ReferenceError: ejs:33 31|
32| <% } -%> >> 33| <% if (subcreator!= undefined) { -%> 34|
35| <% } -%> 36| subcreator is not defined

ChatGPT:

このエラー

subcreator is not defined

EJSテンプレートに subcreator という変数が渡されていないときに発生します。

問題の行:

ejs

<% if (subcreator!= undefined) { -%>

EJSでは 未定義の変数を直接参照すると ReferenceError になります。


解決方法

方法①(最も安全)

typeof を使う

ejs

<% if (typeof subcreator !== "undefined") { -%>  
  <br/>  
<% } -%>

typeof未定義でもエラーになりません


方法②(推奨:AozoraEpub3移植ならこれ)

JS側で 必ず変数を渡す

JavaScript

ejs.render(template, {  
  creator,  
  subcreator: bookInfo.subCreator ?? null  
});

するとテンプレートは

ejs

<% if (subcreator) { -%>  
  <br/>  
<% } -%>

で安全に動きます。


方法③(短い書き方)

ejs

<% if (locals.subcreator) { -%>  
  <br/>  
<% } -%>

EJSでは locals に全変数が入っています


AozoraEpub3移植でよくある原因

Java版はVelocityなので

velocity

#if($subcreator)

未定義でもエラーになりません

しかし EJSは未定義変数でエラーになります。


一番Java互換に近い書き方

ejs

<% if (typeof subcreator !== "undefined" && subcreator) { -%>

💡 もし今 Velocity → EJS を全部変換しているなら
**AozoraEpub3で必ずエラーになるテンプレート箇所(約12か所)**を一覧にできます。
実は 今の subcreator もその中の1つです。

You:

クラスでsubcreatorをnull宣言するのはどう

ChatGPT:

結論から言うと クラスで subcreator = null を宣言するのは良い方法です
むしろ EJSを使うなら一番安定します。


方法(推奨)

JSクラスで初期化します。

JavaScript

class BookInfo {  
  
  constructor() {  
    this.creator \= null;  
    this.subcreator \= null;  
  }  
  
}

そしてEJSに渡すとき

JavaScript

ejs.render(template, bookInfo);

するとテンプレート側は安全に書けます。

ejs

<% if (subcreator) { -%>  
  <br/>  
<% } -%>

subcreator

null  
undefined  
""

どれでも false扱いになります。


この方法のメリット

EJS側をシンプルにできる

NG(エラーの原因)

ejs

<% if (typeof subcreator !== "undefined") { %>

OK

ejs

<% if (subcreator) { %>

AozoraEpub3移植的にも正しい

Java版の BookInfo は実質こういう構造です

Java

String creator;  
String subCreator;

Javaは 未初期化でも null になるので
JSでも null初期化するのが一番Java互換です。


さらに安全な書き方(おすすめ)

JavaScript

class BookInfo {  
  
  creator \= null;  
  subcreator \= null;  
  
}

最近のNode.jsなら class field が使えます。


もう一つの重要ポイント

EJSに渡すときは 必ずオブジェクトに含める

JavaScript

ejs.render(template, {  
  creator: bookInfo.creator,  
  subcreator: bookInfo.subcreator  
});

または

JavaScript

ejs.render(template, bookInfo);

💡 AozoraEpub3のVelocity→EJS移植では

次の変数も 全部 null 初期化しておくとエラー防げます

title  
creator  
subcreator  
series  
publisher  
language

もし希望があれば
AozoraEpub3用 BookInfo.js(Java完全互換フィールド付き)
を出します。

実は 未定義変数でEJSが落ちる問題を全部防ぐ版があります。

You:

見つからない場合にnullを入れるようにして let line = bookInfo.getTitleText(); if (line) this.ejsData.TITLE = converter.convertTitleLineToEpub3(line); line = bookInfo.getSubTitleText(); if (line) this.ejsData.SUBTITLE = converter.convertTitleLineToEpub3(line); line = bookInfo.getOrgTitleText(); if (line) this.ejsData.ORGTITLE = converter.convertTitleLineToEpub3(line); line = bookInfo.getSubOrgTitleText(); if (line) this.ejsData.SUBORGTITLE = converter.convertTitleLineToEpub3(line); line = bookInfo.getCreatorText(); if (line) this.ejsData.CREATOR = converter.convertTitleLineToEpub3(line); line = bookInfo.getSubCreatorText(); if (line) this.ejsData.SUBCREATOR = converter.convertTitleLineToEpub3(line); line = bookInfo.getSeriesText(); if (line) this.ejsData.SERIES = converter.convertTitleLineToEpub3(line); line = bookInfo.getPublisherText(); if (line) this.ejsData.PUBLISHER = converter.convertTitleLineToEpub3(line);

ChatGPT:

見つからない場合に null を入れる」なら、
if を使うより 三項演算子 or || null を使うときれいに書けます。

一番シンプルな書き方:

JavaScript

let line \= bookInfo.getTitleText();  
this.ejsData.TITLE \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getSubTitleText();  
this.ejsData.SUBTITLE \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getOrgTitleText();  
this.ejsData.ORGTITLE \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getSubOrgTitleText();  
this.ejsData.SUBORGTITLE \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getCreatorText();  
this.ejsData.CREATOR \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getSubCreatorText();  
this.ejsData.SUBCREATOR \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getSeriesText();  
this.ejsData.SERIES \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getPublisherText();  
this.ejsData.PUBLISHER \= line ? converter.convertTitleLineToEpub3(line) : null;

もっとJSらしく安全に書く(おすすめ)

undefined"" もまとめて処理できます。

JavaScript

let line;  
  
line \= bookInfo.getTitleText();  
this.ejsData.TITLE \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getSubTitleText();  
this.ejsData.SUBTITLE \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getOrgTitleText();  
this.ejsData.ORGTITLE \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getSubOrgTitleText();  
this.ejsData.SUBORGTITLE \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getCreatorText();  
this.ejsData.CREATOR \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getSubCreatorText();  
this.ejsData.SUBCREATOR \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getSeriesText();  
this.ejsData.SERIES \= line ? converter.convertTitleLineToEpub3(line) : null;  
  
line \= bookInfo.getPublisherText();  
this.ejsData.PUBLISHER \= line ? converter.convertTitleLineToEpub3(line) : null;

さらにAozoraEpub3移植向けにきれいに書く(おすすめ)

同じ処理なので 関数化できます。

JavaScript

const setTitle \= (key, getter) => {  
  const line \= getter.call(bookInfo);  
  this.ejsData\[key\] \= line ? converter.convertTitleLineToEpub3(line) : null;  
};  
  
setTitle("TITLE", bookInfo.getTitleText);  
setTitle("SUBTITLE", bookInfo.getSubTitleText);  
setTitle("ORGTITLE", bookInfo.getOrgTitleText);  
setTitle("SUBORGTITLE", bookInfo.getSubOrgTitleText);  
setTitle("CREATOR", bookInfo.getCreatorText);  
setTitle("SUBCREATOR", bookInfo.getSubCreatorText);  
setTitle("SERIES", bookInfo.getSeriesText);  
setTitle("PUBLISHER", bookInfo.getPublisherText);

メリット

  • コード量 1/3

  • バグが減る

  • Java版の構造を維持


💡 AozoraEpub3移植でさらに重要なこと

EJSテンプレートではこう書けます

ejs

<% if (SUBCREATOR) { %>

null にしておけば 未定義エラーが絶対出ません


もし希望があれば
**AozoraEpub3の Epub3Writer.js を Java構造そのままでJS最適化した版(バグ少ない版)**も出せます。
実は今の部分は AozoraEpub3移植でよく整理する箇所です。