Chrome 119 で select タグに hr が使えるようになる
来週に Stable リリースされる Chrome119
新機能に地味だけど便利かもしれない機能があった

select タグに hr タグを入れて横線を表示できるようになる

<select>
<option>Option1</option>
<option>Option2</option>
<hr/>
<option>Option3</option>
<option>Option4</option>
<hr/>
<option>Option5</option>
<option>Option6</option>
</select>

これまでは option 以外の関係無いタグを入れても自動で削除されてた
Chrome 119 からは hr タグのところに横線が表示されるので 区切りをわかりやすくできる

ただ 見た目はあまりきれいじゃない
スタイルに height や margin などを設定しても無視される
hr タグの有無しか反映されてなさそう
hr タグを連続して入れることで太い線にできるけど 見た目のためにそれはどうなのって思う
HTML 要素の innerText
HTML から見た目通りの改行のテキストを取り出したいです
innerText を使うと見た目に合わせて文字列が受け取れます
例えばこういう HTML があるとき

foo
bar<br/>baz
<div>div</div>
<p>p</p>
text

受け取る文字列はこうなります

foo bar
baz
div

p

text

HTML 中の改行はスペースになるので スペースに変換されています
br タグは改行に変換されています
また div タグでも改行され p タグでは前後に空行が追加されます

この変換を大きめのファイルにまとめて行いたいです
Node.js でやろうかと思って jsdom を使ってみたのですが innerText はサポートされていませんでした
レンダリングエンジンありきみたいな機能なので実装されてないそうです
issue で要望は出てましたが 未だに実装されていないのであまり期待はできません

happy-dom ならどうかなと試してみたらこっちはサポートされていました
しかし 思ったような出力になりません

foo
barbaz
div
ptext

br タグは単純に無視され 改行がそのまま反映されています
また div は前後で改行されていますが p は改行されず直後の改行も無視されて text とくっついてしまっています

しかたなくブラウザを使うことにしました
ただブラウザを使っても期待する結果を得るにはドキュメントと接続されて画面に表示されている必要があるようでした
document.createElement を使って div を作った直後に innerHTML を設定して innerText を取り出してもこうなります

foo
barbaz
div
p
text

HTML の改行がそのまま反映されて br タグは無視されています
happy-dom と同じ感じです
p タグのあとに改行ができるところだけ違っています

ブラウザでも一旦表示させないといけないのが面倒ですね
速度にも影響出そうですし

一応簡単に調べてみたところ 8 ~ 10 倍くらいの時間がかかるようになってました
適当なページから持ってきた HTML で文字数は 6500 くらいでタグ数は 150 くらいのもので確認しました
Chrome 118 で HTML の search 要素に対応するみたい
https://chromestatus.com/feature/5126108151808000

以前話題になっていた HTML の search タグですが Chrome 118 から対応するみたいです

機能としては特になくアクセシビリティのためのものらしいです
「<div role=search>」 と書くのと同じらしいです

検索ボックスが表示されたりはしないので アクセシビリティ系を気にしない人にとっては 無視していいタグです
main とか aside とか header とか nav とか こういう系統のタグです
コンストラクタを見ても main などと同じで HTMLElement になっています

document.createElement("search").constructor.name
// 'HTMLElement'
popover 属性使ってみた
来週 Stable リリース予定の Chrome 114 で Popover API がリリースされる予定

div など好きな要素に popover 属性をつけれる

<div popover>
あいうえお
</div>

popover 属性をつけた要素は表示されない
UA のスタイルで display: none されてる

[popover]:not(:popover-open):not(dialog[open]) {
display: none;
}

popover 要素は showPopover メソッドで表示できる

document.querySelector("div").showPopover()

dialog と同じ感じで中央に表示される
表示は最上位になるので position: fixed で z-index 大きめの要素があってもさらにその上に来る

popover の要素の外側の適当なところをクリックすると自動で閉じてくれる
プログラムから閉じるときは hidePopover()

属性で popover="manual" を指定すると自動で閉じなくなる
デフォルトの value 省略形は popover="auto" を指定するのと一緒

popover 属性がついてない要素に showPopover を実行するとエラーになる

Failed to execute 'showPopover' on 'HTMLElement': Not supported on elements that do not have a valid value for the 'popover' attribute.

表示中に showPopover を呼び出してもエラーになる

Failed to execute 'showPopover' on 'HTMLElement': Invalid on popover elements which aren't hidden.

この辺はもっとゆるくていいと思うのに
togglePopover() メソッドもあって 引数の true/false で表示・非表示を指定できる
こっちだと表示中に表示しようとしてもエラーは出ないのでこっちのほうが扱いやすいかも

