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:
parent
c85063ee2f
commit
0d92451271
4 changed files with 363 additions and 40 deletions
141
apkg_builder.py
141
apkg_builder.py
|
|
@ -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
110
card_preview.html
Normal 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
114
card_preview_conj.html
Normal 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>
|
||||
|
|
@ -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&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&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&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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue