enrich 13 more ru lives from Bulgakov's Desktop Book
This commit is contained in:
1 parent
bc1cf5a939
commit
e32d7878a9
9 files changed
+364
-31
No files matched your search
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* Convert Bulgakov's "Настольная книга для священно-церковнослужителей"
|
||||
* EPUB to date-keyed JSON for enrichment of ru/lives bundles.
|
||||
*
|
||||
* Only processes the Menaion section (months 01-12).
|
||||
* Keeps original pre-revolutionary orthography.
|
||||
* Resolves footnotes inline.
|
||||
* Excludes hymn texts (Troparia/Kontakia).
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/convert-bulgakov.ts <epub-path> [output.json]
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const MONTH_NAMES: Record<string, string> = {
|
||||
'январь': '01', 'января': '01',
|
||||
'февраль': '02', 'февраля': '02',
|
||||
'март': '03', 'марта': '03',
|
||||
'апрель': '04', 'апреля': '04',
|
||||
'май': '05', 'мая': '05',
|
||||
'июнь': '06', 'июня': '06',
|
||||
'июль': '07', 'июля': '07',
|
||||
'август': '08', 'августа': '08',
|
||||
'сентябрь': '09', 'сентября': '09',
|
||||
'октябрь': '10', 'октября': '10',
|
||||
'ноябрь': '11', 'ноября': '11',
|
||||
'декабрь': '12', 'декабря': '12',
|
||||
};
|
||||
|
||||
interface BulgakovEntry {
|
||||
title: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
interface BulgakovDay {
|
||||
[dateKey: string]: BulgakovEntry[];
|
||||
}
|
||||
|
||||
/** Strip HTML tags, collapse whitespace. */
|
||||
function stripTags(s: string): string {
|
||||
return s.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
/** Clean a paragraph's HTML, resolving footnotes inline. */
|
||||
function cleanParagraph(raw: string, notes: string[]): string {
|
||||
let s = raw.trim();
|
||||
// Resolve footnotes: <a href="..." title="NOTE TEXT"...><sup>[N]</sup></a>
|
||||
s = s.replace(
|
||||
/<a\s[^>]*title="([^"]*)"[^>]*>(<sup[^>]*>\[(\d+)\]<\/sup>)<\/a>/g,
|
||||
(_m: string, note: string, sup: string) => {
|
||||
notes.push(note.trim());
|
||||
return sup;
|
||||
}
|
||||
);
|
||||
// Remove remaining <a> tags (keep inner content)
|
||||
s = s.replace(/<a\s[^>]*>(.*?)<\/a>/gs, (_m: string, inner: string) => inner);
|
||||
// Strip calibre-specific classes
|
||||
s = s.replace(/<(\w+)\s[^>]*class="[^"]*calibre[^"]*"[^>]*>/g, '<$1>');
|
||||
s = s.replace(/<span\s[^>]*class="ponomar"[^>]*>(.*?)<\/span>/g, '$1');
|
||||
s = s.replace(/<br\s[^>]*class="calibre\d+"\s*\/?>/g, '<br>');
|
||||
s = s.replace(/\u00a0/g, ' ').replace(/\s+/g, ' ');
|
||||
return `<p>${s}</p>`;
|
||||
}
|
||||
|
||||
/** Check if a paragraph is a hymn (Troparion/Kontakion) or liturgical rubric. */
|
||||
function isHymn(text: string): boolean {
|
||||
const clean = stripTags(text).replace(/^\d+\s*\.?\s*/, '').trim();
|
||||
const hymnPatterns = [
|
||||
/^Тропарь,/i, /^Кондак,/i, /^Парем\./i, /^Утр\.\s*Ев\./i,
|
||||
/^Ап\./i, /^Ев\./i, /^Совершается собор/i, /^В этот день поется/i,
|
||||
/^Совершается/i, /^Служба/i, /^См\.\s/i,
|
||||
];
|
||||
return hymnPatterns.some(p => p.test(clean));
|
||||
}
|
||||
|
||||
/** Check if a paragraph is a cross-reference (starts with "См." or "см."). */
|
||||
function isCrossRef(text: string): boolean {
|
||||
const clean = stripTags(text).replace(/^\d+\s*\.?\s*/, '').trim();
|
||||
return /^см\.\s/i.test(clean);
|
||||
}
|
||||
|
||||
/** Extract the day number from the start of a paragraph. */
|
||||
function extractDayNum(text: string): number | null {
|
||||
const m = text.match(/<strong[^>]*>(\d+)<\/strong>\.?\s*/i);
|
||||
return m ? parseInt(m[1], 10) : null;
|
||||
}
|
||||
|
||||
/** Extract the title from a paragraph (first significant text after day number and ponomar span). */
|
||||
function extractTitle(text: string): string {
|
||||
let s = text;
|
||||
// Remove any leading anchor tags
|
||||
s = s.replace(/^<a\s[^>]*><\/a>\s*/i, '');
|
||||
// Remove day number prefix
|
||||
s = s.replace(/^<strong[^>]*>\d+<\/strong>\.?\s*/i, '');
|
||||
|
||||
// Handle ponomar span: may contain a month znamenanie (single letter) or saint name
|
||||
const ponomarMatch = s.match(/^<[^>]*>([^<]+)<\/[^>]+>\s*/);
|
||||
if (ponomarMatch) {
|
||||
let ponomarContent = ponomarMatch[1].trim();
|
||||
// If ponomar is just a single letter (with optional period), it's a znamenanie — skip entire span
|
||||
if (/^\p{L}\.?\s*$/u.test(ponomarContent)) {
|
||||
s = s.replace(/^<[^>]*>[^<]+<\/[^>]+>\s*/, '');
|
||||
} else {
|
||||
// Ponomar may contain znamenanie + title (e.g. "Л. 🕀 Срѣтенїе...")
|
||||
// Check if it starts with a single letter + period
|
||||
const znamenanieMatch = ponomarContent.match(/^(\p{L}\.)\s*(.*)$/su);
|
||||
if (znamenanieMatch) {
|
||||
// Replace the span with just the title part (remove znamenanie)
|
||||
s = s.replace(/^<[^>]*>[^<]+<\/[^>]+>\s*/, znamenanieMatch[2] + ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip all HTML tags
|
||||
s = stripTags(s);
|
||||
// Remove leading non-letter symbols (like †, 🕀, 🕂, 🕃)
|
||||
s = s.replace(/^[\s\p{P}\p{S}]+/u, '');
|
||||
// Take first 100 chars, breaking at a period where possible
|
||||
const MAX_TITLE = 100;
|
||||
if (s.length <= MAX_TITLE) return s;
|
||||
// Try to break at a period within the first 100 chars
|
||||
const periodIdx = s.lastIndexOf('.', MAX_TITLE);
|
||||
if (periodIdx > 10) return s.substring(0, periodIdx + 1);
|
||||
return s.substring(0, MAX_TITLE) + '...';
|
||||
}
|
||||
|
||||
/** Check if text contains a month name (for heading detection). */
|
||||
function findMonthName(text: string): string | null {
|
||||
const lower = text.toLowerCase();
|
||||
for (const [name, _code] of Object.entries(MONTH_NAMES)) {
|
||||
if (lower.includes(name)) return name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function convertBulgakov(epubPath: string, outputPath: string): Promise<BulgakovDay> {
|
||||
const tmpDir = join(dirname(outputPath), 'tmp_bulgakov');
|
||||
if (existsSync(tmpDir)) {
|
||||
execSync(`rm -rf "${tmpDir}"`);
|
||||
}
|
||||
mkdirSync(tmpDir, { recursive: true });
|
||||
|
||||
console.log(`Extracting EPUB: ${epubPath}`);
|
||||
execSync(`unzip -o "${epubPath}" -d "${tmpDir}" > /dev/null 2>&1`);
|
||||
|
||||
const out: BulgakovDay = {};
|
||||
let currentMonth: string | null = null;
|
||||
let currentDay: number | null = null;
|
||||
let inMenaion = false;
|
||||
|
||||
// Parse all files in order
|
||||
for (let i = 0; ; i++) {
|
||||
const fileNum = String(i).padStart(3, '0');
|
||||
const filePath = join(tmpDir, `index_split_${fileNum}.xhtml`);
|
||||
if (!existsSync(filePath)) break;
|
||||
|
||||
if (i % 10 === 0) console.log(` Processing file ${fileNum}...`);
|
||||
|
||||
const html = readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Check for h2 headings containing month names
|
||||
// h2 looks like: <h2 ...><a...></a><a...></a>Январь <br.../></h2>
|
||||
const h2Regex = /<h2[^>]*>([\s\S]*?)<\/h2>/gi;
|
||||
let h2Match: RegExpExecArray | null;
|
||||
while ((h2Match = h2Regex.exec(html)) !== null) {
|
||||
const h2Content = stripTags(h2Match[1]);
|
||||
const monthName = findMonthName(h2Content);
|
||||
if (monthName) {
|
||||
currentMonth = MONTH_NAMES[monthName];
|
||||
currentDay = null;
|
||||
inMenaion = true;
|
||||
console.log(` → Month ${currentMonth}: ${monthName} (file ${fileNum})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for section headers (Месяцеслов / Триодион)
|
||||
if (/<h1[^>]*>[\s\S]*?Месяцеслов[\s\S]*?<\/h1>/i.test(html)) {
|
||||
inMenaion = true;
|
||||
console.log(` → Entered Месяцеслов section (file ${fileNum})`);
|
||||
}
|
||||
if (/<h1[^>]*>[\s\S]*?Триодион[\s\S]*?<\/h1>/i.test(html)) {
|
||||
inMenaion = false;
|
||||
console.log(` → Left Месяцеслов, entered Триодион (file ${fileNum})`);
|
||||
}
|
||||
|
||||
if (!inMenaion || !currentMonth) continue;
|
||||
|
||||
// Extract all paragraph divs
|
||||
const paraRegex = /<div class="paragraph">([\s\S]*?)<\/div>/g;
|
||||
let paraMatch: RegExpExecArray | null;
|
||||
|
||||
// We process paragraphs in order. Each paragraph may be:
|
||||
// - A new day entry (strong with day number)
|
||||
// - Continuation of current day
|
||||
// - A hymn (skipped)
|
||||
|
||||
while ((paraMatch = paraRegex.exec(html)) !== null) {
|
||||
const raw = paraMatch[1];
|
||||
|
||||
// Check for day number
|
||||
const dayNum = extractDayNum(raw);
|
||||
|
||||
if (dayNum !== null) {
|
||||
currentDay = dayNum;
|
||||
|
||||
// Skip hymns and cross-refs
|
||||
if (isHymn(raw) || isCrossRef(raw)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// New entry for this day
|
||||
const title = extractTitle(raw);
|
||||
const notes: string[] = [];
|
||||
const cleanHtml = cleanParagraph(raw, notes);
|
||||
|
||||
const dateKey = `${currentMonth}-${String(dayNum).padStart(2, '0')}`;
|
||||
if (!out[dateKey]) out[dateKey] = [];
|
||||
|
||||
const entry: BulgakovEntry = { title, html: cleanHtml };
|
||||
if (notes.length > 0) {
|
||||
entry.html += `<p>Примечания:</p>${notes.map((n, i) => `<p>[${i + 1}] ${n}</p>`).join('')}`;
|
||||
}
|
||||
out[dateKey].push(entry);
|
||||
} else if (currentDay !== null) {
|
||||
// Skip hymn continuations
|
||||
if (isHymn(raw) || isCrossRef(raw)) continue;
|
||||
|
||||
// Continuation paragraph for current day - append to last entry
|
||||
const dateKey = `${currentMonth}-${String(currentDay).padStart(2, '0')}`;
|
||||
const lives = out[dateKey];
|
||||
if (lives && lives.length > 0) {
|
||||
const last = lives[lives.length - 1];
|
||||
const notes: string[] = [];
|
||||
const cleanHtml = cleanParagraph(raw, notes);
|
||||
last.html += cleanHtml;
|
||||
if (notes.length > 0) {
|
||||
const noteBlock = `<p>Примечания:</p>${notes.map((n, i) => `<p>[${i + 1}] ${n}</p>`).join('')}`;
|
||||
last.html += noteBlock;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup temp directory
|
||||
execSync(`rm -rf "${tmpDir}"`);
|
||||
|
||||
// Write output
|
||||
mkdirSync(dirname(outputPath), { recursive: true });
|
||||
writeFileSync(outputPath, JSON.stringify(out, null, 0));
|
||||
|
||||
let totalEntries = 0;
|
||||
for (const lives of Object.values(out)) totalEntries += lives.length;
|
||||
console.log(`Converted ${Object.keys(out).length} days, ${totalEntries} entries → ${outputPath}`);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const epubPath = process.argv[2];
|
||||
if (!epubPath) {
|
||||
console.error('Usage: npx tsx scripts/convert-bulgakov.ts <epub-path> [output.json]');
|
||||
process.exit(1);
|
||||
}
|
||||
const outputPath = process.argv[3] || join(process.cwd(), 'scripts', 'output', 'bulgakov.json');
|
||||
convertBulgakov(epubPath, outputPath).catch(err => {
|
||||
console.error('Conversion failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
+84
-25
@@ -20,11 +20,13 @@ import { join, dirname } from 'path';
|
||||
|
||||
const ROOT = join(new URL('.', import.meta.url).pathname, '..');
|
||||
const MINEI_PATH = join(ROOT, 'scripts', 'output', 'minei.json');
|
||||
const BULGAKOV_PATH = join(ROOT, 'scripts', 'output', 'bulgakov.json');
|
||||
const MENAION_BUNDLE_PATH = join(ROOT, 'static', 'data', 'cu', 'menaion-bundle.json');
|
||||
const LIVES_DIR = join(ROOT, 'static', 'data', 'ru', 'lives');
|
||||
const REPORT_PATH = join(ROOT, 'scripts', 'output', 'lives-match-report.json');
|
||||
|
||||
const COPYRIGHT = 'Четьи-Минеи свт. Димитрия Ростовского';
|
||||
const COPYRIGHT_MINEI = 'Четьи-Минеи свт. Димитрия Ростовского';
|
||||
const COPYRIGHT_BULGAKOV = 'Протоиерей Сергий Булгаков. Настольная Книга.';
|
||||
|
||||
interface SourceLife {
|
||||
title: string;
|
||||
@@ -42,6 +44,7 @@ interface Candidate {
|
||||
interface ReportEntry {
|
||||
date: string;
|
||||
sourceTitle: string;
|
||||
source: 'minei' | 'bulgakov' | 'unknown';
|
||||
matchedCid: string | null;
|
||||
matchedName: string | null;
|
||||
score: number;
|
||||
@@ -296,6 +299,16 @@ const MANUAL_FIXES: Record<string, string> = {
|
||||
'11-20|саверия': '257401',
|
||||
'11-20|нирсы': '257401',
|
||||
'11-28|стефаном новым': '3244',
|
||||
// Bulgakov fixes (feast-related entries)
|
||||
'02-22|обретение честных мощей': '491',
|
||||
'11-20|во храм входа пресвятой': '257510',
|
||||
'11-25|отда́нїе праздника входа': '257512',
|
||||
'12-20|по плоти рождества господа бога и спаса': '09001',
|
||||
'12-22|из церковных песнопений этого дня': '09001',
|
||||
'12-23|из церковных песнопений этого дня': '09001',
|
||||
'12-24|из церковных песнопений этого дня': '09006',
|
||||
'12-26|собо́р прест҃ы́ѧ бг҃оро́дицы': '12260098',
|
||||
'12-31|отда́нїе праздника рождества̀ христо́ва': '123101',
|
||||
};
|
||||
|
||||
/** True if a token is a year, year-range, or Roman numeral (e.g. 309, 1238, V, IX). */
|
||||
@@ -366,6 +379,14 @@ export async function enrichLives(args: Args): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
const minei = JSON.parse(readFileSync(MINEI_PATH, 'utf-8')) as Record<string, SourceLife[]>;
|
||||
|
||||
// Optional second source: Bulgakov
|
||||
let bulgakov: Record<string, SourceLife[]> = {};
|
||||
if (existsSync(BULGAKOV_PATH)) {
|
||||
bulgakov = JSON.parse(readFileSync(BULGAKOV_PATH, 'utf-8'));
|
||||
console.log(`Loaded Bulgakov source: ${Object.keys(bulgakov).length} days, ${Object.values(bulgakov).flat().length} entries`);
|
||||
}
|
||||
|
||||
const bundle = JSON.parse(readFileSync(MENAION_BUNDLE_PATH, 'utf-8')) as Record<string, { id: string }[]>;
|
||||
|
||||
// Load all ru/lives bundles into one map: cid -> {name, life, file}
|
||||
@@ -382,28 +403,19 @@ export async function enrichLives(args: Args): Promise<void> {
|
||||
let autoApplied = 0;
|
||||
let needReview = 0;
|
||||
let unmatched = 0;
|
||||
const filledByMinei = new Set<string>();
|
||||
|
||||
for (const [date, lives] of Object.entries(minei)) {
|
||||
if (args.month && !date.startsWith(args.month)) continue;
|
||||
|
||||
// Candidate CIds for this date from the menaion bundle
|
||||
const bundleIds = (bundle[date] || []).map(e => String(e.id));
|
||||
const candidates: Candidate[] = bundleIds
|
||||
.map(cid => {
|
||||
const entry = livesByCid[cid];
|
||||
if (!entry) return null;
|
||||
const name = entry.name.nominative || entry.name.short || '';
|
||||
return {
|
||||
cid,
|
||||
name,
|
||||
file: entry.file,
|
||||
hasLife: !!entry.life?.text,
|
||||
score: 0,
|
||||
} as Candidate;
|
||||
})
|
||||
.filter((c): c is Candidate => c !== null);
|
||||
|
||||
for (const life of lives) {
|
||||
// Process a single source (minei or bulgakov) for a given date
|
||||
function processSource(
|
||||
date: string,
|
||||
entries: SourceLife[],
|
||||
source: 'minei' | 'bulgakov',
|
||||
copyright: string,
|
||||
idPrefix: string,
|
||||
candidates: Candidate[],
|
||||
skipCids: Set<string>,
|
||||
): void {
|
||||
for (const life of entries) {
|
||||
let best: Candidate | null = null;
|
||||
|
||||
const manualCid = findManualFix(date, life.title);
|
||||
@@ -428,11 +440,17 @@ export async function enrichLives(args: Args): Promise<void> {
|
||||
}
|
||||
|
||||
if (!best || best.score === 0) {
|
||||
report.push({ date, sourceTitle: life.title, matchedCid: null, matchedName: null, score: 0, status: 'unmatched' });
|
||||
report.push({ date, sourceTitle: life.title, source, matchedCid: null, matchedName: null, score: 0, status: 'unmatched' });
|
||||
unmatched++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// For Bulgakov: skip if this CId was already filled by Minei
|
||||
if (source === 'bulgakov' && skipCids.has(best.cid)) {
|
||||
report.push({ date, sourceTitle: life.title, source, matchedCid: best.cid, matchedName: best.name, score: best.score, status: 'already-filled' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const status = best.hasLife
|
||||
? 'already-filled'
|
||||
: best.score >= 0.6
|
||||
@@ -444,6 +462,7 @@ export async function enrichLives(args: Args): Promise<void> {
|
||||
report.push({
|
||||
date,
|
||||
sourceTitle: life.title,
|
||||
source,
|
||||
matchedCid: best.score >= 0.35 ? best.cid : null,
|
||||
matchedName: best.score >= 0.35 ? best.name : null,
|
||||
score: best.score,
|
||||
@@ -458,10 +477,11 @@ export async function enrichLives(args: Args): Promise<void> {
|
||||
const entry = pendingWrites[key][best.cid] as Record<string, unknown>;
|
||||
const existingLife = entry.life as { id?: string; text?: string } | undefined;
|
||||
if (existingLife?.text) {
|
||||
entry.life = { id: existingLife.id!, copyright: COPYRIGHT, text: `${existingLife.text}<p><b>${life.title}</b></p>${life.html}` };
|
||||
entry.life = { id: existingLife.id!, copyright, text: `${existingLife.text}<p><b>${life.title}</b></p>${life.html}` };
|
||||
} else {
|
||||
entry.life = { id: `minei-${date.replace('-', '')}-${best.cid}`, copyright: COPYRIGHT, text: life.html };
|
||||
entry.life = { id: `${idPrefix}-${date.replace('-', '')}-${best.cid}`, copyright, text: life.html };
|
||||
}
|
||||
if (source === 'minei') skipCids.add(best.cid);
|
||||
} else if (status === 'review') {
|
||||
needReview++;
|
||||
} else if (status === 'unmatched') {
|
||||
@@ -470,6 +490,45 @@ export async function enrichLives(args: Args): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Build a set of all dates from both sources
|
||||
const allDates = new Set(Object.keys(minei));
|
||||
if (Object.keys(bulgakov).length > 0) {
|
||||
for (const d of Object.keys(bulgakov)) allDates.add(d);
|
||||
}
|
||||
|
||||
for (const date of [...allDates].sort()) {
|
||||
if (args.month && !date.startsWith(args.month)) continue;
|
||||
|
||||
// Candidate CIds for this date from the menaion bundle
|
||||
const bundleIds = (bundle[date] || []).map(e => String(e.id));
|
||||
const candidates: Candidate[] = bundleIds
|
||||
.map(cid => {
|
||||
const entry = livesByCid[cid];
|
||||
if (!entry) return null;
|
||||
const name = entry.name.nominative || entry.name.short || '';
|
||||
return {
|
||||
cid,
|
||||
name,
|
||||
file: entry.file,
|
||||
hasLife: !!entry.life?.text,
|
||||
score: 0,
|
||||
} as Candidate;
|
||||
})
|
||||
.filter((c): c is Candidate => c !== null);
|
||||
|
||||
// Process Minei entries first (primary source)
|
||||
const mineiLives = minei[date] || [];
|
||||
processSource(date, mineiLives, 'minei', COPYRIGHT_MINEI, 'minei', candidates, filledByMinei);
|
||||
|
||||
// Process Bulgakov entries second (fills gaps only)
|
||||
if (Object.keys(bulgakov).length > 0) {
|
||||
const bulgakovLives = bulgakov[date] || [];
|
||||
if (bulgakovLives.length > 0) {
|
||||
processSource(date, bulgakovLives, 'bulgakov', COPYRIGHT_BULGAKOV, 'bulgakov', candidates, filledByMinei);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mkdirSync(dirname(REPORT_PATH), { recursive: true });
|
||||
writeFileSync(REPORT_PATH, JSON.stringify(report, null, 0));
|
||||
console.log(`Auto-applied: ${autoApplied}, needs review: ${needReview}, unmatched: ${unmatched}`);
|
||||
|
||||
@@ -1 +1 @@
|
||||
[{"date":"01-01","sourceTitle":"Слово на Обрезание Христово","matchedCid":"010101","matchedName":"Обрезание Господне","score":0.5,"status":"already-filled"},{"date":"01-01","sourceTitle":"Житие святого отца нашего Василия Великого, архиепископа Кесарийского","matchedCid":"772426","matchedName":"Свт. Василия Великого, Архиепископа Кесарии Каппадокийской","score":0.5714285714285714,"status":"already-filled"},{"date":"01-01","sourceTitle":"Память святого мученика Василия Анкирского","matchedCid":"772440","matchedName":"Мч. Василия Анкирского (ок. 362)","score":1,"status":"already-filled"},{"date":"01-02","sourceTitle":"Житие преподобного отца нашего Серафима Саровского","matchedCid":"010202","matchedName":"Преставление (1833), второе обретение (1991) мощей прп. Серафима, Саровского чудотворца","score":1,"status":"already-filled"},{"date":"01-02","sourceTitle":"Житие святого отца нашего Сильвестра, папы Римского","matchedCid":"772455","matchedName":"Свт. Сильвестра, Папы Римского (335)","score":1,"status":"already-filled"},{"date":"01-02","sourceTitle":"Память святого священномученика Феогена, епископа Парийского","matchedCid":"772459","matchedName":"Сщмч. Феогена, еп Парийского (ок. 320)","score":1,"status":"already-filled"},{"date":"01-03","sourceTitle":"Страдание святого мученика Гордия","matchedCid":"144","matchedName":"Мч. Гордия (IV)","score":1,"status":"already-filled"},{"date":"01-03","sourceTitle":"Память святого пророка Малахии","matchedCid":"143","matchedName":"Прор. Малахии (ок. 400 г. до Р.Х.)","score":1,"status":"already-filled"},{"date":"01-04","sourceTitle":"Собор святых семидесяти Апостолов","matchedCid":"146","matchedName":"Собор 70-ти апостолов: Иакова, брата Господня, Марка и Луки евангелистов, Клеопы, Симеона, Варнавы(Иосифа), Варсавы(Иуста, Иосифа, Иосии), Фаддея, Анании, первомч. архидиакона Стефана, Филиппа, Прохора, Никанора, Тимона, Пармена, Тимофея, Тита, Филимона, Онисима, Епафраса, Архиппа, Силы, Силуана, Крискента, Криспа, Епенета, Андроника, Стахия, Амплия, Урвана, Наркисса, Апеллия, Аристовула, Родиона (Иродиона), Агава, Руфа, Асинкрита, Флегонта, Ерма, Патрова, Ермия, Лина, Гаия, Филолога, Лукия, Иасона, Сосипатра, Олимпа (Олимпана), Тертия, Ераста, Куарта, Евода, Онисифора, Климента, Сосфена, Аполлоса, Тихика, Епафродита, Карпа, Кодрата, Марка, Зины, Аристарха, Пуда, Трофима, Марка, Артемы, Акилы, Фортуната и Ахаика, Дионисия Ареопагита и Симеона Нигера,(Кифы, Кесаря и Алфея)","score":1,"status":"already-filled"},{"date":"01-04","sourceTitle":"Житие святого Евстафия, архиепископа Сербского","matchedCid":"714","matchedName":"Свт. Евстафия I, архиеп. Сербского (ок. 1285)","score":1,"status":"already-filled"},{"date":"01-04","sourceTitle":"Память святых преподобномученика Зосимы и мученика Афанасия","matchedCid":"150","matchedName":"Прмч. Зосимы, пустынника Киликийского и мч. Афанасия комментарисия (III-IV)","score":0.5,"status":"already-filled"},{"date":"01-05","sourceTitle":"Страдание святых мучеников Феопемпта и Феоны","matchedCid":"155","matchedName":"Сщмч. Феопемпта, еп. Никомидийского и мч. Феоны волхва (303)","score":0.6666666666666666,"status":"already-filled"},{"date":"01-05","sourceTitle":"Память преподобной Синклитикии","matchedCid":"156","matchedName":"Прп. Синклитикии Александрийской (ок. 350)","score":0.6666666666666666,"status":"already-filled"},{"date":"01-05","sourceTitle":"Житие преподобной Аполлинарии","matchedCid":"158","matchedName":"Прп. АполлинаLine truncated
|
||||
[{"date":"01-01","sourceTitle":"Слово на Обрезание Христово","source":"minei","matchedCid":"010101","matchedName":"Обрезание Господне","score":0.5,"status":"already-filled"},{"date":"01-01","sourceTitle":"Житие святого отца нашего Василия Великого, архиепископа Кесарийского","source":"minei","matchedCid":"772426","matchedName":"Свт. Василия Великого, Архиепископа Кесарии Каппадокийской","score":0.5714285714285714,"status":"already-filled"},{"date":"01-01","sourceTitle":"Память святого мученика Василия Анкирского","source":"minei","matchedCid":"772440","matchedName":"Мч. Василия Анкирского (ок. 362)","score":1,"status":"already-filled"},{"date":"01-01","sourceTitle":"Новый год. В настоящее время 1 января у нас начинается новый гражданский (см. 1 сент.) год.","source":"bulgakov","matchedCid":null,"matchedName":null,"score":0,"status":"unmatched"},{"date":"01-02","sourceTitle":"Житие преподобного отца нашего Серафима Саровского","source":"minei","matchedCid":"010202","matchedName":"Преставление (1833), второе обретение (1991) мощей прп. Серафима, Саровского чудотворца","score":1,"status":"already-filled"},{"date":"01-02","sourceTitle":"Житие святого отца нашего Сильвестра, папы Римского","source":"minei","matchedCid":"772455","matchedName":"Свт. Сильвестра, Папы Римского (335)","score":1,"status":"already-filled"},{"date":"01-02","sourceTitle":"Память святого священномученика Феогена, епископа Парийского","source":"minei","matchedCid":"772459","matchedName":"Сщмч. Феогена, еп Парийского (ок. 320)","score":1,"status":"already-filled"},{"date":"01-02","sourceTitle":"Предпра́зднство просвѣще́нїѧ. Из церковных песнопений этого дня.","source":"bulgakov","matchedCid":null,"matchedName":null,"score":0.2222222222222222,"status":"unmatched"},{"date":"01-03","sourceTitle":"Страдание святого мученика Гордия","source":"minei","matchedCid":"144","matchedName":"Мч. Гордия (IV)","score":1,"status":"already-filled"},{"date":"01-03","sourceTitle":"Память святого пророка Малахии","source":"minei","matchedCid":"143","matchedName":"Прор. Малахии (ок. 400 г. до Р.Х.)","score":1,"status":"already-filled"},{"date":"01-03","sourceTitle":"Препра́зднство просвѣще́нїѧ. Из церковных песнопений этого дня.","source":"bulgakov","matchedCid":null,"matchedName":null,"score":0,"status":"unmatched"},{"date":"01-04","sourceTitle":"Собор святых семидесяти Апостолов","source":"minei","matchedCid":"146","matchedName":"Собор 70-ти апостолов: Иакова, брата Господня, Марка и Луки евангелистов, Клеопы, Симеона, Варнавы(Иосифа), Варсавы(Иуста, Иосифа, Иосии), Фаддея, Анании, первомч. архидиакона Стефана, Филиппа, Прохора, Никанора, Тимона, Пармена, Тимофея, Тита, Филимона, Онисима, Епафраса, Архиппа, Силы, Силуана, Крискента, Криспа, Епенета, Андроника, Стахия, Амплия, Урвана, Наркисса, Апеллия, Аристовула, Родиона (Иродиона), Агава, Руфа, Асинкрита, Флегонта, Ерма, Патрова, Ермия, Лина, Гаия, Филолога, Лукия, Иасона, Сосипатра, Олимпа (Олимпана), Тертия, Ераста, Куарта, Евода, Онисифора, Климента, Сосфена, Аполлоса, Тихика, Епафродита, Карпа, Кодрата, Марка, Зины, Аристарха, Пуда, Трофима, Марка, Артемы, Акилы, Фортуната и Ахаика, Дионисия Ареопагита и Симеона Нигера,(Кифы, Кесаря и Алфея)","score":1,"status":"already-filled"},{"date":"01-04","sourceTitle":"Житие святого Евстафия, архиепископа Сербского","source":"minei","matchedCid":"714","matchedName":"Свт. Евстафия I, архиеп. Сербского (ок. 1285)","score":1,"status":"already-filled"},{"date":"01-04","sourceTitle":"Память святых преподобномученика Зосимы и мLine truncated
|
||||
Reference in new issue
Block a user