Sprint 16: collapsible card details + related words table

- All secondary fields (shoresh, PoS, ktiv male, plural, related words)
  behind a "מידע נוסף" toggle button using HTML <details>/<summary>
- Conjugation back: English meaning, binyan also behind toggle
- Related words: table format with word + meaning, sorted by frequency
- Hebrew words not bold, English meanings 24px gray (#555)
- "מִילִים קְשׁוּרוֹת" sub-header with nikkud inside toggle
- "אֵיךְ אוֹמְרִים" prompt centered using hint class
- New CSS: .more-toggle, .more-header, .related-header, .rw-word, .rw-meaning
- Dark mode support for all new classes
- Bump to v0.18

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sochen 2026-03-11 01:34:14 +00:00
parent c85063ee2f
commit 0d92451271
4 changed files with 363 additions and 40 deletions

View file

@ -35,7 +35,7 @@ COMPLETE_PLURAL_DECK_ID = 1_234_567_903
# Release version tag added to all notes so users can identify which release
# their cards come from (visible in Anki's Browse view and card info).
RELEASE_TAG = "v0.17"
RELEASE_TAG = "v0.18"
# Regex for extracting emoji and Hebrew prepositions from meaning strings
EMOJI_RE = re.compile(r"[\U0001F000-\U0001FFFF\u2600-\u27FF\u2300-\u23FF\uFE00-\uFE0F]+")
@ -152,12 +152,6 @@ CARD_CSS = """
direction: rtl;
text-align: center;
}
.root-info {
font-size: 26px;
color: #222;
margin-top: 6px;
direction: rtl;
}
.example {
font-size: 24px;
color: #222;
@ -218,17 +212,54 @@ CARD_CSS = """
direction: rtl;
text-align: center;
}
.more-toggle {
text-align: center;
direction: rtl;
margin-top: 8px;
}
.more-header {
display: inline-block;
font-size: 18px;
color: #555;
cursor: pointer;
list-style: none;
border: 1px solid #ccc;
border-radius: 16px;
padding: 4px 16px;
margin: 4px 0;
background: #f8f8f8;
}
.more-header::-webkit-details-marker { display: none; }
.more-header::before { content: ""; font-size: 14px; }
details[open] > .more-header::before { content: ""; }
.related-header {
font-size: 22px;
color: #555;
text-align: center;
margin: 4px 0;
}
.rw-word {
display: table-cell;
font-size: 28px;
color: #222;
font-weight: normal;
text-align: right;
padding: 2px 0 2px 8px;
white-space: nowrap;
}
.rw-meaning {
display: table-cell;
font-size: 24px;
color: #555;
text-align: left;
direction: ltr;
padding: 2px 0;
}
.conf-entry {
margin: 8px 0;
font-size: 28px;
direction: rtl;
}
.related-group {
direction: rtl;
text-align: center;
margin: 2px 0;
font-size: 26px;
}
.emoji-img {
font-size: 3.5em;
text-align: center;
@ -244,7 +275,6 @@ CARD_CSS = """
.hebrew { color: #f0f0f0; }
.hebrew-sm { color: #e0e0e0; }
.meaning { color: #82b0ff; }
.root-info { color: #e0e0e0; }
.sec-label { color: #e0e0e0; }
.sec-key { color: #e0e0e0; }
.sec-val { color: #e0e0e0; }
@ -254,6 +284,10 @@ CARD_CSS = """
.example { color: #e0e0e0; border-right-color: #555; }
.divider { border-top-color: #333; }
.freq-badge { color: #888; border-color: #444; }
.more-header { color: #bbb; background: #2a2a2e; border-color: #555; }
.related-header { color: #999; }
.rw-word { color: #e0e0e0; }
.rw-meaning { color: #999; }
}
"""
@ -272,6 +306,7 @@ VOCAB_BACK_HEB = """
<div class="meaning">{{Meaning}}</div>
{{#Emoji}}<div class="emoji-img">{{Emoji}}</div>{{/Emoji}}
{{^Emoji}}{{#Image}}<div><img src="{{Image}}" style="max-height:150px;margin-top:8px;" onerror="this.parentElement.style.display='none'"></div>{{/Image}}{{/Emoji}}
<details class="more-toggle"><summary class="more-header">מידע נוסף</summary>
<div class="sec-table">
{{#WordNoNikkud}}<div class="sec-label"><span class="sec-key">לְלֹא נִיקּוּד:</span><span class="sec-val">{{WordNoNikkud}}</span></div>{{/WordNoNikkud}}
{{#Root}}<div class="sec-label"><span class="sec-key">שֹׁרֶשׁ:</span><span class="sec-val">{{Root}}</span></div>{{/Root}}
@ -280,9 +315,10 @@ VOCAB_BACK_HEB = """
</div>
{{#SharedRoots}}
<div class="divider" style="margin:6px 0;"></div>
<div class="sec-label" style="text-align:center;display:block;"><span class="sec-key">מִילִים קְשׁוּרוֹת:</span></div>
<div class="root-info">{{SharedRoots}}</div>
<div class="related-header" style="cursor:default;">מִילִים קְשׁוּרוֹת</div>
<div class="sec-table">{{SharedRoots}}</div>
{{/SharedRoots}}
</details>
"""
VOCAB_FRONT_ENG = """
@ -297,6 +333,7 @@ VOCAB_BACK_ENG = """
<div class="divider"></div>
<div class="hebrew">{{Word}}{{#Prep}} <span class="hebrew-sm">{{Prep}}</span>{{/Prep}}</div>
{{#Audio}}<div>{{Audio}}</div>{{/Audio}}
<details class="more-toggle"><summary class="more-header">מידע נוסף</summary>
<div class="sec-table">
{{#WordNoNikkud}}<div class="sec-label"><span class="sec-key">לְלֹא נִיקּוּד:</span><span class="sec-val">{{WordNoNikkud}}</span></div>{{/WordNoNikkud}}
{{#Root}}<div class="sec-label"><span class="sec-key">שֹׁרֶשׁ:</span><span class="sec-val">{{Root}}</span></div>{{/Root}}
@ -305,9 +342,10 @@ VOCAB_BACK_ENG = """
</div>
{{#SharedRoots}}
<div class="divider" style="margin:6px 0;"></div>
<div class="sec-label" style="text-align:center;display:block;"><span class="sec-key">מִילִים קְשׁוּרוֹת:</span></div>
<div class="root-info">{{SharedRoots}}</div>
<div class="related-header" style="cursor:default;">מִילִים קְשׁוּרוֹת</div>
<div class="sec-table">{{SharedRoots}}</div>
{{/SharedRoots}}
</details>
"""
VOCAB_FRONT_CLOZE = """
@ -373,7 +411,7 @@ VOCAB_MODEL = genanki.Model(
# ──────────────────────────────────────────────────────────────────────────────
CONJ_FRONT = """
<div class="sec-label">אֵיךְ אוֹמְרִים</div>
<div class="hint">אֵיךְ אוֹמְרִים</div>
<div class="hebrew">{{Pronoun}}</div>
<div class="hebrew" style="color:#1a1a8c;">{{Infinitive}}{{#Prep}} <span class="hebrew-sm">({{Prep}})</span>{{/Prep}}{{#Voice}} <span class="voice-label">({{Voice}})</span>{{/Voice}}</div>
<div class="hebrew">{{Tense}}</div>
@ -383,6 +421,7 @@ CONJ_BACK = """
{{FrontSide}}<hr>
<div class="hebrew">{{ConjugatedForm}}{{#Prep}} ({{Prep}}){{/Prep}}</div>
{{#Audio}}<div>{{Audio}}</div>{{/Audio}}
<details class="more-toggle"><summary class="more-header">מידע נוסף</summary>
{{#Meaning}}<div class="sec-label" style="text-align:center;display:block;">{{Meaning}}</div>{{/Meaning}}
<div class="sec-table">
<div class="sec-label"><span class="sec-key">שֹׁרֶשׁ:</span><span class="sec-val">{{Root}}</span></div>
@ -390,9 +429,10 @@ CONJ_BACK = """
</div>
{{#RelatedVocab}}
<div class="divider" style="margin:6px 0;"></div>
<div class="sec-label" style="text-align:center;display:block;"><span class="sec-key">מִילִים קְשׁוּרוֹת:</span></div>
<div class="root-info">{{RelatedVocab}}</div>
<div class="related-header" style="cursor:default;">מִילִים קְשׁוּרוֹת</div>
<div class="sec-table">{{RelatedVocab}}</div>
{{/RelatedVocab}}
</details>
"""
CONJ_CSS = CARD_CSS
@ -925,28 +965,33 @@ def build_vocab_deck(
if pos_cat == "Verb" and pos_heb:
cloze_hint = f"{meaning} ({pos_heb})"
# Related words (shared roots) grouped by PoS category
# Related words (shared roots) as a table: word — meaning, sorted by frequency
related_html = ""
if shared_roots_keys:
groups: dict[str, list[str]] = {}
rw_items: list[tuple[int, str, str]] = [] # (sort_key, nikkud, meaning)
for rw_key in shared_roots_keys:
rw_entry = words.get(rw_key)
if rw_entry:
rw_nikkud = rw_entry["word"]["nikkud"]
cat = _categorize_pos(rw_entry.get("pos", ""))
rw_meaning = rw_entry.get("meaning") or ""
if len(rw_meaning) > 40:
rw_meaning = rw_meaning[:37] + ""
rw_freq = rw_entry.get("frequency") or 999999
else:
# Key not found: use the key itself as display text
rw_nikkud = rw_key
cat = "Other"
groups.setdefault(cat, []).append(rw_nikkud)
parts = []
for cat, rw_words in groups.items():
if cat == "Other":
parts.append(f'<div class="related-group">{" ".join(rw_words)}</div>')
else:
label = POS_CATEGORY_LABELS.get(cat, cat)
parts.append(f'<div class="related-group"><b>{label}:</b> {" ".join(rw_words)}</div>')
related_html = "\n".join(parts)
rw_meaning = ""
rw_freq = 999999
rw_items.append((rw_freq, rw_nikkud, rw_meaning))
rw_items.sort(key=lambda x: x[0])
rows_html: list[str] = []
for _freq, rw_nikkud, rw_meaning in rw_items:
rows_html.append(
f'<div class="sec-label">'
f'<span class="rw-word">{rw_nikkud}</span>'
f'<span class="rw-meaning">{rw_meaning}</span>'
f"</div>"
)
related_html = "\n".join(rows_html)
# Plural form and gender (nouns only)
plural_str = ""
@ -1042,13 +1087,17 @@ def build_conj_deck(
note_count = 0
verb_count = 0
# Build root → [related word nikkud] lookup for cross-linking
root_words: dict[str, list[str]] = {}
# Build root → [(freq, nikkud, meaning)] lookup for cross-linking
root_words: dict[str, list[tuple[int, str, str]]] = {}
for entry in words.values():
root_list = entry.get("root") or []
root_key = " ".join(root_list)
if root_key:
root_words.setdefault(root_key, []).append(entry["word"]["nikkud"])
rw_meaning = entry.get("meaning") or ""
if len(rw_meaning) > 40:
rw_meaning = rw_meaning[:37] + ""
rw_freq = entry.get("frequency") or 999999
root_words.setdefault(root_key, []).append((rw_freq, entry["word"]["nikkud"], rw_meaning))
for _unique_key, entry in words.items():
conj = entry.get("conjugation")
@ -1089,8 +1138,20 @@ def build_conj_deck(
# Clean up double spaces and trailing commas
meaning = re.sub(r"\s{2,}", " ", meaning).strip(", ")
related = [w for w in root_words.get(root, []) if w != infinitive]
related_str = " ".join(related[:8]) if related else ""
related = [(f, w, m) for f, w, m in root_words.get(root, []) if w != infinitive]
if related:
related.sort(key=lambda x: x[0])
related_rows = []
for _freq, rw_nikkud, rw_meaning in related[:8]:
related_rows.append(
f'<div class="sec-label">'
f'<span class="rw-word">{rw_nikkud}</span>'
f'<span class="rw-meaning">{rw_meaning}</span>'
f"</div>"
)
related_str = "\n".join(related_rows)
else:
related_str = ""
forms = _forms_list_to_dict(active_forms_list)

110
card_preview.html Normal file
View file

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html dir="rtl">
<head>
<meta charset="utf-8">
<style>
body { font-family: 'Heebo', 'Arial Hebrew', sans-serif; background: #fff; max-width: 600px; margin: 20px auto; }
.card-container { border: 1px solid #ccc; border-radius: 8px; margin: 20px 0; overflow: hidden; }
.card-label { background: #333; color: #fff; padding: 6px 12px; font-size: 14px; font-family: sans-serif; direction: ltr; }
.card-content { padding: 16px; text-align: center; }
.card-content hr { border: none; border-top: 1px solid #ccc; margin: 12px 0; }
.hebrew { font-size: 48px; font-weight: bold; color: #222; direction: rtl; text-align: center; }
.hebrew-sm { font-size: 28px; font-weight: normal; color: #222; direction: rtl; }
.meaning { font-size: 28px; color: #1a1a8c; text-align: center; direction: ltr; margin: 4px 0; }
.emoji-img { font-size: 48px; text-align: center; margin: 4px 0; }
.divider { border-top: 1px solid #ccc; margin: 8px 0; }
.sec-table { display: table; margin: 6px auto 0; direction: rtl; border-collapse: collapse; }
.sec-label { display: table-row; font-size: 28px; font-weight: normal; color: #222; direction: rtl; }
.sec-key { display: table-cell; font-size: 28px; color: #222; font-weight: bold; text-align: right; padding: 2px 0 2px 8px; white-space: nowrap; }
.sec-val { display: table-cell; font-size: 28px; color: #222; text-align: right; padding: 2px 0; }
.hint { font-size: 22px; color: #555; margin: 4px 0; direction: rtl; text-align: center; }
.example { font-size: 24px; color: #222; padding: 6px 8px; direction: rtl; text-align: center; border-left: 3px solid #ccc; font-style: italic; margin: 6px auto; max-width: 90%; }
.voice-label { font-size: 20px; color: #888; }
.more-toggle { text-align: center; direction: rtl; margin-top: 8px; }
.more-header {
display: inline-block; font-size: 18px; color: #555; cursor: pointer; list-style: none;
border: 1px solid #ccc; border-radius: 16px; padding: 4px 16px; margin: 4px 0; background: #f8f8f8;
}
.more-header::-webkit-details-marker { display: none; }
.more-header::before { content: "○ "; font-size: 14px; }
details[open] > .more-header::before { content: "● "; }
.related-header { font-size: 22px; color: #555; text-align: center; margin: 4px 0; }
.rw-word { display: table-cell; font-size: 28px; color: #222; font-weight: normal; text-align: right; padding: 2px 0 2px 8px; white-space: nowrap; }
.rw-meaning { display: table-cell; font-size: 24px; color: #555; text-align: left; direction: ltr; padding: 2px 0; }
</style>
</head>
<body>
<h2 style="font-family:sans-serif;direction:ltr;">Vocab: English → Hebrew (BACK) — collapsed</h2>
<div class="card-container">
<div class="card-label">English → Hebrew — Back (default: collapsed)</div>
<div class="card-content">
<div class="meaning">time (occasion), time round; once (when used as an adverb)</div>
<div class="emoji-img">📍</div>
<div class="divider"></div>
<div class="hebrew">פַּעַם</div>
<details class="more-toggle"><summary class="more-header">מידע נוסף</summary>
<div class="sec-table">
<div class="sec-label"><span class="sec-key">לְלֹא נִיקּוּד:</span><span class="sec-val">פעם</span></div>
<div class="sec-label"><span class="sec-key">שֹׁרֶשׁ:</span><span class="sec-val">פ.ע.ם</span></div>
<div class="sec-label"><span class="sec-key">חֵלֶק דִּיבּוּר:</span><span class="sec-val">שֵׁם עֶצֶם, נְקֵבָה</span></div>
<div class="sec-label"><span class="sec-key">רַבִּים:</span><span class="sec-val">פְּעָמִים</span></div>
</div>
<div class="divider" style="margin:6px 0;"></div>
<div class="related-header">מִילִים קְשׁוּרוֹת</div>
<div class="sec-table">
<div class="sec-label"><span class="rw-word">פַּעְמַיִם</span><span class="rw-meaning">twice, two times</span></div>
<div class="sec-label"><span class="rw-word">לְפַעֵם</span><span class="rw-meaning">to surge (feeling, emotion)</span></div>
<div class="sec-label"><span class="rw-word">פַּעֲמוֹן</span><span class="rw-meaning">bell</span></div>
<div class="sec-label"><span class="rw-word">פְּעִימָה</span><span class="rw-meaning">heartbeat; beat; stroke (technolo…</span></div>
<div class="sec-label"><span class="rw-word">לִפְעֹם</span><span class="rw-meaning">to beat, to pulse, to throb</span></div>
<div class="sec-label"><span class="rw-word">לְהִתְפַּעֵם</span><span class="rw-meaning">to be excited (emotionally)</span></div>
<div class="sec-label"><span class="rw-word">לְהַפְעִים</span><span class="rw-meaning">to excite, to agitate (lit.)</span></div>
<div class="sec-label"><span class="rw-word">לְהִפָּעֵם</span><span class="rw-meaning">to be excited, to be thrilled</span></div>
</div>
</details>
</div>
</div>
<h2 style="font-family:sans-serif;direction:ltr;">Same card — EXPANDED</h2>
<div class="card-container">
<div class="card-label">English → Hebrew — Back (expanded)</div>
<div class="card-content">
<div class="meaning">time (occasion), time round; once (when used as an adverb)</div>
<div class="emoji-img">📍</div>
<div class="divider"></div>
<div class="hebrew">פַּעַם</div>
<details class="more-toggle" open><summary class="more-header">מידע נוסף</summary>
<div class="sec-table">
<div class="sec-label"><span class="sec-key">לְלֹא נִיקּוּד:</span><span class="sec-val">פעם</span></div>
<div class="sec-label"><span class="sec-key">שֹׁרֶשׁ:</span><span class="sec-val">פ.ע.ם</span></div>
<div class="sec-label"><span class="sec-key">חֵלֶק דִּיבּוּר:</span><span class="sec-val">שֵׁם עֶצֶם, נְקֵבָה</span></div>
<div class="sec-label"><span class="sec-key">רַבִּים:</span><span class="sec-val">פְּעָמִים</span></div>
</div>
<div class="divider" style="margin:6px 0;"></div>
<div class="related-header">מִילִים קְשׁוּרוֹת</div>
<div class="sec-table">
<div class="sec-label"><span class="rw-word">פַּעְמַיִם</span><span class="rw-meaning">twice, two times</span></div>
<div class="sec-label"><span class="rw-word">לְפַעֵם</span><span class="rw-meaning">to surge (feeling, emotion)</span></div>
<div class="sec-label"><span class="rw-word">פַּעֲמוֹן</span><span class="rw-meaning">bell</span></div>
<div class="sec-label"><span class="rw-word">פְּעִימָה</span><span class="rw-meaning">heartbeat; beat; stroke (technolo…</span></div>
<div class="sec-label"><span class="rw-word">לִפְעֹם</span><span class="rw-meaning">to beat, to pulse, to throb</span></div>
<div class="sec-label"><span class="rw-word">לְהִתְפַּעֵם</span><span class="rw-meaning">to be excited (emotionally)</span></div>
<div class="sec-label"><span class="rw-word">לְהַפְעִים</span><span class="rw-meaning">to excite, to agitate (lit.)</span></div>
<div class="sec-label"><span class="rw-word">לְהִפָּעֵם</span><span class="rw-meaning">to be excited, to be thrilled</span></div>
</div>
</details>
</div>
</div>
</body>
</html>

114
card_preview_conj.html Normal file
View file

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html dir="rtl">
<head>
<meta charset="utf-8">
<style>
body { font-family: 'Heebo', 'Arial Hebrew', sans-serif; background: #fff; max-width: 600px; margin: 20px auto; }
.card-container { border: 1px solid #ccc; border-radius: 8px; margin: 20px 0; overflow: hidden; }
.card-label { background: #333; color: #fff; padding: 6px 12px; font-size: 14px; font-family: sans-serif; direction: ltr; }
.card-content { padding: 16px; text-align: center; }
.card-content hr { border: none; border-top: 1px solid #ccc; margin: 12px 0; }
.hebrew { font-size: 48px; font-weight: bold; color: #222; direction: rtl; text-align: center; }
.hebrew-sm { font-size: 28px; font-weight: normal; color: #222; direction: rtl; }
.meaning { font-size: 28px; color: #1a1a8c; text-align: center; direction: ltr; margin: 4px 0; }
.hint { font-size: 22px; color: #555; margin: 4px 0; direction: rtl; text-align: center; }
.divider { border-top: 1px solid #ccc; margin: 8px 0; }
.sec-table { display: table; margin: 6px auto 0; direction: rtl; border-collapse: collapse; }
.sec-label { display: table-row; font-size: 28px; font-weight: normal; color: #222; direction: rtl; }
.sec-key { display: table-cell; font-size: 28px; color: #222; font-weight: bold; text-align: right; padding: 2px 0 2px 8px; white-space: nowrap; }
.sec-val { display: table-cell; font-size: 28px; color: #222; text-align: right; padding: 2px 0; }
.voice-label { font-size: 20px; color: #888; }
.more-toggle { text-align: center; direction: rtl; margin-top: 8px; }
.more-header {
display: inline-block; font-size: 18px; color: #555; cursor: pointer; list-style: none;
border: 1px solid #ccc; border-radius: 16px; padding: 4px 16px; margin: 4px 0; background: #f8f8f8;
}
.more-header::-webkit-details-marker { display: none; }
.more-header::before { content: "○ "; font-size: 14px; }
details[open] > .more-header::before { content: "● "; }
.related-header { font-size: 22px; color: #555; text-align: center; margin: 4px 0; }
.rw-word { display: table-cell; font-size: 28px; color: #222; font-weight: normal; text-align: right; padding: 2px 0 2px 8px; white-space: nowrap; }
.rw-meaning { display: table-cell; font-size: 24px; color: #555; text-align: left; direction: ltr; padding: 2px 0; }
</style>
</head>
<body>
<h2 style="font-family:sans-serif;direction:ltr;">Conjugation Card — FRONT</h2>
<div class="card-container">
<div class="card-label">Front</div>
<div class="card-content">
<div class="hint">אֵיךְ אוֹמְרִים</div>
<div class="hebrew">אַתָּה</div>
<div class="hebrew" style="color:#1a1a8c;">לִשְׁמֹר <span class="hebrew-sm">(על)</span></div>
<div class="hebrew">בַּהוֹוֶה</div>
</div>
</div>
<h2 style="font-family:sans-serif;direction:ltr;">Conjugation Card — BACK (collapsed)</h2>
<div class="card-container">
<div class="card-label">Back — default state</div>
<div class="card-content">
<div class="hint">אֵיךְ אוֹמְרִים</div>
<div class="hebrew">אַתָּה</div>
<div class="hebrew" style="color:#1a1a8c;">לִשְׁמֹר <span class="hebrew-sm">(על)</span></div>
<div class="hebrew">בַּהוֹוֶה</div>
<hr>
<div class="hebrew">שׁוֹמֵר (על)</div>
<details class="more-toggle"><summary class="more-header">מידע נוסף</summary>
<div class="sec-label" style="text-align:center;display:block;">to guard; to keep, to maintain</div>
<div class="sec-table">
<div class="sec-label"><span class="sec-key">שֹׁרֶשׁ:</span><span class="sec-val">שׁ.מ.ר</span></div>
<div class="sec-label"><span class="sec-key">בִּנְיָן:</span><span class="sec-val">פָּעַל</span></div>
</div>
<div class="divider" style="margin:6px 0;"></div>
<div class="related-header">מִילִים קְשׁוּרוֹת</div>
<div class="sec-table">
<div class="sec-label"><span class="rw-word">מִשְׁמָר</span><span class="rw-meaning">guard, watch; shift</span></div>
<div class="sec-label"><span class="rw-word">שׁוֹמֵר</span><span class="rw-meaning">guard, watchman</span></div>
<div class="sec-label"><span class="rw-word">שְׁמִירָה</span><span class="rw-meaning">guarding, watching</span></div>
<div class="sec-label"><span class="rw-word">לְהִשָּׁמֵר</span><span class="rw-meaning">to beware, to be careful</span></div>
</div>
</details>
</div>
</div>
<h2 style="font-family:sans-serif;direction:ltr;">Conjugation Card — BACK (expanded)</h2>
<div class="card-container">
<div class="card-label">Back — expanded</div>
<div class="card-content">
<div class="hint">אֵיךְ אוֹמְרִים</div>
<div class="hebrew">אַתָּה</div>
<div class="hebrew" style="color:#1a1a8c;">לִשְׁמֹר <span class="hebrew-sm">(על)</span></div>
<div class="hebrew">בַּהוֹוֶה</div>
<hr>
<div class="hebrew">שׁוֹמֵר (על)</div>
<details class="more-toggle" open><summary class="more-header">מידע נוסף</summary>
<div class="sec-label" style="text-align:center;display:block;">to guard; to keep, to maintain</div>
<div class="sec-table">
<div class="sec-label"><span class="sec-key">שֹׁרֶשׁ:</span><span class="sec-val">שׁ.מ.ר</span></div>
<div class="sec-label"><span class="sec-key">בִּנְיָן:</span><span class="sec-val">פָּעַל</span></div>
</div>
<div class="divider" style="margin:6px 0;"></div>
<div class="related-header">מִילִים קְשׁוּרוֹת</div>
<div class="sec-table">
<div class="sec-label"><span class="rw-word">מִשְׁמָר</span><span class="rw-meaning">guard, watch; shift</span></div>
<div class="sec-label"><span class="rw-word">שׁוֹמֵר</span><span class="rw-meaning">guard, watchman</span></div>
<div class="sec-label"><span class="rw-word">שְׁמִירָה</span><span class="rw-meaning">guarding, watching</span></div>
<div class="sec-label"><span class="rw-word">לְהִשָּׁמֵר</span><span class="rw-meaning">to beware, to be careful</span></div>
</div>
</details>
</div>
</div>
</body>
</html>

View file

@ -484,3 +484,41 @@ class TestScrapePrepositionDetail:
def test_empty_on_no_table(self) -> None:
result = _scrape_preposition_detail("missing", "<html><body></body></html>", "<html><body></body></html>")
assert result == {}
# ---------------------------------------------------------------------------
# Tests for _parse_noun_gender_mishkal mishkal extraction
# ---------------------------------------------------------------------------
from bs4 import BeautifulSoup # noqa: E402
from pealim_detail_scrape import _parse_noun_gender_mishkal # noqa: E402
class TestNounGenderMishkal:
def test_noun_with_mishkal(self):
html = '<p>Noun <a href="/dict/?pos=noun&amp;nm=qetel"><i>ketel</i> pattern</a>, masculine</p>'
soup = BeautifulSoup(html, "html.parser")
gender, mishkal = _parse_noun_gender_mishkal(soup)
assert gender == "masculine"
assert mishkal == "ketel"
def test_noun_without_mishkal(self):
html = "<p>Noun masculine</p>"
soup = BeautifulSoup(html, "html.parser")
gender, mishkal = _parse_noun_gender_mishkal(soup)
assert gender == "masculine"
assert mishkal == ""
def test_adjective_mishkal(self):
html = '<p>Adjective <a href="/dict/?pos=adjective&amp;am=qatul"><i>katul</i> pattern</a></p>'
soup = BeautifulSoup(html, "html.parser")
_, mishkal = _parse_noun_gender_mishkal(soup)
assert mishkal == "katul"
def test_feminine_noun(self):
html = '<p>Noun <a href="/dict/?pos=noun&amp;nm=qetel"><i>ketel</i> pattern</a>, feminine</p>'
soup = BeautifulSoup(html, "html.parser")
gender, mishkal = _parse_noun_gender_mishkal(soup)
assert gender == "feminine"
assert mishkal == "ketel"