mirror of
https://github.com/spenhos/obsidian-torah-verse-inserter.git
synced 2026-07-22 07:43:27 +00:00
Initial scaffold: MAM Tanakh corpus (bundled, lazy gzip), reference parser, transliteration search, insert modal, settings
This commit is contained in:
commit
cbd2cd8120
18 changed files with 5972 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
main.js
|
||||
src/data/
|
||||
.DS_Store
|
||||
data.json
|
||||
39
esbuild.config.mjs
Normal file
39
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import { builtinModules } from "node:module";
|
||||
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
esbuild.build({
|
||||
banner: { js: banner },
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtinModules,
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2020",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
}).catch(() => process.exit(1));
|
||||
17
eslint.config.mjs
Normal file
17
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import tsparser from "@typescript-eslint/parser";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default defineConfig([
|
||||
...obsidianmd.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: { project: "./tsconfig.json" },
|
||||
},
|
||||
},
|
||||
{ ignores: ["main.js", "esbuild.config.mjs", "eslint.config.mjs", "node_modules/**"] },
|
||||
]);
|
||||
14
manifest.json
Normal file
14
manifest.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"id": "pasuk",
|
||||
"name": "Pasuk — Tanakh Verses",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.8.7",
|
||||
"description": "Insert any Tanakh verse into your notes — search by reference or transliteration, full MAM text with nikud and te'amim bundled offline, optional translations from Sefaria.",
|
||||
"author": "Saleh Penhos",
|
||||
"authorUrl": "https://github.com/spenhos",
|
||||
"fundingUrl": {
|
||||
"Ko-fi": "https://ko-fi.com/elevalma",
|
||||
"GitHub Sponsors": "https://github.com/sponsors/spenhos"
|
||||
},
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
4953
package-lock.json
generated
Normal file
4953
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
24
package.json
Normal file
24
package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "obsidian-pasuk",
|
||||
"version": "0.1.0",
|
||||
"description": "Obsidian plugin to insert Tanakh verses (MAM text, offline) with optional translations",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"build-corpus": "node tools/build-corpus.mjs",
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"keywords": ["obsidian", "tanakh", "torah", "hebrew", "pasuk", "bible"],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"esbuild": "0.17.3",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "^5.6.0",
|
||||
"typescript-eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
85
src/books.ts
Normal file
85
src/books.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Metadata de los 39 libros del Tanaj (orden tradicional).
|
||||
// `key` debe coincidir con los nombres de archivo del corpus (codigos-torah).
|
||||
|
||||
export interface BookInfo {
|
||||
key: string; // corpus key
|
||||
en: string;
|
||||
es: string;
|
||||
he: string;
|
||||
translit: string; // nombre hebreo romanizado
|
||||
aliases: string[]; // abreviaturas y variantes (sin acentos, lowercase)
|
||||
}
|
||||
|
||||
export const BOOKS: BookInfo[] = [
|
||||
// --- Torá ---
|
||||
{ key: "Genesis", en: "Genesis", es: "Génesis", he: "בראשית", translit: "Bereshit", aliases: ["gen", "gn", "ber", "bereshit", "genesis"] },
|
||||
{ key: "Exodus", en: "Exodus", es: "Éxodo", he: "שמות", translit: "Shemot", aliases: ["ex", "exo", "shem", "shemot", "exodo"] },
|
||||
{ key: "Leviticus", en: "Leviticus", es: "Levítico", he: "ויקרא", translit: "Vayikra", aliases: ["lev", "lv", "vay", "vayikra", "levitico"] },
|
||||
{ key: "Numbers", en: "Numbers", es: "Números", he: "במדבר", translit: "Bemidbar", aliases: ["num", "nm", "bam", "bem", "bamidbar", "bemidbar", "numeros"] },
|
||||
{ key: "Deuteronomy", en: "Deuteronomy", es: "Deuteronomio", he: "דברים", translit: "Devarim", aliases: ["deut", "dt", "dev", "devarim", "deuteronomio"] },
|
||||
// --- Neviim ---
|
||||
{ key: "Joshua", en: "Joshua", es: "Josué", he: "יהושע", translit: "Yehoshúa", aliases: ["jos", "josh", "yeho", "yehoshua", "josue"] },
|
||||
{ key: "Judges", en: "Judges", es: "Jueces", he: "שופטים", translit: "Shoftim", aliases: ["jue", "jud", "shof", "shoftim", "jueces"] },
|
||||
{ key: "ISamuel", en: "I Samuel", es: "1 Samuel", he: "שמואל א", translit: "Shmuel Alef", aliases: ["1sam", "1sa", "1samuel", "isamuel", "shmuel1", "shmuela"] },
|
||||
{ key: "IISamuel", en: "II Samuel", es: "2 Samuel", he: "שמואל ב", translit: "Shmuel Bet", aliases: ["2sam", "2sa", "2samuel", "iisamuel", "shmuel2", "shmuelb"] },
|
||||
{ key: "IKings", en: "I Kings", es: "1 Reyes", he: "מלכים א", translit: "Melajim Alef", aliases: ["1re", "1rey", "1reyes", "1ki", "1kings", "ikings", "melajim1", "melachim1"] },
|
||||
{ key: "IIKings", en: "II Kings", es: "2 Reyes", he: "מלכים ב", translit: "Melajim Bet", aliases: ["2re", "2rey", "2reyes", "2ki", "2kings", "iikings", "melajim2", "melachim2"] },
|
||||
{ key: "Isaiah", en: "Isaiah", es: "Isaías", he: "ישעיהו", translit: "Yeshaiahu", aliases: ["isa", "is", "yesh", "yeshaiahu", "isaias"] },
|
||||
{ key: "Jeremiah", en: "Jeremiah", es: "Jeremías", he: "ירמיהו", translit: "Yirmiyahu", aliases: ["jer", "yirm", "yirmiyahu", "jeremias"] },
|
||||
{ key: "Ezekiel", en: "Ezekiel", es: "Ezequiel", he: "יחזקאל", translit: "Yejezkel", aliases: ["eze", "ez", "yejez", "yechezkel", "yejezkel", "ezequiel"] },
|
||||
{ key: "Hosea", en: "Hosea", es: "Oseas", he: "הושע", translit: "Hoshea", aliases: ["os", "hos", "hoshea", "oseas"] },
|
||||
{ key: "Joel", en: "Joel", es: "Joel", he: "יואל", translit: "Yoel", aliases: ["joe", "yoel", "joel"] },
|
||||
{ key: "Amos", en: "Amos", es: "Amós", he: "עמוס", translit: "Amós", aliases: ["am", "amos"] },
|
||||
{ key: "Obadiah", en: "Obadiah", es: "Abdías", he: "עובדיה", translit: "Ovadiá", aliases: ["abd", "oba", "ovadia", "abdias", "obadiah"] },
|
||||
{ key: "Jonah", en: "Jonah", es: "Jonás", he: "יונה", translit: "Yoná", aliases: ["jon", "yona", "jonas", "jonah"] },
|
||||
{ key: "Micah", en: "Micah", es: "Miqueas", he: "מיכה", translit: "Mijá", aliases: ["miq", "mic", "mija", "micha", "miqueas", "micah"] },
|
||||
{ key: "Nahum", en: "Nahum", es: "Nahúm", he: "נחום", translit: "Najum", aliases: ["nah", "najum", "nahum"] },
|
||||
{ key: "Habakkuk", en: "Habakkuk", es: "Habacuc", he: "חבקוק", translit: "Javakuk", aliases: ["hab", "javakuk", "habacuc", "habakkuk"] },
|
||||
{ key: "Zephaniah", en: "Zephaniah", es: "Sofonías", he: "צפניה", translit: "Tzefaniá", aliases: ["sof", "zep", "tzefania", "sofonias", "zephaniah"] },
|
||||
{ key: "Haggai", en: "Haggai", es: "Ageo", he: "חגי", translit: "Jagai", aliases: ["age", "hag", "jagai", "ageo", "haggai"] },
|
||||
{ key: "Zechariah", en: "Zechariah", es: "Zacarías", he: "זכריה", translit: "Zejariá", aliases: ["zac", "zec", "zejaria", "zacarias", "zechariah"] },
|
||||
{ key: "Malachi", en: "Malachi", es: "Malaquías", he: "מלאכי", translit: "Malají", aliases: ["mal", "malaji", "malaquias", "malachi"] },
|
||||
// --- Ketuvim ---
|
||||
{ key: "Psalms", en: "Psalms", es: "Salmos", he: "תהלים", translit: "Tehilim", aliases: ["sal", "ps", "psa", "teh", "tehilim", "salmos", "psalms", "salmo"] },
|
||||
{ key: "Proverbs", en: "Proverbs", es: "Proverbios", he: "משלי", translit: "Mishlei", aliases: ["pro", "prov", "mish", "mishlei", "proverbios"] },
|
||||
{ key: "Job", en: "Job", es: "Job", he: "איוב", translit: "Iyov", aliases: ["job", "iyov", "iov"] },
|
||||
{ key: "SongOfSongs", en: "Song of Songs", es: "Cantar de los Cantares", he: "שיר השירים", translit: "Shir HaShirim", aliases: ["cant", "shir", "shirhashirim", "cantares", "songofsongs", "song"] },
|
||||
{ key: "Ruth", en: "Ruth", es: "Rut", he: "רות", translit: "Rut", aliases: ["rut", "ruth"] },
|
||||
{ key: "Lamentations", en: "Lamentations", es: "Lamentaciones", he: "איכה", translit: "Eijá", aliases: ["lam", "eija", "eicha", "lamentaciones", "lamentations"] },
|
||||
{ key: "Ecclesiastes", en: "Ecclesiastes", es: "Eclesiastés", he: "קהלת", translit: "Kohélet", aliases: ["ecl", "ecc", "kohelet", "qohelet", "eclesiastes", "ecclesiastes"] },
|
||||
{ key: "Esther", en: "Esther", es: "Ester", he: "אסתר", translit: "Ester", aliases: ["est", "ester", "esther"] },
|
||||
{ key: "Daniel", en: "Daniel", es: "Daniel", he: "דניאל", translit: "Daniel", aliases: ["dan", "daniel"] },
|
||||
{ key: "Ezra", en: "Ezra", es: "Esdras", he: "עזרא", translit: "Ezrá", aliases: ["esd", "ezr", "ezra", "esdras"] },
|
||||
{ key: "Nehemiah", en: "Nehemiah", es: "Nehemías", he: "נחמיה", translit: "Nejemiá", aliases: ["neh", "nejemia", "nehemias", "nehemiah"] },
|
||||
{ key: "IChronicles", en: "I Chronicles", es: "1 Crónicas", he: "דברי הימים א", translit: "Divrei HaYamim Alef", aliases: ["1cr", "1cro", "1cron", "1chronicles", "ichronicles", "divrei1"] },
|
||||
{ key: "IIChronicles", en: "II Chronicles", es: "2 Crónicas", he: "דברי הימים ב", translit: "Divrei HaYamim Bet", aliases: ["2cr", "2cro", "2cron", "2chronicles", "iichronicles", "divrei2"] },
|
||||
];
|
||||
|
||||
/** Normaliza para matching: minúsculas, sin acentos latinos, sin espacios/puntos. */
|
||||
export function normName(s: string): string {
|
||||
return s
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[̀-ͯ]/g, "")
|
||||
.replace(/[\s.''׳]/g, "");
|
||||
}
|
||||
|
||||
const lookup = new Map<string, BookInfo>();
|
||||
for (const b of BOOKS) {
|
||||
const names = [b.en, b.es, b.he, b.translit, b.key, ...b.aliases];
|
||||
for (const n of names) lookup.set(normName(n), b);
|
||||
}
|
||||
|
||||
/** Resuelve un nombre de libro (exacto o por prefijo único). */
|
||||
export function resolveBook(input: string): BookInfo | null {
|
||||
const n = normName(input);
|
||||
if (!n) return null;
|
||||
const exact = lookup.get(n);
|
||||
if (exact) return exact;
|
||||
// prefijo único
|
||||
const hits = new Set<BookInfo>();
|
||||
for (const [name, book] of lookup) {
|
||||
if (name.startsWith(n)) hits.add(book);
|
||||
}
|
||||
return hits.size === 1 ? [...hits][0] : null;
|
||||
}
|
||||
41
src/corpus.ts
Normal file
41
src/corpus.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Carga perezosa del corpus: cada libro se descomprime (gzip) la primera vez
|
||||
// que se usa y queda cacheado en memoria.
|
||||
import { CORPUS_GZ, BOOK_SHAPE } from "./data/corpus";
|
||||
|
||||
const cache = new Map<string, string[][]>();
|
||||
|
||||
export async function loadBook(key: string): Promise<string[][]> {
|
||||
const hit = cache.get(key);
|
||||
if (hit) return hit;
|
||||
const b64 = CORPUS_GZ[key];
|
||||
if (!b64) throw new Error(`Unknown book: ${key}`);
|
||||
const bin = atob(b64);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
|
||||
const text = await new Response(stream).text();
|
||||
const chapters = JSON.parse(text) as string[][];
|
||||
cache.set(key, chapters);
|
||||
return chapters;
|
||||
}
|
||||
|
||||
/** [versículos por capítulo] — disponible sin descomprimir. */
|
||||
export function bookShape(key: string): number[] | undefined {
|
||||
return BOOK_SHAPE[key];
|
||||
}
|
||||
|
||||
/** Obtiene un rango de versículos (1-based). Devuelve null si la ref no existe. */
|
||||
export async function getVerses(
|
||||
key: string,
|
||||
chapter: number,
|
||||
verseStart: number,
|
||||
verseEnd: number
|
||||
): Promise<string[] | null> {
|
||||
const shape = bookShape(key);
|
||||
if (!shape || chapter < 1 || chapter > shape.length) return null;
|
||||
const maxV = shape[chapter - 1];
|
||||
if (verseStart < 1 || verseEnd < verseStart || verseStart > maxV) return null;
|
||||
const end = Math.min(verseEnd, maxV);
|
||||
const book = await loadBook(key);
|
||||
return book[chapter - 1].slice(verseStart - 1, end);
|
||||
}
|
||||
27
src/hebrew.ts
Normal file
27
src/hebrew.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Utilidades de texto hebreo: quitar te'amim (cantilación) y/o nikud (vocales).
|
||||
|
||||
const TEAMIM_RE = /[֑-֯]/g; // cantilación
|
||||
const NIKUD_RE = /[ְ-ׇֽׁׂ]/g; // vocales + dagesh + meteg + shin/sin dots
|
||||
|
||||
/** Quita SOLO los te'amim (U+0591–U+05AF), conserva el nikud. */
|
||||
export function stripTeamim(s: string): string {
|
||||
return s.replace(TEAMIM_RE, "");
|
||||
}
|
||||
|
||||
/** Quita el nikud (vocales, dagesh, meteg, puntos de shin/sin). */
|
||||
export function stripNikud(s: string): string {
|
||||
return s.replace(NIKUD_RE, "");
|
||||
}
|
||||
|
||||
/** Texto consonantal puro: sin te'amim ni nikud. */
|
||||
export function consonantal(s: string): string {
|
||||
return stripNikud(stripTeamim(s));
|
||||
}
|
||||
|
||||
/** Aplica el formato elegido por el usuario. */
|
||||
export function formatHebrew(s: string, opts: { nikud: boolean; teamim: boolean }): string {
|
||||
let out = s;
|
||||
if (!opts.teamim) out = stripTeamim(out);
|
||||
if (!opts.nikud) out = stripNikud(out);
|
||||
return out.replace(/ {2,}/g, " ").trim();
|
||||
}
|
||||
86
src/i18n.ts
Normal file
86
src/i18n.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// i18n mínimo (EN/ES por ahora; extender a HE/AR/FR/RU/PT antes del release público,
|
||||
// siguiendo el patrón de obsidian-diacritics-free-search).
|
||||
import { getLanguage } from "obsidian";
|
||||
|
||||
type Lang = "en" | "es";
|
||||
const SUPPORTED: Lang[] = ["en", "es"];
|
||||
|
||||
const translations: Record<Lang, Record<string, string>> = {
|
||||
en: {
|
||||
cmdInsert: "Insert pasuk (Tanakh verse)",
|
||||
modalTitle: "Insert pasuk",
|
||||
searchPlaceholder: "Reference (Gen 1:1) or search (bereshit / בראשית)...",
|
||||
noResults: "No results",
|
||||
hint: "Type a reference like “Bereshit 1:1-3”, or search by transliteration or Hebrew.",
|
||||
insertHebrewOnly: "Hebrew only",
|
||||
settings: "Settings",
|
||||
includeNikud: "Include nikud (vowels)",
|
||||
includeNikudDesc: "Insert verses with vowel points",
|
||||
includeTeamim: "Include te'amim (cantillation)",
|
||||
includeTeamimDesc: "Insert verses with cantillation marks",
|
||||
quoteFormat: "Insert as quote block",
|
||||
quoteFormatDesc: "Wrap the inserted verses in a Markdown blockquote with the reference",
|
||||
maxResults: "Maximum search results",
|
||||
maxResultsDesc: "Limit how many verses appear when searching by text",
|
||||
support: "Support this plugin",
|
||||
supportDesc: "Pasuk is free and open source. If it helps your study, you can support its development with a coffee. ☕",
|
||||
supportBtn: "Support on Ko-fi",
|
||||
viewGithub: "View on GitHub",
|
||||
reportIssue: "Report an issue",
|
||||
searching: "Searching…",
|
||||
},
|
||||
es: {
|
||||
cmdInsert: "Insertar pasuk (versículo del Tanaj)",
|
||||
modalTitle: "Insertar pasuk",
|
||||
searchPlaceholder: "Referencia (Gén 1:1) o búsqueda (bereshit / בראשית)...",
|
||||
noResults: "Sin resultados",
|
||||
hint: "Escribe una referencia como “Bereshit 1:1-3”, o busca por transliteración o hebreo.",
|
||||
insertHebrewOnly: "Solo hebreo",
|
||||
settings: "Ajustes",
|
||||
includeNikud: "Incluir nikud (vocales)",
|
||||
includeNikudDesc: "Insertar los versículos con puntos vocálicos",
|
||||
includeTeamim: "Incluir te'amim (cantilación)",
|
||||
includeTeamimDesc: "Insertar los versículos con marcas de cantilación",
|
||||
quoteFormat: "Insertar como cita (quote)",
|
||||
quoteFormatDesc: "Envolver los versículos en un blockquote de Markdown con la referencia",
|
||||
maxResults: "Máximo de resultados",
|
||||
maxResultsDesc: "Límite de versículos al buscar por texto",
|
||||
support: "Apoya este plugin",
|
||||
supportDesc: "Pasuk es gratuito y de código abierto. Si te ayuda en tu estudio, puedes apoyar su desarrollo con un café. ☕",
|
||||
supportBtn: "Apóyame en Ko-fi",
|
||||
viewGithub: "Ver en GitHub",
|
||||
reportIssue: "Reportar un problema",
|
||||
searching: "Buscando…",
|
||||
},
|
||||
};
|
||||
|
||||
let current: Lang = "en";
|
||||
|
||||
function detect(): Lang {
|
||||
let raw = "en";
|
||||
try {
|
||||
if (typeof getLanguage === "function") raw = getLanguage() || "en";
|
||||
} catch {
|
||||
raw = "en";
|
||||
}
|
||||
const two = raw.toLowerCase().split(/[-_]/)[0];
|
||||
return SUPPORTED.includes(two as Lang) ? (two as Lang) : "en";
|
||||
}
|
||||
|
||||
export function initI18n(): void {
|
||||
current = detect();
|
||||
}
|
||||
|
||||
export function currentLang(): "es" | "en" {
|
||||
return current;
|
||||
}
|
||||
|
||||
export function t(key: string, params?: Record<string, string | number>): string {
|
||||
let s = translations[current]?.[key] ?? translations.en[key] ?? key;
|
||||
if (params) {
|
||||
for (const p of Object.keys(params)) {
|
||||
s = s.split("{" + p + "}").join(String(params[p]));
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
132
src/main.ts
Normal file
132
src/main.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { App, Editor, Plugin, PluginSettingTab, Setting } from "obsidian";
|
||||
import { PasukModal } from "./modal";
|
||||
import { initI18n, t } from "./i18n";
|
||||
|
||||
export interface PasukSettings {
|
||||
includeNikud: boolean;
|
||||
includeTeamim: boolean;
|
||||
quoteFormat: boolean;
|
||||
maxResults: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: PasukSettings = {
|
||||
includeNikud: true,
|
||||
includeTeamim: false,
|
||||
quoteFormat: true,
|
||||
maxResults: 30,
|
||||
};
|
||||
|
||||
const GITHUB_URL = "https://github.com/spenhos/obsidian-pasuk";
|
||||
const KOFI_URL = "https://ko-fi.com/elevalma";
|
||||
|
||||
export default class PasukPlugin extends Plugin {
|
||||
settings: PasukSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
initI18n();
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-verse",
|
||||
name: t("cmdInsert"),
|
||||
editorCallback: (editor: Editor) => {
|
||||
new PasukModal(this.app, editor, this.settings).open();
|
||||
},
|
||||
});
|
||||
|
||||
this.addSettingTab(new PasukSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
onunload() {}
|
||||
|
||||
async loadSettings() {
|
||||
const data = (await this.loadData()) as Partial<PasukSettings> | null;
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class PasukSettingTab extends PluginSettingTab {
|
||||
plugin: PasukPlugin;
|
||||
|
||||
constructor(app: App, plugin: PasukPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
const s = this.plugin.settings;
|
||||
const save = () => void this.plugin.saveSettings();
|
||||
|
||||
new Setting(containerEl).setName(t("settings")).setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("includeNikud"))
|
||||
.setDesc(t("includeNikudDesc"))
|
||||
.addToggle((tg) =>
|
||||
tg.setValue(s.includeNikud).onChange((v) => {
|
||||
s.includeNikud = v;
|
||||
save();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("includeTeamim"))
|
||||
.setDesc(t("includeTeamimDesc"))
|
||||
.addToggle((tg) =>
|
||||
tg.setValue(s.includeTeamim).onChange((v) => {
|
||||
s.includeTeamim = v;
|
||||
save();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("quoteFormat"))
|
||||
.setDesc(t("quoteFormatDesc"))
|
||||
.addToggle((tg) =>
|
||||
tg.setValue(s.quoteFormat).onChange((v) => {
|
||||
s.quoteFormat = v;
|
||||
save();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("maxResults"))
|
||||
.setDesc(t("maxResultsDesc"))
|
||||
.addText((txt) =>
|
||||
txt.setValue(String(s.maxResults)).onChange((v) => {
|
||||
const n = parseInt(v, 10);
|
||||
if (!isNaN(n) && n > 0) {
|
||||
s.maxResults = n;
|
||||
save();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl).setName(t("support")).setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("supportBtn"))
|
||||
.setDesc(t("supportDesc"))
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText("☕ " + t("supportBtn"))
|
||||
.onClick(() => window.open(KOFI_URL, "_blank"));
|
||||
button.buttonEl.addClass("pasuk-kofi-btn");
|
||||
return button;
|
||||
});
|
||||
|
||||
const about = containerEl.createDiv({ cls: "pasuk-about" });
|
||||
about.createSpan({ text: `Pasuk v${this.plugin.manifest.version} · ` });
|
||||
const gh = about.createEl("a", { text: t("viewGithub"), href: GITHUB_URL });
|
||||
gh.setAttr("target", "_blank");
|
||||
about.createSpan({ text: " · " });
|
||||
const issue = about.createEl("a", { text: t("reportIssue"), href: GITHUB_URL + "/issues" });
|
||||
issue.setAttr("target", "_blank");
|
||||
}
|
||||
}
|
||||
165
src/modal.ts
Normal file
165
src/modal.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
// Modal de búsqueda e inserción de pesukim.
|
||||
import { App, Editor, Modal } from "obsidian";
|
||||
import { formatRefLabel, parseRef } from "./refparse";
|
||||
import { getVerses } from "./corpus";
|
||||
import { searchText, SearchHit } from "./search";
|
||||
import { formatHebrew } from "./hebrew";
|
||||
import { currentLang, t } from "./i18n";
|
||||
import type { PasukSettings } from "./main";
|
||||
|
||||
interface ResultItem {
|
||||
label: string; // ej. "Génesis 1:1"
|
||||
verses: string[]; // texto original
|
||||
preview: string; // primera línea para mostrar
|
||||
}
|
||||
|
||||
export class PasukModal extends Modal {
|
||||
private editor: Editor;
|
||||
private settings: PasukSettings;
|
||||
private inputEl: HTMLInputElement;
|
||||
private resultsEl: HTMLElement;
|
||||
private hintEl: HTMLElement;
|
||||
private items: ResultItem[] = [];
|
||||
private selected = 0;
|
||||
private debounce: number | null = null;
|
||||
private searchSeq = 0;
|
||||
|
||||
constructor(app: App, editor: Editor, settings: PasukSettings) {
|
||||
super(app);
|
||||
this.editor = editor;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.addClass("pasuk-modal");
|
||||
this.titleEl.setText(t("modalTitle"));
|
||||
|
||||
this.inputEl = contentEl.createEl("input", {
|
||||
type: "text",
|
||||
placeholder: t("searchPlaceholder"),
|
||||
cls: "pasuk-input",
|
||||
});
|
||||
this.hintEl = contentEl.createDiv({ cls: "pasuk-hint", text: t("hint") });
|
||||
this.resultsEl = contentEl.createDiv({ cls: "pasuk-results" });
|
||||
|
||||
this.inputEl.addEventListener("input", () => {
|
||||
if (this.debounce) window.clearTimeout(this.debounce);
|
||||
this.debounce = window.setTimeout(() => void this.runSearch(), 250);
|
||||
});
|
||||
this.inputEl.addEventListener("keydown", (evt) => {
|
||||
if (evt.key === "ArrowDown") {
|
||||
evt.preventDefault();
|
||||
this.select(this.selected + 1);
|
||||
} else if (evt.key === "ArrowUp") {
|
||||
evt.preventDefault();
|
||||
this.select(this.selected - 1);
|
||||
} else if (evt.key === "Enter") {
|
||||
evt.preventDefault();
|
||||
this.insertSelected();
|
||||
} else if (evt.key === "Escape") {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
this.inputEl.focus();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
private async runSearch() {
|
||||
const q = this.inputEl.value.trim();
|
||||
const seq = ++this.searchSeq;
|
||||
this.items = [];
|
||||
this.selected = 0;
|
||||
if (!q) {
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) ¿Es una referencia?
|
||||
const ref = parseRef(q);
|
||||
if (ref) {
|
||||
const verses = await getVerses(ref.book.key, ref.chapter, ref.verseStart, ref.verseEnd);
|
||||
if (seq !== this.searchSeq) return;
|
||||
if (verses) {
|
||||
this.items = [
|
||||
{
|
||||
label: formatRefLabel(ref, currentLang()),
|
||||
verses,
|
||||
preview: verses[0],
|
||||
},
|
||||
];
|
||||
}
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Búsqueda de texto (hebreo o transliteración)
|
||||
this.resultsEl.setText(t("searching"));
|
||||
const hits = await searchText(q, this.settings.maxResults);
|
||||
if (seq !== this.searchSeq) return;
|
||||
const lang = currentLang();
|
||||
this.items = hits.map((h: SearchHit) => ({
|
||||
label: `${lang === "es" ? h.book.es : h.book.en} ${h.chapter}:${h.verse}`,
|
||||
verses: [h.text],
|
||||
preview: h.text,
|
||||
}));
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.resultsEl.empty();
|
||||
if (!this.items.length) {
|
||||
if (this.inputEl.value.trim()) {
|
||||
this.resultsEl.createDiv({ cls: "pasuk-empty", text: t("noResults") });
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.items.forEach((item, i) => {
|
||||
const el = this.resultsEl.createDiv({
|
||||
cls: "pasuk-result" + (i === this.selected ? " is-selected" : ""),
|
||||
});
|
||||
el.createDiv({ cls: "pasuk-result-ref", text: item.label });
|
||||
el.createDiv({
|
||||
cls: "pasuk-result-text",
|
||||
text: formatHebrew(item.preview, {
|
||||
nikud: this.settings.includeNikud,
|
||||
teamim: this.settings.includeTeamim,
|
||||
}),
|
||||
});
|
||||
el.addEventListener("click", () => {
|
||||
this.selected = i;
|
||||
this.insertSelected();
|
||||
});
|
||||
el.addEventListener("mousemove", () => this.select(i));
|
||||
});
|
||||
}
|
||||
|
||||
private select(i: number) {
|
||||
if (!this.items.length) return;
|
||||
this.selected = Math.max(0, Math.min(i, this.items.length - 1));
|
||||
const children = Array.from(this.resultsEl.children);
|
||||
children.forEach((c, idx) => c.toggleClass("is-selected", idx === this.selected));
|
||||
children[this.selected]?.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
private insertSelected() {
|
||||
const item = this.items[this.selected];
|
||||
if (!item) return;
|
||||
const opts = { nikud: this.settings.includeNikud, teamim: this.settings.includeTeamim };
|
||||
const lines = item.verses.map((v) => formatHebrew(v, opts));
|
||||
|
||||
let text: string;
|
||||
if (this.settings.quoteFormat) {
|
||||
const quoted = lines.map((l) => `> ${l}`).join("\n");
|
||||
text = `${quoted}\n> — ${item.label}\n`;
|
||||
} else {
|
||||
text = `${lines.join(" ")} (${item.label})`;
|
||||
}
|
||||
|
||||
this.editor.replaceSelection(text);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
43
src/refparse.ts
Normal file
43
src/refparse.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Parser de referencias: "Gen 1:1", "bereshit 1:1-3", "Salmos 23", "תהלים כג" (futuro).
|
||||
import { BookInfo, resolveBook } from "./books";
|
||||
import { bookShape } from "./corpus";
|
||||
|
||||
export interface ParsedRef {
|
||||
book: BookInfo;
|
||||
chapter: number;
|
||||
verseStart: number;
|
||||
verseEnd: number;
|
||||
wholeChapter: boolean;
|
||||
}
|
||||
|
||||
// "<libro> <cap>[:<v>[-<v2>]]" — separadores aceptados: ":", ".", ","
|
||||
const REF_RE = /^(.+?)\s+(\d{1,3})(?:\s*[:.,]\s*(\d{1,3})(?:\s*[-–—]\s*(\d{1,3}))?)?$/;
|
||||
|
||||
export function parseRef(input: string): ParsedRef | null {
|
||||
const m = input.trim().match(REF_RE);
|
||||
if (!m) return null;
|
||||
const book = resolveBook(m[1]);
|
||||
if (!book) return null;
|
||||
const shape = bookShape(book.key);
|
||||
if (!shape) return null;
|
||||
|
||||
const chapter = parseInt(m[2], 10);
|
||||
if (chapter < 1 || chapter > shape.length) return null;
|
||||
const maxV = shape[chapter - 1];
|
||||
|
||||
if (!m[3]) {
|
||||
// capítulo completo
|
||||
return { book, chapter, verseStart: 1, verseEnd: maxV, wholeChapter: true };
|
||||
}
|
||||
const v1 = parseInt(m[3], 10);
|
||||
const v2 = m[4] ? parseInt(m[4], 10) : v1;
|
||||
if (v1 < 1 || v1 > maxV || v2 < v1) return null;
|
||||
return { book, chapter, verseStart: v1, verseEnd: Math.min(v2, maxV), wholeChapter: false };
|
||||
}
|
||||
|
||||
export function formatRefLabel(ref: ParsedRef, lang: "es" | "en"): string {
|
||||
const name = lang === "es" ? ref.book.es : ref.book.en;
|
||||
if (ref.wholeChapter) return `${name} ${ref.chapter}`;
|
||||
const range = ref.verseStart === ref.verseEnd ? `${ref.verseStart}` : `${ref.verseStart}-${ref.verseEnd}`;
|
||||
return `${name} ${ref.chapter}:${range}`;
|
||||
}
|
||||
69
src/search.ts
Normal file
69
src/search.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Búsqueda de texto en todo el Tanaj (hebreo directo o transliteración).
|
||||
import { BOOKS, BookInfo } from "./books";
|
||||
import { loadBook } from "./corpus";
|
||||
import { consonantal, stripNikud, stripTeamim } from "./hebrew";
|
||||
import { isLatin, translitToRegex } from "./translit";
|
||||
|
||||
export interface SearchHit {
|
||||
book: BookInfo;
|
||||
chapter: number; // 1-based
|
||||
verse: number; // 1-based
|
||||
text: string; // texto original (con nikud + te'amim)
|
||||
}
|
||||
|
||||
// Índice consonantal en memoria (se construye una vez por sesión, bajo demanda)
|
||||
let consIndex: Map<string, string[][]> | null = null;
|
||||
|
||||
async function buildIndex(): Promise<Map<string, string[][]>> {
|
||||
if (consIndex) return consIndex;
|
||||
const idx = new Map<string, string[][]>();
|
||||
for (const b of BOOKS) {
|
||||
const chapters = await loadBook(b.key);
|
||||
idx.set(
|
||||
b.key,
|
||||
chapters.map((ch) => ch.map((v) => consonantal(v)))
|
||||
);
|
||||
}
|
||||
consIndex = idx;
|
||||
return idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca `query` en todo el Tanaj. Acepta hebreo (con o sin nikud)
|
||||
* o transliteración latina. Devuelve hasta `limit` resultados.
|
||||
*/
|
||||
export async function searchText(query: string, limit: number): Promise<SearchHit[]> {
|
||||
const q = query.trim();
|
||||
if (!q) return [];
|
||||
|
||||
let matcher: (cons: string) => boolean;
|
||||
if (isLatin(q)) {
|
||||
const re = translitToRegex(q);
|
||||
if (!re) return [];
|
||||
matcher = (cons) => re.test(cons);
|
||||
} else {
|
||||
// hebreo directo: normalizar a consonantal y buscar como substring
|
||||
const needle = consonantal(stripNikud(stripTeamim(q)));
|
||||
if (!needle) return [];
|
||||
matcher = (cons) => cons.includes(needle);
|
||||
}
|
||||
|
||||
const idx = await buildIndex();
|
||||
const hits: SearchHit[] = [];
|
||||
|
||||
for (const b of BOOKS) {
|
||||
const chapters = idx.get(b.key);
|
||||
if (!chapters) continue;
|
||||
const original = await loadBook(b.key);
|
||||
for (let c = 0; c < chapters.length; c++) {
|
||||
const verses = chapters[c];
|
||||
for (let v = 0; v < verses.length; v++) {
|
||||
if (matcher(verses[v])) {
|
||||
hits.push({ book: b, chapter: c + 1, verse: v + 1, text: original[c][v] });
|
||||
if (hits.length >= limit) return hits;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
107
src/translit.ts
Normal file
107
src/translit.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Transliteración latina → patrón de búsqueda en hebreo consonantal.
|
||||
//
|
||||
// Idea: las consonantes mapean a clases de letras hebreas (incluyendo finales);
|
||||
// las vocales latinas (a,e,i,o,u) son flexibles — pueden corresponder a nada
|
||||
// (solo nikud) o a una mater lectionis (א ה ו י). Así "bereshit" encuentra
|
||||
// בראשית y "mayim" encuentra מים.
|
||||
|
||||
interface Token {
|
||||
pattern: string; // fragmento de regex
|
||||
isVowel: boolean;
|
||||
}
|
||||
|
||||
// Dígrafos primero (orden importa)
|
||||
const DIGRAPHS: Array<[string, string]> = [
|
||||
["sh", "ש"],
|
||||
["tz", "[צץ]"],
|
||||
["ts", "[צץ]"],
|
||||
["ch", "[חכך]"],
|
||||
["kh", "[כך]"],
|
||||
["th", "ת"],
|
||||
["ph", "[פף]"],
|
||||
];
|
||||
|
||||
const CONSONANTS: Record<string, string> = {
|
||||
b: "ב",
|
||||
v: "[בו]",
|
||||
g: "ג",
|
||||
d: "ד",
|
||||
h: "ה",
|
||||
w: "ו",
|
||||
z: "ז",
|
||||
j: "[חכך]", // español: jet
|
||||
t: "[תט]",
|
||||
y: "י",
|
||||
k: "[כךק]",
|
||||
c: "[כךקס]", // comodín latino
|
||||
l: "ל",
|
||||
m: "[מם]",
|
||||
n: "[נן]",
|
||||
s: "[סש]",
|
||||
x: "[סש]",
|
||||
p: "[פף]",
|
||||
f: "[פף]",
|
||||
q: "ק",
|
||||
r: "ר",
|
||||
};
|
||||
|
||||
// Vocales: opcionalmente una mater lectionis. La ' (apóstrofe) fuerza א/ע.
|
||||
const VOWELS: Record<string, string> = {
|
||||
a: "[אהע]?",
|
||||
e: "[אהעי]?",
|
||||
i: "י?",
|
||||
o: "[וא]?",
|
||||
u: "ו?",
|
||||
};
|
||||
|
||||
/** ¿El texto parece transliteración latina (y no hebreo directo)? */
|
||||
export function isLatin(s: string): boolean {
|
||||
return /^[a-z'\s]+$/i.test(s.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte una transliteración a RegExp sobre texto hebreo consonantal.
|
||||
* Devuelve null si no hay consonantes utilizables.
|
||||
*/
|
||||
export function translitToRegex(input: string): RegExp | null {
|
||||
const words = input.trim().toLowerCase().split(/\s+/);
|
||||
const wordPatterns: string[] = [];
|
||||
|
||||
for (const word of words) {
|
||||
const tokens: Token[] = [];
|
||||
let i = 0;
|
||||
while (i < word.length) {
|
||||
const two = word.slice(i, i + 2);
|
||||
const dig = DIGRAPHS.find(([d]) => d === two);
|
||||
if (dig) {
|
||||
tokens.push({ pattern: dig[1], isVowel: false });
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
const ch = word[i];
|
||||
if (ch === "'") {
|
||||
tokens.push({ pattern: "[אע]", isVowel: false });
|
||||
} else if (CONSONANTS[ch]) {
|
||||
tokens.push({ pattern: CONSONANTS[ch], isVowel: false });
|
||||
} else if (VOWELS[ch]) {
|
||||
tokens.push({ pattern: VOWELS[ch], isVowel: true });
|
||||
}
|
||||
// caracteres desconocidos se ignoran
|
||||
i += 1;
|
||||
}
|
||||
// vocal inicial: suele ser א/ע presente (ej. "adam" → אדם)
|
||||
if (tokens.length && tokens[0].isVowel) {
|
||||
tokens[0] = { pattern: "[אהעיו]", isVowel: false };
|
||||
}
|
||||
const consonantCount = tokens.filter((t) => !t.isVowel).length;
|
||||
if (consonantCount === 0) continue;
|
||||
wordPatterns.push(tokens.map((t) => t.pattern).join(""));
|
||||
}
|
||||
|
||||
if (!wordPatterns.length) return null;
|
||||
try {
|
||||
return new RegExp(wordPatterns.join("[ ־]"), "u");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
85
styles.css
Normal file
85
styles.css
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/* ===== Pasuk modal ===== */
|
||||
.pasuk-modal {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.pasuk-input {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
font-size: 15px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.pasuk-input:focus {
|
||||
border-color: var(--interactive-accent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.pasuk-hint {
|
||||
margin: 6px 2px 10px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pasuk-results {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pasuk-result {
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pasuk-result.is-selected {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.pasuk-result-ref {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-accent);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.pasuk-result-text {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
font-size: 17px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.pasuk-empty {
|
||||
padding: 12px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ===== Settings ===== */
|
||||
.pasuk-kofi-btn.pasuk-kofi-btn {
|
||||
background-color: #ff5e5b;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pasuk-kofi-btn.pasuk-kofi-btn:hover {
|
||||
background-color: #e8514e;
|
||||
}
|
||||
|
||||
.pasuk-about {
|
||||
margin-top: 1.5em;
|
||||
padding-top: 0.75em;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pasuk-about a {
|
||||
color: var(--text-accent);
|
||||
}
|
||||
63
tools/build-corpus.mjs
Normal file
63
tools/build-corpus.mjs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Empaqueta el corpus MAM del Tanaj (39 libros, nikud + te'amim) desde el
|
||||
// proyecto codigos-torah hacia src/data/corpus.ts como gzip+base64 por libro.
|
||||
// El plugin lo descomprime bajo demanda con DecompressionStream (lazy, por libro).
|
||||
//
|
||||
// Run: npm run build-corpus
|
||||
import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import { join, basename } from "node:path";
|
||||
|
||||
const CORPUS_DIR =
|
||||
"/Users/salehpenhos/Documents/Claude/Codigo/codigos-torah/public/data/corpus";
|
||||
const OUT_DIR = new URL("../src/data/", import.meta.url).pathname;
|
||||
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
const files = readdirSync(CORPUS_DIR).filter((f) => f.endsWith(".json"));
|
||||
if (files.length !== 39) {
|
||||
throw new Error(`Se esperaban 39 libros, hay ${files.length}`);
|
||||
}
|
||||
|
||||
let totalRaw = 0;
|
||||
let totalGz = 0;
|
||||
const entries = [];
|
||||
const meta = [];
|
||||
|
||||
for (const f of files.sort()) {
|
||||
const book = basename(f, ".json");
|
||||
const raw = readFileSync(join(CORPUS_DIR, f));
|
||||
const data = JSON.parse(raw.toString("utf-8"));
|
||||
const chapters = data.he;
|
||||
if (!Array.isArray(chapters)) throw new Error(`${book}: formato inesperado`);
|
||||
const verseCounts = chapters.map((c) => c.length);
|
||||
|
||||
// Re-serializar solo los capítulos (sin el wrapper {he:...})
|
||||
const payload = JSON.stringify(chapters);
|
||||
const gz = gzipSync(Buffer.from(payload, "utf-8"), { level: 9 });
|
||||
totalRaw += payload.length;
|
||||
totalGz += gz.length;
|
||||
|
||||
entries.push(`\t${JSON.stringify(book)}: ${JSON.stringify(gz.toString("base64"))},`);
|
||||
meta.push({ book, chapters: chapters.length, verseCounts });
|
||||
}
|
||||
|
||||
const ts = `// GENERATED FILE — do not edit. Run: npm run build-corpus
|
||||
// Fuente: corpus MAM (Miqra according to the Masorah) del proyecto codigos-torah,
|
||||
// versión fijada de Sefaria, con parches Koren en la Torá. 39 libros del Tanaj.
|
||||
// Formato por libro: gzip+base64 de JSON string[][] (capítulos -> versículos).
|
||||
|
||||
export const CORPUS_GZ: Record<string, string> = {
|
||||
${entries.join("\n")}
|
||||
};
|
||||
|
||||
/** Capítulos y versículos por libro (para validar referencias sin descomprimir). */
|
||||
export const BOOK_SHAPE: Record<string, number[]> = {
|
||||
${meta.map((m) => `\t${JSON.stringify(m.book)}: ${JSON.stringify(m.verseCounts)},`).join("\n")}
|
||||
};
|
||||
`;
|
||||
|
||||
writeFileSync(join(OUT_DIR, "corpus.ts"), ts);
|
||||
console.log(
|
||||
`✅ ${files.length} libros → src/data/corpus.ts ` +
|
||||
`(raw ${(totalRaw / 1048576).toFixed(1)} MB → gz ${(totalGz / 1048576).toFixed(1)} MB)`
|
||||
);
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
Loading…
Reference in a new issue