diff --git a/scripts/table_filter.js b/scripts/table_filter.js
index 0b43745b427..a9498942d58 100644
--- a/scripts/table_filter.js
+++ b/scripts/table_filter.js
@@ -6,8 +6,17 @@
// もとの markd を参考に拡張する
// https://github.com/markedjs/marked/blob/e5796ecc435a30f96939e6a7b2229c14264b4bf8/src/Renderer.js#L92
hexo.extend.filter.register('marked:renderer', function (renderer) {
+ // renderer は1ページの描画ごとに作り直されるので、直前の見出しと既出のラベルを
+ // ここに持てばページ単位で数えられる (#3231)
+ const heading = renderer.heading;
+ renderer.heading = function (text, level, ...rest) {
+ this._lastHeading = plainText(text);
+ return heading.call(this, text, level, ...rest);
+ };
renderer.table = function (header, body) {
- return `
${table(header, body)}
\n`;
+ this._tableLabels ||= new Set();
+ const label = uniqueLabel(regionLabel(header), this._lastHeading, this._tableLabels);
+ return `${table(header, body)}
\n`;
};
});
@@ -17,6 +26,12 @@ const table = (header, body) => {
return '\n' + '\n' + header + '\n' + body + '
';
};
+const plainText = (html) =>
+ html
+ .replace(/<[^>]*>/g, '')
+ .replace(/\s+/g, ' ')
+ .trim();
+
/**
* 横スクロールする器の読み上げ名を、1行目の見出しから組む (#2961)。
*
@@ -33,18 +48,28 @@ const table = (header, body) => {
* 名前を列名から作るのは、1ページに複数の表があると「表」だけでは区別できないため。
*/
const regionLabel = (header) => {
- const names = (header.match(/]*>([\s\S]*?)<\/th>/g) || [])
- .map((cell) =>
- cell
- .replace(/<[^>]*>/g, '')
- .replace(/\s+/g, ' ')
- .trim(),
- )
- .filter(Boolean);
+ const names = (header.match(/ | ]*>([\s\S]*?)<\/th>/g) || []).map(plainText).filter(Boolean);
if (!names.length) return '表';
// 長い見出しが並ぶ表もあるので頭を取る。
+ const joined = names.join(' / ').slice(0, 60);
+ return `表 ${joined}`;
+};
+
+/**
+ * 同じ列名の表が同じページに並ぶと名前が重複する (#3231。axe landmark-unique)。
+ * 2つ目以降は直前の見出しを先に置く。連番は順序しか言わないが、見出しは
+ * どの節の表かを言う。見出しが無いか、足しても重なるときだけ連番に落とす。
+ */
+const uniqueLabel = (base, heading, used) => {
+ const candidates = [base];
+ if (heading) candidates.push(`${heading.slice(0, 40)}の${base}`);
+ let label = candidates.find((c) => !used.has(c));
+ for (let n = 2; !label; n++) {
+ const numbered = `${candidates.at(-1)}(${n})`;
+ if (!used.has(numbered)) label = numbered;
+ }
+ used.add(label);
// **& は再エスケープしない。** header は marked が出した HTML で、本文の & は
// すでに & になっている。もう一度潰すと & になって読み上げに出る
- const joined = names.join(' / ').slice(0, 60);
- return `表 ${joined}`.replace(/"/g, '"').replace(/ |