ST_FUNC開発日記

建築構造設計Excelアドイン開発の記録

htmlのタイトルを見出しにする

現在ドキュメントのhtml作成にmarkdownからhtmlの変換をかけているが、titleをファイル名にしていく。 しかしtitleがindexとかだと味気ないので、最初の見出しをタイトルにしてみえる。

まず、楽しようと思ってchatGPTに聞いてみる。

PythonMarkdown形式のテキストから最初の見出しを取得する方法を示します。これにはmarkdownパッケージと正規表現を使用します。

まず、markdownパッケージをインストールします。

pip install markdown

次に、次のPythonコードを使用して最初の見出しを取得できます。

import markdown
import re

def extract_first_heading(markdown_text):
    # MarkdownをHTMLに変換
    html_text = markdown.markdown(markdown_text)

    # HTMLから最初の見出しを抽出
    match = re.search(r'<h(\d)>(.*?)</h\1>', html_text)
    if match:
        heading_level = match.group(1)
        heading_text = match.group(2)
        return f"Level {heading_level}: {heading_text}"
    else:
        return "No heading found"

# テスト用のMarkdownテキスト
markdown_text = """
# 最初の見出し

これはテストです。
"""

# 最初の見出しを取得
first_heading = extract_first_heading(markdown_text)
print(first_heading)

このコードは、与えられたMarkdownテキストから最初の見出しを抽出します。見出しはHTMLタグ <h1>, <h2>, <h3> などに変換されるため、正規表現を使用してHTMLから見出しを抽出しています。

今回はもともとhtmlに変換しているけど、なんでmarkdownから直接拾ってこないんだろう?

Markdownのヘッダー構造は、#で始まる行で表されますが、これを正規表現で抽出することも可能です。しかし、マークダウンのヘッダーは、さまざまな形式で記述されることができます。たとえば、ヘッダーの前に空白文字があるかどうか、#の後に空白文字があるかどうかなどが異なります。そのため、正規表現パターンを作成する必要があります。しかし、MarkdownからHTMLに変換することで、統一されたHTML構造を取得することができます。

HTMLの場合、<h1><h2><h3>などのヘッダータグは、明確に定義されており、パースが容易です。そのため、HTMLに変換してからヘッダーを抽出することで、パースの一貫性を確保しやすくなります。

ただし、Markdownテキストが信頼できる形式で提供される場合や、パフォーマンス上の理由からHTML変換を避けたい場合は、直接Markdownテキストからヘッダーを抽出する方法を選択することも可能です。

そんなもんか。

といっても今回はそんな複雑なmarkdownを作るつもりはないので、chartGPTが提案してくれたところ悪いけど、markdownのから直接取得してしまおう。

ということで、適当に自分でコードを書いてみる。

    # タイトルの取得
    title: str = None
    for line in md_text.splitlines():
        if line[0] == "#":
            title = line.replace("#", "").strip()
            break
    if title is None:
        title = os.path.splitext(os.path.basename(input_path))[0]

見出しがなかった時の対策にファイル名を取得する部分は残しておく。

これでタイトルがそれっぽくなった。

md_to_html.pyでhtmlのtitleを最初の見出しにするように変更 · st-func/st_func_docs@601abae · GitHub

st-func.main.jp

metaデータにhelpurlの追加

前に関数を作ったときはまだドキュメントのHPを用意していなかったが、作成したので

前回はスルーしたhelpurlメタデータを追加する。

learn.microsoft.com

シンプルにhelpurlというタグを入力するだけ。

/**
 *  組立角形鋼管の断面性能。
 * @customfunction secBuildBox secBuildBox
 * @param propertyType 表示したい断面性能のタイプ
 * @param a 成 A
 * @param b 幅 B
 * @param t1 成方向の板厚 t1
 * @param t2 幅方向の板厚 t2
 * @returns 断面性能
 * @helpurl https://st-func.main.jp/docs/st_func_addin/functions/sec_build_box.html
 */
export function secBuildBox(propertyType: string[][], a: number, b: number, t1: number, t2: number): number[][] {
  const secBuildBox: SecBuildBox = new SecBuildBox();
  secBuildBox.setDimensions(Unit.input(a, "mm"), Unit.input(b, "mm"), Unit.input(t1, "mm"), Unit.input(t2, "mm"));
  return getSecSteelPropertys(propertyType, secBuildBox);
}

さて、これで設定したものの、どこからもヘルプに飛べない。

この関数挿入画面の「詳細情報」というのをクリックするとヘルプページに飛べると思っていたのだが、クリックしても反応しない・・・。

昔の記事で書いたように、どうもExcel Onlineの関数挿入画面はバグが多いのではないだろうか・・・。

ということで、デスクトップ版にインストールして試してみることに。

関数挿入画面の「この関数のヘルプ」というのをクリックしたら、ちゃんとHPに飛べた!

やっぱりExcel Onlineがダメな気がする。

そして、なんと下記画像のサジェストに出てくる関数名をクリックするだけでもヘルプに飛ぶことができた。この辺はデスクトップ版はしっかりしているようだ。

はやくExcel Onlineも修正対応してほしい。

断面性能関数のメタデータにhelpurlを追加 · st-func/st_func_addin@572e6d4 · GitHub

TypeDocによるドキュメントの作成

アドインのほうは丁寧に書きたいため手動ででドキュメントを作成したが、 ライブラリの関数については、自動生成してみてもいいかと思うので、下記ページを見ながら自動生成を試みる。

qiita.com

まず、TypeDocのインストール。

ライブラリに含める必要はないので-Dオプションをつける。

$ npm install -D typedoc

最初にTypeDocと書いてやったらエラーになった。大文字はだめですね。

typedocのインストール · st-func/st-func-ts@5448831 · GitHub

またpackage.jsにスクリプトを登録する

"scripts": {
    "build": "tsc",
    "prepare": "npm run build",
    "test": "jest",
    "typedoc": "typedoc --out ./docs/ ./src/"
  },

また、docsフォルダができるので、.gitignoreと.npmignoreを対応

typedocをscriptに登録 · st-func/st-func-ts@fe0c155 · GitHub

これで、あとはスクリプトを実行するだけ

$ npm run typedoc

あっという間にドキュメントができた。

こんな感じ

st-func.main.jp

なんとREADMEもしっかりhtmlにしてindex.htmlにしてくれるんですね。

mdからhtmlの変換 その4

htmlができたので、ロリポップにアップロードしてみたら、htmlが文字化けしてしまっていた。

なんでかなと思ってみていたら、meta charsetのところで、"が全角になってしまっていた。

ということで、修正。

また、htmlフォルダの中にさらにhtmlフォルダができてしまっていることを発見。

これはmdのみの時は問題なかったが、画像コピーの処理を入れたときに、再帰的にコピーされてしまった聖なようだ。

除外する設定を書いてもよかったけど、面倒だったので、markdown置き場はmarkdownフォルダ内として、

ややこしいことが起こらないようにした。

そんな感じで最終的なコードはこちら。

template.html

<!DOCTYPE html>
<html lang=ja>
<head>
<meta charset="utf-8" />
<title>{{TITLE}}</title>
<script type="text/javascript" async
  src="https://cdn.jsdelivr.net/npm/mathjax@2.7.9/MathJax.js?config=TeX-MML-AM_CHTML">
</script>
</head>
<body>
{{BODY}}
</body>
</html>

md_to_html.py

import os
import glob
import markdown
import shutil


def convert_md_to_html(input_path, output_path):
    with open(input_path, "r", encoding="utf-8") as md_file:
        md_text = md_file.read()
        html: str = markdown.markdown(
            md_text,
            extensions=["tables", "mdx_math"],
            extension_configs={"mdx_math": {"enable_dollar_delimiter": True}},
        )

    with open("template.html", "r", encoding="utf-8") as template_file:
        template_text: str = template_file.read()
        title: str = os.path.splitext(os.path.basename(input_path))[0]
        html = template_text.replace("{{TITLE}}", title).replace(
            "{{BODY}}", html
        )
        html = html.replace('.md"', '.html"')

    with open(output_path, "w", encoding="utf-8") as html_file:
        html_file.write(html)


def main():
    input_folder = "markdown"
    output_folder = "html"

    if not os.path.exists(output_folder):
        os.makedirs(output_folder)

    for md_file_path in glob.glob(f"{input_folder}/**/*.md", recursive=True):
        relative_path = os.path.relpath(md_file_path, input_folder)
        output_path = (
            os.path.splitext(os.path.join(output_folder, relative_path))[0]
            + ".html"
        )
        output_dir = os.path.dirname(output_path)

        if not os.path.exists(output_dir):
            os.makedirs(output_dir)

        convert_md_to_html(md_file_path, output_path)
        print(f"変換完了: {md_file_path} -> {output_path}")
    IMAGE_EXTENSIONS = (
        ".gif",
        ".jpg",
        ".png",
        ".svg",
    )
    for image_extension in IMAGE_EXTENSIONS:
        for image_file_path in glob.glob(
            f"{input_folder}/**/*{image_extension}", recursive=True
        ):
            relative_path = os.path.relpath(image_file_path, input_folder)
            output_path = os.path.join(output_folder, relative_path)
            output_dir = os.path.dirname(output_path)
            if not os.path.exists(output_dir):
                os.makedirs(output_dir)
            shutil.copy2(image_file_path, output_path)
            print(f"コピー完了: {image_file_path} -> {output_path}")


if __name__ == "__main__":
    main()

ホームページにもアップロードした。

https://st-func.main.jp/

準備が整ったので、今後はドキュメントを少し整備していこうと思う。

テンプレートのmetaの修正、markdownの置き場を修正 · st-func/st_func_docs@69bdfde · GitHub

mdからhtmlの変換 その3

前回の下記の問題点を解決する

  • 単純にそのまま変換するので、ヘッダーとかがない
  • 表が変換されていない
  • 数式が変換されていない
  • ハイパーリンクが機能しない
  • 画像が表示されない。

ヘッダー等の作成

htmlのテンプレートを用意し、bodyのみ置き換えることにする。

簡単なテンプレート

<!DOCTYPE html>
<html lang=ja>
<head>
<meta charset=”utf-8” >
<title>{{TITLE}}</title>
</head>
<body>
{{BODY}}
</body>
</html>

この{{TITLE}}と{{BODY}}をmarkdownから変換したhtmlに置き換えることによって、htmlページを作る。

{{}}はとくに意味はないが、なんとなくdjangoのテンプレートのイメージで作ってみた。

ということでconvert_md_to_html関数の途中に下記を追加

with open("template.html", "r", encoding="utf-8") as template_file:
        template_text: str = template_file.read()
        title: str = os.path.splitext(os.path.basename(input_path))[0]
        html = template_text.replace("{{TITLE}}", title).replace(
            "{{BODY}}", html
        )

表が変換されない

qiita.com

上記記事を参考にするとextensionsでtablesを指定するとよいらしい。

ということで、変換部分を下記の様に修正

    with open(input_path, "r", encoding="utf-8") as md_file:
        md_text = md_file.read()
        html: str = markdown.markdown(md_text, extensions=["tables"])

よし、表は無事に変換されたけど罫線ないな。htmlの表ってデフォルトで罫線なかったっけ?

まあ、あとでcssでも書こう。

数式が変換されない

python-markdown-mathの公式説明を見てみる。

pypi.org

これによるとhtmlにmathjexを使うことを記載

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js">
</script>

さらに設定を記載

<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  config: ["MMLorHTML.js"],
  jax: ["input/TeX", "output/HTML-CSS", "output/NativeMML"],
  extensions: ["MathMenu.js", "MathZoom.js"]
});
</script>

そしてextensionsにmdx_mathを指定すればよいようだ。

ということで、変換部分を下記の様に修正

    with open(input_path, "r", encoding="utf-8") as md_file:
        md_text = md_file.read()
        html: str = markdown.markdown(
            md_text, extensions=["tables", "mdx_math"]
        )

これで変換をかけてみるとソースを見てみると

<h3>iY:Y軸まわりの断面二次半径</h3>
<p>
<script type="math/tex; mode=display"> iY=\sqrt{\frac{IY}{A}} </script>
</p>

の様になっているので、うまくいったように見えるけど、数式が表示されない。 pythonの変換はうまくいっているはずなので、htmlの問題かな?

chatGPTに相談したら下記のような回答が

あなたの提供したコードでは、MathJaxを使用して数式を表示しようとしていますが、いくつかの問題があります。

  1. MathJaxのスクリプトが正しく読み込まれていない可能性があります。
  2. MathJaxの設定が不足している可能性があります。
  3. 数式を表示するための正しいスクリプト構文を使用していない可能性があります。