複数の popover 要素があって 表示中の要素があるときに 別の要素を表示させようとした場合は showPopover でエラーにならない
すでに表示していた popover 要素は自動で閉じられる
popover="manual" の場合は閉じられないので 複数の popover 要素が表示できる

popovertarget と popovertargetaction 属性もあって これを使うと JavaScript なしで HTML だけでも表示・非表示を制御できる
属性名が長い

<button popovertarget="foo" popovertargetaction="show">
ここをクリックすると POPOVER を表示
</button>

<div popover id="foo">
<h1>POPOVER</h1>
<button popovertarget="foo" popovertargetaction="hide">
ここか外側をクリックすると POPOVER を隠す
</button>
</div>

マニュアルの場合

<button popovertarget="foo" popovertargetaction="show">
SHOW
</button>
<button popovertarget="foo" popovertargetaction="hide">
HIDE
</button>
<button popovertarget="foo" popovertargetaction="toggle">
TOGGLE
</button>

<div popover="manual" id="foo">
<h1>POPOVER</h1>
</div>

popover のネストもできる
popover で表示される要素の中に popover 要素を配置する

<button popovertarget="foo" popovertargetaction="show">SHOW</button>

<div popover id="foo">
<h1>POPOVER1</h1>

<button popovertarget="bar" popovertargetaction="show">SHOW</button>
<div popover id="bar">
<h1>POPOVER2</h1>
</div>
</div>

POPOVER2 を表示すると POPOVER1 は後ろで表示されたまま
POPOVER2 の外側かつ POPOVER1 の内側のエリアをクリックすると POPOVER2 だけを閉じれる
POPOVER1 の外側をクリックすると両方とも閉じられる

現状の機能だと位置は中央にしか来ないみたい
表示するボタンのすぐ下に持ってきたりする機能はなさそう
中央に来てるのはデフォルトの UA スタイルで margin: auto だからなので margin 等を JavaScript で調整して期待の位置に持っていくことになりそう

中身まで見てないけど Chrome 116 の機能で CSS Anchor Positioning というのがあるので これでアンカーを指定できるようになるのかも?
HTML タグ一覧 (2023/3)
search タグが仕様に追加されたと聞いたので以前書いた HTML のタグリストの新しい版
whatwg の目次から抽出

The document element
html

Document metadata
head
title
base
link
meta
style

Sections
body
article
section
nav
aside
h1
h2
h3
h4
h5
h6
hgroup
header
footer
address

Grouping content
p
hr
pre
blockquote
ol
ul
menu
li
dl
dt
dd
figure
figcaption
main
search
div

Text-level semantics
a
em
strong
small
s
cite
q
dfn
abbr
ruby
rt
rp
data
time
code
var
samp
kbd
sub
sup
i
b
u
mark
bdi
bdo
span
br
wbr

Edits
ins
del

Embedded content
picture
source
img
iframe
embed
object
video
audio
track
map
area

Tabular data
table
caption
colgroup
col
tbody
thead
tfoot
tr
td
th

Forms
form
label
input
button
select
datalist
optgroup
option
textarea
output
progress
meter
fieldset
legend

Interactive elements
details
summary
dialog

Scripting
script
noscript
template
slot
canvas

全部で 112 個

前回の 106 から 6 つ増えてるけど 実際の差分は少し
前回はセクション名をそのまま使っていたので h1 ~ h6 や sub と sup が 1 行にまとまっていたけど 今回はそれぞれを 1 行にしてる

追加されたのは search 要素
消えたのは param 要素

section 16 に廃止機能がまとまっていて param はこっちに移動したみたい
このセクションの中だと marquee だけが他の有効な要素と同じ感じでセクション名に載ってる
そのせいで自動的に取り出したときに marquee も含まれてた
仕様に載ってるんだしとリストに入れておこうとしたけど セクションの中身を見ると他にも param とか frame とかの廃止要素も色々
入れるならそれらも入れないとバランスが悪い気がするけど 廃止要素も全部探すのは面倒なので section 16 は除外



ついでなので Chrome 上でコンストラクタがある要素 (Chrome 112 時点)

