GAS の制約に首を締められながら、 Google ドキュメントを Markdown 化するスクリプトを書きました。こんなの:
やったこと
アドオンメニューに項目を追加:
function addAddonMenu(caption, functionName) {
DocumentApp.getUi().createAddonMenu().addItem(caption, functionName).addToUi();
}
function onOpen() {
addAddonMenu("Markdownify", "showAsMarkdown");
}
(GAS では onOpen
という名前の関数はドキュメントが開かれたときに実行される。)
showAsMarkdown
は、アクティブドキュメントを読み込み、 Markdown 文字列を表示する関数です。今回はこのように実装しました。:
function showAsMarkdown() {
const doc = DocumentApp.getActiveDocument();
const ui = DocumentApp.getUi();
const markdownify = compose(parse, serialize);
ui.alert(markdownify(doc));
compose
は関数合成関数です。 parse
と serialize
はこんな感じに書きました。:
function parse(doc) {
const builder = new DocumentParseTreeBuilder();
const tree = builder.fromGoogleDocumentElement(doc);
return tree;
}
function serialize(tree) {
return tree.asMarkdown();
}
ParseTree
の実装
パーズ木はこんな感じです。:
function ParseTree(value, children) {
}
ParseTree.prototype.belongTo = function (types) {
const that = this;
return types.some(function (type) {
return (that instanceof type);
});
};
ParseTree.prototype.asMarkdown = function () {
return this.childrenAsMarkdown();
};
ParseTree.prototype.childrenAsMarkdown = function () {
const childrenAsMarkdown = this.children.map(function (child) {
return child.asMarkdown();
});
return childrenAsMarkdown.join("");
};
belongTo()
メソッドは、 types
にそのオブジェクトのコンストラクターが含まれているかどうかを検査します。 asMarkdown()
メソッドは、そのオブジェクトを Markdown 化した文字列を返します。 childrenAsMarkdown()
メソッドは、そのオブジェクトの子に対して、それぞれ asMarkdown()
を適用し、それを連結したものを返します。
function DocumentParseTree(value, children) {
const childTypes = [BodyParseTree];
this.children = children;
if (!this.children.every(function (child) {
return child.belongTo(childTypes);
})) {
throw new ParseException("DocumentParseTree's child should be " + conjoin(this.childTypes, "or"));
}
}
DocumentParseTree.prototype = Object.create(ParseTree.prototype);
DocumentParseTree.prototype.constructor = DocumentParseTree;
DocumentParseTree
コンストラクターで記述されている if
文は、葉以外の全てのクラスのコンストラクターに記述しますが、この記事では省略します。
function BodyParseTree(value, children) {
const childTypes = [
ParagraphParseTree,
TitleParseTree,
SubtitleParseTree,
Heading1ParseTree,
Heading2ParseTree,
Heading3ParseTree,
Heading4ParseTree,
Heading5ParseTree,
Heading6ParseTree,
UnorderedListItemParseTree,
OrderedListItemParseTree,
];
this.value = value;
this.children = children;
}
BodyParseTree.prototype = Object.create(ParseTree.prototype);
BodyParseTree.prototype.constructor = BodyParseTree;
function ParagraphParseTree(value, children) {
const childTypes = [Text];
this.value = value;
this.children = children;
}
ParagraphParseTree.prototype = Object.create(ParseTree.prototype);
ParagraphParseTree.prototype.constructor = ParagraphParseTree;
TitleParseTree
, SubtitleParseTree
, Heading1ParseTree
, .. も同様に実装します。
function UnorderedListItemParseTree(value, children) {
const childTypes = [Text];
this.value = value;
this.children = children;
}
UnorderedListItemParseTree.prototype = Object.create(ParseTree.prototype);
UnorderedListItemParseTree.prototype.constructor = UnorderedListItemParseTree;
OrderedlistItemParseTree
も同様に実装します。
最後に、 Text
クラスを実装します。今回のパーズ木の唯一の葉クラスで、 value
には文字列を格納します。( GSuite Text でもよかったんだけど、取得が面倒だった。):
function Text(value, children) {
const childTypes = [];
this.value = value;
this.children = children;
}
Text.prototype = Object.create(ParseTree.prototype);
Text.prototype.constructor = Text;
ParseTreeBuilder
の実装
次に、適当なドキュメントを
{
constructor: DocumentParseTree,
value: [Object Document],
children: [{
constructor: BodyParseTree,
value: [Object BodySection],
children: [{
constructor: Heading1ParseTree,
value: [Object Heading1],
children: [{
constructor: Text,
value: "Heading String",
children: []
}]
}, {
constructor: ParagraphParseTree,
value: [Object Paragraph],
children: [{
constructor: Text,
value: "The quick brown fox jumps over the lazy dog.",
children: []
}]
}]
}]
}
のようなオブジェクトにパーズするためのクラス ParseTreeBuilder
を作りました。
function ParseTreeBuilder() {
}
ParseTreeBuilder.prototype.fromGoogleDocumentElement = function (lmnt) {
return new this.constructor.Product(lmnt, this.getChildren(lmnt))
};
fromGoogleDocumentElement()
メソッドは Google ドキュメントの Element
からパーズ木を生成するやつです。これしか作っていませんが。
function DocumentParseTreeBuilder() {
}
DocumentParseTreeBuilder.Product = DocumentParseTree;
DocumentParseTreeBuilder.prototype.getChildren = function (doc) {
const body = doc.getBody();
const bodyBuilder = new BodyParseTreeBuilder();
return [bodyBuilder.fromGoogleDocumentElement(body)];
};
getChildren()
は子要素の ParseTree
の配列を返します。
function BodyParseTreeBuilder() {
}
BodyParseTreeBuilder.Product = BodyParseTree;
BodyParseTreeBuilder.prototype.getChildren = function (body) {
const children = childrenOf(body);
return children.map(function (child) {
var childBuilder = null;
with (DocumentApp.ElementType) switch (child.getType()) {
case PARAGRAPH:
childBuilder = new HeadingOrParagraphParseTreeBuilder().getConcreteBuilder(child);
break;
}
return childBuilder.fromGoogleDocumentElement(child);
});
};
function HeadingOrParagraphParseTreeBuilder() {
}
// HeadingOrParagraphParseTreeBuilder.Product = ParagraphParseTree;
HeadingOrParagraphParseTreeBuilder.prototype.getConcreteBuilder = function (p) {
with (DocumentApp.ParagraphHeading) switch (p.getHeading()) {
case NORMAL:
return new ParagraphParseTreeBuilder();
break;
case HEADING1:
return new Heading1ParseTreeBuilder();
break;
}
};
getConcreteBuilder()
は DocumentApp.ParagraphHeading
や DocumentApp.GlyphType
に応じて適切なビルダーを返すメソッドです。
function ParagraphParseTreeBuilder() {
}
ParagraphParseTreeBuilder.Product = ParagraphParseTree;
ParagraphParseTreeBuilder.prototype = Object.create(HeadingOrParagraphParseTreeBuilder.prototype);
ParagraphParseTreeBuilder.prototype.constructor = ParagraphParseTreeBuilder;
function TitleParseTreeBuilder() {
}
TitleParseTreeBuilder.Product = TitleParseTree;
TitleParseTreeBuilder.prototype = Object.create(HeadingOrParagraphParseTreeBuilder.prototype);
TitleParseTreeBuilder.prototype.constructor = TitleParseTreeBuilder;
他の ParseTree
に対応するものも適当に作ります。
TextBuilder
の getChildren()
メソッドは空の配列を返すようにします。
asMarkdown()
の実装
最後に、それぞれのパーズ木に、木を Markdown 化するメソッドを追加します。凡そこんな感じです。:
ParagraphParseTree.prototype.asMarkdown = function () {
return EOL + EOL + this.childrenAsMarkdown();
}
Heading1ParseTree.prototype.asMarkdown = function () {
return EOL + EOL + "# " + this.childrenAsMarkdown() + EOL;
};
UnorderedListItemParseTree.prototype.asMarkdown = function () {
var ret = "";
if (this.isBeginning()) {
ret += EOL;
}
ret += EOL + "* " + this.childrenAsMarkdown();
return ret;
};
課題
-
ListParseTree
が無い - テーブルが使えない
- パーズエラーを無視しない