nejimakibird_model-weave/main.js
2026-04-20 11:24:33 +09:00

1763 lines
49 KiB
JavaScript

"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => ModelingToolPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian4 = require("obsidian");
// src/core/schema-detector.ts
var SCHEMA_TO_FILE_TYPE = {
model_object_v1: "object",
model_relations_v1: "relations",
diagram_v1: "diagram"
};
function detectFileType(value) {
const schema = typeof value === "string" ? value : value?.schema;
if (!schema) {
return "markdown";
}
return SCHEMA_TO_FILE_TYPE[schema] ?? "markdown";
}
// src/parsers/frontmatter-parser.ts
function parseFrontmatter(markdown) {
const normalized = markdown.replace(/\r\n/g, "\n");
const warnings = [];
if (!normalized.startsWith("---\n")) {
return {
file: {
body: normalized
},
warnings
};
}
const lines = normalized.split("\n");
let closingIndex = -1;
for (let index = 1; index < lines.length; index += 1) {
if (lines[index].trim() === "---") {
closingIndex = index;
break;
}
}
if (closingIndex === -1) {
warnings.push(createWarning("frontmatter parse error: missing closing delimiter"));
return {
file: {
body: normalized
},
warnings
};
}
const frontmatterLines = lines.slice(1, closingIndex);
const body = lines.slice(closingIndex + 1).join("\n");
const parsed = parseYamlLikeFrontmatter(frontmatterLines);
warnings.push(...parsed.warnings);
return {
file: {
frontmatter: parsed.frontmatter,
body
},
warnings
};
}
function parseYamlLikeFrontmatter(lines) {
const warnings = [];
const frontmatter = {};
let activeListKey = null;
for (const rawLine of lines) {
const trimmed = rawLine.trim();
if (!trimmed || trimmed.startsWith("#")) {
continue;
}
const listItemMatch = rawLine.match(/^\s*-\s+(.+)$/);
if (listItemMatch) {
if (!activeListKey) {
warnings.push(
createWarning(
`frontmatter parse error: unexpected list item "${trimmed}"`
)
);
continue;
}
const currentValue = frontmatter[activeListKey];
if (!Array.isArray(currentValue)) {
frontmatter[activeListKey] = [];
}
frontmatter[activeListKey].push(
parseScalarValue(listItemMatch[1].trim())
);
continue;
}
activeListKey = null;
const keyValueMatch = rawLine.match(/^\s*([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
if (!keyValueMatch) {
warnings.push(
createWarning(`frontmatter parse error: malformed line "${trimmed}"`)
);
continue;
}
const [, key, rawValue] = keyValueMatch;
const value = rawValue.trim();
if (!value) {
frontmatter[key] = [];
activeListKey = key;
continue;
}
frontmatter[key] = parseScalarValue(value);
}
return {
frontmatter,
warnings
};
}
function parseScalarValue(value) {
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
if (/^\[(.*)\]$/.test(value)) {
const inner = value.slice(1, -1).trim();
if (!inner) {
return [];
}
return inner.split(",").map((entry) => stripQuotes(entry.trim()));
}
if (/^-?\d+(\.\d+)?$/.test(value)) {
return Number(value);
}
return stripQuotes(value);
}
function stripQuotes(value) {
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
return value.slice(1, -1);
}
return value;
}
function createWarning(message) {
return {
code: "frontmatter-parse-error",
message,
severity: "warning"
};
}
// src/parsers/markdown-sections.ts
var SECTION_HEADINGS = {
"# Summary": "Summary",
"## Attributes": "Attributes",
"## Methods": "Methods",
"## Notes": "Notes",
"## Relations": "Relations",
"## Objects": "Objects"
};
function extractMarkdownSections(body) {
const normalized = body.replace(/\r\n/g, "\n");
const sections = {};
let currentSection = null;
for (const line of normalized.split("\n")) {
const trimmed = line.trim();
const nextSection = SECTION_HEADINGS[trimmed];
if (nextSection) {
currentSection = nextSection;
sections[currentSection] = [];
continue;
}
if (/^#{1,6}\s+/.test(trimmed)) {
currentSection = null;
continue;
}
if (currentSection) {
sections[currentSection].push(line);
}
}
return sections;
}
// src/types/enums.ts
var CORE_OBJECT_KINDS = [
"class",
"entity",
"interface",
"enum",
"component"
];
var RESERVED_OBJECT_KINDS = [
"actor",
"usecase"
];
var CORE_RELATION_KINDS = [
"association",
"dependency",
"composition",
"aggregation",
"inheritance",
"implementation",
"reference",
"flow"
];
var RESERVED_RELATION_KINDS = [
"include",
"extend",
"transition",
"message"
];
var CORE_DIAGRAM_KINDS = [
"class",
"er",
"flow",
"component"
];
var RESERVED_DIAGRAM_KINDS = [
"usecase",
"activity",
"sequence"
];
// src/parsers/object-parser.ts
function parseObjectFile(markdown, path) {
const frontmatterResult = parseFrontmatter(markdown);
const warnings = [...frontmatterResult.warnings];
const frontmatter = frontmatterResult.file.frontmatter ?? {};
const schema = getString(frontmatter, "schema");
if (detectFileType(frontmatter) !== "object" || schema !== "model_object_v1") {
warnings.push(
createWarning2(
"unknown-schema",
`object parser expected schema "model_object_v1" but received "${schema ?? "none"}"`,
path,
"schema"
)
);
return {
file: null,
warnings
};
}
const sections = extractMarkdownSections(frontmatterResult.file.body);
const name = getString(frontmatter, "name");
const rawKind = getString(frontmatter, "kind");
const summary = joinSectionLines(sections.Summary);
const attributes = parseAttributes(sections.Attributes, warnings, path);
const methods = parseMethods(sections.Methods, warnings, path);
warnIfMissingSection(sections, "Summary", warnings, path);
warnIfMissingSection(sections, "Attributes", warnings, path);
warnIfMissingSection(sections, "Methods", warnings, path);
warnIfMissingSection(sections, "Notes", warnings, path);
if (!name) {
warnings.push(
createWarning2("missing-name", 'missing required field "name"', path, "name")
);
}
if (!rawKind) {
warnings.push(
createWarning2("missing-kind", 'missing required field "kind"', path, "kind")
);
} else if (isReservedObjectKind(rawKind)) {
warnings.push(
createInfoWarning(
"reserved-kind-used",
`reserved kind used: "${rawKind}"`,
path,
"kind"
)
);
} else if (!isCoreObjectKind(rawKind)) {
warnings.push(
createWarning2("invalid-kind", `invalid kind "${rawKind}"`, path, "kind")
);
}
const file = {
fileType: "object",
schema: "model_object_v1",
path,
title: getString(frontmatter, "title"),
frontmatter,
sections,
name: name ?? getString(frontmatter, "id") ?? "unknown",
kind: normalizeObjectKind(rawKind),
description: summary || void 0,
attributes,
methods
};
return {
file,
warnings
};
}
function parseAttributes(lines, warnings, path) {
if (!lines) {
return [];
}
const attributes = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
const match = trimmed.match(/^-\s+([^:]+?)\s*:\s*(.+?)(?:\s+-\s+(.+))?$/);
if (!match) {
warnings.push(
createWarning2(
"invalid-attribute-line",
`malformed attribute line: "${trimmed}"`,
path,
"Attributes"
)
);
continue;
}
const [, name, type, note] = match;
attributes.push({
name: name.trim(),
type: type.trim(),
description: note?.trim(),
raw: trimmed
});
}
return attributes;
}
function parseMethods(lines, warnings, path) {
if (!lines) {
return [];
}
const methods = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
const match = trimmed.match(
/^-\s+([A-Za-z_][\w]*)\(([^)]*)\)\s+([^\-].*?)(?:\s+-\s+(.+))?$/
);
if (!match) {
warnings.push(
createWarning2(
"invalid-method-line",
`malformed method line: "${trimmed}"`,
path,
"Methods"
)
);
continue;
}
const [, name, rawParameters, rawReturnType, note] = match;
methods.push({
name,
parameters: parseMethodParameters(rawParameters),
returnType: rawReturnType.trim(),
description: note?.trim(),
raw: trimmed
});
}
return methods;
}
function parseMethodParameters(rawParameters) {
const trimmed = rawParameters.trim();
if (!trimmed) {
return [];
}
return trimmed.split(",").map((parameter) => {
const value = parameter.trim();
const match = value.match(/^([A-Za-z_][\w]*)(\?)?\s*:\s*(.+)$/);
if (!match) {
return {
name: value,
required: true
};
}
const [, name, optionalFlag, type] = match;
return {
name,
type: type.trim(),
required: optionalFlag !== "?"
};
});
}
function warnIfMissingSection(sections, sectionName, warnings, path) {
if (sections[sectionName]) {
return;
}
warnings.push(
createInfoWarning(
"section-missing",
`section missing: "${sectionName}"`,
path,
sectionName
)
);
}
function joinSectionLines(lines) {
if (!lines) {
return "";
}
return lines.map((line) => line.trim()).filter(Boolean).join("\n");
}
function getString(frontmatter, key) {
const value = frontmatter[key];
return typeof value === "string" && value.trim() ? value.trim() : void 0;
}
function normalizeObjectKind(kind) {
if (kind && (isCoreObjectKind(kind) || isReservedObjectKind(kind))) {
return kind;
}
return "class";
}
function isCoreObjectKind(kind) {
return CORE_OBJECT_KINDS.includes(kind);
}
function isReservedObjectKind(kind) {
return RESERVED_OBJECT_KINDS.includes(kind);
}
function createWarning2(code, message, path, field) {
return {
code,
message,
severity: "warning",
path,
field
};
}
function createInfoWarning(code, message, path, field) {
return {
code,
message,
severity: "info",
path,
field
};
}
// src/parsers/relations-parser.ts
function parseRelationsFile(markdown, path) {
const frontmatterResult = parseFrontmatter(markdown);
const warnings = [...frontmatterResult.warnings];
const frontmatter = frontmatterResult.file.frontmatter ?? {};
const schema = getString2(frontmatter, "schema");
if (detectFileType(frontmatter) !== "relations" || schema !== "model_relations_v1") {
warnings.push(
createWarning3(
"unknown-schema",
`relations parser expected schema "model_relations_v1" but received "${schema ?? "none"}"`,
path,
"schema"
)
);
return {
file: null,
warnings
};
}
const sections = extractMarkdownSections(frontmatterResult.file.body);
if (!sections.Relations) {
warnings.push(
createInfoWarning2(
"section-missing",
'section missing: "Relations"',
path,
"Relations"
)
);
}
const relations = parseRelationsSection(sections.Relations, warnings, path);
return {
file: {
fileType: "relations",
schema: "model_relations_v1",
path,
title: getString2(frontmatter, "title"),
frontmatter,
sections,
relations
},
warnings
};
}
function parseRelationsSection(lines, warnings, path) {
if (!lines) {
return [];
}
const relations = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
const record = parseRelationRecord(trimmed);
if (!record) {
warnings.push(
createWarning3(
"invalid-relation-record",
`malformed relation record: "${trimmed}"`,
path,
"Relations"
)
);
continue;
}
const missingFields = ["id", "from", "to", "kind"].filter(
(field) => !record[field]
);
if (missingFields.length > 0) {
warnings.push(
createWarning3(
"invalid-relation-record",
`malformed relation record: missing ${missingFields.join(", ")}`,
path,
"Relations"
)
);
continue;
}
const rawKind = record.kind;
if (isReservedRelationKind(rawKind)) {
warnings.push(
createInfoWarning2(
"reserved-relation-kind-used",
`reserved kind used: "${rawKind}"`,
path,
"kind"
)
);
} else if (!isCoreRelationKind(rawKind)) {
warnings.push(
createWarning3(
"invalid-relation-kind",
`invalid relation kind "${rawKind}"`,
path,
"kind"
)
);
}
relations.push({
id: record.id,
source: record.from,
target: record.to,
kind: normalizeRelationKind(rawKind),
label: typeof record.label === "string" ? record.label : void 0,
sourceCardinality: typeof record.from_multiplicity === "string" ? record.from_multiplicity : void 0,
targetCardinality: typeof record.to_multiplicity === "string" ? record.to_multiplicity : void 0,
metadata: {
raw: trimmed
}
});
}
return relations;
}
function parseRelationRecord(line) {
const bulletMatch = line.match(/^-\s+(.+)$/);
if (!bulletMatch) {
return null;
}
const record = {};
for (const part of bulletMatch[1].split(",")) {
const segment = part.trim();
if (!segment) {
continue;
}
const match = segment.match(/^([A-Za-z_][\w]*)\s*:\s*(.+)$/);
if (!match) {
return null;
}
const [, key, value] = match;
record[key] = value.trim();
}
return record;
}
function getString2(frontmatter, key) {
const value = frontmatter[key];
return typeof value === "string" && value.trim() ? value.trim() : void 0;
}
function isCoreRelationKind(kind) {
return CORE_RELATION_KINDS.includes(kind);
}
function isReservedRelationKind(kind) {
return RESERVED_RELATION_KINDS.includes(kind);
}
function normalizeRelationKind(kind) {
if (isCoreRelationKind(kind) || isReservedRelationKind(kind)) {
return kind;
}
return "association";
}
function createWarning3(code, message, path, field) {
return {
code,
message,
severity: "warning",
path,
field
};
}
function createInfoWarning2(code, message, path, field) {
return {
code,
message,
severity: "info",
path,
field
};
}
// src/parsers/diagram-parser.ts
function parseDiagramFile(markdown, path) {
const frontmatterResult = parseFrontmatter(markdown);
const warnings = [...frontmatterResult.warnings];
const frontmatter = frontmatterResult.file.frontmatter ?? {};
const schema = getString3(frontmatter, "schema");
if (detectFileType(frontmatter) !== "diagram" || schema !== "diagram_v1") {
warnings.push(
createWarning4(
"unknown-schema",
`diagram parser expected schema "diagram_v1" but received "${schema ?? "none"}"`,
path,
"schema"
)
);
return {
file: null,
warnings
};
}
const sections = extractMarkdownSections(frontmatterResult.file.body);
const name = getString3(frontmatter, "name");
const rawDiagramKind = getString3(frontmatter, "diagram_kind");
const objectRefs = parseObjectRefs(sections.Objects, warnings, path);
const autoRelations = normalizeAutoRelations(frontmatter.auto_relations);
const nodes = objectRefs.map((ref) => ({
id: ref,
ref
}));
if (!name) {
warnings.push(
createWarning4("missing-name", 'missing required field "name"', path, "name")
);
}
if (!rawDiagramKind) {
warnings.push(
createWarning4(
"missing-kind",
'missing required field "diagram_kind"',
path,
"diagram_kind"
)
);
} else if (isReservedDiagramKind(rawDiagramKind)) {
warnings.push(
createInfoWarning3(
"reserved-diagram-kind-used",
`reserved kind used: "${rawDiagramKind}"`,
path,
"diagram_kind"
)
);
} else if (!isCoreDiagramKind(rawDiagramKind)) {
warnings.push(
createWarning4(
"invalid-diagram-kind",
`invalid diagram kind "${rawDiagramKind}"`,
path,
"diagram_kind"
)
);
}
if (!sections.Objects) {
warnings.push(
createInfoWarning3(
"section-missing",
'section missing: "Objects"',
path,
"Objects"
)
);
}
return {
file: {
fileType: "diagram",
schema: "diagram_v1",
path,
title: getString3(frontmatter, "title"),
frontmatter,
sections,
name: name ?? getString3(frontmatter, "id") ?? "unknown",
kind: normalizeDiagramKind(rawDiagramKind),
objectRefs,
autoRelations,
nodes,
edges: []
},
warnings
};
}
function parseObjectRefs(lines, warnings, path) {
if (!lines) {
return [];
}
const objectRefs = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
const match = trimmed.match(/^-\s+ref\s*:\s*(.+)$/);
if (!match) {
warnings.push(
createWarning4(
"invalid-object-ref",
`malformed object ref: "${trimmed}"`,
path,
"Objects"
)
);
continue;
}
const ref = match[1].trim();
objectRefs.push(ref);
}
return objectRefs;
}
function getString3(frontmatter, key) {
const value = frontmatter[key];
return typeof value === "string" && value.trim() ? value.trim() : void 0;
}
function isCoreDiagramKind(kind) {
return CORE_DIAGRAM_KINDS.includes(kind);
}
function isReservedDiagramKind(kind) {
return RESERVED_DIAGRAM_KINDS.includes(kind);
}
function normalizeDiagramKind(kind) {
if (kind && (isCoreDiagramKind(kind) || isReservedDiagramKind(kind))) {
return kind;
}
return "class";
}
function normalizeAutoRelations(value) {
if (typeof value === "boolean") {
return value;
}
if (typeof value === "string") {
return value.trim().toLowerCase() === "true";
}
return false;
}
function createWarning4(code, message, path, field) {
return {
code,
message,
severity: "warning",
path,
field
};
}
function createInfoWarning3(code, message, path, field) {
return {
code,
message,
severity: "info",
path,
field
};
}
// src/core/validator.ts
var RESERVED_OBJECT_KINDS2 = /* @__PURE__ */ new Set(["actor", "usecase"]);
var RESERVED_RELATION_KINDS2 = /* @__PURE__ */ new Set([
"include",
"extend",
"transition",
"message"
]);
var RESERVED_DIAGRAM_KINDS2 = /* @__PURE__ */ new Set(["usecase", "activity", "sequence"]);
function validateVaultIndex(index) {
const warnings = [];
const idRegistry = /* @__PURE__ */ new Map();
for (const [objectId, object] of Object.entries(index.objectsById)) {
registerId(idRegistry, objectId, object.path, warnings);
validateFilenameMatchesId(objectId, object.path, warnings);
validateReservedObjectKind(object, objectId, warnings);
}
for (const [fileId, relationsFile] of Object.entries(index.relationsFilesById)) {
registerId(idRegistry, fileId, relationsFile.path, warnings);
validateFilenameMatchesId(fileId, relationsFile.path, warnings);
for (const relation of relationsFile.relations) {
if (relation.id) {
registerId(idRegistry, relation.id, relationsFile.path, warnings);
}
validateRelationEndpoints(relation.source, relation.target, relationsFile.path, index, warnings);
if (RESERVED_RELATION_KINDS2.has(relation.kind)) {
warnings.push({
code: "reserved-relation-kind-used",
message: `reserved kind used: "${relation.kind}"`,
severity: "info",
path: relationsFile.path,
field: "kind"
});
}
}
}
for (const [diagramId, diagram] of Object.entries(index.diagramsById)) {
registerId(idRegistry, diagramId, diagram.path, warnings);
validateFilenameMatchesId(diagramId, diagram.path, warnings);
validateDiagram(diagram, index, warnings);
}
return dedupeWarnings(warnings);
}
function validateDiagram(diagram, index, warnings) {
if (RESERVED_DIAGRAM_KINDS2.has(diagram.kind)) {
warnings.push({
code: "reserved-diagram-kind-used",
message: `reserved kind used: "${diagram.kind}"`,
severity: "info",
path: diagram.path,
field: "diagram_kind"
});
}
for (const objectRef of diagram.objectRefs) {
if (!index.objectsById[objectRef]) {
warnings.push({
code: "unresolved-reference",
message: `unresolved object ref "${objectRef}"`,
severity: "warning",
path: diagram.path,
field: "objectRefs"
});
}
}
}
function validateReservedObjectKind(object, objectId, warnings) {
if (!RESERVED_OBJECT_KINDS2.has(object.kind)) {
return;
}
warnings.push({
code: "reserved-kind-used",
message: `reserved kind used: "${object.kind}"`,
severity: "info",
path: object.path,
field: objectId
});
}
function validateRelationEndpoints(source, target, path, index, warnings) {
if (!index.objectsById[source] || !index.objectsById[target]) {
warnings.push({
code: "unresolved-reference",
message: `unresolved relation endpoint: "${source}" -> "${target}"`,
severity: "warning",
path,
field: "relations"
});
}
}
function registerId(registry, id, path, warnings) {
const existing = registry.get(id);
if (!existing) {
registry.set(id, path);
return;
}
warnings.push({
code: "invalid-structure",
message: `duplicate id detected: "${id}"`,
severity: "warning",
path,
field: "id"
});
}
function validateFilenameMatchesId(id, path, warnings) {
const baseName = path.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "");
if (!baseName || baseName === id) {
return;
}
warnings.push({
code: "invalid-structure",
message: `filename and id mismatch: "${baseName}" != "${id}"`,
severity: "info",
path,
field: "id"
});
}
function dedupeWarnings(warnings) {
return warnings.filter((warning, index) => {
return warnings.findIndex(
(entry) => entry.code === warning.code && entry.message === warning.message && entry.path === warning.path && entry.field === warning.field
) === index;
});
}
// src/core/vault-index.ts
function buildVaultIndex(files) {
const index = {
sourceFilesByPath: {},
objectsById: {},
relationsFilesById: {},
diagramsById: {},
modelsByFilePath: {},
relationsById: {},
relationsByObjectId: {},
warningsByFilePath: {}
};
for (const file of files) {
index.sourceFilesByPath[file.path] = file;
indexSingleFile(index, file);
}
for (const warning of validateVaultIndex(index)) {
pushWarning(index.warningsByFilePath, warning.path ?? "vault", warning);
}
return index;
}
function indexSingleFile(index, file) {
const parseResult = parseVaultFile(file);
for (const warning of parseResult.warnings) {
pushWarning(index.warningsByFilePath, file.path, {
...warning,
path: warning.path ?? file.path
});
}
if (!parseResult.file) {
return;
}
index.modelsByFilePath[file.path] = parseResult.file;
switch (parseResult.file.fileType) {
case "object": {
const objectId = getModelId(parseResult.file);
addModelById(
index.objectsById,
objectId,
parseResult.file,
index.warningsByFilePath,
file.path
);
break;
}
case "relations": {
const relationsFileId = getModelId(parseResult.file);
addModelById(
index.relationsFilesById,
relationsFileId,
parseResult.file,
index.warningsByFilePath,
file.path
);
for (const relation of parseResult.file.relations) {
if (relation.id) {
addModelById(
index.relationsById,
relation.id,
relation,
index.warningsByFilePath,
file.path
);
}
addRelationForObject(index.relationsByObjectId, relation.source, relation);
addRelationForObject(index.relationsByObjectId, relation.target, relation);
}
break;
}
case "diagram": {
const diagramId = getModelId(parseResult.file);
addModelById(
index.diagramsById,
diagramId,
parseResult.file,
index.warningsByFilePath,
file.path
);
break;
}
case "markdown":
break;
}
}
function parseVaultFile(file) {
const frontmatterResult = parseFrontmatter(file.content);
const frontmatter = frontmatterResult.file.frontmatter;
const fileType = detectFileType(frontmatter);
switch (fileType) {
case "object":
return parseObjectFile(file.content, file.path);
case "relations":
return parseRelationsFile(file.content, file.path);
case "diagram":
return parseDiagramFile(file.content, file.path);
case "markdown":
default:
return {
file: createMarkdownModel(file.path, frontmatterResult.file.body, frontmatter),
warnings: frontmatterResult.warnings
};
}
}
function createMarkdownModel(path, body, frontmatter) {
return {
fileType: "markdown",
path,
title: typeof frontmatter?.title === "string" ? frontmatter.title : void 0,
frontmatter: frontmatter ?? {},
sections: extractMarkdownSections(body),
content: body
};
}
function addModelById(target, id, model, warningsByFilePath, path) {
if (!target[id]) {
target[id] = model;
return;
}
pushWarning(warningsByFilePath, path, {
code: "invalid-structure",
message: `duplicate id detected: "${id}"`,
severity: "warning",
path,
field: "id"
});
}
function addRelationForObject(relationsByObjectId, objectId, relation) {
if (!relationsByObjectId[objectId]) {
relationsByObjectId[objectId] = [];
}
relationsByObjectId[objectId].push(relation);
}
function getModelId(model) {
const explicitId = model.frontmatter.id;
if (typeof explicitId === "string" && explicitId.trim()) {
return explicitId.trim();
}
if ("name" in model && typeof model.name === "string" && model.name.trim()) {
return model.name.trim();
}
return getBasename(model.path);
}
function getBasename(path) {
const slashNormalized = path.replace(/\\/g, "/");
const rawName = slashNormalized.split("/").pop() ?? path;
return rawName.replace(/\.md$/i, "");
}
function pushWarning(warningsByFilePath, path, warning) {
if (!warningsByFilePath[path]) {
warningsByFilePath[path] = [];
}
const exists = warningsByFilePath[path].some(
(entry) => entry.code === warning.code && entry.message === warning.message && entry.field === warning.field
);
if (!exists) {
warningsByFilePath[path].push(warning);
}
}
// src/core/relation-resolver.ts
function resolveDiagramRelations(diagram, index) {
const warnings = [];
const resolvedNodes = [];
const presentObjectIds = /* @__PURE__ */ new Set();
for (const objectRef of diagram.objectRefs) {
const object = index.objectsById[objectRef];
if (!object) {
warnings.push({
code: "unresolved-reference",
message: `unresolved object ref "${objectRef}"`,
severity: "warning",
path: diagram.path,
field: "objectRefs"
});
} else {
presentObjectIds.add(objectRef);
}
resolvedNodes.push({
id: objectRef,
ref: objectRef,
object
});
}
if (!diagram.autoRelations) {
warnings.push({
code: "invalid-structure",
message: "autoRelations: false is treated as true in v1 preview",
severity: "info",
path: diagram.path,
field: "autoRelations"
});
}
const edges = resolveEdges(diagram, index, presentObjectIds, warnings);
return {
diagram,
nodes: resolvedNodes,
edges,
missingObjects: diagram.objectRefs.filter((ref) => !index.objectsById[ref]),
warnings
};
}
function resolveEdges(diagram, index, presentObjectIds, warnings) {
const edges = [];
const seenRelationIds = /* @__PURE__ */ new Set();
for (const objectId of presentObjectIds) {
const relations = index.relationsByObjectId[objectId] ?? [];
for (const relation of relations) {
const relationKey = relation.id ?? buildRelationKey(relation);
if (seenRelationIds.has(relationKey)) {
continue;
}
seenRelationIds.add(relationKey);
if (!index.objectsById[relation.source] || !index.objectsById[relation.target]) {
warnings.push({
code: "unresolved-reference",
message: `unresolved relation endpoint in relation "${relation.id ?? relationKey}"`,
severity: "warning",
path: diagram.path,
field: "relations"
});
continue;
}
if (presentObjectIds.has(relation.source) && presentObjectIds.has(relation.target)) {
edges.push(toDiagramEdge(relation));
}
}
}
return edges;
}
function toDiagramEdge(relation) {
return {
id: relation.id,
source: relation.source,
target: relation.target,
kind: relation.kind,
label: relation.label,
metadata: {
sourceCardinality: relation.sourceCardinality,
targetCardinality: relation.targetCardinality
}
};
}
function buildRelationKey(relation) {
return `${relation.source}:${relation.kind}:${relation.target}:${relation.label ?? ""}`;
}
// src/views/diagram-preview-view.ts
var import_obsidian = require("obsidian");
// src/renderers/object-renderer.ts
function renderObjectModel(model) {
const root = document.createElement("section");
root.className = `mdspec-object mdspec-object--${model.kind}`;
const header = document.createElement("header");
header.className = "mdspec-object__header";
const title = document.createElement("h2");
title.textContent = model.name;
const badge = document.createElement("span");
badge.className = "mdspec-badge";
badge.textContent = model.kind;
header.append(title, badge);
root.appendChild(header);
if (model.description) {
root.appendChild(createSection("Summary", createParagraph(model.description)));
}
const variant = document.createElement("p");
variant.className = "mdspec-object__variant";
variant.textContent = describeObjectKind(model.kind);
root.appendChild(variant);
root.appendChild(createMembersSection("Attributes", model.attributes.map((attribute) => {
const detail = attribute.type ? `: ${attribute.type}` : "";
const note = attribute.description ? ` - ${attribute.description}` : "";
return `${attribute.name}${detail}${note}`;
})));
root.appendChild(createMembersSection("Methods", model.methods.map((method) => {
const parameters = method.parameters.map(
(parameter) => `${parameter.name}${parameter.type ? `: ${parameter.type}` : ""}`
).join(", ");
const returnType = method.returnType ? ` ${method.returnType}` : "";
const note = method.description ? ` - ${method.description}` : "";
return `${method.name}(${parameters})${returnType}${note}`;
})));
return root;
}
function describeObjectKind(kind) {
switch (kind) {
case "class":
return "Class-style object";
case "entity":
return "Entity-style object";
case "interface":
return "Interface contract";
case "enum":
return "Enum definition";
case "component":
return "Component boundary";
default:
return "Reserved kind preview";
}
}
function createMembersSection(title, items) {
const section = document.createElement("section");
section.className = "mdspec-section";
const heading = document.createElement("h3");
heading.textContent = title;
section.appendChild(heading);
if (items.length === 0) {
const empty = document.createElement("p");
empty.textContent = "No items.";
section.appendChild(empty);
return section;
}
const list = document.createElement("ul");
for (const item of items) {
const entry = document.createElement("li");
entry.textContent = item;
list.appendChild(entry);
}
section.appendChild(list);
return section;
}
function createSection(title, content) {
const section = document.createElement("section");
section.className = "mdspec-section";
const heading = document.createElement("h3");
heading.textContent = title;
section.append(heading, content);
return section;
}
function createParagraph(text) {
const paragraph = document.createElement("p");
paragraph.textContent = text;
return paragraph;
}
// src/renderers/class-renderer.ts
function renderClassDiagram(diagram) {
const root = document.createElement("section");
root.className = "mdspec-diagram mdspec-diagram--class";
const title = document.createElement("h2");
title.textContent = `${diagram.diagram.name} (class)`;
root.appendChild(title);
const grid = document.createElement("div");
grid.className = "mdspec-diagram__grid";
for (const node of diagram.nodes) {
const card = document.createElement("div");
card.className = "mdspec-diagram__node";
if (node.object) {
card.appendChild(renderObjectModel(node.object));
} else {
card.appendChild(createFallbackNode(node.ref ?? node.id));
}
grid.appendChild(card);
}
root.appendChild(grid);
root.appendChild(createEdgesSection(diagram));
return root;
}
function createEdgesSection(diagram) {
const section = document.createElement("section");
section.className = "mdspec-section";
const heading = document.createElement("h3");
heading.textContent = "Relations";
section.appendChild(heading);
if (diagram.edges.length === 0) {
const empty = document.createElement("p");
empty.textContent = "No relations resolved.";
section.appendChild(empty);
return section;
}
const list = document.createElement("ul");
for (const edge of diagram.edges) {
const item = document.createElement("li");
item.textContent = `${edge.source} -[${edge.kind ?? "relation"}]-> ${edge.target}`;
list.appendChild(item);
}
section.appendChild(list);
return section;
}
function createFallbackNode(id) {
const box = document.createElement("div");
box.className = "mdspec-fallback";
box.textContent = `Unresolved object: ${id}`;
return box;
}
// src/renderers/component-renderer.ts
function renderComponentDiagram(diagram) {
const root = document.createElement("section");
root.className = "mdspec-diagram mdspec-diagram--component";
const title = document.createElement("h2");
title.textContent = `${diagram.diagram.name} (component)`;
root.appendChild(title);
const grid = document.createElement("div");
grid.className = "mdspec-component-grid";
for (const node of diagram.nodes) {
const box = document.createElement("article");
box.className = "mdspec-component";
const heading = document.createElement("h3");
heading.textContent = node.object?.name ?? node.ref ?? node.id;
box.appendChild(heading);
const description = document.createElement("p");
description.textContent = node.object?.description ?? "No component description available.";
box.appendChild(description);
grid.appendChild(box);
}
if (grid.childElementCount === 0) {
const empty = document.createElement("p");
empty.textContent = "No components resolved.";
root.appendChild(empty);
} else {
root.appendChild(grid);
}
return root;
}
// src/renderers/er-renderer.ts
function renderErDiagram(diagram) {
const root = document.createElement("section");
root.className = "mdspec-diagram mdspec-diagram--er";
const title = document.createElement("h2");
title.textContent = `${diagram.diagram.name} (ER)`;
root.appendChild(title);
const entities = document.createElement("div");
entities.className = "mdspec-diagram__entities";
for (const node of diagram.nodes) {
const entity = document.createElement("article");
entity.className = "mdspec-entity";
const heading = document.createElement("h3");
heading.textContent = node.object?.name ?? node.ref ?? node.id;
entity.appendChild(heading);
const list = document.createElement("ul");
for (const attribute of node.object?.attributes ?? []) {
const item = document.createElement("li");
item.textContent = `${attribute.name}${attribute.type ? `: ${attribute.type}` : ""}`;
list.appendChild(item);
}
if (list.childElementCount === 0) {
const empty = document.createElement("p");
empty.textContent = "No attributes.";
entity.appendChild(empty);
} else {
entity.appendChild(list);
}
entities.appendChild(entity);
}
root.appendChild(entities);
root.appendChild(createRelationList(diagram));
return root;
}
function createRelationList(diagram) {
const section = document.createElement("section");
const heading = document.createElement("h3");
heading.textContent = "Resolved relations";
section.appendChild(heading);
if (diagram.edges.length === 0) {
const empty = document.createElement("p");
empty.textContent = "No relations resolved.";
section.appendChild(empty);
return section;
}
const list = document.createElement("ul");
for (const edge of diagram.edges) {
const item = document.createElement("li");
const sourceMultiplicity = typeof edge.metadata?.sourceCardinality === "string" ? ` [${edge.metadata.sourceCardinality}]` : "";
const targetMultiplicity = typeof edge.metadata?.targetCardinality === "string" ? ` [${edge.metadata.targetCardinality}]` : "";
item.textContent = `${edge.source}${sourceMultiplicity} -[${edge.kind ?? "relation"}]-> ${edge.target}${targetMultiplicity}`;
list.appendChild(item);
}
section.appendChild(list);
return section;
}
// src/renderers/flow-renderer.ts
function renderFlowDiagram(diagram) {
const root = document.createElement("section");
root.className = "mdspec-diagram mdspec-diagram--flow";
const title = document.createElement("h2");
title.textContent = `${diagram.diagram.name} (flow)`;
root.appendChild(title);
const list = document.createElement("ol");
list.className = "mdspec-flow";
for (const node of diagram.nodes) {
const item = document.createElement("li");
item.textContent = node.object?.name ?? node.ref ?? node.id;
list.appendChild(item);
}
if (list.childElementCount === 0) {
const empty = document.createElement("p");
empty.textContent = "No objects referenced.";
root.appendChild(empty);
} else {
root.appendChild(list);
}
if (diagram.edges.length > 0) {
const relations = document.createElement("ul");
for (const edge of diagram.edges) {
const item = document.createElement("li");
item.textContent = `${edge.source} -> ${edge.target}${edge.label ? ` (${edge.label})` : ""}`;
relations.appendChild(item);
}
root.appendChild(relations);
}
return root;
}
// src/renderers/diagram-renderer.ts
function renderDiagramModel(diagram) {
switch (diagram.diagram.kind) {
case "class":
return renderClassDiagram(diagram);
case "er":
return renderErDiagram(diagram);
case "flow":
return renderFlowDiagram(diagram);
case "component":
return renderComponentDiagram(diagram);
default:
return createReservedKindFallback(diagram.diagram.kind);
}
}
function createReservedKindFallback(kind) {
const root = document.createElement("section");
root.className = "mdspec-fallback";
const title = document.createElement("h2");
title.textContent = "Diagram preview is not available";
const message = document.createElement("p");
message.textContent = `Reserved diagram kind "${kind}" is not rendered in v1.`;
root.append(title, message);
return root;
}
// src/views/diagram-preview-view.ts
var DIAGRAM_PREVIEW_VIEW_TYPE = "mdspec-diagram-preview";
var DiagramPreviewView = class extends import_obsidian.ItemView {
constructor(leaf) {
super(leaf);
this.diagram = null;
this.warnings = [];
}
getViewType() {
return DIAGRAM_PREVIEW_VIEW_TYPE;
}
getDisplayText() {
return "Diagram Preview";
}
async onOpen() {
this.render();
}
async onClose() {
this.contentEl.empty();
}
setPreview(diagram, warnings = []) {
this.diagram = diagram;
this.warnings = warnings;
this.render();
}
render() {
this.contentEl.empty();
renderWarningBar(this.contentEl, this.warnings);
if (!this.diagram) {
this.contentEl.createEl("p", { text: "No diagram model available for preview." });
return;
}
this.contentEl.appendChild(renderDiagramModel(this.diagram));
}
};
function renderWarningBar(container, warnings) {
if (warnings.length === 0) {
return;
}
const bar = container.createDiv({ cls: "mdspec-warning-bar" });
bar.createEl("strong", { text: `Warnings (${warnings.length})` });
const list = bar.createEl("ul");
for (const warning of warnings) {
list.createEl("li", { text: warning.message });
}
}
// src/views/object-preview-view.ts
var import_obsidian2 = require("obsidian");
var OBJECT_PREVIEW_VIEW_TYPE = "mdspec-object-preview";
var ObjectPreviewView = class extends import_obsidian2.ItemView {
constructor(leaf) {
super(leaf);
this.model = null;
this.warnings = [];
}
getViewType() {
return OBJECT_PREVIEW_VIEW_TYPE;
}
getDisplayText() {
return "Object Preview";
}
async onOpen() {
this.render();
}
async onClose() {
this.contentEl.empty();
}
setPreview(model, warnings = []) {
this.model = model;
this.warnings = warnings;
this.render();
}
render() {
this.contentEl.empty();
renderWarningBar2(this.contentEl, this.warnings);
if (!this.model) {
this.contentEl.createEl("p", { text: "No object model available for preview." });
return;
}
this.contentEl.appendChild(renderObjectModel(this.model));
}
};
function renderWarningBar2(container, warnings) {
if (warnings.length === 0) {
return;
}
const bar = container.createDiv({ cls: "mdspec-warning-bar" });
bar.createEl("strong", { text: `Warnings (${warnings.length})` });
const list = bar.createEl("ul");
for (const warning of warnings) {
list.createEl("li", { text: warning.message });
}
}
// src/views/relations-preview-view.ts
var import_obsidian3 = require("obsidian");
var RELATIONS_PREVIEW_VIEW_TYPE = "mdspec-relations-preview";
var RelationsPreviewView = class extends import_obsidian3.ItemView {
constructor(leaf) {
super(leaf);
this.model = null;
this.warnings = [];
}
getViewType() {
return RELATIONS_PREVIEW_VIEW_TYPE;
}
getDisplayText() {
return "Relations Preview";
}
async onOpen() {
this.render();
}
async onClose() {
this.contentEl.empty();
}
setPreview(model, warnings = []) {
this.model = model;
this.warnings = warnings;
this.render();
}
render() {
this.contentEl.empty();
renderWarningBar3(this.contentEl, this.warnings);
if (!this.model) {
this.contentEl.createEl("p", { text: "No relations model available for preview." });
return;
}
this.contentEl.createEl("h2", { text: this.model.title ?? this.model.frontmatter.id?.toString() ?? "Relations" });
if (this.model.relations.length === 0) {
this.contentEl.createEl("p", { text: "No relations defined." });
return;
}
const list = this.contentEl.createEl("ul");
for (const relation of this.model.relations) {
const label = relation.label ? ` (${relation.label})` : "";
list.createEl("li", {
text: `${relation.source} -[${relation.kind}]-> ${relation.target}${label}`
});
}
}
};
function renderWarningBar3(container, warnings) {
if (warnings.length === 0) {
return;
}
const bar = container.createDiv({ cls: "mdspec-warning-bar" });
bar.createEl("strong", { text: `Warnings (${warnings.length})` });
const list = bar.createEl("ul");
for (const warning of warnings) {
list.createEl("li", { text: warning.message });
}
}
// src/main.ts
var ModelingToolPlugin = class extends import_obsidian4.Plugin {
constructor() {
super(...arguments);
this.index = null;
this.previewLeaf = null;
}
async onload() {
this.registerView(
OBJECT_PREVIEW_VIEW_TYPE,
(leaf) => new ObjectPreviewView(leaf)
);
this.registerView(
RELATIONS_PREVIEW_VIEW_TYPE,
(leaf) => new RelationsPreviewView(leaf)
);
this.registerView(
DIAGRAM_PREVIEW_VIEW_TYPE,
(leaf) => new DiagramPreviewView(leaf)
);
this.addCommand({
id: "rebuild-modeling-index",
name: "Rebuild modeling index",
callback: async () => {
await this.rebuildIndex();
new import_obsidian4.Notice("Modeling index rebuilt");
}
});
this.addCommand({
id: "open-modeling-preview",
name: "Open modeling preview for active file",
callback: async () => {
await this.openPreviewForActiveFile();
}
});
this.registerEvent(
this.app.workspace.on("file-open", async () => {
if (this.previewLeaf) {
await this.refreshOpenPreview();
}
})
);
this.registerEvent(
this.app.vault.on("modify", async () => {
await this.rebuildIndex();
})
);
this.registerEvent(
this.app.vault.on("create", async () => {
await this.rebuildIndex();
})
);
this.registerEvent(
this.app.vault.on("delete", async () => {
await this.rebuildIndex();
})
);
this.registerEvent(
this.app.vault.on("rename", async () => {
await this.rebuildIndex();
})
);
await this.rebuildIndex();
console.info("[modeling-tool-obsidian] plugin loaded");
}
onunload() {
if (this.previewLeaf) {
this.previewLeaf.detach();
this.previewLeaf = null;
}
console.info("[modeling-tool-obsidian] plugin unloaded");
}
async rebuildIndex() {
const files = await Promise.all(
this.app.vault.getMarkdownFiles().map(async (file) => ({
path: file.path,
content: await this.app.vault.cachedRead(file)
}))
);
this.index = buildVaultIndex(files);
}
async openPreviewForActiveFile() {
if (!this.index) {
await this.rebuildIndex();
}
const file = this.app.workspace.getActiveFile();
if (!file) {
new import_obsidian4.Notice("No active markdown file");
return;
}
await this.showPreviewForFile(file);
}
async refreshOpenPreview() {
const file = this.app.workspace.getActiveFile();
if (!file || !this.previewLeaf) {
return;
}
await this.showPreviewForFile(file, this.previewLeaf);
}
async showPreviewForFile(file, preferredLeaf) {
if (!this.index) {
await this.rebuildIndex();
}
if (!this.index) {
return;
}
const model = this.index.modelsByFilePath[file.path];
if (!model) {
new import_obsidian4.Notice("No parsed model for the active file");
return;
}
switch (detectFileType(model.frontmatter)) {
case "object":
await this.showObjectPreview(model.path, preferredLeaf);
return;
case "relations":
await this.showRelationsPreview(model.path, preferredLeaf);
return;
case "diagram":
await this.showDiagramPreview(model.path, preferredLeaf);
return;
case "markdown":
default:
new import_obsidian4.Notice("Active file is not a supported modeling document");
}
}
async showObjectPreview(path, preferredLeaf) {
const leaf = await this.ensureLeaf(OBJECT_PREVIEW_VIEW_TYPE, preferredLeaf);
const view = leaf.view;
const model = this.index?.modelsByFilePath[path];
if (view instanceof ObjectPreviewView) {
view.setPreview(
model?.fileType === "object" ? model : null,
this.index?.warningsByFilePath[path] ?? []
);
}
}
async showRelationsPreview(path, preferredLeaf) {
const leaf = await this.ensureLeaf(RELATIONS_PREVIEW_VIEW_TYPE, preferredLeaf);
const view = leaf.view;
const model = this.index?.modelsByFilePath[path];
if (view instanceof RelationsPreviewView) {
view.setPreview(
model?.fileType === "relations" ? model : null,
this.index?.warningsByFilePath[path] ?? []
);
}
}
async showDiagramPreview(path, preferredLeaf) {
const leaf = await this.ensureLeaf(DIAGRAM_PREVIEW_VIEW_TYPE, preferredLeaf);
const view = leaf.view;
const model = this.index?.modelsByFilePath[path];
if (view instanceof DiagramPreviewView) {
const resolved = model?.fileType === "diagram" && this.index ? resolveDiagramRelations(model, this.index) : null;
const warnings = [
...this.index?.warningsByFilePath[path] ?? [],
...resolved?.warnings ?? []
];
view.setPreview(resolved, warnings);
}
}
async ensureLeaf(viewType, preferredLeaf) {
const leaf = preferredLeaf ?? this.previewLeaf ?? this.app.workspace.getRightLeaf(false) ?? this.app.workspace.getLeaf(true);
await leaf.setViewState({
type: viewType,
active: true
});
this.previewLeaf = leaf;
return leaf;
}
};