106 lines
2.9 KiB
TypeScript
106 lines
2.9 KiB
TypeScript
|
|
import type { EdgeVocabulary } from "./types";
|
||
|
|
|
||
|
|
const BACKTICK_RE = /`([^`]+)`/g;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Parses markdown tables containing edge vocabulary definitions.
|
||
|
|
*
|
||
|
|
* Expected format:
|
||
|
|
* | System-Typ (Canonical) | Inverser Typ | Erlaubte Aliasse (User) | Beschreibung ... |
|
||
|
|
* | **`caused_by`** | `resulted_in` | `ausgelöst_durch`, `wegen`, ... | ... |
|
||
|
|
*
|
||
|
|
* Rules:
|
||
|
|
* - Extract all backticked tokens from each row
|
||
|
|
* - First token = canonical (may be wrapped in ** **)
|
||
|
|
* - Second token = inverse (optional)
|
||
|
|
* - Remaining tokens = aliases (skip if cell contains "(Kein Alias)")
|
||
|
|
* - Skip rows with less than 1 token (with warning counter)
|
||
|
|
* - Store canonical as-is, but use lowercase for lookup keys
|
||
|
|
*/
|
||
|
|
export function parseEdgeVocabulary(md: string): EdgeVocabulary {
|
||
|
|
const lines = md.split(/\r?\n/);
|
||
|
|
const byCanonical = new Map<string, { canonical: string; inverse?: string; aliases: string[] }>();
|
||
|
|
const aliasToCanonical = new Map<string, string>();
|
||
|
|
|
||
|
|
let skippedRows = 0;
|
||
|
|
|
||
|
|
for (const line of lines) {
|
||
|
|
// Skip header separator rows (e.g., "| :--- | :--- |")
|
||
|
|
if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Only process table rows
|
||
|
|
if (!line.trim().startsWith("|")) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extract all backticked tokens
|
||
|
|
const tokens: string[] = [];
|
||
|
|
let match: RegExpExecArray | null;
|
||
|
|
BACKTICK_RE.lastIndex = 0;
|
||
|
|
while ((match = BACKTICK_RE.exec(line)) !== null) {
|
||
|
|
if (match[1]) {
|
||
|
|
const token = match[1].trim();
|
||
|
|
if (token) {
|
||
|
|
tokens.push(token);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Skip rows with no tokens or only one token (need at least canonical)
|
||
|
|
if (tokens.length < 1) {
|
||
|
|
skippedRows++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if aliases cell contains "(Kein Alias)"
|
||
|
|
const hasNoAliases = /\(Kein Alias\)/i.test(line);
|
||
|
|
|
||
|
|
const canonical = tokens[0];
|
||
|
|
if (!canonical) {
|
||
|
|
skippedRows++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const inverse = tokens.length >= 2 && tokens[1] ? tokens[1] : undefined;
|
||
|
|
|
||
|
|
// Extract aliases: all tokens after the first two, but only if not "(Kein Alias)"
|
||
|
|
const aliases: string[] = [];
|
||
|
|
if (!hasNoAliases && tokens.length > 2) {
|
||
|
|
for (let i = 2; i < tokens.length; i++) {
|
||
|
|
const alias = tokens[i];
|
||
|
|
if (alias) {
|
||
|
|
aliases.push(alias);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Store canonical entry
|
||
|
|
byCanonical.set(canonical, {
|
||
|
|
canonical,
|
||
|
|
inverse,
|
||
|
|
aliases,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Build alias-to-canonical mapping (case-insensitive keys)
|
||
|
|
for (const alias of aliases) {
|
||
|
|
if (alias) {
|
||
|
|
const lowerAlias = alias.toLowerCase();
|
||
|
|
if (!aliasToCanonical.has(lowerAlias)) {
|
||
|
|
aliasToCanonical.set(lowerAlias, canonical);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (skippedRows > 0) {
|
||
|
|
console.warn(`parseEdgeVocabulary: Skipped ${skippedRows} rows with insufficient tokens`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
byCanonical,
|
||
|
|
aliasToCanonical,
|
||
|
|
};
|
||
|
|
}
|