const ctors = Object.getOwnPropertyNames(window).filter(x => x.match(/^HTML.+Element$/)).toSorted()
copy(ctors.join("\n"))
HTMLAnchorElement
HTMLAreaElement
HTMLAudioElement
HTMLBRElement
HTMLBaseElement
HTMLBodyElement
HTMLButtonElement
HTMLCanvasElement
HTMLDListElement
HTMLDataElement
HTMLDataListElement
HTMLDetailsElement
HTMLDialogElement
HTMLDirectoryElement
HTMLDivElement
HTMLEmbedElement
HTMLFieldSetElement
HTMLFontElement
HTMLFormElement
HTMLFrameElement
HTMLFrameSetElement
HTMLHRElement
HTMLHeadElement
HTMLHeadingElement
HTMLHtmlElement
HTMLIFrameElement
HTMLImageElement
HTMLInputElement
HTMLLIElement
HTMLLabelElement
HTMLLegendElement
HTMLLinkElement
HTMLMapElement
HTMLMarqueeElement
HTMLMediaElement
HTMLMenuElement
HTMLMetaElement
HTMLMeterElement
HTMLModElement
HTMLOListElement
HTMLObjectElement
HTMLOptGroupElement
HTMLOptionElement
HTMLOutputElement
HTMLParagraphElement
HTMLParamElement
HTMLPictureElement
HTMLPreElement
HTMLProgressElement
HTMLQuoteElement
HTMLScriptElement
HTMLSelectElement
HTMLSlotElement
HTMLSourceElement
HTMLSpanElement
HTMLStyleElement
HTMLTableCaptionElement
HTMLTableCellElement
HTMLTableColElement
HTMLTableElement
HTMLTableRowElement
HTMLTableSectionElement
HTMLTemplateElement
HTMLTextAreaElement
HTMLTimeElement
HTMLTitleElement
HTMLTrackElement
HTMLUListElement
HTMLUnknownElement
HTMLVideoElement

70 種類

一部要素は共通のコンストラクタだし↓

document.createElement("del").constructor.name
// 'HTMLModElement'
document.createElement("ins").constructor.name
// 'HTMLModElement'

独自のコンストラクタを持たない要素もあるし↓

document.createElement("b").constructor.name
// 'HTMLElement'
document.createElement("article").constructor.name
// 'HTMLElement'

HTMLUnknownElement も含まれてるしで HTML タグの数と一致はしてない
非表示要素を検索時にみつかったら表示させる
hidden 属性に until-found を指定

<div>foo</div>
<div hidden="until-found">bar</div>
<div>baz</div>

bar は表示されないけど Ctrl-F で bar を検索すると見つかり表示されるようになる
hidden="until-found" があるとスタイルに

content-visibility: hidden;

が設定されて非表示になってる
検索で見つかると 要素から属性が消えるのでスタイルも消えて表示される

display: none ではないので 非表示の挙動が少し違う
flex/grid の子要素だと要素があるように扱われる
中のサイズは計算されないけど その要素自身のスタイルの width/height は反映される
margin/padding も反映されて余白ができる

これを避けるために別のスタイルで display: none をつけると完全にないものとして扱われるので 検索対象からも外れてみつからなくなる
これまでの hidden 属性で 属性だけつけてもクラスのスタイルが優先されて表示されることがあるので

[hidden] {
display: none !important;
}

をつけて回避することがあったけど これをやると until-found が無意味になってしまう

非表示時のサイズを指定したい場合は contain-intrinsic-size を使える

div {
contain-intrinsic-size: 100px 50px;
}

表示されたら hidden 属性が消えるので

[hidden="until-found"] {
width: 100px;
height: 50px;
}

でも良い
HTML に popup 属性が追加されるみたい
Chrome 110 予定
https://chromestatus.com/feature/5463833265045504

select タグのメニューとかで一番上のレイヤーに出てくる UI
標準の機能として簡単に使えるようになるみたい
最初の提案は popup タグだったらしいけど 今は属性になったらしい
最上位に表示してブラウザのウィンドウから出れるとかキーボード制御はわかるけど 属性だけだと表示位置の調整はどうするのでしょう
id で基準の要素を指定したり?

この辺が関連する whatwg の issue ぽいので探せば色々書いてそうだけど長いのでパス
詳しくは dev や canary で使えるようになった頃に調べるつもり

https://github.com/whatwg/html/issues/7785
https://github.com/whatwg/html/pull/8221

(その後)

Chrome 110 が出たけど有効になってなくて 現在の予定だと Chrome 114 かららしい
ドキュメントの言語の指定は charset みたいに meta タグで書かせてほしい
html タグに lang 属性をつけてるページがありますが 個人的にはつけません
html タグに属性がなければ html, head, body を省略して直接 script や div 等のタグから書けます
余計なタグが無くシンプルでスッキリします

<!doctype html>
<meta charset="utf-8"/>

<script>console.log(1)</script>

<div>テキスト</div>

もう何年か前ですが Google の bot は lang 属性を見ていないから不要という話がありました
人が見る場合は本文を見ればだいたい言語は分かりますし いちいちソースコードを見て言語の確認なんてしません
使えない検索エンジンの Bing 等はどうでもいいし スクレイピングするようなユーザーのために親切なことをする必要はないです
Google のお墨付きがあるんだからと 積極的に lang 属性は書かずに html 等のタグは書かないって方針にしてました