以下は修正されたコードです。MathJaxのスクリプトを正しく読み込み、数式を正しく表示するために必要な変更が加えられています。

<head>
略
<script type="text/javascript" async
  src="https://cdn.jsdelivr.net/npm/mathjax@2.7.9/MathJax.js?config=TeX-MML-AM_CHTML">
</script>
</head>
略

変更点: - MathJaxのスクリプトの読み込み方法を変更しました。

これにより、数式が正しく表示されるはずです。

ということで、template.htmlを下記の様に修正してみた。

<!DOCTYPE html>
<html lang=ja>
<head>
<meta charset=”utf-8” >
<title>{{TITLE}}</title>
<script type="text/javascript" async
  src="https://cdn.jsdelivr.net/npm/mathjax@2.7.9/MathJax.js?config=TeX-MML-AM_CHTML">
</script>
</head>
<body>
{{BODY}}
</body>
</html>

無事に数式が表示できた。 ・・・と思ったら、インラインの数式($が一つのやつ)が変換できていない。

ドキュメントを見ると下記の設定が必要らしい。

enable_dollar_delimiter=True

どこで設定するんや?ということで、chatGPTさんに聞きながら変換部分を下記のように設定。

    with open(input_path, "r", encoding="utf-8") as md_file:
        md_text = md_file.read()
        html: str = markdown.markdown(
            md_text,
            extensions=["tables", "mdx_math"],
            extension_configs={"mdx_math": {"enable_dollar_delimiter": True}},
        )

これで、インラインの数式も変換できた。

ハイパーリンクが機能しない

htmlを見てみるとリンクがa hrefにはなっているが、リンク先が.mdのままになってしまっている。

なので、リンク先の拡張子だけ変えてあげればいい。 ということで、テンプレート修正に下記を追加

html = html.replace('.md"', '.html"')

画像が表示されない

これは、画像はhtmlフォルダ内にコピーされないことが原因であるようだ。

ということでコピーするルーチンを足してあげる。

mdを再帰的に取得するglobのコードを参考に画像をコピーしていく。

IMAGE_EXTENSIONS = (
        ".gif",
        ".jpg",
        ".png",
        ".svg",
    )
    for image_extension in IMAGE_EXTENSIONS:
        for image_file_path in glob.glob(
            f"{input_folder}/**/*{image_extension}", recursive=True
        ):
            relative_path = os.path.relpath(image_file_path, input_folder)
            output_path = os.path.join(output_folder, relative_path)
            output_dir = os.path.dirname(output_path)
            if not os.path.exists(output_dir):
                os.makedirs(output_dir)
            shutil.copy2(image_file_path, output_path)
            print(f"コピー完了: {image_file_path} -> {output_path}")

copy2って適当なネーミングだな。

これで画像も表示されるようになった。

md_to_htmlの更新、htmlテンプレートの作成 · st-func/st_func_docs@7e3fc87 · GitHub

mdからhtmlの変換 その2

環境準備が整ったので、変換のコードを書く。

qiita.com

こちらのコードを使わせていただく。

convert_md_to_html関数が実際の変換の部分。

def convert_md_to_html(input_path, output_path):
    with open(input_path, "r", encoding="utf-8") as md_file:
        md_text = md_file.read()
        html = markdown.markdown(md_text)

    with open(output_path, "w", encoding="utf-8") as html_file:
        html_file.write(html)

ファイルを開いて、markdown.markdownで変換して書き込む。

main関数の中で、フォルダ内のmdファイルをすべて変換する処理を行う。

def main():
    input_folder = "."
    output_folder = "html"

    if not os.path.exists(output_folder):
        os.makedirs(output_folder)

    for md_file_path in glob.glob(f"{input_folder}/**/*.md", recursive=True):
        relative_path = os.path.relpath(md_file_path, input_folder)
        output_path = (
            os.path.splitext(os.path.join(output_folder, relative_path))[0]
            + ".html"
        )
        output_dir = os.path.dirname(output_path)

        if not os.path.exists(output_dir):
            os.makedirs(output_dir)

        convert_md_to_html(md_file_path, output_path)
        print(f"変換完了: {md_file_path} -> {output_path}")

初めにoutput_folderがなければ作る。

次に・・・globってなんだろう?

note.nkmk.me

ファイルリストを取得するんですね。

