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 で、本文の & は // すでに & になっている。もう一度潰すと &amp; になって読み上げに出る - const joined = names.join(' / ').slice(0, 60); - return `表 ${joined}`.replace(/"/g, '"').replace(/