ただそれだと自動翻訳系ツールが誤った言語と判定して翻訳しますか?みたいな通知を出してくることがあります
そういうのの対策をしようとすると lang 属性を書かざるをえないのですが そのために html タグを追加したくないです
charset のように meta タグで書ければいいのですけど 過去にあった content-language は廃止されています

JavaScript を使えば対処できますが あまり気持ちの良い方法ではないですし 翻訳ツール等が言語判定をするのが JavaScript の処理後になるかはわかりません

<!doctype html>
<meta charset="utf-8" />

<script>document.documentElement.lang = "ja"</script>

<div>テキスト</div>
ドキュメントの PDF をテキストから作りたいけど
ドキュメントの PDF を出力したいけど Word は色々と辛い
スタイルが言うことを聞かなかったり バージョン管理や差分確認のしづらさがあるし 画像が多いと重くなったり 大きい画像の位置調整がしづらかったり
デメリットが多いけど 見たままだしマウス操作できるなどのメリットもあるしでとりあえず Word にしてたけどそろそろやめたい

テキストベースでドキュメントを書く記法だと markdown が有名でよく使われてる
HTML にすれば PDF 化もできる
だけど パーサごとに独自拡張とかあったりで記法が完全に統一されてない
ツール次第で出力が変わるものだと その場限りのものならともかく 長期間修正を繰り返す前提のものには使いづらい

方向的にはこれでいいので markdown に近いマークアップ言語を探してみる
markdown よりはしっかりした規格があるようなのだと AsciiDoc や reStructuredText が候補
AsciiDoc の方は記法があまりわかりやすくなく 直感だと reStructuredText の方が良さそう
だけど記法の詳細を見ると ソースコード上での見た目を重視な分 本文に合わせた装飾が必要みたい
見出しの下線や表のレイアウトなど
なんか日本語の全角と相性が悪そう

他のドキュメント記法は何かないか探していたら pandoc というツールを発見
これ自体は記法ではなく ドキュメント間の変換ツール
数十はあるドキュメントフォーマットを変換できる結構すごいものだった
名前は聞いたことあったけど使ったことなかった……
PDF への変換までサポートしてるみたいなので このツールの標準フォーマットを使えばいい気がする
と思って調べると 独自拡張の markdown だった
markdown は上に書いた問題あるしなー と思ったけど PDF 変換まで pandoc で行うなら pandoc 用の拡張 markdown に限定すれば別にいいような気もする
それなりにしっかりとしたツールだし その他 markdown 独自拡張やパーサみたいに気づけば消えていってるようなことはなさそうだし



で 使っては見たけど 色々問題が
本当にちょっとしたドキュメントの PDF 化ならすごく簡単だった
Docker のイメージが用意されてるので 説明どおりにすれば PDF 化までお手軽変換
ただし英語限定とスタイルがそのままでよければ

デフォルトだと latex を中間データにして PDF を生成していて その latex エンジンが日本語をサポートしてない
昔 latex テンプレートしか用意されてないフォーマットの PDF を作る必要があったときに latex を使ったときは platex という日本語用のバージョンを使ったはず
pandoc のデフォルトは pdflatex
platex はサポートしてなくて変わりに Unicode 対応の xelatex と lualatex なら使えるらしい

--pdf-engine=lualatex -V documentclass=ltjsarticle -V luatexjapresetoptions=ms

というオプションを pandoc に渡せば日本語も使えるらしいけど Docker 環境だと ltjsarticle が入ってないみたいでエラー
調べても Windows に texlive を scheme-full で完全インストールしておけば良いとかそういう前提で Docker 環境なら何を入れればよいかよくわからない

とりあえずコンテナ内で tlmgr で scheme-full をインストールしてみたけど full というだけあって重すぎて数時間かかるペース
しかも途中で Docker のホストの WSL ごと落ちて完了してない
メモリ不足とか?

とりあえず tlmgr のインストールで ltjsarticle を直接指定したらインストールできたらしい
他に足りないのがあれば都度エラーが出ると思うのでそのたびに入れればいいか という感じで

試してみると次はフォントがないというエラー
haranoaji というフォントが必要みたい
ms を指定してるのになぜ?
ipa とかに変えてもこのフォントが要求される
とりあえず tlmgr で入れる

これで一応 PDF は出るようになった……けど スタイル的に求めるのには程遠い
なんというか論文?とか出版物?にありそうな感じ
もうちょっと親しみやすい感じにしたい
見た目的には Word をちょっとカスタムしたくらいのでいいんだけど



スタイルを変えようとしたけど latex のスタイル調整なんてしたことないし マクロ構文が複雑すぎて読む気も起きない
あー CSS は偉大だなー

