mirror of
https://github.com/kennethreitz/rhymepad.org.git
synced 2026-06-11 17:08:33 +00:00
Sub-word rhyme highlighting: dim word fill + bright tail underline
Whole rhyming word fills dimly; a bright underline marks just the rhyming part (tonight -> under 'ight'), so the precise interlock shows without the fill looking ragged when the phoneme->letter guess is a letter off. Removed 'scheme' from the toolbar readout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -282,6 +282,25 @@ def _grapheme_tail(raw: str) -> str | None:
|
||||
return tail
|
||||
|
||||
|
||||
def rhyme_char_start(word: str) -> int:
|
||||
"""Best-guess character index where a word's rhyming tail begins:
|
||||
take as many vowel-letter groups from the end as the pronunciation's
|
||||
rhyming part has vowels (tonight -> 'ight', creation -> 'ation').
|
||||
Spelling can't map phonemes exactly, so this is an approximation."""
|
||||
ph = phones_for(word)
|
||||
if not ph:
|
||||
return 0
|
||||
nv = len(_tail_vowels(ph)) or 1
|
||||
groups = [m.start() for m in re.finditer(r"[aeiouyAEIOUY]+", word)]
|
||||
# drop a silent trailing 'e' (write, time, fire) so it doesn't eat a slot
|
||||
if (len(groups) > 1 and word[-1] in "eE"
|
||||
and groups[-1] == len(word) - 1):
|
||||
groups.pop()
|
||||
if len(groups) < nv:
|
||||
return 0
|
||||
return groups[len(groups) - nv]
|
||||
|
||||
|
||||
def rime_keys(word: str) -> tuple[str, ...]:
|
||||
"""Perfect-rhyme keys, one per candidate pronunciation."""
|
||||
cands = phones_candidates(word)
|
||||
@@ -1295,12 +1314,16 @@ def analyze(draft: Draft):
|
||||
"legend": legend,
|
||||
})
|
||||
|
||||
toks_out = [
|
||||
{"l": t["line"], "s": t["start"], "e": t["end"], "g": t["gid"],
|
||||
"end": t["is_end"], "ph": "vowels" in t,
|
||||
"slant": t["slant"] or groups_out[t["gid"]]["slant"]}
|
||||
for t in [*tokens, *phrases] if t["gid"] is not None
|
||||
]
|
||||
toks_out = []
|
||||
for t in [*tokens, *phrases]:
|
||||
if t["gid"] is None:
|
||||
continue
|
||||
d = {"l": t["line"], "s": t["start"], "e": t["end"], "g": t["gid"],
|
||||
"end": t["is_end"], "ph": "vowels" in t,
|
||||
"slant": t["slant"] or groups_out[t["gid"]]["slant"]}
|
||||
if "vowels" not in t: # single word: where its rhyming tail starts
|
||||
d["rs"] = t["start"] + rhyme_char_start(t["word"])
|
||||
toks_out.append(d)
|
||||
meter, meter_by_line = [], {}
|
||||
for i, line in enumerate(lines):
|
||||
if sids[i] is None:
|
||||
|
||||
+11
-10
@@ -609,7 +609,7 @@ function render(){
|
||||
h = esc(line);
|
||||
}else{
|
||||
const cuts = new Set([0, line.length]);
|
||||
toks.forEach(t=>{ cuts.add(t.s); cuts.add(t.e); });
|
||||
toks.forEach(t=>{ cuts.add(t.s); cuts.add(t.e); if(t.rs != null) cuts.add(t.rs); });
|
||||
als.forEach(t=>{ cuts.add(t.s); cuts.add(t.e); });
|
||||
opens.forEach(t=>{ cuts.add(t.s); cuts.add(t.e); });
|
||||
const pts = [...cuts].sort((a,b)=>a-b);
|
||||
@@ -622,25 +622,31 @@ function render(){
|
||||
const al = als.find(t=>t.s <= a && b <= t.e);
|
||||
const op = opens.find(t=>t.s <= a && b <= t.e);
|
||||
if(!w && !p && !al && !op){ h += text; continue; }
|
||||
// fills = tail sound (rhyme); underline = head sound (alliteration);
|
||||
// whole word fills dimly; the rhyming part gets a bright underline.
|
||||
// gray = an ending still waiting for its answer
|
||||
let style = '';
|
||||
const shadows = [];
|
||||
if(w || p){
|
||||
const t = (w && (focusGid === null || w.g === focusGid)) ? w
|
||||
: (p && (focusGid === null || p.g === focusGid)) ? p
|
||||
: (w || p);
|
||||
let alpha = !t.ph ? (t.end ? 34 : 19) : (t.end ? 24 : 14);
|
||||
let alpha = !t.ph ? (t.end ? 28 : 16) : (t.end ? 20 : 12);
|
||||
if(t.slant) alpha = Math.round(alpha * 0.62); // slant sits back
|
||||
// the caret's family brightens; everything else stays itself
|
||||
if(focusGid !== null && t.g === focusGid){
|
||||
alpha = Math.min(85, Math.round(alpha * 2.8));
|
||||
style += 'color:#1a120c;font-weight:600;'; // dark ink on the lit chip
|
||||
}
|
||||
style += `background:color-mix(in srgb, ${colorOf(t)} ${alpha}%, transparent);`;
|
||||
// bright underline on the rhyming tail (rs..e) of a word
|
||||
if(w && (w.rs == null || a >= w.rs)){
|
||||
const u = w.slant ? 55 : 80;
|
||||
shadows.push(`inset 0 -2px 0 0 color-mix(in srgb, ${colorOf(w)} ${u}%, transparent)`);
|
||||
}
|
||||
}else if(op){
|
||||
style += `background:color-mix(in srgb, var(--ink-dim) 13%, transparent);`;
|
||||
}
|
||||
if(al) style += `box-shadow:inset 0 -2px 0 0 color-mix(in srgb, var(--r${al.g % COLORS}) 75%, transparent);`;
|
||||
if(al) shadows.push(`inset 0 -2px 0 0 color-mix(in srgb, var(--r${al.g % COLORS}) 75%, transparent)`);
|
||||
if(shadows.length) style += `box-shadow:${shadows.join(',')};`;
|
||||
h += `<span class="hseg" style="${style}">${text}</span>`;
|
||||
}
|
||||
}
|
||||
@@ -798,11 +804,6 @@ function buildReadout(){
|
||||
const bi = st.lines.indexOf(ln);
|
||||
if(bi >= 0) parts.unshift(`bar ${bi + 1}/${st.lines.length}`);
|
||||
}
|
||||
if(st && st.scheme){
|
||||
const sch = st.scheme.toUpperCase();
|
||||
const shown = sch.slice(0, 16).split('').join(' ') + (sch.length > 16 ? ' …' : '');
|
||||
parts.push(`scheme <b>${shown}</b>`);
|
||||
}
|
||||
if(focusGid !== null && analysis){
|
||||
const g = analysis.groups.find(x=>x.id === focusGid);
|
||||
const fam = [...new Set(analysis.tokens.filter(t=>t.g===focusGid)
|
||||
|
||||
@@ -740,3 +740,9 @@ def test_no_matching_across_stanzas():
|
||||
# but within each stanza they still rhyme
|
||||
assert "night" in s1
|
||||
assert any({"bright", "sight"} <= s for s in bg.values())
|
||||
|
||||
|
||||
def test_rhyme_char_start():
|
||||
from app import rhyme_char_start
|
||||
assert rhyme_char_start("tonight") == 4 # 'ight'
|
||||
assert rhyme_char_start("write") == 2 # 'ite' (silent-e skipped)
|
||||
|
||||
Reference in New Issue
Block a user