diff --git a/src/components/GrammarPanel.jsx b/src/components/GrammarPanel.jsx
new file mode 100644
index 0000000..d877652
--- /dev/null
+++ b/src/components/GrammarPanel.jsx
@@ -0,0 +1,19 @@
+// What an original-language word's grammar label means, in plain English.
+import { grammarNotes } from '../utils/grammar.js';
+
+export default function GrammarPanel({ label, className = '' }) {
+ const notes = grammarNotes(label);
+ if (!label) return null;
+ return (
+
+
Grammar · {label}
+ {notes.length > 0 && (
+
+ {notes.map((n) => (
+
{n.term}
{n.meaning}
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/study/ScripturePane.jsx b/src/components/study/ScripturePane.jsx
index a5af161..c453382 100644
--- a/src/components/study/ScripturePane.jsx
+++ b/src/components/study/ScripturePane.jsx
@@ -2,10 +2,12 @@
// interlinear words; click an original word to add it to the chunk's word study.
import { useState } from 'react';
import { NT_BOOK_NUMBER } from '../../utils/bibleUtils.jsx';
+import GrammarPanel from '../GrammarPanel.jsx';
export default function ScripturePane({ verses, chapter, interlinear, interlinearLoading, addedStrongs, onPickWord, verseNotes = {}, onVerseNote }) {
const [openVerse, setOpenVerse] = useState(null);
const [editingNote, setEditingNote] = useState(null);
+ const [inspect, setInspect] = useState(null); // the original word whose grammar is showing: { key, index }
const isHebrew = chapter && !NT_BOOK_NUMBER[chapter.bookAbbrev];
return (
@@ -38,6 +40,7 @@ export default function ScripturePane({ verses, chapter, interlinear, interlinea
const added = w.s && addedStrongs.has(w.s.toUpperCase());
return (
);
})}
+ {inspect?.key === `${v.chapter}:${v.number}` && words[inspect.index]?.p && (
+
+ )}
)}
diff --git a/src/components/study/WordStudyTab.jsx b/src/components/study/WordStudyTab.jsx
index 0c5525a..ff669fb 100644
--- a/src/components/study/WordStudyTab.jsx
+++ b/src/components/study/WordStudyTab.jsx
@@ -6,6 +6,7 @@ import { externalLookupLinks, speakOriginalWord } from '../../utils/wordStudy.js
import { useIqAvailable } from '../../hooks/useIq.js';
import { normalizeStrongs } from '../../utils/iqbible.js';
import IqLexicon from '../iq/IqLexicon.jsx';
+import GrammarPanel from '../GrammarPanel.jsx';
const inputCls = 'mt-1 block w-full rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-2.5 py-1.5 text-sm text-[var(--r-text)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
const btn = 'rounded-lg border border-[var(--r-line)] px-2.5 py-1.5 text-xs hover:bg-black/5 disabled:opacity-50';
@@ -50,6 +51,7 @@ function WordCard({ word, open, onToggle, chunkId, wordStudy }) {
{field('Part of speech', 'partOfSpeech', 'e.g. Noun')}
{field('English word', 'englishGloss', 'e.g. grace')}
+ {word.parsing && }
{field('Short definition', 'shortDefinition')}
{word.definitionHtml && (
diff --git a/src/hooks/useWordStudy.js b/src/hooks/useWordStudy.js
index 8e3c050..c72fd4a 100644
--- a/src/hooks/useWordStudy.js
+++ b/src/hooks/useWordStudy.js
@@ -94,6 +94,7 @@ export function useWordStudy({ project, allChunks, updateProject, notify }) {
lexeme: w.o || entry?.lemma || '',
transliteration: w.t || entry?.translit || entry?.xlit || '',
partOfSpeech: '',
+ parsing: w.p || '', // the grammar label from the interlinear, explained in the word's card
shortDefinition: entry?.kjv_def || w.g || '',
definitionHtml: entry ? buildGreekDefinitionHtml(key, entry) : '',
loading: false,
diff --git a/src/utils/grammar.js b/src/utils/grammar.js
new file mode 100644
index 0000000..cedd6a8
--- /dev/null
+++ b/src/utils/grammar.js
@@ -0,0 +1,111 @@
+// Plain-English meanings for the grammar labels on the original-language words ("Verb - Aorist Indicative Active -
+// 3rd Person Singular"). The labels come with the interlinear text; this only explains them, so a reader who never
+// studied Greek or Hebrew can see what "aorist" or "Hiphil" tells them. Meanings are the usual textbook ones and
+// are a guide, not a rule: the context decides.
+
+// Hebrew verbs name a "stem" (how the action is done); when one is present, Perfect, Imperfect and the rest
+// mean the Hebrew thing, not the Greek one.
+const HEBREW_STEMS = [
+ ['qal', 'The basic, simple action ("he kept").'],
+ ['niphal', 'Often passive or reflexive ("it was kept", "he kept himself").'],
+ ['piel', 'Intensive or repeated action ("he kept carefully").'],
+ ['pual', 'The passive of the Piel ("it was kept carefully").'],
+ ['hithpael', 'Reflexive or repeated action ("he kept himself busy").'],
+ ['hiphil', 'Causing the action ("he made someone keep").'],
+ ['hophal', 'The passive of the Hiphil ("he was made to keep").'],
+];
+const HEBREW_FORMS = [
+ ['perfect', 'Action seen as complete, often past ("he kept").'],
+ ['imperfect', 'Action seen as incomplete: ongoing, repeated or still to come ("he keeps", "he will keep").'],
+ ['imperative', 'A command ("keep!").'],
+ ['infinitive construct', 'The verb as a noun, linked to what follows ("keeping of…").'],
+ ['infinitive absolute', 'The verb as a noun on its own, often to stress the action ("keeping, keep").'],
+ ['participle', 'A verb used like an adjective or noun ("keeping", "the one who keeps").'],
+];
+const GREEK_TENSES = [
+ ['present', 'Ongoing or repeated action ("is doing").'],
+ ['imperfect', 'Past action that was ongoing or repeated ("was doing").'],
+ ['future', 'Action that will happen ("will do").'],
+ ['aorist', 'Action seen as a whole, with no focus on how long it took. Often just "did".'],
+ ['perfect', 'A finished action whose results still stand ("has done").'],
+ ['pluperfect', 'A finished action whose results stood at some past time ("had done").'],
+];
+const MOODS = [
+ ['indicative', 'States something as a fact or asks a question.'],
+ ['subjunctive', 'Possibility, purpose or doubt ("may", "might", "let us").'],
+ ['imperative', 'A command or request.'],
+ ['optative', 'A wish or a remote possibility ("may it be").'],
+ ['infinitive', 'The verb as a noun ("to do").'],
+ ['participle', 'A verb used like an adjective ("-ing", "the one who…").'],
+];
+const VOICES = [
+ ['middle or passive', 'The form can mean the subject acts on itself, or receives the action; context decides.'],
+ ['middle', 'The subject acts on itself or for its own benefit.'],
+ ['passive', 'The subject receives the action ("was done").'],
+ ['active', 'The subject does the action.'],
+];
+const CASES = [
+ ['nominative', 'The subject of the sentence, or a name for something.'],
+ ['genitive', 'Belonging, source or description. Often "of".'],
+ ['dative', 'The one the action is to, for or by. Often "to", "for", "in", "with".'],
+ ['accusative', 'The direct object, the thing acted on.'],
+ ['vocative', 'The one being spoken to.'],
+];
+const PARTS = [
+ ['noun', 'A person, place or thing.'],
+ ['verb', 'An action or state.'],
+ ['adjective', 'Describes a noun.'],
+ ['adverb', 'Describes a verb, adjective or another adverb.'],
+ ['article', 'The word "the".'],
+ ['conjunction', 'Joins words or clauses ("and", "but", "for").'],
+ ['preposition', 'Shows how a noun relates to something else ("in", "to", "from").'],
+ ['personal pronoun', 'Stands in for a person ("he", "you").'],
+ ['relative pronoun', 'Links a clause back to a noun ("who", "which").'],
+ ['demonstrative pronoun', 'Points something out ("this", "that").'],
+ ['reflexive pronoun', 'Points back to the subject ("himself").'],
+ ['interrogative pronoun', 'Asks a question ("who?", "what?").'],
+ ['pronoun', 'Stands in for a noun.'],
+ ['particle', 'A small word that adds emphasis or links ideas.'],
+ ['interjection', 'An exclamation.'],
+ ['numeral', 'A number.'],
+];
+const PREFIXES = [
+ ['conjunctive waw', '"And" (or "but", "then") joined to the front of the next word.'],
+ ['preposition-b', '"In", "with" or "by", joined to the front of the next word.'],
+ ['preposition-l', '"To" or "for", joined to the front of the next word.'],
+ ['preposition-k', '"Like" or "as", joined to the front of the next word.'],
+ ['preposition-m', '"From", joined to the front of the next word.'],
+ ['construct', 'Linked to the next word, usually "of".'],
+];
+const PERSONS = { '1st': 'I / we', first: 'I / we', '2nd': 'you', second: 'you', '3rd': 'he / she / it / they', third: 'he / she / it / they' };
+
+const NUMBER_WORDS = { singular: 'one', plural: 'more than one', dual: 'two' };
+
+// -> [{ term: 'Aorist', meaning: '…' }] in the order the terms appear in the label; [] when there is nothing to say.
+export function grammarNotes(label) {
+ const text = String(label ?? '').toLowerCase();
+ if (!text.trim()) return [];
+ const isHebrewVerb = HEBREW_STEMS.some(([t]) => new RegExp(`\\b${t}\\b`).test(text));
+ const found = [];
+ const add = (table, tableName) => {
+ for (const [term, meaning] of table) {
+ const m = new RegExp(`(?:^|[^a-z-])${term.replace(/[-]/g, '[- ]?')}(?![a-z])`).exec(text);
+ if (m) found.push({ at: m.index, term, meaning, tableName });
+ }
+ };
+ add(PARTS, 'part');
+ if (isHebrewVerb) { add(HEBREW_STEMS, 'stem'); add(HEBREW_FORMS, 'form'); } else { add(GREEK_TENSES, 'tense'); }
+ add(MOODS.filter(([t]) => !isHebrewVerb || (t !== 'indicative' && t !== 'subjunctive' && t !== 'optative' && !['imperative', 'infinitive', 'participle'].includes(t))), 'mood');
+ if (!isHebrewVerb) add(VOICES, 'voice'); else add([['active', VOICES[3][1]], ['passive', VOICES[2][1]]], 'voice');
+ add(CASES, 'case');
+ add(PREFIXES, 'prefix');
+ // the same label can match a short term inside a longer one ("middle" inside "middle or passive", "pronoun" inside "personal pronoun")
+ const covered = (a, b) => b !== a && b.term.length > a.term.length && b.term.replace(/-/g, ' ').includes(a.term.replace(/-/g, ' ')) && a.at >= b.at - 1 && a.at < b.at + b.term.length + 1;
+ const kept = found.filter((a) => !found.some((b) => covered(a, b) && (b.tableName === a.tableName || b.tableName === 'prefix')));
+ const person = text.match(/\b(1st|2nd|3rd|first|second|third)\s+person\b/);
+ const number = text.match(/\b(singular|plural|dual)\b/);
+ const first = (t) => t.charAt(0).toUpperCase() + t.slice(1);
+ const notes = kept.sort((a, b) => a.at - b.at).map(({ term, meaning }) => ({ term: first(term), meaning }));
+ if (person) notes.push({ term: `${person[1]} person${number ? ` ${number[1]}` : ''}`.replace(/^\w/, (c) => c.toUpperCase()), meaning: `${PERSONS[person[1]]}${number ? ` (${NUMBER_WORDS[number[1]]})` : ''}.` });
+ return notes;
+}
diff --git a/src/utils/grammar.test.js b/src/utils/grammar.test.js
new file mode 100644
index 0000000..e1448bf
--- /dev/null
+++ b/src/utils/grammar.test.js
@@ -0,0 +1,41 @@
+import { test, expect } from 'vitest';
+import { grammarNotes } from './grammar.js';
+
+const terms = (label) => grammarNotes(label).map((n) => n.term);
+
+test('a Greek verb is explained part by part', () => {
+ const notes = grammarNotes('Verb - Aorist Indicative Active - 3rd Person Singular');
+ expect(notes.map((n) => n.term)).toEqual(['Verb', 'Aorist', 'Indicative', 'Active', '3rd person singular']);
+ expect(notes.find((n) => n.term === 'Aorist').meaning).toMatch(/as a whole/);
+ expect(notes.at(-1).meaning).toBe('he / she / it / they (one).');
+});
+
+test('nouns and articles give their case', () => {
+ expect(terms('Noun - Nominative Masculine Singular')).toEqual(['Noun', 'Nominative']);
+ expect(terms('Article - Genitive Neuter Plural')).toEqual(['Article', 'Genitive']);
+ expect(terms('Personal / Relative Pronoun - Accusative 1st Person Singular')).toContain('Accusative');
+});
+
+test('moods, participles and voices that share words are not double counted', () => {
+ expect(terms('Verb - Present Participle Middle or Passive - Nominative Masculine Singular')).toEqual(['Verb', 'Present', 'Participle', 'Middle or passive', 'Nominative']);
+ expect(terms('Verb - Perfect Infinitive Active')).toEqual(['Verb', 'Perfect', 'Infinitive', 'Active']);
+});
+
+test('a Hebrew verb is read with Hebrew meanings, not Greek ones', () => {
+ const notes = grammarNotes('Verb - Qal - Perfect - third person masculine singular');
+ expect(notes.map((n) => n.term)).toEqual(['Verb', 'Qal', 'Perfect', 'Third person singular']);
+ expect(notes.find((n) => n.term === 'Perfect').meaning).toMatch(/complete/);
+ expect(grammarNotes('Verb - Hiphil - Imperfect - third person masculine singular').find((n) => n.term === 'Hiphil').meaning).toMatch(/Causing/);
+});
+
+test('Hebrew words with attached prefixes explain the prefix', () => {
+ const notes = grammarNotes('Preposition-b | Noun - feminine singular');
+ expect(notes.map((n) => n.term)).toEqual(expect.arrayContaining(['Noun', 'Preposition-b']));
+ expect(notes.find((n) => n.term === 'Preposition-b').meaning).toMatch(/joined to the front/);
+});
+
+test('nothing to explain gives an empty list', () => {
+ expect(grammarNotes('')).toEqual([]);
+ expect(grammarNotes(null)).toEqual([]);
+ expect(grammarNotes('???')).toEqual([]);
+});