latex をマスターするつもりでやるならともかく サクッとスタイルだけ好みにしたいだけで何時間かかるかもわからない苦労はしたくない
デフォルトスタイルでいいとか スタイル定義がすでにあるわけでもないなら PDF 生成のエンジンに latex 使うのはやめたほうがいいかも

スタイルが Word みたいのでいいなら docx を使えばいいのかも
pandoc は docx への変換もサポートしてるし そこから PDF 化できる
試してみると docx はそれなりにはいい感じ
--reference-doc でテンプレートの docx を指定するとそのスタイルを使ってくれる
だけどこれまで使ってた Word ファイルをそのまま指定だと スタイルが完全に思い通りにあたってないことも
内部的に使うスタイル名が決まってるみたいなので pandoc でデフォルトテンプレートの docx を出力して その docx のスタイルをカスタマイズして --reference-doc に指定するのが良いみたい

テキストベースなものと違ってコピペで済ませられないのでスタイル設定し直すのは結構面倒
それに本文・見出し・箇条書き・表くらいなシンプルなドキュメントなら十分そうだけど 表紙ページとかインデックスページとかヘッダ・フッタとか複数セクションの分割とかってどうすればいいんだろう?

docx 変換は情報が少なく苦労も多そう
やっぱり HTML を通した PDF 変換が一番無難でカスタマイズしやすいのかも
中間ファイルが HTML なら HTML をパースして構造置き換えたりも簡単にできるし スタイル調整も CSS で楽にできるし



HTML を使う場合は デフォルトエンジンは wkhtmltopdf
懐かしい というかまだ生きてたんだ
公式ページを見てみると QtWebkit の開発が停止したり復活したり Chrome の Blink 化や PhantomJS の終了だったり 色々あったみたい
https://wkhtmltopdf.org/status.html
プロジェクトは続いてるけど あまり活発に開発されてるわけでもないようで 別の類似ツールの使用も検討してくださいって言ってるくらいだし 最近は Chrome のヘッドレスモードで PDF 化できるから使わなくてもいい気はしてる

一応入れてみる
pandoc のエンジンとしては対応してるけど同梱されてないし Docker イメージに wkhtmltopdf を含んでるのもなかったので ubuntu を使った pandoc イメージで apt を使ってインストール
Qt 関係が依存で必要だからか GB 単位でインストールが必要で結構重い
これだけだと日本語が□になってたので日本語フォントも追加でインストールが必要
wkhtmltopdf のオプションに詳しくないので全体的な調整がやりづらいし wkhtmltopdf じゃなくて Chrome で PDF 化でいいかな



とりあえず pandoc で HTML 化して Chrome で PDF 化する方向にしようとしたけど ブラウザの HTML 印刷機能は弱かった
そもそもウェブページの印刷用だから ドキュメントの作成レベルのクオリティは要らないはずといえばそうなんだけど

ヘッダ・フッタをうまく扱えない
CSS の定義上は各コーナーを扱うプロパティは存在するけど未だにブラウザはサポートしてないらしい
position に absolute, fixed を使う方法はブラウザ依存だし Chrome だと各ページに表示はできるけど カウンターが更新されない
全てのページに同じページ数が表示される
全ページ数を表示する方法もない
JavaScript でコンテンツサイズとページサイズを計算してページ分けして それぞれのページに別々のヘッダ・フッタの要素を配置するという無理やりなものもあったけど画像や表とかがページを挟むとかもあるとどこかで問題が出そう

思ったより良かった部分はセクション分け機能は動いて

@page foo {
size: A4 portrait;
}
@page bar {
size: A4 landscape;
}

.section1 {
page: foo;
}
.section2 {
page: bar;
}

というスタイルを当てれば section1 と section2 でページが分かれて セクションごとに用紙の縦横を切り替えられる
ヘッダ・フッタのコーナーがサポートされれば @page のブロックに配置することでセクションごとのヘッダ・フッタも作れるはず
ただここも完璧ではなくて セクションごとに用紙の縦横は切り替わったけど内部の計算は用紙サイズの変更を反映してないみたい

1 つめのセクションは用紙が縦で 2 つめのセクションは用紙が横として それぞれの本文は同じで数字付きの箇条書き

1. aaa
2. aaa
3. aaa
4. aaa
...

みたいなの

数字は 1 ~ 20 までで

縦:
1 枚目 → 1 ~ 15
2 枚目 → 16 ~ 20
横:
1 枚目 → 1 ~ 8
2 枚目 → 9 ~ 16
3 枚目 → 17 ~ 20

を期待したのに横でも縦と同じ 2 枚になって 1 枚目に 15 まで入って 2 枚目が 16 からになってる
縮小されて全部が見えるならまだいいけど 8 までしか入ってなくて 9 から 15 は用紙の範囲外でプレビューにも映ってない
実際には使い物にならない

こういう問題があるし HTML はドキュメントの PDF 作成には向いてなさそう
latex でスタイルカスタマイズを頑張るか pandoc の PDF 化エンジンで選択できて wkhtmltopdf でも推奨してた weasyprint を試してみるとかかなー
HTML の body タグ内に書かれた内容を表示しない
条件に当てはまるときには body の中身を表示せず代わりのコンテンツを表示したい
サーバサイドでは静的ファイルをサーブするだけなので JavaScript や CSS での対応

ロード後に画面を書き換えだと HTML が重いページだと時間がかかる
ロード完了を待たずに表示されるようにしたい

一番楽なのは CSS で body を 「display: none」 してアラートでメッセージ表示
ただこれだと画面は真っ白
できれば画面にも代替コンテンツを表示したい

普通にやるならたぶん CSS で代替コンテンツ以外を 「display: none」
CSS に⇩を追加して HTML の最初に 「<div id="alternative">コンテンツ</div>」 を入れる

body * {
display: none !important;
}
body > #alternative {
display: block !important;
}

思いつきで JavaScript を使う方法を試してみたらこれでも実現できた

<body>
<script>document.body.replaceWith(document.createElement("body"))</script>

<div>a</div>
</body>

body を最初に入れ替えてしまう
HTML パーサが追加するのは document とはつながっていない古い方の body
これで 「a」 は表示されない
代替コンテンツは新しい body に JavaScript で追加する
CSS の方法と違って body の中がスッキリするのが嬉しいところ
HTML を使って CustomElement2
前回の続き
前は JavaScript だけ扱いを特別にしたけど HTML ファイル中にあったほうがいいかなと思ったので HTML ファイルに全部書く

使う側は一緒で foo-bar.js を読み込んで foo-bar タグを書いておく

[page.html]
<script type="module" src="foo-bar.js"></script>

<foo-bar></foo-bar>

[foo-bar.js]
import { importComponent } from "./base.js"

importComponent("foo-bar.html").then(component => {
customElements.define("foo-bar", component)
})

コンポーネントを import して customElements.define で定義

[base.js]
export const importComponent = async (htmlpath) => {
const res = await fetch(htmlpath)
const html = await res.text()
const doc = new DOMParser().parseFromString(html, "text/html")
const fragment = document.createDocumentFragment()
const scripts = [...doc.querySelectorAll("script")]
scripts.forEach(x => x.remove())
fragment.append(...doc.head.childNodes, ...doc.body.childNodes)

const module = {}
class Base extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" })
this.shadowRoot.append(this.constructor.fragment.cloneNode(true))
this.elements = Object.fromEntries(
Array.from(
this.shadowRoot.querySelectorAll("[id]"),
elem => [elem.id, elem]
)
)
}
}
Base.fragment = fragment

for(const script of scripts) {
const source = script.textContent
const fn = Function("module", "Base", source)
fn(module, Base)
}

return module.exports
}

importComponent は HTML を fetch してパースしていろいろ
script を含んだコンポーネントの HTML

[foo-bar.html]
<style>
.foo { font-size: 20px; }
.bar { color: red; }
.active { font-weight: bold; }
</style>

<div>
<div class="foo">FOO</div>
<div class="bar">BAR</div>
<button id="btn">Button</button>
</div>

<script>

module.exports = class extends Base {
connectedCallback() {
this.elements.btn.addEventListener("click", (eve) => {
eve.target.classList.toggle("active")
})
}
}

</script>

script タグ中のエクスポートは Node.js 形式で module.exports に代入
Base を継承したクラスをエクスポートする
HTML を使って CustomElement
ふと HTMLImports を思い出したのでこんなことしてみた

[base.js]
const initCustomElement = async (template) => {
const res = await fetch(template)
const html = await res.text()
const doc = new DOMParser().parseFromString(html, "text/html")
const fragment = document.createDocumentFragment()
fragment.append(...doc.head.childNodes, ...doc.body.childNodes)
return fragment
}

export class Base extends HTMLElement {
constructor() {
super()
const C = this.constructor
if (!C.ready) {
C.ready = initCustomElement(C.template)
}

this.attachShadow({ mode: "open" })
C.ready.then((fragment) => {
this.shadowRoot.append(fragment.cloneNode(true))
})
}
}

[foo-bar.js]
import { Base } from "./base.js"

customElements.define("foo-bar", class extends Base {
static template = "foo-bar.html"
})

[foo-bar.html]
<style>
.foo { font-size: 20px; }
.bar { color: red; }
</style>

