// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2023-2026 Yomitan Authors
// Copyright (C) 2021-2022 Yomichan Authors
// Graph notation follows Yomitan's pronunciation-generator.js (GPL-3.0-or-later),
// including its Jidoujisho-style kana graph: https://github.com/yomidevs/yomitan
import "./render/glossary.js";
import { escapeAnkiHtml as escape } from "./anki-templates.js";
function pitchContour(reading, pitch) {
const { splitPitchAccentMorae, buildPitchAccentMorae } = globalThis.HDGlossary;
if (pitch.pattern) {
const morae = splitPitchAccentMorae(reading);
// The engine stores string positions in pattern, with a placeholder numeric
// position of zero. Never interpret an unsupported pattern as heiban.
if (!morae.length || !/^[HL]+$/u.test(pitch.pattern)
|| pitch.pattern.length < morae.length || pitch.pattern.length > morae.length + 1) return null;
return { morae, levels: [...pitch.pattern.padEnd(morae.length + 1, pitch.pattern.at(-1))] };
}
const morae = buildPitchAccentMorae(reading, pitch.position);
if (!morae) return null;
return { morae: morae.map(mora => mora.text),
levels: [...morae.map(mora => mora.level === "high" ? "H" : "L"), pitch.position === 0 ? "H" : "L"] };
}
function graphLine(from, to, radius) {
// Stop at the dots' edges so hollow downstep/particle marks stay transparent
// on both light and dark cards, without masks or document-wide SVG IDs.
const dx = to.x - from.x, dy = to.y - from.y;
const scale = radius / Math.hypot(dx, dy);
return `M${from.x + dx * scale} ${from.y + dy * scale}L${to.x - dx * scale} ${to.y - dy * scale}`;
}
function pitchGraph(reading, pitch, kana) {
const contour = pitchContour(reading, pitch);
if (!contour) return "";
const { morae, levels } = contour;
const step = kana ? 35 : 50, height = kana ? 80 : 100, radius = kana ? 5 : 15;
const highY = kana ? 10 : 25;
const lowY = kana ? 35 : 75;
const points = levels.map((level, index) => ({ x: step * (index + 0.5),
y: level === "H" ? highY : lowY }));
const width = step * points.length;
const label = `${reading}: pitch accent ${pitch.pattern || pitch.position}`;
const lines = points.slice(1).map((point, index) => {
const tail = index === morae.length - 1;
return ``;
});
const dots = points.slice(0, -1).map(({ x, y }, index) => {
const downstep = !kana && levels[index] === "H" && levels[index + 1] === "L";
return ``
+ (downstep ? `` : "");
});
const tail = points.at(-1);
const tailAttributes = `class="pronunciation-graph-tail" data-pitch="${levels.at(-1) === "H" ? "high" : "low"}" fill="none" stroke="currentColor"`;
// Match Yomitan's distinct particle radius so card CSS for 5-unit mora dots
// does not fill the hollow JJ particle.
const particle = kana
? ``
: ``;
const labels = kana ? morae.map((mora, index) =>
` 1 ? ' textLength="30" lengthAdjust="spacingAndGlyphs"' : ""} style="font: 20px sans-serif; fill: currentColor;">${escape(mora)}`).join("") : "";
return ``;
}
export function ankiPitchGraphs(term, kana = false) {
const reading = term.reading || term.expression;
return term.pitches.map(group => {
const graphs = group.pitches.map(pitch => pitchGraph(reading, pitch, kana)).filter(Boolean);
return graphs.length ? `${escape(group.dictionary)}: ${graphs.join(" ")}` : "";
}).filter(Boolean).join("
");
}