39
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Google ドキュメントを Markdown 化した

Last updated at Posted at 2017-09-29

GAS の制約に首を締められながら、 Google ドキュメントを Markdown 化するスクリプトを書きました。こんなの:

demo.png

やったこと

アドオンメニューに項目を追加:

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 は関数合成関数です。 parseserialize はこんな感じに書きました。:

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.ParagraphHeadingDocumentApp.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 に対応するものも適当に作ります。
TextBuildergetChildren() メソッドは空の配列を返すようにします。

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 が無い
  • テーブルが使えない
  • パーズエラーを無視しない
39
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?