<div>
<div class="foo">FOO</div>
<div class="bar">BAR</div>
</div>

innerHTML やスタイルは別の HTML ファイルに書く
script タグは HTML に含めず import される JavaScript ファイル側に書いてる
CustomElement の constructor の処理で HTML を fetch して ShadowDOM に append

表示するためのページ

[page.html]
<script type="module" src="foo-bar.js"></script>

<foo-bar></foo-bar>
VSCode でエラーが出る
VSCode で HTML として編集中のファイルで script タグの中で 継承したクラスの static プロパティを追加しようとするとエラーが出ます

まずは HTML として新規ファイルを編集します

Ctrl-N Ctrl-K,M html Enter

空のクラス定義を書きます(ここはコピペ可)

<script>
class A extends Foo {

}
</script>

ここの A の定義に

static aaa = 1

を追加します

「static a」 まで入力したタイミングで右下にエラーの通知が出ます
気にせず 「static aaa」 まで入力すると 画面の下半分にコンソールが出てきてエラーが表示されます
3 連続エラーで発生?

LitElement 使ってると 「static properties = {}」 とか 「static styles = css``」 とかよく書くのでそのたびに出てくるのは結構邪魔です
HTML じゃなくて .js ファイルにすればいいのですが file:/// スキームだと module の .js ファイルをロードできないので HTML の sript タグに埋め込んでるのですが そうするとこれの影響を受けます
script の innerHTML と textContent
普通 (div) の場合

const d = document.createElement("div")
d.textContent = "<&>"
d.innerHTML
// "&lt;&amp;&gt;"

d.innerHTML = "</div>"
d.outerHTML
// "<div></div>"

script の場合

const s = document.createElement("script")
s.textContent = "<&>"
s.innerHTML
// "<&>"

s.innerHTML = "</script>"
s.outerHTML
// "<script></script></script>"

innerHTML と textContent が一緒でエスケープされないから outerHTML を出力するとおかしなことになる
HTML テスター
書いた HTML をファイルに保存せず動作確認したいときに使う
適当なページで devtools を開いてコンソールで⇩を実行

document.head.innerHTML = `<style>textarea{width:50vw;height:50vh;}</style>`
document.body.innerHTML = `<textarea></textarea><div><button>OPEN</button></div>`
document.body.querySelector("button").onclick = eve => {
const w = window.open()
w.document.write(document.querySelector("textarea").value)
w.document.close()
}

textarea に HTML を書いて OPEN ボタン
元になったページのスキームになるので https のページでやれば https 機能が使える
差分更新 DOM ライブラリ作ってると POST も楽
久々にサーバに POST するもの作ってみると hyperhtml や lit-html を使ってると楽だった
テンプレートに埋め込むために データは変数上にすでに持ってるから form からいちいち取り出したりしなくても JSON で送れる
エラーチェックも変数のデータなので好きに処理できるし form のバリデーション機能より柔軟にできる
form の POST じゃなくて ajax でバックグラウンドリクエストだから 画面遷移せずレスポンスに応じて遷移したり警告出したり DOM 書き換えのみだったり色々選べる
それと form の POST だと disabled のときに送られないとか面倒な仕様があるけど変数で持ってるデータを送るだけなら DOM 上で disabled かどうかなんて関係なし

もう lit-html とかを使わないくらいシンプルに作るとき以外は全部 ajax で JSON POST でいいくらい
HTML 文字列から簡単に append する
document.body.append(`<div class="x"><span>1</span>23</div><div>45</div>`)

って書きたい
innerHTML ならできるけど append にはできない
これだとそのまま文字列として追加される

一旦 innerHTML に入れて全部の子要素を追加するの面倒
なので HTML テキストを DocumentFragment 化するもの

const fr = html => Object.assign(document.createElement("template"), {innerHTML: html}).content

これを使って

document.body.append(fr(`<div class="x"><span>1</span>23</div><div>45</div>`))
document.body.append(fr`<div class="x"><span>1</span>23</div><div>45</div>`)

() 書くの面倒ならなくしてテンプレートストリングのタグとしても使える
ただし間に ${} による埋め込みがないときだけ

せっかくだからタグならではの機能でエスケープ機能を入れる

const html = (strs, ...values) => {
const esc = value => Object.assign(document.createElement("div"), { textContent: value }).innerHTML
const fmt = value => value != null
? (value.html ? value.html : value.text ? esc(value.text) : esc(value))
: ""
return fr(strs.reduce((a, e, i) => a + fmt(values[i - 1]) + e))
}

これを使うと

document.body.append(html`<div class="x"><span>1</span>23</div><div>45</div>`)
document.body.append(html`<div class="x"><span>${1}</span>${"<br>"}${{html:"<b>bold</b>"}}</div><div>45</div>`)