ファイルパスについてはとりあえずルートディレクトリの中身をhtmlフォルダに出力する設定にする。

gitに入らないようにhtmlフォルダはgitignoreに入れておく。

試しに変換テスト用にいろいろな要素をいれたmdを作ってみる。

# タイトルです

## 小見出し

* リスト1
* リスト2
1. 数字リスト1
1. 数字リスト2

> 引用
>
> 引用

*強調*や**さらに強調**

```python
print("ソースコード")
```

| header|header|
|:--|--:|
|テーブル1|テーブル2|

数式

$$e^{i\pi}=-1$$

インライン数式$e^{i\pi}=-1$もできるかな

ハイパーリンク

 [STF.secBuildBox関数](st_func_addin/functions/sec_build_box.md)

画像

![組立H形鋼の寸法定義](st_func_addin/images/sec_build_h.svg)

さて、これで変換を実行してみる。

python md_to_html.py

で実行!

出てきた結果が下記。

<h1>タイトルです</h1>
<h2>小見出し</h2>
<ul>
<li>リスト1</li>
<li>リスト2</li>
<li>数字リスト1</li>
<li>数字リスト2</li>
</ul>
<blockquote>
<p>引用</p>
<p>引用</p>
</blockquote>
<p><em>強調</em><strong>さらに強調</strong></p>
<p><code>python
print("ソースコード")</code></p>
<p>| header|header|
|:--|--:|
|テーブル1|テーブル2|</p>
<p>数式</p>
<p>$$e^{i\pi}=-1$$</p>
<p>インライン数式$e^{i\pi}=-1$もできるかな</p>
<p>ハイパーリンク</p>
<p><a href="st_func_addin/functions/sec_build_box.md">STF.secBuildBox関数</a></p>
<p>画像</p>
<p><img alt="組立H形鋼の寸法定義" src="st_func_addin/images/sec_build_h.svg" /></p>

変換はうまくいったが、問題点がいくつか。

  • 単純にそのまま変換するので、ヘッダーとかがない
  • 表が変換されていない
  • 数式が変換されていない
  • ハイパーリンクが機能しない
  • 画像が表示されない。

次回以降にこれらを対応していく。

mdをhtmlに変換するスクリプトの作成 · st-func/st_func_docs@01c738e · GitHub

mdからhtmlの変換 その1 環境の準備

アドインを設置するためにホームページのサーバーを作ったので、せっかくなのでドキュメントはそこに置こうと思う。

githubに飛ぶのもあまり美しくはないので。

ということで、mdをhtmlに変換することをやってみる。

md html変換で検索するとVsCodeなどでやる手法が出てくるが、コマンドで自動化したいのでそういうのが得意そうなpythonを使うことにする。

python入れてたっけな?と思ってpowershellpythonと打ったらストアへ飛んだ。

Windowsさんはプリインストールはされてないけど、コマンドは認識してくれてるのね。

そこで入手をクリック。

そしたら勝手にインストールされて使えるようになった。すごい簡単!

markdownを変換するためのライブラリをインストールする。

qiita.com

このへんを参考にすると、シンプルにmarkdownというパッケージを使うようだ。

また、構造設計のため数式が必須になってくる。

markdown数式変換とかで検索してもいまいちヒットしないのだが、いろいろ探していると

python-markdown-mathというパッケージがあるらしいので、それを使うことにする。

ということでパッケージのインストール。

$ pip install markdown
$ pip install python-markdown-math

さて、VsCodepythonのコードを書こうとしたら、Python拡張入れませんか?と聞かれたので、おすすめ通りインストール。

ついでにコードフォーマットのためにFlake8とBlack Formatterもインストールしてしまう。

qiita.com

上記ページを見ながら、フォーマッタの設定を作成。

.vscode/settings.json

{
    "[python]": {
        "editor.defaultFormatter": "ms-python.black-formatter",
        "editor.formatOnSave": true
    },
    "black-formatter.args": [
        "--line-length=79"
    ]
}

を追加でフォーマッタが動くようになった。

(settings.jsonをsetting.jsonにしてしまって、あれ?あれ?ってなってしまった・・・)

それにしてもpythonの79行って気持ちはわかるけど短いよなあ・・・。

black-formatterの設定 · st-func/st_func_docs@2d45086 · GitHub