「<br>」はエスケープされて文字列として表示されて 「<b>」 は太文字になる
localName
elem.tagName だと DIV みたいな大文字になる
小文字化面倒だと思ってたら localName だと小文字でとれる

document.createElement("div").localName
// "div"

document.createElement("div").tagName
// "DIV"

document.createElement("div").nodeName
// "DIV"

「For example, in the qualified name "ecomm:partners", "partners" is the local name and "ecomm" is the prefix」
https://developer.mozilla.org/en-US/docs/Web/API/Element/localName

って書いてるのに

document.createElement("ecomm:partners").localName
// "ecomm:partners"

コロン含めて全部取れる
display: table の使いどころ
grid もあるしわざわざ display に table を指定する機会なんてもうなさそう……と思ってたけど
<div>
<h1>header</h1>
body<br/>
body
</div>
みたいな構造のときに h1 を block のように扱いたいけど 幅は inline-block のようにしたい
そんなときに使えます

h1 は block で 「body」 は 「header」 の次の行に来る
h1 の幅は 「header」 の文字列の幅だけで右側にはマージンができる

というちょっと特殊な動きがしたいとき

さらに
h1 の before/after 疑似要素は別の用途で使用済み
h1 を div などで囲みたくない

そんなときに display: table を h1 に指定するとこの block のような inline のような状態を実現できます
タグ一覧の jsonp
全部のタグに対して何か処理したいことがたまにあるけど毎回タグ一覧のデータ作るのが面倒
簡単に使えるようにしたいなと考えていて とりあえず json 形式でどこかに上げておけばいいかなと思ったけど JavaScript で使うのでクロスオリジン制限がある
なので jsonp 形式にしてみた

<script>
function init(tags){
console.log(tags)
}
</script>
<script src="https://cdn.rawgit.com/ryls-nmm/96fc54f28d7d7e89aaf7f3c456782079/raw/85e71a97087873871aa45708de7068330e24a499/htmlTags.js?callback=init"></script>

でもこれ HTML を書くのなら簡単だけど 基本その時に開いてるどこかのページ上でコンソールを開いて実行するので script タグつき HTML を書くのはちょっと面倒


と ここまでやって rawgit は CORS 許可されてるようで普通に json 上げて fetch できることに気づいた
単純に配列とかのデータだけを json 形式で gist に上げておけばいいなんて便利だね
main タグの仕様の違い
main タグってあまり意識せずに main っぽいところの div を main タグに置き換える程度に使ってるけどなにか制限とか気にするべきことあったかなと調べてみると

「article や header タグの内側に書けない」と書いてるページが

え そんなことはさすがにないよね?
1 ドキュメントに 1 つだけというのは聞いたことあった気がするけど

MDN を見てみると w3c と whatwg で違うようです
https://developer.mozilla.org/ja/docs/Web/HTML/Element/main

違いは

○ whatwg
- フローコンテンツが書けるところならどこでも書ける
- ページ内に複数あってもおっけい

○ w3c
- <article>, <aside>, <footer>, <header>, or <nav> の内側に書けない
- ページに 1 つだけ (1 つ以外が hidden 属性を持ってると複数書ける)
  見えてるのが 1 つだけになってる必要があるということ


w3c のほうが 5.2 とかバージョンついてて正式なもの感あるし こっちを参考に説明記事を書いてる人が多いみたい
でも実際 w3c って whatwg をコピーしてるようなものだし ブラウザの実装は whatwg を元にしてるっていうくらいだし whatwg の方だけ見てていいと思う
HTML タグ一覧
HTML のタグ一覧
2018/2/8 時点の whatwg のスペックから

The *** element の部分を抽出

The document element
html

Document metadata
head
title
base
link
meta
style

Sections
body
article
section
nav
aside
h1, h2, h3, h4, h5, and h6
hgroup
header
footer
address

Grouping content
p
hr
pre
blockquote
ol
ul
menu
li
dl
dt
dd
figure
figcaption
main
div

Text-level semantics
a
em
strong
small
s
cite
q
dfn
abbr
ruby
rt
rp
data
time
code
var
samp
kbd
sub and sup
i
b
u
mark
bdi
bdo
span
br
wbr

Edits
ins
del

Embedded content
picture
source
img
iframe
embed
object
param
video
audio
track
map
area

Tabular data
table
caption
colgroup
col
tbody
thead
tfoot
tr
td
th

Forms
form
label
input
button
select
datalist
optgroup
option
textarea
output
progress
meter
fieldset
legend

Interactive
details
summary
dialog

Scripting
script
noscript
template
slot
canvas

全部で 106 個