mirror of
https://github.com/nejimakibird/model-weave.git
synced 2026-07-22 06:51:24 +00:00
19765 lines
663 KiB
JavaScript
19765 lines
663 KiB
JavaScript
"use strict";
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// src/main.ts
|
|
var main_exports = {};
|
|
__export(main_exports, {
|
|
default: () => ModelWeavePlugin
|
|
});
|
|
module.exports = __toCommonJS(main_exports);
|
|
var import_obsidian8 = require("obsidian");
|
|
|
|
// src/core/dfd-object-scene.ts
|
|
function buildDfdObjectScene(object) {
|
|
const nodes = /* @__PURE__ */ new Map();
|
|
const warnings = [];
|
|
nodes.set(object.id, {
|
|
id: object.id,
|
|
ref: object.id,
|
|
kind: object.kind,
|
|
object
|
|
});
|
|
const diagram = {
|
|
fileType: "dfd-diagram",
|
|
schema: "dfd_diagram",
|
|
path: object.path,
|
|
title: `${object.name} related`,
|
|
frontmatter: {
|
|
type: "dfd_diagram",
|
|
id: `${object.id}-related`,
|
|
name: `${object.name} related`
|
|
},
|
|
sections: {},
|
|
sourceLinks: [],
|
|
id: `${object.id}-related`,
|
|
name: `${object.name} related`,
|
|
kind: "dfd",
|
|
objectRefs: Array.from(nodes.keys()),
|
|
objectEntries: [
|
|
{
|
|
id: object.id,
|
|
label: object.name,
|
|
kind: object.kind,
|
|
ref: object.id,
|
|
rowIndex: 0,
|
|
compatibilityMode: "explicit"
|
|
}
|
|
],
|
|
nodes: Array.from(nodes.values()).map(({ object: ignored, ...node }) => node),
|
|
edges: [],
|
|
flows: []
|
|
};
|
|
return {
|
|
diagram,
|
|
nodes: Array.from(nodes.values()),
|
|
edges: [],
|
|
missingObjects: [],
|
|
warnings
|
|
};
|
|
}
|
|
|
|
// src/core/reference-resolver.ts
|
|
function normalizeReferenceTarget(reference) {
|
|
const parsed = parseReferenceValue(reference);
|
|
const target = parsed?.target?.trim();
|
|
if (target) {
|
|
return target;
|
|
}
|
|
return reference.trim().replace(/\.md$/i, "").replace(/\\/g, "/");
|
|
}
|
|
function parseReferenceValue(reference) {
|
|
const trimmed = reference.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
if (trimmed.startsWith("[[") && trimmed.endsWith("]]")) {
|
|
const inner = trimmed.slice(2, -2).trim();
|
|
const [targetPart, aliasPart] = splitWikilinkParts(inner);
|
|
return {
|
|
raw: trimmed,
|
|
kind: "wikilink",
|
|
target: normalizeLinkTarget(unescapeWikilinkTarget(targetPart ?? "")),
|
|
display: unescapeReferenceLabel(aliasPart?.trim() || "") || void 0
|
|
};
|
|
}
|
|
const markdownLinkMatch = trimmed.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
if (markdownLinkMatch) {
|
|
const [, label, target] = markdownLinkMatch;
|
|
return {
|
|
raw: trimmed,
|
|
kind: "markdown_link",
|
|
target: isExternalLinkTarget(target) ? void 0 : normalizeLinkTarget(target),
|
|
display: label.trim() || void 0,
|
|
isExternal: isExternalLinkTarget(target)
|
|
};
|
|
}
|
|
return {
|
|
raw: trimmed,
|
|
kind: "raw",
|
|
target: normalizeLinkTarget(trimmed)
|
|
};
|
|
}
|
|
function parseQualifiedRef(reference) {
|
|
const trimmed = reference.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
if (trimmed.startsWith("[[")) {
|
|
const closeIndex = trimmed.indexOf("]]");
|
|
if (closeIndex >= 0) {
|
|
const baseRefRaw = trimmed.slice(0, closeIndex + 2);
|
|
const remainder = trimmed.slice(closeIndex + 2);
|
|
const memberRef = parseQualifiedMemberSuffix(remainder);
|
|
return {
|
|
raw: trimmed,
|
|
baseRefRaw,
|
|
memberRef: memberRef || void 0,
|
|
hasMemberRef: Boolean(memberRef)
|
|
};
|
|
}
|
|
}
|
|
const markdownLinkMatch = trimmed.match(/^\[[^\]]+\]\([^)]+\)/);
|
|
if (markdownLinkMatch) {
|
|
const baseRefRaw = markdownLinkMatch[0];
|
|
const remainder = trimmed.slice(baseRefRaw.length);
|
|
const memberRef = parseQualifiedMemberSuffix(remainder);
|
|
return {
|
|
raw: trimmed,
|
|
baseRefRaw,
|
|
memberRef: memberRef || void 0,
|
|
hasMemberRef: Boolean(memberRef)
|
|
};
|
|
}
|
|
const rawMatch = trimmed.match(/^(.+)\.([A-Za-z0-9_-]+)$/);
|
|
if (rawMatch && !isExternalLinkTarget(trimmed)) {
|
|
return {
|
|
raw: trimmed,
|
|
baseRefRaw: rawMatch[1].trim(),
|
|
memberRef: rawMatch[2],
|
|
hasMemberRef: true
|
|
};
|
|
}
|
|
return {
|
|
raw: trimmed,
|
|
baseRefRaw: trimmed,
|
|
memberRef: void 0,
|
|
hasMemberRef: false
|
|
};
|
|
}
|
|
function buildReferenceCandidates(reference) {
|
|
const normalized = normalizeReferenceTarget(reference);
|
|
if (!normalized) {
|
|
return [];
|
|
}
|
|
const basename = getBasename(normalized);
|
|
return Array.from(
|
|
new Set(
|
|
[reference.trim(), normalized, basename].filter(
|
|
(value) => Boolean(value && value.trim())
|
|
)
|
|
)
|
|
);
|
|
}
|
|
function resolveObjectModelReference(reference, index) {
|
|
for (const candidate of buildReferenceCandidates(reference)) {
|
|
const direct = index.objectsById[candidate];
|
|
if (direct) {
|
|
return direct;
|
|
}
|
|
}
|
|
const model = findModelByReference(reference, index);
|
|
return model?.fileType === "object" ? model : null;
|
|
}
|
|
function resolveErEntityReference(reference, index) {
|
|
for (const candidate of buildReferenceCandidates(reference)) {
|
|
const byId = index.erEntitiesById[candidate];
|
|
if (byId) {
|
|
return byId;
|
|
}
|
|
const byPhysicalName = index.erEntitiesByPhysicalName[candidate];
|
|
if (byPhysicalName) {
|
|
return byPhysicalName;
|
|
}
|
|
}
|
|
const model = findModelByReference(reference, index);
|
|
return model?.fileType === "er-entity" ? model : null;
|
|
}
|
|
function resolveDfdObjectReference(reference, index) {
|
|
for (const candidate of buildReferenceCandidates(reference)) {
|
|
const direct = index.dfdObjectsById[candidate];
|
|
if (direct) {
|
|
return direct;
|
|
}
|
|
}
|
|
const model = findModelByReference(reference, index);
|
|
return model?.fileType === "dfd-object" ? model : null;
|
|
}
|
|
function resolveDataObjectReference(reference, index) {
|
|
for (const candidate of buildReferenceCandidates(reference)) {
|
|
const direct = index.dataObjectsById[candidate];
|
|
if (direct) {
|
|
return direct;
|
|
}
|
|
}
|
|
const model = findModelByReference(reference, index);
|
|
return model?.fileType === "data-object" ? model : null;
|
|
}
|
|
function findModelByReference(reference, index) {
|
|
const candidates = buildReferenceCandidates(reference);
|
|
for (const candidate of candidates) {
|
|
const directPath = index.modelsByFilePath[candidate];
|
|
if (directPath) {
|
|
return directPath;
|
|
}
|
|
const markdownPath = `${candidate}.md`;
|
|
if (index.modelsByFilePath[markdownPath]) {
|
|
return index.modelsByFilePath[markdownPath];
|
|
}
|
|
}
|
|
for (const candidate of candidates) {
|
|
const byId = index.objectsById[candidate] ?? index.appProcessesById[candidate] ?? index.screensById[candidate] ?? index.codesetsById[candidate] ?? index.messagesById[candidate] ?? index.rulesById[candidate] ?? index.mappingsById[candidate] ?? index.dataObjectsById[candidate] ?? index.dfdObjectsById[candidate] ?? index.erEntitiesById[candidate] ?? index.erEntitiesByPhysicalName[candidate] ?? index.relationsFilesById[candidate] ?? index.diagramsById[candidate];
|
|
if (byId) {
|
|
return byId;
|
|
}
|
|
}
|
|
for (const candidate of candidates) {
|
|
const normalizedCandidate = candidate.replace(/\\/g, "/");
|
|
const withExtension = normalizedCandidate.endsWith(".md") ? normalizedCandidate : `${normalizedCandidate}.md`;
|
|
for (const [path2, model] of Object.entries(index.modelsByFilePath)) {
|
|
const normalizedPath = path2.replace(/\\/g, "/");
|
|
if (normalizedPath === withExtension || normalizedPath.endsWith(`/${withExtension}`) || getBasename(normalizedPath) === getBasename(normalizedCandidate)) {
|
|
return model;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function resolveReferenceIdentity(reference, index) {
|
|
const parsed = parseReferenceValue(reference);
|
|
const model = findModelByReference(reference, index);
|
|
return {
|
|
raw: reference.trim(),
|
|
parsed,
|
|
target: parsed?.target,
|
|
displayLabel: parsed?.display,
|
|
resolvedFile: model?.path,
|
|
resolvedId: getResolvedModelId(model),
|
|
resolvedModelType: model?.fileType,
|
|
resolvedModel: model
|
|
};
|
|
}
|
|
function getQualifiedMemberCandidates(baseReference, index) {
|
|
const identity = resolveReferenceIdentity(baseReference, index);
|
|
const merged = [];
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const addCandidates = (candidates) => {
|
|
for (const candidate of candidates ?? []) {
|
|
const key = [
|
|
candidate.ownerPath,
|
|
candidate.memberKind,
|
|
candidate.memberId,
|
|
candidate.sourceSection,
|
|
candidate.displayName ?? ""
|
|
].join("|");
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
merged.push(candidate);
|
|
}
|
|
};
|
|
if (identity.resolvedId) {
|
|
addCandidates(index.membersByOwnerId[identity.resolvedId]);
|
|
}
|
|
if (identity.resolvedFile) {
|
|
addCandidates(index.membersByOwnerPath[identity.resolvedFile]);
|
|
}
|
|
return merged;
|
|
}
|
|
function resolveQualifiedMemberReference(reference, index) {
|
|
const qualified = parseQualifiedRef(reference) ?? {
|
|
raw: reference.trim(),
|
|
baseRefRaw: reference.trim(),
|
|
memberRef: void 0,
|
|
hasMemberRef: false
|
|
};
|
|
const baseIdentity = resolveReferenceIdentity(qualified.baseRefRaw, index);
|
|
const memberResolution = !qualified.memberRef || !baseIdentity.resolvedModel ? "not-requested" : !isResolvedModelFullyParsed(index, baseIdentity.resolvedModel) ? "deferred" : "unresolved";
|
|
const member = memberResolution !== "deferred" && qualified.memberRef && baseIdentity.resolvedModel ? getQualifiedMemberCandidates(qualified.baseRefRaw, index).find(
|
|
(candidate) => candidate.memberId === qualified.memberRef
|
|
) ?? null : null;
|
|
return {
|
|
qualified,
|
|
baseIdentity,
|
|
member,
|
|
memberResolution: member ? "resolved" : memberResolution
|
|
};
|
|
}
|
|
function isResolvedModelFullyParsed(index, model) {
|
|
return Boolean(model?.path && index.state.fullParsedFilePaths[model.path]);
|
|
}
|
|
function buildReferenceIdentityKeys(identity) {
|
|
const targetBasename = identity.target ? getBasename(identity.target) : void 0;
|
|
const rawBasename = identity.raw ? getBasename(normalizeLinkTarget(identity.raw)) : void 0;
|
|
return Array.from(
|
|
new Set(
|
|
[
|
|
identity.resolvedFile ? `file:${identity.resolvedFile}` : null,
|
|
identity.resolvedId ? `id:${identity.resolvedId}` : null,
|
|
identity.target ? `target:${identity.target}` : null,
|
|
identity.parsed?.target ? `target:${identity.parsed.target}` : null,
|
|
targetBasename ? `basename:${targetBasename}` : null,
|
|
identity.raw ? `raw:${normalizeLinkTarget(identity.raw)}` : null,
|
|
rawBasename ? `basename:${rawBasename}` : null
|
|
].filter((value) => Boolean(value))
|
|
)
|
|
);
|
|
}
|
|
function referencesMatch(left, right, index) {
|
|
const leftKeys = new Set(buildReferenceIdentityKeys(resolveReferenceIdentity(left, index)));
|
|
const rightKeys = buildReferenceIdentityKeys(resolveReferenceIdentity(right, index));
|
|
return rightKeys.some((key) => leftKeys.has(key));
|
|
}
|
|
function getReferenceDisplayName(reference, resolvedModel) {
|
|
const parsed = parseReferenceValue(reference);
|
|
if (parsed?.display) {
|
|
return parsed.display;
|
|
}
|
|
if (resolvedModel) {
|
|
return getReferencedModelDisplayName(resolvedModel);
|
|
}
|
|
if (parsed?.target) {
|
|
return getBasename(parsed.target);
|
|
}
|
|
return parsed?.raw ?? reference.trim();
|
|
}
|
|
function getReferencedModelDisplayName(model) {
|
|
switch (model.fileType) {
|
|
case "data-object":
|
|
case "app-process":
|
|
case "screen":
|
|
case "codeset":
|
|
case "message":
|
|
case "rule":
|
|
case "mapping":
|
|
case "dfd-object":
|
|
case "dfd-diagram":
|
|
case "object":
|
|
return model.name;
|
|
case "er-entity":
|
|
return model.logicalName || model.physicalName || model.id;
|
|
case "diagram":
|
|
return model.name;
|
|
case "relations":
|
|
return model.title ?? model.path;
|
|
case "markdown":
|
|
default:
|
|
return typeof model.frontmatter.name === "string" && model.frontmatter.name || typeof model.frontmatter.title === "string" && model.frontmatter.title || getBasename(model.path);
|
|
}
|
|
}
|
|
function getResolvedModelId(model) {
|
|
if (!model) {
|
|
return void 0;
|
|
}
|
|
switch (model.fileType) {
|
|
case "object":
|
|
return typeof model.frontmatter.id === "string" && model.frontmatter.id.trim() ? model.frontmatter.id.trim() : model.name;
|
|
case "er-entity":
|
|
case "dfd-object":
|
|
case "dfd-diagram":
|
|
case "data-object":
|
|
case "app-process":
|
|
case "screen":
|
|
case "codeset":
|
|
case "message":
|
|
case "rule":
|
|
case "mapping":
|
|
return model.id;
|
|
case "diagram":
|
|
return model.name;
|
|
case "relations":
|
|
return typeof model.frontmatter.id === "string" && model.frontmatter.id.trim() ? model.frontmatter.id.trim() : model.title;
|
|
case "markdown":
|
|
default:
|
|
return void 0;
|
|
}
|
|
}
|
|
function getBasename(path2) {
|
|
const normalized = path2.replace(/\\/g, "/");
|
|
const leaf = normalized.split("/").pop() ?? normalized;
|
|
return leaf.replace(/\.md$/i, "");
|
|
}
|
|
function normalizeLinkTarget(value) {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return "";
|
|
}
|
|
if (trimmed.startsWith("#") || trimmed.startsWith("^")) {
|
|
return trimmed;
|
|
}
|
|
const withoutHeading = trimmed.split("#", 1)[0].trim();
|
|
const withoutBlock = withoutHeading.split("^", 1)[0].trim();
|
|
return withoutBlock.replace(/\.md$/i, "").replace(/\\/g, "/");
|
|
}
|
|
function isExternalLinkTarget(value) {
|
|
const trimmed = value.trim();
|
|
return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(trimmed);
|
|
}
|
|
function parseQualifiedMemberSuffix(value) {
|
|
const match = value.match(/^\.\s*([A-Za-z0-9_-]+)\s*$/);
|
|
return match?.[1] ?? null;
|
|
}
|
|
function splitWikilinkParts(value) {
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
const char = value[index];
|
|
if (char === "|") {
|
|
return [value.slice(0, index), value.slice(index + 1)];
|
|
}
|
|
if (char === "\\" && value[index + 1] === "|") {
|
|
return [value.slice(0, index), value.slice(index + 2)];
|
|
}
|
|
}
|
|
return [value];
|
|
}
|
|
function unescapeReferenceLabel(value) {
|
|
return value.replace(/\\\|/g, "|").trim();
|
|
}
|
|
function unescapeWikilinkTarget(value) {
|
|
return value.replace(/\\\|/g, "|").trim();
|
|
}
|
|
|
|
// src/core/object-context-resolver.ts
|
|
function resolveObjectContext(object, index) {
|
|
return object.fileType === "er-entity" ? resolveErEntityContext(object, index) : resolveClassLikeContext(object, index);
|
|
}
|
|
function resolveClassLikeContext(object, index) {
|
|
const warnings = [];
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const relatedObjects = [];
|
|
const objectId = getObjectId(object);
|
|
for (const relation of object.relations) {
|
|
const relationKey = buildClassRelationKey(relation);
|
|
if (seen.has(relationKey)) {
|
|
continue;
|
|
}
|
|
seen.add(relationKey);
|
|
const relatedReference = relation.targetClass;
|
|
const relatedObject = resolveObjectModelReference(relatedReference, index) ?? void 0;
|
|
const relatedObjectId = relatedObject ? getObjectId(relatedObject) : relation.targetClass;
|
|
if (!relatedObject) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved related object "${relatedObjectId}"`,
|
|
severity: "warning",
|
|
path: object.path,
|
|
field: "relatedObjects"
|
|
});
|
|
}
|
|
relatedObjects.push({
|
|
relation,
|
|
relatedObjectId,
|
|
relatedObject,
|
|
direction: "outgoing"
|
|
});
|
|
}
|
|
for (const candidate of Object.values(index.objectsById)) {
|
|
if (getObjectId(candidate) === objectId) {
|
|
continue;
|
|
}
|
|
for (const relation of candidate.relations) {
|
|
const targetObject = resolveObjectModelReference(relation.targetClass, index);
|
|
if (!targetObject || getObjectId(targetObject) !== objectId) {
|
|
continue;
|
|
}
|
|
const relationKey = buildClassRelationKey(relation);
|
|
if (seen.has(relationKey)) {
|
|
continue;
|
|
}
|
|
seen.add(relationKey);
|
|
relatedObjects.push({
|
|
relation,
|
|
relatedObjectId: getObjectId(candidate),
|
|
relatedObject: candidate,
|
|
direction: "incoming"
|
|
});
|
|
}
|
|
}
|
|
for (const relation of index.relationsByObjectId[objectId] ?? []) {
|
|
const relationKey = relation.id ?? buildRelationKey(relation);
|
|
if (seen.has(relationKey)) {
|
|
continue;
|
|
}
|
|
seen.add(relationKey);
|
|
const outgoing = relation.source === objectId;
|
|
const relatedReference = outgoing ? relation.target : relation.source;
|
|
const relatedObject = resolveObjectModelReference(relatedReference, index) ?? void 0;
|
|
const relatedObjectId = relatedObject ? getObjectId(relatedObject) : relatedReference;
|
|
if (!relatedObject) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved related object "${relatedObjectId}"`,
|
|
severity: "warning",
|
|
path: object.path,
|
|
field: "relatedObjects"
|
|
});
|
|
}
|
|
relatedObjects.push({
|
|
relation,
|
|
relatedObjectId,
|
|
relatedObject,
|
|
direction: outgoing ? "outgoing" : "incoming"
|
|
});
|
|
}
|
|
return {
|
|
object,
|
|
relatedObjects,
|
|
warnings
|
|
};
|
|
}
|
|
function resolveErEntityContext(object, index) {
|
|
const warnings = [];
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const relatedObjects = [];
|
|
const allEntities = Object.values(index.erEntitiesById);
|
|
for (const relation of object.outboundRelations) {
|
|
const relationKey = relation.id ?? `${object.id}:${relation.targetEntity}:${relation.kind}`;
|
|
if (seen.has(relationKey)) {
|
|
continue;
|
|
}
|
|
seen.add(relationKey);
|
|
const relatedObject = resolveErEntityReference(relation.targetEntity, index) ?? void 0;
|
|
const relatedObjectId = relatedObject?.id ?? relation.targetEntity;
|
|
if (!relatedObject) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved related entity "${relation.targetEntity}"`,
|
|
severity: "warning",
|
|
path: object.path,
|
|
field: "relatedObjects"
|
|
});
|
|
}
|
|
relatedObjects.push({
|
|
relation,
|
|
relatedObjectId,
|
|
relatedObject,
|
|
direction: "outgoing"
|
|
});
|
|
}
|
|
for (const entity of allEntities) {
|
|
if (entity.id === object.id) {
|
|
continue;
|
|
}
|
|
for (const relation of entity.outboundRelations) {
|
|
const targetEntity = resolveErEntityReference(relation.targetEntity, index);
|
|
if (!targetEntity || targetEntity.id !== object.id) {
|
|
continue;
|
|
}
|
|
const relationKey = relation.id ?? `${entity.id}:${relation.targetEntity}:${relation.kind}`;
|
|
if (seen.has(relationKey)) {
|
|
continue;
|
|
}
|
|
seen.add(relationKey);
|
|
relatedObjects.push({
|
|
relation,
|
|
relatedObjectId: entity.id,
|
|
relatedObject: entity,
|
|
direction: "incoming"
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
object,
|
|
relatedObjects,
|
|
warnings
|
|
};
|
|
}
|
|
function getObjectId(object) {
|
|
const explicitId = object.frontmatter.id;
|
|
if (typeof explicitId === "string" && explicitId.trim()) {
|
|
return explicitId.trim();
|
|
}
|
|
return object.name;
|
|
}
|
|
function buildRelationKey(relation) {
|
|
return `${relation.source}:${relation.kind}:${relation.target}:${relation.label ?? ""}`;
|
|
}
|
|
function buildClassRelationKey(relation) {
|
|
return relation.id ?? `${relation.sourceClass}:${relation.targetClass}:${relation.kind}:${relation.label ?? ""}`;
|
|
}
|
|
|
|
// src/i18n/language.ts
|
|
var import_obsidian = require("obsidian");
|
|
function resolveModelWeaveLanguage(language) {
|
|
return language?.toLowerCase().startsWith("ja") ? "ja" : "en";
|
|
}
|
|
function getModelWeaveLanguage() {
|
|
try {
|
|
return resolveModelWeaveLanguage((0, import_obsidian.getLanguage)());
|
|
} catch {
|
|
return "en";
|
|
}
|
|
}
|
|
function isJapaneseLanguage(language) {
|
|
return resolveModelWeaveLanguage(language ?? getModelWeaveLanguage()) === "ja";
|
|
}
|
|
function modelWeaveText(en, ja, language) {
|
|
return isJapaneseLanguage(language) ? ja : en;
|
|
}
|
|
|
|
// src/core/current-file-diagnostics.ts
|
|
var CLASS_RELATION_KINDS = /* @__PURE__ */ new Set([
|
|
"association",
|
|
"dependency",
|
|
"inheritance",
|
|
"implementation",
|
|
"aggregation",
|
|
"composition"
|
|
]);
|
|
function buildCurrentObjectDiagnostics(model, index, context, warnings) {
|
|
const diagnostics = warnings.map(
|
|
(warning) => normalizeDiagnosticSeverity(warning)
|
|
);
|
|
if (model.fileType === "object") {
|
|
diagnostics.push(...buildClassDiagnostics(model, index));
|
|
} else if (model.fileType === "app-process") {
|
|
diagnostics.push(...buildAppProcessDiagnostics(model, index));
|
|
} else if (model.fileType === "screen") {
|
|
diagnostics.push(...buildScreenDiagnostics(model, index));
|
|
} else if (model.fileType === "codeset") {
|
|
diagnostics.push(...buildCodeSetDiagnostics(model));
|
|
} else if (model.fileType === "message") {
|
|
diagnostics.push(...buildMessageDiagnostics(model));
|
|
} else if (model.fileType === "rule") {
|
|
diagnostics.push(...buildRuleDiagnostics(model, index));
|
|
} else if (model.fileType === "mapping") {
|
|
diagnostics.push(...buildMappingDiagnostics(model, index));
|
|
} else if (model.fileType === "dfd-object") {
|
|
diagnostics.push(...buildDfdObjectDiagnostics(model));
|
|
} else if (model.fileType === "data-object") {
|
|
diagnostics.push(...buildDataObjectDiagnostics(model, index));
|
|
} else {
|
|
diagnostics.push(...buildErEntityDiagnostics(model, index));
|
|
}
|
|
if (context) {
|
|
diagnostics.push(...context.warnings.map((warning) => normalizeDiagnosticSeverity(warning)));
|
|
}
|
|
return dedupeDiagnostics(diagnostics);
|
|
}
|
|
function buildCodeSetDiagnostics(model) {
|
|
const diagnostics = [];
|
|
const codes = /* @__PURE__ */ new Set();
|
|
const sortOrders = /* @__PURE__ */ new Set();
|
|
if (!model.kind?.trim()) {
|
|
diagnostics.push(createSectionWarning(model.path, "kind", "kind is empty"));
|
|
}
|
|
if (model.values.length === 0) {
|
|
diagnostics.push(createSectionWarning(model.path, "Values", "values are empty"));
|
|
return diagnostics;
|
|
}
|
|
for (const value of model.values) {
|
|
const code = value.code?.trim();
|
|
if (!code) {
|
|
diagnostics.push(createSectionError(model.path, "Values", "values.code is empty"));
|
|
} else {
|
|
if (codes.has(code)) {
|
|
diagnostics.push(createSectionError(model.path, "Values", `duplicate code "${code}"`));
|
|
}
|
|
codes.add(code);
|
|
}
|
|
if (!value.label?.trim()) {
|
|
diagnostics.push(createSectionWarning(model.path, "Values", `label is empty for code "${code ?? "(blank)"}"`));
|
|
}
|
|
const active = value.active?.trim();
|
|
if (!active) {
|
|
diagnostics.push(createSectionWarning(model.path, "Values", `active is empty for code "${code ?? "(blank)"}"`));
|
|
} else if (active !== "Y" && active !== "N") {
|
|
diagnostics.push(createSectionWarning(model.path, "Values", `active must be Y or N for code "${code ?? "(blank)"}"`));
|
|
} else if (active === "N") {
|
|
diagnostics.push(createSectionInfo(model.path, "Values", `inactive code "${code ?? "(blank)"}" is defined`));
|
|
}
|
|
const sortOrder = value.sortOrder?.trim();
|
|
if (sortOrder) {
|
|
if (!/^-?\d+(\.\d+)?$/.test(sortOrder)) {
|
|
diagnostics.push(createSectionWarning(model.path, "Values", `sort_order is not numeric for code "${code ?? "(blank)"}"`));
|
|
}
|
|
if (sortOrders.has(sortOrder)) {
|
|
diagnostics.push(createSectionWarning(model.path, "Values", `duplicate sort_order "${sortOrder}"`));
|
|
}
|
|
sortOrders.add(sortOrder);
|
|
} else {
|
|
diagnostics.push(createSectionInfo(model.path, "Values", `sort_order is empty for code "${code ?? "(blank)"}"`));
|
|
}
|
|
if (!value.notes?.trim()) {
|
|
diagnostics.push(createSectionInfo(model.path, "Values", `notes are empty for code "${code ?? "(blank)"}"`));
|
|
}
|
|
}
|
|
return diagnostics;
|
|
}
|
|
function buildMessageDiagnostics(model) {
|
|
const diagnostics = [];
|
|
const messageIds = /* @__PURE__ */ new Set();
|
|
if (!model.kind?.trim()) {
|
|
diagnostics.push(createSectionWarning(model.path, "kind", "kind is empty"));
|
|
}
|
|
if (model.messages.length === 0) {
|
|
diagnostics.push(createSectionWarning(model.path, "Messages", "messages are empty"));
|
|
return diagnostics;
|
|
}
|
|
for (const entry of model.messages) {
|
|
const messageId = entry.messageId?.trim();
|
|
if (!messageId) {
|
|
diagnostics.push(createSectionError(model.path, "Messages", "messages.message_id is empty"));
|
|
} else {
|
|
if (messageIds.has(messageId)) {
|
|
diagnostics.push(createSectionError(model.path, "Messages", `duplicate message_id "${messageId}"`));
|
|
}
|
|
messageIds.add(messageId);
|
|
}
|
|
if (!entry.text?.trim()) {
|
|
diagnostics.push(createSectionError(model.path, "Messages", `text is empty for message_id "${messageId ?? "(blank)"}"`));
|
|
}
|
|
const severity = entry.severity?.trim();
|
|
if (!severity) {
|
|
diagnostics.push(createSectionWarning(model.path, "Messages", `severity is empty for message_id "${messageId ?? "(blank)"}"`));
|
|
} else if (!["info", "success", "warning", "error", "confirm", "other"].includes(severity)) {
|
|
diagnostics.push(createSectionWarning(model.path, "Messages", `severity is invalid for message_id "${messageId ?? "(blank)"}"`));
|
|
}
|
|
if (!entry.timing?.trim()) {
|
|
diagnostics.push(createSectionWarning(model.path, "Messages", `timing is empty for message_id "${messageId ?? "(blank)"}"`));
|
|
}
|
|
if (!entry.audience?.trim()) {
|
|
diagnostics.push(createSectionWarning(model.path, "Messages", `audience is empty for message_id "${messageId ?? "(blank)"}"`));
|
|
}
|
|
const active = entry.active?.trim();
|
|
if (!active) {
|
|
diagnostics.push(createSectionWarning(model.path, "Messages", `active is empty for message_id "${messageId ?? "(blank)"}"`));
|
|
} else if (active !== "Y" && active !== "N") {
|
|
diagnostics.push(createSectionWarning(model.path, "Messages", `active must be Y or N for message_id "${messageId ?? "(blank)"}"`));
|
|
} else if (active === "N") {
|
|
diagnostics.push(createSectionInfo(model.path, "Messages", `inactive message "${messageId ?? "(blank)"}" is defined`));
|
|
}
|
|
if (!entry.notes?.trim()) {
|
|
diagnostics.push(createSectionInfo(model.path, "Messages", `notes are empty for message_id "${messageId ?? "(blank)"}"`));
|
|
}
|
|
}
|
|
return diagnostics;
|
|
}
|
|
function buildRuleDiagnostics(model, index) {
|
|
const diagnostics = [];
|
|
const inputIds = /* @__PURE__ */ new Set();
|
|
if (!model.summary?.trim()) {
|
|
diagnostics.push(createSectionWarning(model.path, "Summary", "summary is empty"));
|
|
}
|
|
if (model.inputs.length === 0) {
|
|
diagnostics.push(createSectionWarning(model.path, "Inputs", "inputs are empty"));
|
|
}
|
|
if (!(model.sections.Conditions ?? []).some((line) => line.trim())) {
|
|
diagnostics.push(createSectionWarning(model.path, "Conditions", "conditions are empty"));
|
|
}
|
|
for (const input of model.inputs) {
|
|
const id = input.id?.trim();
|
|
if (id) {
|
|
if (inputIds.has(id)) {
|
|
diagnostics.push(createSectionWarning(model.path, "Inputs", `duplicate input id "${id}"`));
|
|
}
|
|
inputIds.add(id);
|
|
}
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Inputs", input.data, index, "unresolved rule input data reference"),
|
|
...buildReferenceWarnings(model.path, "Inputs", input.source, index, "unresolved rule input source reference")
|
|
);
|
|
}
|
|
for (const reference of model.references) {
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "References", reference.ref, index, "unresolved rule reference")
|
|
);
|
|
}
|
|
for (const message of model.messages) {
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Messages", message.message, index, "unresolved message reference")
|
|
);
|
|
}
|
|
return diagnostics;
|
|
}
|
|
function buildMappingDiagnostics(model, index) {
|
|
const diagnostics = [];
|
|
const targetRefs = /* @__PURE__ */ new Set();
|
|
for (const scope of model.scope) {
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Scope", scope.ref, index, "unresolved scope reference")
|
|
);
|
|
}
|
|
for (const row of model.mappings) {
|
|
const targetRef = row.targetRef?.trim();
|
|
const sourceRef = row.sourceRef?.trim();
|
|
const transform = row.transform?.trim();
|
|
const required = row.required?.trim();
|
|
if (!targetRef) {
|
|
diagnostics.push(createSectionWarning(model.path, "Mappings", "target_ref is empty"));
|
|
} else {
|
|
if (targetRefs.has(targetRef)) {
|
|
diagnostics.push(createSectionWarning(model.path, "Mappings", `duplicate target_ref "${targetRef}"`));
|
|
}
|
|
targetRefs.add(targetRef);
|
|
}
|
|
if (!sourceRef && !transform) {
|
|
diagnostics.push(createSectionWarning(model.path, "Mappings", "source_ref is empty and transform is also empty"));
|
|
}
|
|
if (sourceRef) {
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Mappings", sourceRef, index, "unresolved mapping source_ref")
|
|
);
|
|
}
|
|
if (targetRef) {
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Mappings", targetRef, index, "unresolved mapping target_ref")
|
|
);
|
|
}
|
|
if (row.rule?.trim()) {
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Mappings", row.rule, index, "unresolved mapping rule reference")
|
|
);
|
|
}
|
|
if (required && required !== "Y" && required !== "N") {
|
|
diagnostics.push(createSectionWarning(model.path, "Mappings", `required must be Y or N for target_ref "${targetRef ?? "(blank)"}"`));
|
|
}
|
|
}
|
|
return diagnostics;
|
|
}
|
|
function buildAppProcessDiagnostics(model, index) {
|
|
const diagnostics = [];
|
|
const inputIds = /* @__PURE__ */ new Set();
|
|
const outputIds = /* @__PURE__ */ new Set();
|
|
for (const input of model.inputs) {
|
|
const id = input.id?.trim();
|
|
if (id) {
|
|
if (inputIds.has(id)) {
|
|
diagnostics.push(createSectionWarning(model.path, "Inputs", `duplicate input id "${id}"`));
|
|
}
|
|
inputIds.add(id);
|
|
}
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Inputs", input.data, index, "unresolved input data reference"),
|
|
...buildReferenceWarnings(model.path, "Inputs", input.source, index, "unresolved input source reference")
|
|
);
|
|
}
|
|
for (const output of model.outputs) {
|
|
const id = output.id?.trim();
|
|
if (id) {
|
|
if (outputIds.has(id)) {
|
|
diagnostics.push(createSectionWarning(model.path, "Outputs", `duplicate output id "${id}"`));
|
|
}
|
|
outputIds.add(id);
|
|
}
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Outputs", output.data, index, "unresolved output data reference"),
|
|
...buildReferenceWarnings(model.path, "Outputs", output.target, index, "unresolved output target reference")
|
|
);
|
|
}
|
|
for (const trigger of model.triggers) {
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Triggers", trigger.source, index, "unresolved trigger source reference")
|
|
);
|
|
}
|
|
for (const transition of model.transitions) {
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Transitions", transition.to, index, "unresolved transition target reference", "screen")
|
|
);
|
|
}
|
|
return diagnostics;
|
|
}
|
|
function buildScreenDiagnostics(model, index) {
|
|
const diagnostics = [];
|
|
const layoutIds = /* @__PURE__ */ new Set();
|
|
const fieldIds = /* @__PURE__ */ new Set();
|
|
const actionIds = /* @__PURE__ */ new Set();
|
|
for (const layout of model.layouts) {
|
|
const id = layout.id?.trim();
|
|
if (!id) {
|
|
continue;
|
|
}
|
|
if (layoutIds.has(id)) {
|
|
diagnostics.push(createSectionError(model.path, "Layout", `duplicate layout id "${id}"`));
|
|
}
|
|
layoutIds.add(id);
|
|
}
|
|
for (const field of model.fields) {
|
|
const id = field.id?.trim();
|
|
if (!id) {
|
|
diagnostics.push({
|
|
code: "invalid-structure",
|
|
message: "field id is empty",
|
|
severity: "error",
|
|
path: model.path,
|
|
field: "Fields",
|
|
line: field.rowLine,
|
|
context: { section: "Fields" }
|
|
});
|
|
} else {
|
|
if (fieldIds.has(id)) {
|
|
diagnostics.push(createSectionWarning(model.path, "Fields", `duplicate field id "${id}"`));
|
|
}
|
|
fieldIds.add(id);
|
|
}
|
|
const layoutId = field.layout?.trim();
|
|
if (layoutId && layoutIds.size > 0 && !layoutIds.has(layoutId)) {
|
|
diagnostics.push(createSectionWarning(model.path, "Fields", `field layout "${layoutId}" does not match any Layout.id`));
|
|
} else if (!layoutId && layoutIds.size > 0) {
|
|
diagnostics.push(createSectionWarning(model.path, "Fields", `layout is empty for field "${id || field.label || "(field)"}"`));
|
|
}
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Fields", field.ref, index, "unresolved field ref"),
|
|
...buildReferenceWarnings(model.path, "Fields", field.rule, index, "unresolved field rule reference")
|
|
);
|
|
}
|
|
const targetEventPairs = /* @__PURE__ */ new Set();
|
|
let hasTransitionAction = false;
|
|
for (const action of model.actions) {
|
|
const id = action.id?.trim();
|
|
if (id) {
|
|
if (actionIds.has(id)) {
|
|
diagnostics.push(createSectionWarning(model.path, "Actions", `duplicate action id "${id}"`));
|
|
}
|
|
actionIds.add(id);
|
|
}
|
|
const target = action.target?.trim();
|
|
const isScreenEvent = action.kind?.trim() === "screen_event";
|
|
if (!target && isScreenEvent) {
|
|
diagnostics.push(createSectionInfo(model.path, "Actions", "action target is empty for screen_event"));
|
|
} else if (target && !fieldIds.has(target)) {
|
|
diagnostics.push(createSectionWarning(model.path, "Actions", `action target "${target}" does not match any Fields.id`));
|
|
}
|
|
const pair = `${target ?? ""}|${action.event?.trim() ?? ""}`;
|
|
if (target && action.event?.trim()) {
|
|
if (targetEventPairs.has(pair)) {
|
|
diagnostics.push({
|
|
code: "invalid-structure",
|
|
message: `duplicate action target/event pair "${target}" + "${action.event}"`,
|
|
severity: "warning",
|
|
path: model.path,
|
|
field: "Actions",
|
|
context: { section: "Actions" }
|
|
});
|
|
}
|
|
targetEventPairs.add(pair);
|
|
}
|
|
const localProcessTarget = resolveScreenLocalProcessTarget(action.invoke, model);
|
|
if (localProcessTarget.kind === "resolved") {
|
|
} else if (localProcessTarget.kind === "unresolved-local") {
|
|
diagnostics.push(
|
|
createSectionWarning(
|
|
model.path,
|
|
"Actions",
|
|
`unresolved local process invoke reference "${action.invoke?.trim() ?? ""}"`
|
|
)
|
|
);
|
|
} else {
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(
|
|
model.path,
|
|
"Actions",
|
|
action.invoke,
|
|
index,
|
|
"unresolved action invoke reference",
|
|
"app-process"
|
|
)
|
|
);
|
|
}
|
|
const transition = action.transition?.trim();
|
|
if (transition) {
|
|
hasTransitionAction = true;
|
|
if (!action.label?.trim()) {
|
|
diagnostics.push(
|
|
createSectionInfo(
|
|
model.path,
|
|
"Actions",
|
|
"transition preview label uses fallback because action label is empty"
|
|
)
|
|
);
|
|
}
|
|
const resolvedTransition = resolveReferenceIdentity(transition, index);
|
|
if (resolvedTransition.resolvedModel?.fileType === "screen" && resolvedTransition.resolvedModel.path === model.path) {
|
|
diagnostics.push(
|
|
createSectionWarning(
|
|
model.path,
|
|
"Actions",
|
|
`action transition "${transition}" points to the current screen`
|
|
)
|
|
);
|
|
}
|
|
}
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Actions", action.transition, index, "unresolved action transition reference", "screen"),
|
|
...buildReferenceWarnings(model.path, "Actions", action.rule, index, "unresolved action rule reference")
|
|
);
|
|
}
|
|
for (const message of model.messages) {
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Messages", message.text, index, "unresolved screen message reference")
|
|
);
|
|
}
|
|
if (!hasTransitionAction) {
|
|
diagnostics.push(
|
|
createSectionInfo(
|
|
model.path,
|
|
"Actions",
|
|
"no actions.transition defined for this screen"
|
|
)
|
|
);
|
|
}
|
|
if (model.legacyTransitions.length > 0 || model.sections.Transitions) {
|
|
diagnostics.push(
|
|
createSectionWarning(
|
|
model.path,
|
|
"Transitions",
|
|
'legacy "Transitions" section detected; migrate to Actions.transition'
|
|
)
|
|
);
|
|
}
|
|
return diagnostics;
|
|
}
|
|
function resolveLocalHeadingTarget(value) {
|
|
const trimmed = value?.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
const parsed = parseReferenceValue(trimmed);
|
|
if (!parsed?.target?.startsWith("#")) {
|
|
return null;
|
|
}
|
|
const heading = parsed.target.slice(1).trim();
|
|
return heading || null;
|
|
}
|
|
function resolveScreenLocalProcessTarget(value, model) {
|
|
const trimmed = value?.trim();
|
|
if (!trimmed) {
|
|
return { kind: "not-local" };
|
|
}
|
|
const localHeadingTarget = resolveLocalHeadingTarget(trimmed);
|
|
if (localHeadingTarget) {
|
|
const exists = model.localProcesses.some(
|
|
(process) => normalizeLocalProcessId(process.id) === normalizeLocalProcessId(localHeadingTarget)
|
|
);
|
|
return exists ? { kind: "resolved", processId: localHeadingTarget } : { kind: "unresolved-local", processId: localHeadingTarget };
|
|
}
|
|
const plainId = normalizeLocalProcessId(trimmed);
|
|
if (!plainId) {
|
|
return { kind: "not-local" };
|
|
}
|
|
const plainExists = model.localProcesses.some(
|
|
(process) => normalizeLocalProcessId(process.id) === plainId
|
|
);
|
|
if (plainExists) {
|
|
return { kind: "resolved", processId: trimmed };
|
|
}
|
|
const looksLocalProcessId = /^PROC[-_A-Z0-9]+$/i.test(trimmed);
|
|
if (looksLocalProcessId) {
|
|
return { kind: "unresolved-local", processId: trimmed };
|
|
}
|
|
return { kind: "not-local" };
|
|
}
|
|
function normalizeLocalProcessId(value) {
|
|
return value?.trim().replace(/^#+/, "").trim().toUpperCase() ?? "";
|
|
}
|
|
function buildReferenceWarnings(path2, section, ref, index, messagePrefix, expectedFileType) {
|
|
const value = ref?.trim();
|
|
if (!value) {
|
|
return [];
|
|
}
|
|
const qualified = parseQualifiedRef(value);
|
|
if (qualified?.hasMemberRef) {
|
|
const resolved2 = resolveQualifiedMemberReference(value, index);
|
|
if (!resolved2.baseIdentity.resolvedModel) {
|
|
return [createSectionWarning(path2, section, `${messagePrefix} "${value}"`)];
|
|
}
|
|
if (expectedFileType && resolved2.baseIdentity.resolvedModel.fileType !== expectedFileType) {
|
|
return [createSectionWarning(path2, section, `${messagePrefix} "${value}"`)];
|
|
}
|
|
if (resolved2.memberResolution === "deferred") {
|
|
return [];
|
|
}
|
|
if (!resolved2.member) {
|
|
return [
|
|
createSectionWarning(
|
|
path2,
|
|
section,
|
|
`unresolved member ref: ${qualified.memberRef} in ${resolved2.baseIdentity.resolvedId ?? qualified.baseRefRaw}`
|
|
)
|
|
];
|
|
}
|
|
return [];
|
|
}
|
|
const parsed = parseReferenceValue(value);
|
|
if (parsed?.isExternal || parsed?.kind === "raw") {
|
|
return [];
|
|
}
|
|
const resolved = resolveReferenceIdentity(value, index);
|
|
if (!resolved.resolvedModel) {
|
|
return [createSectionWarning(path2, section, `${messagePrefix} "${value}"`)];
|
|
}
|
|
if (expectedFileType && resolved.resolvedModel.fileType !== expectedFileType) {
|
|
return [createSectionWarning(path2, section, `${messagePrefix} "${value}"`)];
|
|
}
|
|
return [];
|
|
}
|
|
function createSectionWarning(path2, section, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: section,
|
|
context: { section }
|
|
};
|
|
}
|
|
function createSectionInfo(path2, section, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "info",
|
|
path: path2,
|
|
field: section,
|
|
context: { section }
|
|
};
|
|
}
|
|
function createSectionError(path2, section, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "error",
|
|
path: path2,
|
|
field: section,
|
|
context: { section }
|
|
};
|
|
}
|
|
function buildDfdObjectDiagnostics(model) {
|
|
const diagnostics = [];
|
|
if (!model.id) {
|
|
diagnostics.push({
|
|
code: "invalid-structure",
|
|
message: 'required frontmatter "id" is missing',
|
|
severity: "error",
|
|
path: model.path,
|
|
field: "id",
|
|
context: {
|
|
section: "frontmatter"
|
|
}
|
|
});
|
|
}
|
|
return diagnostics;
|
|
}
|
|
function buildDataObjectDiagnostics(model, index) {
|
|
const diagnostics = [];
|
|
const fieldNameOccurrences = /* @__PURE__ */ new Map();
|
|
const fieldNumbersByRecordType = /* @__PURE__ */ new Map();
|
|
const fieldPositionsByRecordType = /* @__PURE__ */ new Map();
|
|
const recordTypes = /* @__PURE__ */ new Set();
|
|
if (!model.dataFormat?.trim()) {
|
|
diagnostics.push(createSectionWarning(model.path, "data_format", "data_format is empty"));
|
|
}
|
|
if (!model.kind?.trim()) {
|
|
diagnostics.push(createSectionWarning(model.path, "kind", "kind is empty"));
|
|
}
|
|
if (model.dataFormat?.trim() === "fixed" && !model.recordLength?.trim()) {
|
|
diagnostics.push(createSectionError(model.path, "record_length", "record_length is required when data_format is fixed"));
|
|
}
|
|
if (["csv", "tsv", "delimited"].includes(model.dataFormat?.trim() ?? "") && !model.delimiter?.trim()) {
|
|
diagnostics.push(createSectionWarning(model.path, "delimiter", "delimiter is empty for delimited data_format"));
|
|
}
|
|
for (const record of model.records) {
|
|
const recordType = record.recordType?.trim();
|
|
if (!recordType) {
|
|
continue;
|
|
}
|
|
if (recordTypes.has(recordType)) {
|
|
diagnostics.push(createSectionError(model.path, "Records", `duplicate record_type "${recordType}"`));
|
|
}
|
|
recordTypes.add(recordType);
|
|
}
|
|
for (const field of model.fields) {
|
|
const fieldName = field.name?.trim();
|
|
if (!fieldName) {
|
|
diagnostics.push({
|
|
code: "invalid-structure",
|
|
message: "field name is empty",
|
|
severity: "error",
|
|
path: model.path,
|
|
field: "Fields",
|
|
line: field.rowLine,
|
|
context: {
|
|
section: "Fields"
|
|
}
|
|
});
|
|
continue;
|
|
}
|
|
if (!field.label?.trim()) {
|
|
diagnostics.push(createFieldWarning(model.path, field.rowLine, `label is empty for field "${fieldName}"`));
|
|
}
|
|
if (!field.type?.trim()) {
|
|
diagnostics.push(createFieldWarning(model.path, field.rowLine, `type is empty for field "${fieldName}"`));
|
|
}
|
|
if (field.required?.trim() && !["Y", "N"].includes(field.required.trim())) {
|
|
diagnostics.push(createFieldWarning(model.path, field.rowLine, `required must be Y or N for field "${fieldName}"`));
|
|
}
|
|
if (field.length?.trim() && !/^\d+$/.test(field.length.trim())) {
|
|
diagnostics.push(createFieldWarning(model.path, field.rowLine, `length is not numeric for field "${fieldName}"`));
|
|
}
|
|
fieldNameOccurrences.set(fieldName, (fieldNameOccurrences.get(fieldName) ?? 0) + 1);
|
|
if (model.fieldMode === "file_layout") {
|
|
const recordType = field.recordType?.trim();
|
|
if (model.records.length > 0 && recordType && !recordTypes.has(recordType)) {
|
|
diagnostics.push(createFieldError(model.path, field.rowLine, `record_type "${recordType}" is not defined in Records`));
|
|
}
|
|
if (model.dataFormat?.trim() === "fixed" && !field.position?.trim()) {
|
|
diagnostics.push(createFieldError(model.path, field.rowLine, `position is required for fixed format field "${fieldName}"`));
|
|
}
|
|
const noKey = recordType || "__default__";
|
|
if (field.no?.trim()) {
|
|
if (!fieldNumbersByRecordType.has(noKey)) {
|
|
fieldNumbersByRecordType.set(noKey, /* @__PURE__ */ new Set());
|
|
}
|
|
const numbers = fieldNumbersByRecordType.get(noKey);
|
|
if (numbers.has(field.no.trim())) {
|
|
diagnostics.push(createFieldWarning(model.path, field.rowLine, `duplicate no "${field.no.trim()}" in record_type "${recordType || "(default)"}"`));
|
|
}
|
|
numbers.add(field.no.trim());
|
|
}
|
|
if (field.position?.trim()) {
|
|
if (!fieldPositionsByRecordType.has(noKey)) {
|
|
fieldPositionsByRecordType.set(noKey, /* @__PURE__ */ new Set());
|
|
}
|
|
const positions = fieldPositionsByRecordType.get(noKey);
|
|
if (positions.has(field.position.trim())) {
|
|
diagnostics.push(createFieldWarning(model.path, field.rowLine, `duplicate position "${field.position.trim()}" in record_type "${recordType || "(default)"}"`));
|
|
}
|
|
positions.add(field.position.trim());
|
|
}
|
|
}
|
|
const ref = field.ref?.trim();
|
|
if (!ref) {
|
|
continue;
|
|
}
|
|
const qualified = parseQualifiedRef(ref);
|
|
if (qualified?.hasMemberRef) {
|
|
const resolved2 = resolveQualifiedMemberReference(ref, index);
|
|
if (!resolved2.baseIdentity.resolvedModel) {
|
|
diagnostics.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved field reference "${ref}"`,
|
|
severity: "warning",
|
|
path: model.path,
|
|
field: "Fields",
|
|
line: field.rowLine,
|
|
context: {
|
|
section: "Fields"
|
|
}
|
|
});
|
|
continue;
|
|
}
|
|
if (resolved2.memberResolution === "deferred") {
|
|
continue;
|
|
}
|
|
if (!resolved2.member) {
|
|
diagnostics.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved member ref: ${qualified.memberRef} in ${resolved2.baseIdentity.resolvedId ?? resolved2.baseIdentity.resolvedFile ?? qualified.baseRefRaw}`,
|
|
severity: "warning",
|
|
path: model.path,
|
|
field: "Fields",
|
|
line: field.rowLine,
|
|
context: {
|
|
section: "Fields"
|
|
}
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
const parsed = parseReferenceValue(ref);
|
|
if (parsed?.isExternal || parsed?.kind === "raw") {
|
|
continue;
|
|
}
|
|
const resolved = resolveReferenceIdentity(ref, index);
|
|
if (resolved.resolvedModel) {
|
|
continue;
|
|
}
|
|
diagnostics.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved field reference "${ref}"`,
|
|
severity: "warning",
|
|
path: model.path,
|
|
field: "Fields",
|
|
line: field.rowLine,
|
|
context: {
|
|
section: "Fields"
|
|
}
|
|
});
|
|
}
|
|
for (const [fieldName, count] of fieldNameOccurrences.entries()) {
|
|
if (count > 1) {
|
|
diagnostics.push(createSectionWarning(model.path, "Fields", `duplicate field name "${fieldName}"`));
|
|
}
|
|
}
|
|
return diagnostics;
|
|
}
|
|
function createFieldWarning(path2, line, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "Fields",
|
|
line,
|
|
context: { section: "Fields" }
|
|
};
|
|
}
|
|
function createFieldError(path2, line, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "error",
|
|
path: path2,
|
|
field: "Fields",
|
|
line,
|
|
context: { section: "Fields" }
|
|
};
|
|
}
|
|
function buildCurrentDiagramDiagnostics(diagram, warnings) {
|
|
return dedupeDiagnostics(warnings.map((warning) => normalizeDiagnosticSeverity(warning)));
|
|
}
|
|
function buildClassDiagnostics(model, index) {
|
|
const diagnostics = [];
|
|
for (const relation of model.relations) {
|
|
if (!resolveObjectModelReference(relation.targetClass, index)) {
|
|
diagnostics.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved class relation target "${relation.targetClass}"`,
|
|
severity: "warning",
|
|
path: model.path,
|
|
field: "Relations",
|
|
context: {
|
|
relatedId: relation.id,
|
|
section: "Relations"
|
|
}
|
|
});
|
|
}
|
|
if (!CLASS_RELATION_KINDS.has(relation.kind)) {
|
|
diagnostics.push({
|
|
code: "invalid-kind",
|
|
message: `invalid class relation kind "${relation.kind}"`,
|
|
severity: "warning",
|
|
path: model.path,
|
|
field: "Relations",
|
|
context: {
|
|
relatedId: relation.id,
|
|
section: "Relations"
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return diagnostics;
|
|
}
|
|
function buildErEntityDiagnostics(entity, index) {
|
|
const diagnostics = [];
|
|
const localColumnNames = new Set(entity.columns.map((column) => column.physicalName));
|
|
const relationIds = /* @__PURE__ */ new Set();
|
|
if (entity.relationBlocks.length === 0) {
|
|
diagnostics.push({
|
|
code: "section-missing",
|
|
message: 'No relations are defined in "## Relations".',
|
|
severity: "info",
|
|
path: entity.path,
|
|
field: "Relations",
|
|
context: {
|
|
section: "Relations"
|
|
}
|
|
});
|
|
}
|
|
for (const relationBlock of entity.relationBlocks) {
|
|
const relationId = relationBlock.id?.trim() ?? "";
|
|
if (!relationId) {
|
|
diagnostics.push(createSectionError(entity.path, "Relations", "invalid ER relation id: (empty)"));
|
|
} else {
|
|
if (isIncompleteErRelationId(relationId)) {
|
|
diagnostics.push(createSectionError(entity.path, "Relations", `ER relation id looks incomplete: ${relationId}`));
|
|
}
|
|
if (relationIds.has(relationId)) {
|
|
diagnostics.push(createSectionError(entity.path, "Relations", `duplicate ER relation id: ${relationId}`));
|
|
} else {
|
|
relationIds.add(relationId);
|
|
}
|
|
}
|
|
if (!relationBlock.cardinality) {
|
|
diagnostics.push({
|
|
code: "section-missing",
|
|
message: `relation "${relationBlock.id}" does not specify cardinality`,
|
|
severity: "info",
|
|
path: entity.path,
|
|
field: "Relations",
|
|
context: {
|
|
relatedId: relationBlock.id,
|
|
section: "Relations"
|
|
}
|
|
});
|
|
}
|
|
if (!relationBlock.targetTable) {
|
|
diagnostics.push({
|
|
code: "unresolved-reference",
|
|
message: `relation "${relationBlock.id}" does not resolve target_table`,
|
|
severity: "warning",
|
|
path: entity.path,
|
|
field: "Relations",
|
|
context: {
|
|
relatedId: relationBlock.id,
|
|
section: "Relations"
|
|
}
|
|
});
|
|
continue;
|
|
}
|
|
const targetEntity = resolveErEntityReference(relationBlock.targetTable, index);
|
|
if (!targetEntity) {
|
|
diagnostics.push({
|
|
code: "unresolved-reference",
|
|
message: `relation "${relationBlock.id}" target_table "${relationBlock.targetTable}" could not be resolved`,
|
|
severity: "warning",
|
|
path: entity.path,
|
|
field: "Relations",
|
|
context: {
|
|
relatedId: relationBlock.id,
|
|
section: "Relations"
|
|
}
|
|
});
|
|
continue;
|
|
}
|
|
const targetColumnNames = new Set(
|
|
targetEntity.columns.map((column) => column.physicalName)
|
|
);
|
|
for (const mapping of relationBlock.mappings) {
|
|
if (mapping.localColumn && !localColumnNames.has(mapping.localColumn)) {
|
|
diagnostics.push({
|
|
code: "unresolved-reference",
|
|
message: `relation "${relationBlock.id}" local column "${mapping.localColumn}" does not exist in the current entity`,
|
|
severity: "warning",
|
|
path: entity.path,
|
|
field: "Relations",
|
|
context: {
|
|
relatedId: relationBlock.id,
|
|
section: "Relations"
|
|
}
|
|
});
|
|
}
|
|
if (mapping.targetColumn && !targetColumnNames.has(mapping.targetColumn)) {
|
|
diagnostics.push({
|
|
code: "unresolved-reference",
|
|
message: `relation "${relationBlock.id}" target column "${mapping.targetColumn}" does not exist in "${targetEntity.physicalName}"`,
|
|
severity: "warning",
|
|
path: entity.path,
|
|
field: "Relations",
|
|
context: {
|
|
relatedId: relationBlock.id,
|
|
section: "Relations"
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return diagnostics;
|
|
}
|
|
function isIncompleteErRelationId(id) {
|
|
const normalized = id.trim().toUpperCase();
|
|
return !normalized || normalized === "REL" || normalized === "REL-" || normalized === "REL--" || normalized === "REL-NEW" || normalized === "REL-TODO";
|
|
}
|
|
function normalizeDiagnosticSeverity(warning) {
|
|
const localizedWarning = {
|
|
...warning,
|
|
message: localizeDiagnosticMessage(warning.message)
|
|
};
|
|
if (warning.severity === "info" || warning.severity === "error") {
|
|
return localizedWarning;
|
|
}
|
|
if (warning.code === "frontmatter-parse-error" || warning.code === "unknown-schema" || warning.code === "invalid-table-column" || warning.code === "invalid-table-row" || warning.code === "missing-name" || warning.code === "missing-kind") {
|
|
return { ...localizedWarning, severity: "error" };
|
|
}
|
|
if (warning.code === "invalid-structure" && typeof warning.field === "string" && ["type", "id", "name", "logical_name", "physical_name", "kind"].includes(warning.field)) {
|
|
return { ...localizedWarning, severity: "error" };
|
|
}
|
|
return localizedWarning;
|
|
}
|
|
function localizeDiagnosticMessage(message, language) {
|
|
if (!isJapaneseLanguage(language)) {
|
|
return message;
|
|
}
|
|
const replacements = [
|
|
[/^kind is empty$/, "kind \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^summary is empty$/, "Summary \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^values are empty$/, "Values \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^messages are empty$/, "Messages \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^inputs are empty$/, "Inputs \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^conditions are empty$/, "Conditions \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^target_ref is empty$/, "target_ref \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^source_ref is empty and transform is also empty$/, "source_ref \u3068 transform \u306E\u4E21\u65B9\u304C\u7A7A\u3067\u3059\u3002\u3069\u3061\u3089\u304B\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"],
|
|
[/^field id is empty$/, "Fields \u306E id \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^field name is empty$/, "Fields \u306E name \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^values\.code is empty$/, "Values \u306E code \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^messages\.message_id is empty$/, "Messages \u306E message_id \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^data_format is empty$/, "data_format \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^delimiter is empty for delimited data_format$/, "delimited \u7CFB\u306E data_format \u3067\u306F delimiter \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"],
|
|
[/^record_length is required when data_format is fixed$/, "data_format \u304C fixed \u306E\u5834\u5408\u3001record_length \u304C\u5FC5\u8981\u3067\u3059\u3002"],
|
|
[/^required frontmatter "([^"]+)" is missing$/, (_match, field) => `frontmatter \u306E "${field}" \u304C\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^expected type "([^"]+)"$/, (_match, type) => `type \u306F "${type}" \u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002`],
|
|
[/^frontmatter parse error: unexpected list item "([^"]+)"$/, (_match, value) => `frontmatter \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u4E88\u671F\u3057\u306A\u3044\u30EA\u30B9\u30C8\u9805\u76EE\u3067\u3059: "${value}"`],
|
|
[/^frontmatter parse error: malformed line "([^"]+)"$/, (_match, value) => `frontmatter \u306E\u89E3\u6790\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u884C\u306E\u5F62\u5F0F\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044: "${value}"`],
|
|
[/^table in section "([^"]+)" is incomplete$/, (_match, section) => `"${section}" \u30BB\u30AF\u30B7\u30E7\u30F3\u306E\u30C6\u30FC\u30D6\u30EB\u304C\u672A\u5B8C\u6210\u3067\u3059\u3002\u30D8\u30C3\u30C0\u30FC\u884C\u3068\u533A\u5207\u308A\u884C\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`],
|
|
[/^table columns in section "([^"]+)" do not match expected screen field headers$/, (_match, section) => `"${section}" \u30BB\u30AF\u30B7\u30E7\u30F3\u306E\u30C6\u30FC\u30D6\u30EB\u5217\u304C\u671F\u5F85\u3055\u308C\u308B screen field \u30D8\u30C3\u30C0\u30FC\u3068\u4E00\u81F4\u3057\u307E\u305B\u3093\u3002`],
|
|
[/^table columns in section "([^"]+)" do not match expected legacy headers$/, (_match, section) => `"${section}" \u30BB\u30AF\u30B7\u30E7\u30F3\u306E\u30C6\u30FC\u30D6\u30EB\u5217\u304C\u671F\u5F85\u3055\u308C\u308B legacy \u30D8\u30C3\u30C0\u30FC\u3068\u4E00\u81F4\u3057\u307E\u305B\u3093\u3002`],
|
|
[/^table columns in section "([^"]+)" do not match supported DFD object headers$/, (_match, section) => `"${section}" \u30BB\u30AF\u30B7\u30E7\u30F3\u306E\u30C6\u30FC\u30D6\u30EB\u5217\u304C\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u308B DFD object \u30D8\u30C3\u30C0\u30FC\u3068\u4E00\u81F4\u3057\u307E\u305B\u3093\u3002`],
|
|
[/^table columns in section "([^"]+)" do not match supported class relation headers$/, (_match, section) => `"${section}" \u30BB\u30AF\u30B7\u30E7\u30F3\u306E\u30C6\u30FC\u30D6\u30EB\u5217\u304C\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u308B class relation \u30D8\u30C3\u30C0\u30FC\u3068\u4E00\u81F4\u3057\u307E\u305B\u3093\u3002`],
|
|
[/^table columns in section "([^"]+)" do not match expected headers$/, (_match, section) => `"${section}" \u30BB\u30AF\u30B7\u30E7\u30F3\u306E\u30C6\u30FC\u30D6\u30EB\u5217\u304C\u671F\u5F85\u3055\u308C\u308B\u30D8\u30C3\u30C0\u30FC\u3068\u4E00\u81F4\u3057\u307E\u305B\u3093\u3002`],
|
|
[/^table row in section "([^"]+)" has (\d+) columns, expected (\d+)$/, (_match, section, actual, expected) => `"${section}" \u30BB\u30AF\u30B7\u30E7\u30F3\u306E\u30C6\u30FC\u30D6\u30EB\u884C\u306E\u5217\u6570\u304C ${actual} \u3067\u3059\u3002\u671F\u5F85\u5024\u306F ${expected} \u3067\u3059\u3002`],
|
|
[/^table row in section "([^"]+)" is missing required values$/, (_match, section) => `"${section}" \u30BB\u30AF\u30B7\u30E7\u30F3\u306E\u30C6\u30FC\u30D6\u30EB\u884C\u306B\u5FC5\u9808\u5024\u304C\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^Format table should use: key \| value \| notes$/, "Format \u30C6\u30FC\u30D6\u30EB\u306F key | value | notes \u3092\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044\u3002"],
|
|
[/^Records table should use: record_type \| name \| occurrence \| notes$/, "Records \u30C6\u30FC\u30D6\u30EB\u306F record_type | name | occurrence | notes \u3092\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044\u3002"],
|
|
[/^Fields table mixes standard and file layout columns; parsed as file_layout$/, "Fields \u30C6\u30FC\u30D6\u30EB\u306B standard \u5F62\u5F0F\u3068 file_layout \u5F62\u5F0F\u306E\u5217\u304C\u6DF7\u5728\u3057\u3066\u3044\u307E\u3059\u3002file_layout \u3068\u3057\u3066\u89E3\u6790\u3057\u307E\u3057\u305F\u3002"],
|
|
[/^duplicate field name "([^"]+)"$/, (_match, name) => `\u30D5\u30A3\u30FC\u30EB\u30C9\u540D "${name}" \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^duplicate field id "([^"]+)"$/, (_match, id) => `Fields.id "${id}" \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^duplicate (.+) "([^"]+)"$/, (_match, target, value) => `${target} "${value}" \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^duplicate (ER relation id): (.+)$/, (_match, target, value) => `${target}: ${value} \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^duplicate id detected: "([^"]+)"$/, (_match, id) => `id "${id}" \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^duplicate model id detected: "([^"]+)" in (.+)$/, (_match, id, paths) => `model id "${id}" \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059: ${paths}`],
|
|
[/^filename and id mismatch: "([^"]+)" != "([^"]+)"$/, (_match, filename, id) => `\u30D5\u30A1\u30A4\u30EB\u540D\u3068 id \u304C\u4E00\u81F4\u3057\u3066\u3044\u307E\u305B\u3093: "${filename}" != "${id}"`],
|
|
[/^label is empty for (.+) "([^"]+)"$/, (_match, target, value) => `${target} "${value}" \u306E label \u304C\u7A7A\u3067\u3059\u3002`],
|
|
[/^type is empty for field "([^"]+)"$/, (_match, field) => `field "${field}" \u306E type \u304C\u7A7A\u3067\u3059\u3002`],
|
|
[/^required must be Y or N for (.+) "([^"]+)"$/, (_match, target, value) => `${target} "${value}" \u306E required \u306F Y \u307E\u305F\u306F N \u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002`],
|
|
[/^active must be Y or N for (.+) "([^"]+)"$/, (_match, target, value) => `${target} "${value}" \u306E active \u306F Y \u307E\u305F\u306F N \u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002`],
|
|
[/^active is empty for (.+) "([^"]+)"$/, (_match, target, value) => `${target} "${value}" \u306E active \u304C\u7A7A\u3067\u3059\u3002`],
|
|
[/^severity is empty for message_id "([^"]+)"$/, (_match, id) => `message_id "${id}" \u306E severity \u304C\u7A7A\u3067\u3059\u3002`],
|
|
[/^severity is invalid for message_id "([^"]+)"$/, (_match, id) => `message_id "${id}" \u306E severity \u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^timing is empty for message_id "([^"]+)"$/, (_match, id) => `message_id "${id}" \u306E timing \u304C\u7A7A\u3067\u3059\u3002`],
|
|
[/^audience is empty for message_id "([^"]+)"$/, (_match, id) => `message_id "${id}" \u306E audience \u304C\u7A7A\u3067\u3059\u3002`],
|
|
[/^text is empty for message_id "([^"]+)"$/, (_match, id) => `message_id "${id}" \u306E text \u304C\u7A7A\u3067\u3059\u3002`],
|
|
[/^notes are empty for (.+) "([^"]+)"$/, (_match, target, value) => `${target} "${value}" \u306E notes \u304C\u7A7A\u3067\u3059\u3002`],
|
|
[/^sort_order is empty for code "([^"]+)"$/, (_match, code) => `code "${code}" \u306E sort_order \u304C\u7A7A\u3067\u3059\u3002`],
|
|
[/^sort_order is not numeric for code "([^"]+)"$/, (_match, code) => `code "${code}" \u306E sort_order \u304C\u6570\u5024\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^inactive (code|message) "([^"]+)" is defined$/, (_match, target, value) => `${target} "${value}" \u306F inactive \u3068\u3057\u3066\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^length is not numeric for field "([^"]+)"$/, (_match, field) => `field "${field}" \u306E length \u304C\u6570\u5024\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^position is required for fixed format field "([^"]+)"$/, (_match, field) => `fixed \u5F62\u5F0F\u306E field "${field}" \u306B\u306F position \u304C\u5FC5\u8981\u3067\u3059\u3002`],
|
|
[/^record_type "([^"]+)" is not defined in Records$/, (_match, recordType) => `record_type "${recordType}" \u304C Records \u306B\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002`],
|
|
[/^duplicate no "([^"]+)" in record_type "([^"]+)"$/, (_match, no, recordType) => `record_type "${recordType}" \u5185\u3067 no "${no}" \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^duplicate position "([^"]+)" in record_type "([^"]+)"$/, (_match, position, recordType) => `record_type "${recordType}" \u5185\u3067 position "${position}" \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^field layout "([^"]+)" does not match any Layout\.id$/, (_match, layout) => `field layout "${layout}" \u306B\u4E00\u81F4\u3059\u308B Layout.id \u304C\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^layout is empty for field "([^"]+)"$/, (_match, field) => `field "${field}" \u306E layout \u304C\u7A7A\u3067\u3059\u3002`],
|
|
[/^action target is empty for screen_event$/, "screen_event \u306E action target \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^action target "([^"]+)" does not match any Fields\.id$/, (_match, target) => `action target "${target}" \u306B\u4E00\u81F4\u3059\u308B Fields.id \u304C\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^duplicate action target\/event pair "([^"]+)" \+ "([^"]+)"$/, (_match, target, event) => `action target/event \u306E\u7D44\u307F\u5408\u308F\u305B "${target}" + "${event}" \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^transition preview label uses fallback because action label is empty$/, "action label \u304C\u7A7A\u306E\u305F\u3081\u3001transition \u30D7\u30EC\u30D3\u30E5\u30FC\u3067\u306F\u4EE3\u66FF\u30E9\u30D9\u30EB\u3092\u4F7F\u3044\u307E\u3059\u3002"],
|
|
[/^action transition "([^"]+)" points to the current screen$/, (_match, transition) => `action transition "${transition}" \u304C\u73FE\u5728\u306E screen \u3092\u6307\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^no actions\.transition defined for this screen$/, "\u3053\u306E screen \u306B\u306F actions.transition \u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002"],
|
|
[/^legacy "Transitions" section detected; migrate to Actions\.transition$/, '\u65E7\u5F62\u5F0F\u306E "Transitions" \u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u3042\u308A\u307E\u3059\u3002Actions.transition \u3078\u306E\u79FB\u884C\u3092\u691C\u8A0E\u3057\u3066\u304F\u3060\u3055\u3044\u3002'],
|
|
[/^app_process Flow\.(from|to) references missing step "([^"]+)"$/, (_match, endpoint, step) => `app_process Flow.${endpoint} \u304C\u5B58\u5728\u3057\u306A\u3044 step "${step}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^app_process Flow\.(from|to) is missing a step id$/, (_match, endpoint) => `app_process Flow.${endpoint} \u306E step id \u304C\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^unresolved (.+) "([^"]+)"$/, (_match, target, value) => `${target} "${value}" \u306E\u53C2\u7167\u5148\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002ID\u307E\u305F\u306F\u30D5\u30A1\u30A4\u30EB\u540D\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`],
|
|
[/^unresolved member ref: (.+) in (.+)$/, (_match, member, owner) => `member ref "${member}" \u304C "${owner}" \u5185\u3067\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^relation "([^"]+)" target_table "([^"]+)" could not be resolved$/, (_match, relation, target) => `relation "${relation}" \u306E target_table "${target}" \u304C\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002`],
|
|
[/^relation "([^"]+)" does not resolve target_table$/, (_match, relation) => `relation "${relation}" \u306E target_table \u304C\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002`],
|
|
[/^relation "([^"]+)" does not specify cardinality$/, (_match, relation) => `relation "${relation}" \u306B cardinality \u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002`],
|
|
[/^relation "([^"]+)" local column "([^"]+)" does not exist in the current entity$/, (_match, relation, column) => `relation "${relation}" \u306E local column "${column}" \u306F\u73FE\u5728\u306E entity \u306B\u5B58\u5728\u3057\u307E\u305B\u3093\u3002`],
|
|
[/^relation "([^"]+)" target column "([^"]+)" does not exist in "([^"]+)"$/, (_match, relation, column, entity) => `relation "${relation}" \u306E target column "${column}" \u306F "${entity}" \u306B\u5B58\u5728\u3057\u307E\u305B\u3093\u3002`],
|
|
[/^No relations are defined in "## Relations"\.$/, "## Relations \u306B relation \u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002"],
|
|
[/^invalid ER relation id: \(empty\)$/, "ER relation id \u304C\u7A7A\u3067\u3059\u3002"],
|
|
[/^ER relation id looks incomplete: (.+)$/, (_match, id) => `ER relation id \u304C\u672A\u5B8C\u6210\u306E\u3088\u3046\u3067\u3059: ${id}`],
|
|
[/^invalid class relation kind "([^"]+)"$/, (_match, kind) => `class relation kind "${kind}" \u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^reserved kind used: "([^"]+)"$/, (_match, kind) => `\u4E88\u7D04\u6E08\u307F kind "${kind}" \u304C\u4F7F\u308F\u308C\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^(.+) renderer is not supported for (.+)\. Using the format default renderer\.$/, (_match, renderer, format) => `${format} \u3067\u306F ${renderer} renderer \u306F\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002format \u306E\u65E2\u5B9A renderer \u3092\u4F7F\u3044\u307E\u3059\u3002`],
|
|
[/^Unknown render_mode value "([^"]+)"\. Using the format default renderer\.$/, (_match, value) => `render_mode "${value}" \u306F\u4E0D\u660E\u3067\u3059\u3002format \u306E\u65E2\u5B9A renderer \u3092\u4F7F\u3044\u307E\u3059\u3002`],
|
|
[/^DFD flow shape "([^"]+)" may be unusual$/, (_match, shape) => `DFD flow shape "${shape}" \u306F\u901A\u5E38\u3068\u7570\u306A\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`],
|
|
[/^DFD flow "([^"]+)" is a self-loop$/, (_match, flow) => `DFD flow "${flow}" \u306F\u81EA\u5DF1\u30EB\u30FC\u30D7\u3067\u3059\u3002`],
|
|
[/^DFD local object "([^"]+)" uses diagram-local definition without ref\.$/, (_match, object) => `DFD local object "${object}" \u306F ref \u306A\u3057\u306E\u56F3\u5185\u5B9A\u7FA9\u3068\u3057\u3066\u6271\u308F\u308C\u307E\u3059\u3002`],
|
|
[/^DFD object "([^"]+)" is missing kind and it could not be derived from ref\.$/, (_match, object) => `DFD object "${object}" \u306E kind \u304C\u306A\u304F\u3001ref \u304B\u3089\u3082\u63A8\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002`],
|
|
[/^(.+) resolves to a dfd_object but is not listed in "Objects"$/, (_match, value) => `${value} \u306F dfd_object \u306B\u89E3\u6C7A\u3067\u304D\u307E\u3059\u304C\u3001Objects \u306B listed \u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002`],
|
|
[/^(.+) is not listed in "Objects"$/, (_match, value) => `${value} \u306F Objects \u306B listed \u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002`],
|
|
[/^Duplicate object refs were merged: (.+)$/, (_match, summary) => `\u91CD\u8907\u3057\u305F object ref \u3092\u7D71\u5408\u3057\u307E\u3057\u305F: ${summary}`],
|
|
[/^relation "([^"]+)" is outside diagram scope$/, (_match, relation) => `relation "${relation}" \u306F diagram \u306E\u5BFE\u8C61\u7BC4\u56F2\u5916\u3067\u3059\u3002`],
|
|
[/^diagram relations are empty; using auto-collected class relations from "Objects"$/, "diagram relations \u304C\u7A7A\u306E\u305F\u3081\u3001Objects \u304B\u3089\u81EA\u52D5\u53CE\u96C6\u3057\u305F class relations \u3092\u4F7F\u3044\u307E\u3059\u3002"],
|
|
[/^diagram parser expected type "([^"]+)" or "([^"]+)" but received type "([^"]+)"$/, (_match, left, right, actual) => `diagram parser \u306F type "${left}" \u307E\u305F\u306F "${right}" \u3092\u671F\u5F85\u3057\u307E\u3057\u305F\u304C\u3001\u5B9F\u969B\u306F "${actual}" \u3067\u3057\u305F\u3002`],
|
|
[/^relations parser expected schema "([^"]+)" but received "([^"]+)"$/, (_match, expected, actual) => `relations parser \u306F schema "${expected}" \u3092\u671F\u5F85\u3057\u307E\u3057\u305F\u304C\u3001\u5B9F\u969B\u306F "${actual}" \u3067\u3057\u305F\u3002`],
|
|
[/^object parser expected schema "([^"]+)" or type "([^"]+)" but received schema "([^"]+)" \/ type "([^"]+)"$/, (_match, schema, type, actualSchema, actualType) => `object parser \u306F schema "${schema}" \u307E\u305F\u306F type "${type}" \u3092\u671F\u5F85\u3057\u307E\u3057\u305F\u304C\u3001\u5B9F\u969B\u306F schema "${actualSchema}" / type "${actualType}" \u3067\u3057\u305F\u3002`],
|
|
[/^invalid kind "([^"]+)"$/, (_match, kind) => `kind "${kind}" \u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^invalid dfd_object kind "([^"]+)"$/, (_match, kind) => `dfd_object kind "${kind}" \u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^malformed relation record: missing (.+)$/, (_match, fields) => `relation record \u306E\u5F62\u5F0F\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093\u3002\u4E0D\u8DB3: ${fields}`],
|
|
[/^Relations row is missing required values: (.+)$/, (_match, row) => `Relations \u884C\u306B\u5FC5\u9808\u5024\u304C\u3042\u308A\u307E\u305B\u3093: ${row}`],
|
|
[/^failed to parse numeric value "([^"]+)" for "([^"]+)"$/, (_match, value, field) => `"${field}" \u306E\u6570\u5024 "${value}" \u3092\u89E3\u6790\u3067\u304D\u307E\u305B\u3093\u3002`],
|
|
[/^missing required field "([^"]+)"$/, (_match, field) => `\u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9 "${field}" \u304C\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^relation block "([^"]+)" missing required field "([^"]+)"$/, (_match, block, field) => `relation block "${block}" \u306B\u5FC5\u9808\u30D5\u30A3\u30FC\u30EB\u30C9 "${field}" \u304C\u3042\u308A\u307E\u305B\u3093\u3002`]
|
|
];
|
|
for (const [pattern, replacement] of replacements) {
|
|
const matched = message.match(pattern);
|
|
if (!matched) {
|
|
continue;
|
|
}
|
|
if (typeof replacement === "string") {
|
|
return replacement;
|
|
}
|
|
return replacement(...matched);
|
|
}
|
|
return message;
|
|
}
|
|
function dedupeDiagnostics(warnings) {
|
|
return warnings.filter(
|
|
(warning, index) => warnings.findIndex(
|
|
(entry) => entry.code === warning.code && entry.message === warning.message && entry.severity === warning.severity && entry.path === warning.path && entry.field === warning.field
|
|
) === index
|
|
);
|
|
}
|
|
|
|
// src/core/impact-analyzer.ts
|
|
function buildImpactSummary(model, index) {
|
|
const outboundReferences = collectModelReferences(model).map(
|
|
(reference) => createImpactReference(model, reference, "outbound", index)
|
|
);
|
|
const resolvedOutbound = outboundReferences.filter(
|
|
(reference) => Boolean(reference.targetPath)
|
|
);
|
|
const unresolvedOutbound = outboundReferences.filter(
|
|
(reference) => !reference.targetPath && isExternalModelReference(reference.targetRaw)
|
|
);
|
|
const inboundReferences = [];
|
|
for (const candidate of Object.values(index.modelsByFilePath)) {
|
|
if (candidate.path === model.path || candidate.fileType === "markdown") {
|
|
continue;
|
|
}
|
|
for (const reference of collectModelReferences(candidate)) {
|
|
if (referenceTargetsModel(reference.raw, model, index)) {
|
|
inboundReferences.push(createImpactReference(candidate, reference, "inbound", index));
|
|
}
|
|
}
|
|
}
|
|
const relatedSourceLinks = collectRelatedSourceLinks(
|
|
model,
|
|
resolvedOutbound,
|
|
inboundReferences,
|
|
index
|
|
);
|
|
return {
|
|
modelPath: model.path,
|
|
modelId: getModelId(model),
|
|
modelType: model.fileType,
|
|
modelLabel: getReferencedModelDisplayName(model),
|
|
outboundRelationships: groupOutboundRelationships(resolvedOutbound, index),
|
|
inboundRelationships: groupInboundRelationships(inboundReferences, index),
|
|
valueUsages: model.fileType === "codeset" ? groupValueUsages(model, inboundReferences, index) : [],
|
|
unresolvedOutbound,
|
|
relatedSourceLinks
|
|
};
|
|
}
|
|
function formatImpactSummaryAsMarkdown(summary) {
|
|
return [
|
|
`## Relationship Summary: ${summary.modelLabel}`,
|
|
"",
|
|
formatRelationshipSection(
|
|
"### References from this object",
|
|
summary.outboundRelationships
|
|
),
|
|
"",
|
|
formatRelationshipSection(
|
|
"### Referenced by this object",
|
|
summary.inboundRelationships
|
|
),
|
|
"",
|
|
...summary.modelType === "codeset" ? [formatValueUsageSection("### Value usage", summary.valueUsages), ""] : [],
|
|
formatUnresolvedSection("### Unresolved references", summary.unresolvedOutbound),
|
|
"",
|
|
formatSourceLinkSection("### Related Source Links", summary.relatedSourceLinks)
|
|
].join("\n");
|
|
}
|
|
function collectModelReferences(model) {
|
|
const references = [];
|
|
const add = (raw, relationKind, section, field, notes, sourceContext) => {
|
|
const trimmed = raw?.trim();
|
|
if (!trimmed || !isExternalModelReference(trimmed)) {
|
|
return;
|
|
}
|
|
references.push({
|
|
raw: trimmed,
|
|
relationKind,
|
|
section,
|
|
field,
|
|
sourceContext: sourceContext?.trim() || void 0,
|
|
notes: notes?.trim() || void 0
|
|
});
|
|
};
|
|
switch (model.fileType) {
|
|
case "object":
|
|
for (const relation of model.relations) {
|
|
add(relation.targetClass, relation.kind || "class relation", "Relations", "targetClass", relation.notes);
|
|
}
|
|
break;
|
|
case "er-entity":
|
|
for (const relation of model.outboundRelations) {
|
|
add(relation.targetEntity, relation.kind || "er relation", "Relations", "targetEntity", relation.notes);
|
|
}
|
|
break;
|
|
case "diagram":
|
|
for (const ref of model.objectRefs) {
|
|
add(ref, "diagram object", "Objects", "objectRefs");
|
|
}
|
|
for (const node of model.nodes) {
|
|
add(node.ref, "diagram node", "Nodes", "ref");
|
|
}
|
|
for (const edge of model.edges) {
|
|
add(edge.source, edge.kind || "diagram edge", "Edges", "source");
|
|
add(edge.target, edge.kind || "diagram edge", "Edges", "target");
|
|
}
|
|
break;
|
|
case "dfd-diagram":
|
|
for (const ref of model.objectRefs) {
|
|
add(ref, "dfd object", "Objects", "objectRefs");
|
|
}
|
|
for (const object of model.objectEntries) {
|
|
add(object.ref, "dfd object", "Objects", "ref", object.notes);
|
|
}
|
|
for (const flow of model.flows) {
|
|
add(flow.from, "dfd flow", "Flows", "from", flow.notes);
|
|
add(flow.to, "dfd flow", "Flows", "to", flow.notes);
|
|
add(flow.data, "dfd data", "Flows", "data", flow.notes);
|
|
}
|
|
break;
|
|
case "data-object":
|
|
for (const field of model.fields) {
|
|
add(field.ref, "data field reference", "Fields", "ref", field.notes);
|
|
}
|
|
break;
|
|
case "app-process":
|
|
for (const input of model.inputs) {
|
|
add(input.data, "process input", "Inputs", "data", input.notes);
|
|
add(input.source, "process input source", "Inputs", "source", input.notes);
|
|
}
|
|
for (const output of model.outputs) {
|
|
add(output.data, "process output", "Outputs", "data", output.notes);
|
|
add(output.target, "process output target", "Outputs", "target", output.notes);
|
|
}
|
|
for (const trigger of model.triggers) {
|
|
add(trigger.source, "process trigger", "Triggers", "source", trigger.notes);
|
|
}
|
|
for (const transition of model.transitions) {
|
|
add(transition.to, "process transition", "Transitions", "to", transition.notes);
|
|
}
|
|
for (const flow of model.flows ?? []) {
|
|
if (parseStructuredQualifiedReference(flow.condition)) {
|
|
add(
|
|
flow.condition,
|
|
"process flow condition",
|
|
"Flows",
|
|
"condition",
|
|
flow.notes,
|
|
[flow.from, flow.to].filter(Boolean).join(" -> ")
|
|
);
|
|
}
|
|
}
|
|
for (const step of model.steps ?? []) {
|
|
add(step.input, "process step input", "Steps", "input", step.notes);
|
|
add(step.output, "process step output", "Steps", "output", step.notes);
|
|
add(step.rule, "process step rule", "Steps", "rule", step.notes);
|
|
add(step.invoke, "process step invoke", "Steps", "invoke", step.notes);
|
|
add(step.screen, "process step screen", "Steps", "screen", step.notes);
|
|
}
|
|
break;
|
|
case "screen":
|
|
for (const field of model.fields) {
|
|
add(field.ref, "screen field reference", "Fields", "ref", field.notes);
|
|
add(field.rule, "screen field rule", "Fields", "rule", field.notes);
|
|
if (parseStructuredQualifiedReference(field.condition)) {
|
|
add(
|
|
field.condition,
|
|
"screen field condition",
|
|
"Fields",
|
|
"condition",
|
|
field.notes,
|
|
formatScreenFieldContext(field)
|
|
);
|
|
}
|
|
}
|
|
for (const action of model.actions) {
|
|
add(action.invoke, "screen action invoke", "Actions", "invoke", action.notes);
|
|
add(action.transition, "screen action transition", "Actions", "transition", action.notes);
|
|
add(action.rule, "screen action rule", "Actions", "rule", action.notes);
|
|
if (parseStructuredQualifiedReference(action.condition)) {
|
|
add(
|
|
action.condition,
|
|
"screen action condition",
|
|
"Actions",
|
|
"condition",
|
|
action.notes,
|
|
formatScreenActionContext(action)
|
|
);
|
|
}
|
|
}
|
|
for (const message of model.messages) {
|
|
if (parseStructuredQualifiedReference(message.condition)) {
|
|
add(
|
|
message.condition,
|
|
"screen message condition",
|
|
"Messages",
|
|
"condition",
|
|
message.notes,
|
|
formatScreenMessageContext(message)
|
|
);
|
|
}
|
|
}
|
|
for (const localProcess of model.localProcesses) {
|
|
for (const step of localProcess.steps ?? []) {
|
|
if (parseStructuredQualifiedReference(step.condition)) {
|
|
add(
|
|
step.condition,
|
|
"screen local process step condition",
|
|
"Local Processes",
|
|
"Steps.condition",
|
|
step.notes,
|
|
formatScreenLocalProcessRowContext(localProcess.id, step)
|
|
);
|
|
}
|
|
}
|
|
for (const error of localProcess.errors ?? []) {
|
|
if (parseStructuredQualifiedReference(error.condition)) {
|
|
add(
|
|
error.condition,
|
|
"screen local process error condition",
|
|
"Local Processes",
|
|
"Errors.condition",
|
|
error.notes,
|
|
formatScreenLocalProcessRowContext(localProcess.id, error)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
for (const transition of model.legacyTransitions) {
|
|
add(transition.to, "screen transition", "Transitions", "to", transition.notes);
|
|
}
|
|
break;
|
|
case "rule":
|
|
for (const input of model.inputs) {
|
|
add(input.data, "rule input", "Inputs", "data", input.notes);
|
|
add(input.source, "rule input source", "Inputs", "source", input.notes);
|
|
}
|
|
for (const reference of model.references) {
|
|
add(reference.ref, "rule reference", "References", "ref", reference.notes);
|
|
}
|
|
for (const message of model.messages) {
|
|
add(message.message, "rule message", "Messages", "message", message.notes);
|
|
}
|
|
break;
|
|
case "mapping":
|
|
add(model.source, "mapping source", "Overview", "source");
|
|
add(model.target, "mapping target", "Overview", "target");
|
|
for (const scope of model.scope) {
|
|
add(scope.ref, "mapping scope", "Scope", "ref", scope.notes);
|
|
}
|
|
for (const row of model.mappings) {
|
|
add(row.sourceRef, "mapping source field", "Mappings", "sourceRef", row.notes);
|
|
add(row.targetRef, "mapping target field", "Mappings", "targetRef", row.notes);
|
|
add(row.rule, "mapping rule", "Mappings", "rule", row.notes);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return references;
|
|
}
|
|
function createImpactReference(sourceModel, reference, direction, index) {
|
|
const rawIdentity = resolveReferenceIdentity(reference.raw, index);
|
|
const qualified = resolveQualifiedMemberReference(reference.raw, index);
|
|
const identity = rawIdentity.resolvedModel || !qualified.qualified.hasMemberRef ? rawIdentity : qualified.baseIdentity;
|
|
return {
|
|
direction,
|
|
sourcePath: sourceModel.path,
|
|
sourceId: getModelId(sourceModel),
|
|
sourceType: sourceModel.fileType,
|
|
sourceLabel: getReferencedModelDisplayName(sourceModel),
|
|
targetRaw: reference.raw,
|
|
targetPath: identity.resolvedFile,
|
|
targetId: identity.resolvedId,
|
|
targetType: identity.resolvedModelType,
|
|
targetLabel: getReferenceDisplayName(
|
|
qualified.qualified.hasMemberRef ? qualified.qualified.baseRefRaw : reference.raw,
|
|
identity.resolvedModel
|
|
),
|
|
relationKind: reference.relationKind,
|
|
section: reference.section,
|
|
field: reference.field,
|
|
sourceContext: reference.sourceContext,
|
|
notes: reference.notes
|
|
};
|
|
}
|
|
function groupOutboundRelationships(references, index) {
|
|
return groupRelationships(references, (reference) => {
|
|
const model = reference.targetPath ? index.modelsByFilePath[reference.targetPath] : null;
|
|
if (!model) {
|
|
return null;
|
|
}
|
|
return { model, relationKind: "outbound" };
|
|
});
|
|
}
|
|
function groupInboundRelationships(references, index) {
|
|
return groupRelationships(references, (reference) => {
|
|
const model = index.modelsByFilePath[reference.sourcePath];
|
|
if (!model) {
|
|
return null;
|
|
}
|
|
return { model, relationKind: "inbound" };
|
|
});
|
|
}
|
|
function groupRelationships(references, getGroupModel) {
|
|
const groups = /* @__PURE__ */ new Map();
|
|
for (const reference of references) {
|
|
const groupModel = getGroupModel(reference);
|
|
if (!groupModel) {
|
|
continue;
|
|
}
|
|
const { model, relationKind } = groupModel;
|
|
const key = model.path;
|
|
const existing = groups.get(key);
|
|
if (existing) {
|
|
existing.usages.push(reference);
|
|
existing.usageCount += 1;
|
|
continue;
|
|
}
|
|
groups.set(key, {
|
|
direction: reference.direction,
|
|
modelPath: model.path,
|
|
modelId: getModelId(model),
|
|
modelType: model.fileType,
|
|
modelLabel: getReferencedModelDisplayName(model),
|
|
usageCount: 1,
|
|
usages: [reference],
|
|
sourceLinks: groupImpactSourceLinks(
|
|
(model.sourceLinks ?? []).map(
|
|
(link) => createImpactSourceLink(model, link, relationKind)
|
|
)
|
|
)
|
|
});
|
|
}
|
|
return [...groups.values()].sort(
|
|
(left, right) => left.modelLabel.localeCompare(right.modelLabel)
|
|
);
|
|
}
|
|
function referenceTargetsModel(rawReference, model, index) {
|
|
if (referencesMatch(rawReference, model.path, index)) {
|
|
return true;
|
|
}
|
|
const modelId = getModelId(model);
|
|
if (modelId && referencesMatch(rawReference, modelId, index)) {
|
|
return true;
|
|
}
|
|
if (model.fileType !== "codeset") {
|
|
return false;
|
|
}
|
|
const qualified = resolveQualifiedMemberReference(rawReference, index);
|
|
if (!qualified.qualified.hasMemberRef) {
|
|
return false;
|
|
}
|
|
if (referencesMatch(qualified.qualified.baseRefRaw, model.path, index)) {
|
|
return true;
|
|
}
|
|
return Boolean(modelId && referencesMatch(qualified.qualified.baseRefRaw, modelId, index));
|
|
}
|
|
function groupValueUsages(model, inboundReferences, index) {
|
|
if (model.fileType !== "codeset") {
|
|
return [];
|
|
}
|
|
const byMember = /* @__PURE__ */ new Map();
|
|
for (const reference of inboundReferences) {
|
|
if (!isCodesetValueUsageSource(reference)) {
|
|
continue;
|
|
}
|
|
const structuredQualified = parseStructuredQualifiedReference(reference.targetRaw);
|
|
if (!structuredQualified) {
|
|
continue;
|
|
}
|
|
const qualified = resolveQualifiedMemberReference(reference.targetRaw, index);
|
|
if (!structuredQualified.memberRef || !referenceTargetsModel(qualified.qualified.baseRefRaw, model, index)) {
|
|
continue;
|
|
}
|
|
const entry = byMember.get(structuredQualified.memberRef) ?? {
|
|
memberLabel: qualified.member?.displayName,
|
|
references: []
|
|
};
|
|
if (!entry.memberLabel && qualified.member?.displayName) {
|
|
entry.memberLabel = qualified.member.displayName;
|
|
}
|
|
entry.references.push(reference);
|
|
byMember.set(structuredQualified.memberRef, entry);
|
|
}
|
|
return [...byMember.entries()].map(([member, entry]) => ({
|
|
member,
|
|
memberLabel: entry.memberLabel,
|
|
relationships: groupInboundRelationships(entry.references, index)
|
|
})).sort((left, right) => left.member.localeCompare(right.member));
|
|
}
|
|
function isCodesetValueUsageSource(reference) {
|
|
return reference.sourceType === "data-object" && reference.section === "Fields" && reference.field === "ref" || reference.sourceType === "screen" && reference.section === "Fields" && reference.field === "ref" || reference.sourceType === "screen" && (reference.section === "Fields" || reference.section === "Actions" || reference.section === "Messages") && reference.field === "condition" || reference.sourceType === "screen" && reference.section === "Local Processes" && (reference.field === "Steps.condition" || reference.field === "Errors.condition") || reference.sourceType === "app-process" && (reference.section === "Inputs" || reference.section === "Outputs") && reference.field === "data" || reference.sourceType === "app-process" && reference.section === "Flows" && reference.field === "condition" || reference.sourceType === "rule" && reference.section === "References" && reference.field === "ref" || reference.sourceType === "mapping" && (reference.section === "Scope" && reference.field === "ref" || reference.section === "Mappings" && reference.field === "rule");
|
|
}
|
|
function parseStructuredQualifiedReference(reference) {
|
|
const trimmed = reference?.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
const qualified = parseQualifiedRef(trimmed);
|
|
if (!qualified?.hasMemberRef || !qualified.memberRef) {
|
|
return null;
|
|
}
|
|
return isExternalModelReference(qualified.baseRefRaw) ? qualified : null;
|
|
}
|
|
function formatScreenFieldContext(field) {
|
|
return [field.id, field.label].filter(Boolean).join(" / ");
|
|
}
|
|
function formatScreenActionContext(action) {
|
|
const identity = [action.id, action.label].filter(Boolean).join(" / ");
|
|
const trigger = [action.target, action.event].filter(Boolean).join(" / ");
|
|
return [identity, trigger].filter(Boolean).join("; ");
|
|
}
|
|
function formatScreenMessageContext(message) {
|
|
return [message.id, message.timing].filter(Boolean).join(" / ");
|
|
}
|
|
function formatScreenLocalProcessRowContext(processId, row) {
|
|
return [processId, row.id].filter(Boolean).join(" / ");
|
|
}
|
|
function collectRelatedSourceLinks(model, outbound, inbound, index) {
|
|
const links = [];
|
|
const addLinks = (owner, relationKind) => {
|
|
if (!owner) {
|
|
return;
|
|
}
|
|
for (const link of owner.sourceLinks ?? []) {
|
|
links.push(createImpactSourceLink(owner, link, relationKind));
|
|
}
|
|
};
|
|
addLinks(model, "self");
|
|
for (const reference of outbound) {
|
|
addLinks(reference.targetPath ? index.modelsByFilePath[reference.targetPath] : null, "outbound");
|
|
}
|
|
for (const reference of inbound) {
|
|
addLinks(index.modelsByFilePath[reference.sourcePath], "inbound");
|
|
}
|
|
return groupImpactSourceLinks(links);
|
|
}
|
|
function createImpactSourceLink(owner, link, relationKind) {
|
|
return {
|
|
ownerPath: owner.path,
|
|
ownerId: getModelId(owner),
|
|
ownerType: owner.fileType,
|
|
ownerLabel: getReferencedModelDisplayName(owner),
|
|
path: link.path,
|
|
label: link.label,
|
|
notes: link.notes?.trim() ? [link.notes.trim()] : [],
|
|
relationKind
|
|
};
|
|
}
|
|
function groupImpactSourceLinks(sourceLinks) {
|
|
const groups = /* @__PURE__ */ new Map();
|
|
for (const link of sourceLinks) {
|
|
const key = [
|
|
link.relationKind,
|
|
link.ownerLabel,
|
|
link.path
|
|
].join("::");
|
|
const existing = groups.get(key);
|
|
if (existing) {
|
|
if (!existing.label && link.label) {
|
|
existing.label = link.label;
|
|
}
|
|
for (const note of link.notes) {
|
|
if (note && !existing.notes.includes(note)) {
|
|
existing.notes.push(note);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
groups.set(key, {
|
|
...link,
|
|
notes: [...new Set(link.notes.filter(Boolean))]
|
|
});
|
|
}
|
|
return [...groups.values()].sort(
|
|
(left, right) => [left.relationKind, left.ownerLabel, left.path].join("|").localeCompare([right.relationKind, right.ownerLabel, right.path].join("|"))
|
|
);
|
|
}
|
|
function isExternalModelReference(reference) {
|
|
const parsed = parseReferenceValue(reference);
|
|
if (parsed?.isExternal) {
|
|
return false;
|
|
}
|
|
const target = parsed?.target ?? reference.trim();
|
|
return Boolean(target && !target.startsWith("#"));
|
|
}
|
|
function formatRelationshipSection(title, relationships) {
|
|
if (relationships.length === 0) {
|
|
return `${title}
|
|
- None`;
|
|
}
|
|
return [
|
|
title,
|
|
...relationships.map(
|
|
(relationship) => `- ${relationship.modelLabel} (${relationship.modelType}; ${relationship.usageCount} usage${relationship.usageCount === 1 ? "" : "s"})`
|
|
)
|
|
].join("\n");
|
|
}
|
|
function formatValueUsageSection(title, valueUsages) {
|
|
if (valueUsages.length === 0) {
|
|
return `${title}
|
|
- None`;
|
|
}
|
|
const lines = [title];
|
|
for (const valueUsage of valueUsages) {
|
|
lines.push(`- ${valueUsage.member}:`);
|
|
if (valueUsage.relationships.length === 0) {
|
|
lines.push(" - None");
|
|
continue;
|
|
}
|
|
for (const relationship of valueUsage.relationships) {
|
|
for (const usage of relationship.usages) {
|
|
const context = formatValueUsageContext(usage);
|
|
lines.push(
|
|
` - ${relationship.modelLabel} (${relationship.modelType}; 1 usage${context ? `; ${context}` : ""})`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function formatValueUsageContext(reference) {
|
|
return [formatReferenceLocation(reference), reference.sourceContext].filter(Boolean).join("; ");
|
|
}
|
|
function formatReferenceLocation(reference) {
|
|
return [reference.section, reference.field].filter(Boolean).join(".");
|
|
}
|
|
function formatUnresolvedSection(title, references) {
|
|
if (references.length === 0) {
|
|
return `${title}
|
|
- None`;
|
|
}
|
|
return [
|
|
title,
|
|
...references.map((reference) => {
|
|
const location = [reference.section, reference.field].filter(Boolean).join("/");
|
|
return `- ${reference.targetRaw} (${[reference.relationKind, location].filter(Boolean).join("; ")})`;
|
|
})
|
|
].join("\n");
|
|
}
|
|
function formatSourceLinkSection(title, sourceLinks) {
|
|
if (sourceLinks.length === 0) {
|
|
return `${title}
|
|
- None`;
|
|
}
|
|
return [
|
|
title,
|
|
...sourceLinks.map((link) => {
|
|
const label = link.label ? `${link.label}: ` : "";
|
|
const notes = link.notes.length === 0 ? "" : link.notes.length === 1 ? ` - ${link.notes[0]}` : ` (${link.notes.length} notes)`;
|
|
return `- [${link.relationKind}] ${link.ownerLabel}: ${label}${link.path}${notes}`;
|
|
})
|
|
].join("\n");
|
|
}
|
|
function getModelId(model) {
|
|
switch (model.fileType) {
|
|
case "object":
|
|
return typeof model.frontmatter.id === "string" && model.frontmatter.id.trim() ? model.frontmatter.id.trim() : model.name;
|
|
case "er-entity":
|
|
case "dfd-object":
|
|
case "dfd-diagram":
|
|
case "data-object":
|
|
case "app-process":
|
|
case "screen":
|
|
case "codeset":
|
|
case "message":
|
|
case "rule":
|
|
case "mapping":
|
|
return model.id;
|
|
case "diagram":
|
|
return model.name;
|
|
case "relations":
|
|
return typeof model.frontmatter.id === "string" && model.frontmatter.id.trim() ? model.frontmatter.id.trim() : void 0;
|
|
case "markdown":
|
|
default:
|
|
return void 0;
|
|
}
|
|
}
|
|
|
|
// src/core/relation-resolver.ts
|
|
function resolveDiagramRelations(diagram, index) {
|
|
if (diagram.kind === "er") {
|
|
return resolveErDiagramRelations(diagram, index);
|
|
}
|
|
if (diagram.kind === "dfd") {
|
|
return resolveDfdDiagramRelations(diagram, index);
|
|
}
|
|
const warnings = [];
|
|
const presentObjectIds = /* @__PURE__ */ new Set();
|
|
const deduped = dedupeDiagramNodes(
|
|
diagram,
|
|
(objectRef) => resolveObjectModelReference(objectRef, index) ?? void 0,
|
|
(object, objectRef) => object ? getObjectId2(object) : objectRef,
|
|
(object, objectRef) => object ? getObjectId2(object) : `ref:${objectRef}`,
|
|
(object, objectRef) => getClassDiagramNodeDisplayName(objectRef, object),
|
|
(objectRef) => `unresolved object ref "${objectRef}"`,
|
|
"Objects"
|
|
);
|
|
for (const node of deduped.nodes) {
|
|
if (node.object) {
|
|
presentObjectIds.add(getObjectId2(node.object));
|
|
}
|
|
}
|
|
const edges = resolveEdges(diagram, index, presentObjectIds, warnings);
|
|
return {
|
|
diagram,
|
|
nodes: deduped.nodes,
|
|
edges,
|
|
missingObjects: deduped.missingObjects,
|
|
warnings: [...warnings, ...deduped.warnings]
|
|
};
|
|
}
|
|
function resolveErDiagramRelations(diagram, index) {
|
|
const warnings = [];
|
|
const presentEntities = [];
|
|
const deduped = dedupeDiagramNodes(
|
|
diagram,
|
|
(objectRef) => resolveErEntityReference(objectRef, index) ?? void 0,
|
|
(entity, objectRef) => entity?.id ?? objectRef,
|
|
(entity, objectRef) => entity?.id ?? `ref:${objectRef}`,
|
|
(entity, objectRef) => getErDiagramNodeDisplayName(objectRef, entity),
|
|
(objectRef) => `unresolved ER entity ref "${objectRef}"`,
|
|
"Objects"
|
|
);
|
|
for (const node of deduped.nodes) {
|
|
if (node.object) {
|
|
presentEntities.push(node.object);
|
|
}
|
|
}
|
|
return {
|
|
diagram,
|
|
nodes: deduped.nodes,
|
|
edges: resolveErEdges(diagram, index, presentEntities, warnings),
|
|
missingObjects: deduped.missingObjects,
|
|
warnings: [...warnings, ...deduped.warnings]
|
|
};
|
|
}
|
|
function resolveDfdDiagramRelations(diagram, index) {
|
|
const warnings = [];
|
|
const objectResolution = resolveDfdDiagramObjects(diagram, index);
|
|
const edges = [];
|
|
diagram.flows.forEach((flow, rowIndex) => {
|
|
const context = {
|
|
section: "Flows",
|
|
rowIndex: rowIndex + 1,
|
|
relatedId: flow.id
|
|
};
|
|
const sourceEntry = resolveDfdFlowEndpoint(flow.from, objectResolution, index);
|
|
const targetEntry = resolveDfdFlowEndpoint(flow.to, objectResolution, index);
|
|
if (!sourceEntry) {
|
|
const listedObject = resolveDfdObjectReference(flow.from, index);
|
|
if (listedObject) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `flow source "${listedObject.id}" resolves to a dfd_object but is not listed in "Objects"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Flows",
|
|
context
|
|
});
|
|
}
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved DFD flow source "${flow.from}"`,
|
|
severity: "error",
|
|
path: diagram.path,
|
|
field: "Flows",
|
|
context
|
|
});
|
|
return;
|
|
}
|
|
if (!targetEntry) {
|
|
const listedObject = resolveDfdObjectReference(flow.to, index);
|
|
if (listedObject) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `flow target "${listedObject.id}" resolves to a dfd_object but is not listed in "Objects"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Flows",
|
|
context
|
|
});
|
|
}
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved DFD flow target "${flow.to}"`,
|
|
severity: "error",
|
|
path: diagram.path,
|
|
field: "Flows",
|
|
context
|
|
});
|
|
return;
|
|
}
|
|
if (sourceEntry.node.id === targetEntry.node.id) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: `DFD flow "${flow.id ?? rowIndex + 1}" is a self-loop`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Flows",
|
|
context
|
|
});
|
|
}
|
|
if (sourceEntry.kind === "external" && targetEntry.kind === "external") {
|
|
warnings.push(createDfdFlowShapeWarning(diagram.path, context, "external -> external"));
|
|
} else if (sourceEntry.kind === "external" && targetEntry.kind === "datastore") {
|
|
warnings.push(createDfdFlowShapeWarning(diagram.path, context, "external -> datastore"));
|
|
} else if (sourceEntry.kind === "datastore" && targetEntry.kind === "datastore") {
|
|
warnings.push(createDfdFlowShapeWarning(diagram.path, context, "datastore -> datastore"));
|
|
}
|
|
const flowData = resolveDfdFlowDataDisplay(flow.data, index);
|
|
if (flowData.warning) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: flowData.warning,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Flows",
|
|
context
|
|
});
|
|
}
|
|
edges.push({
|
|
id: flow.id,
|
|
source: sourceEntry.node.id,
|
|
target: targetEntry.node.id,
|
|
kind: "flow",
|
|
label: flowData.label,
|
|
metadata: {
|
|
notes: flow.notes,
|
|
rowIndex,
|
|
sourceKind: sourceEntry.kind,
|
|
targetKind: targetEntry.kind,
|
|
dataRaw: flow.data,
|
|
dataReference: flowData.reference,
|
|
dataModelPath: flowData.model?.path
|
|
}
|
|
});
|
|
});
|
|
return {
|
|
diagram,
|
|
nodes: objectResolution.nodes,
|
|
edges,
|
|
missingObjects: objectResolution.missingObjects,
|
|
warnings: [...warnings, ...objectResolution.warnings]
|
|
};
|
|
}
|
|
function resolveDfdDiagramObjects(diagram, index) {
|
|
const warnings = [];
|
|
const nodes = [];
|
|
const missingObjects = [];
|
|
const byId = /* @__PURE__ */ new Map();
|
|
const byReferenceKey = /* @__PURE__ */ new Map();
|
|
const entries = diagram.objectEntries.length > 0 ? diagram.objectEntries : diagram.objectRefs.map((ref, rowIndex) => ({
|
|
ref,
|
|
rowIndex,
|
|
compatibilityMode: "legacy_ref_only"
|
|
}));
|
|
for (const entry of entries) {
|
|
const ref = entry.ref?.trim();
|
|
const resolvedObject = ref ? resolveDfdObjectReference(ref, index) ?? void 0 : void 0;
|
|
if (!ref) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: `DFD local object "${entry.id ?? entry.label ?? entry.rowIndex + 1}" uses diagram-local definition without ref.`,
|
|
severity: "info",
|
|
path: diagram.path,
|
|
field: "Objects",
|
|
context: { rowIndex: entry.rowIndex + 1 }
|
|
});
|
|
} else if (!resolvedObject) {
|
|
missingObjects.push(ref);
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved DFD object ref "${ref}"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Objects",
|
|
context: { rowIndex: entry.rowIndex + 1 }
|
|
});
|
|
}
|
|
const effectiveKind = entry.kind ?? resolvedObject?.kind ?? "other";
|
|
if (!entry.kind && !resolvedObject?.kind) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: `DFD object "${entry.id ?? ref ?? entry.rowIndex + 1}" is missing kind and it could not be derived from ref.`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Objects",
|
|
context: { rowIndex: entry.rowIndex + 1 }
|
|
});
|
|
}
|
|
const resolvedLabel = getDfdDiagramNodeDisplayName(entry, resolvedObject);
|
|
const nodeId = entry.id?.trim() || resolvedObject?.id || ref || `dfd-object-${entry.rowIndex + 1}`;
|
|
const node = {
|
|
id: nodeId,
|
|
ref,
|
|
label: resolvedLabel,
|
|
kind: effectiveKind,
|
|
metadata: {
|
|
notes: entry.notes,
|
|
rowIndex: entry.rowIndex,
|
|
local: !ref,
|
|
compatibilityMode: entry.compatibilityMode
|
|
},
|
|
object: resolvedObject
|
|
};
|
|
const registryEntry = {
|
|
entry,
|
|
node,
|
|
object: resolvedObject,
|
|
kind: effectiveKind
|
|
};
|
|
nodes.push(node);
|
|
if (entry.id?.trim()) {
|
|
byId.set(entry.id.trim(), registryEntry);
|
|
}
|
|
const keySourceRefs = [
|
|
ref,
|
|
resolvedObject?.id,
|
|
resolvedObject?.path
|
|
].filter((value) => Boolean(value && value.trim()));
|
|
for (const sourceRef of keySourceRefs) {
|
|
const keys = buildReferenceIdentityKeys(resolveReferenceIdentity(sourceRef, index));
|
|
for (const key of keys) {
|
|
if (!byReferenceKey.has(key)) {
|
|
byReferenceKey.set(key, registryEntry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return { nodes, missingObjects, warnings, byId, byReferenceKey };
|
|
}
|
|
function resolveDfdFlowEndpoint(value, registry, index) {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
const byId = registry.byId.get(trimmed);
|
|
if (byId) {
|
|
return byId;
|
|
}
|
|
for (const key of buildReferenceIdentityKeys(resolveReferenceIdentity(trimmed, index))) {
|
|
const matched = registry.byReferenceKey.get(key);
|
|
if (matched) {
|
|
return matched;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function resolveDfdFlowDataDisplay(rawValue, index) {
|
|
const trimmed = rawValue?.trim();
|
|
if (!trimmed) {
|
|
return {};
|
|
}
|
|
const reference = parseReferenceValue(trimmed);
|
|
if (!reference) {
|
|
return { label: trimmed };
|
|
}
|
|
if (reference.kind === "raw") {
|
|
return { label: trimmed, reference };
|
|
}
|
|
if (reference.isExternal) {
|
|
return {
|
|
label: reference.display || trimmed,
|
|
reference
|
|
};
|
|
}
|
|
const model = reference.target ? resolveDataObjectReference(reference.target, index) ?? findModelByReference(reference.target, index) : null;
|
|
if (reference.display) {
|
|
return {
|
|
label: reference.display,
|
|
reference,
|
|
model
|
|
};
|
|
}
|
|
if (model) {
|
|
return {
|
|
label: getReferenceDisplayName(trimmed, model),
|
|
reference,
|
|
model
|
|
};
|
|
}
|
|
if (reference.target) {
|
|
return {
|
|
label: getReferenceDisplayName(trimmed),
|
|
reference,
|
|
warning: `unresolved flow data reference "${trimmed}"`
|
|
};
|
|
}
|
|
return { label: getReferenceDisplayName(trimmed), reference };
|
|
}
|
|
function dedupeDiagramNodes(diagram, resolveObject, buildResolvedId, buildCanonicalKey, buildDisplayName, buildUnresolvedMessage, field = "objectRefs") {
|
|
const nodes = [];
|
|
const missingObjects = [];
|
|
const warnings = [];
|
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
const seenMissingRefs = /* @__PURE__ */ new Set();
|
|
const duplicateCounts = /* @__PURE__ */ new Map();
|
|
for (const objectRef of diagram.objectRefs) {
|
|
const object = resolveObject(objectRef);
|
|
const canonicalKey = buildCanonicalKey(object, objectRef);
|
|
if (seenKeys.has(canonicalKey)) {
|
|
const existing = duplicateCounts.get(canonicalKey);
|
|
if (existing) {
|
|
existing.count += 1;
|
|
} else {
|
|
duplicateCounts.set(canonicalKey, {
|
|
displayRef: objectRef,
|
|
count: 2
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
seenKeys.add(canonicalKey);
|
|
if (!object && !seenMissingRefs.has(objectRef)) {
|
|
seenMissingRefs.add(objectRef);
|
|
missingObjects.push(objectRef);
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: buildUnresolvedMessage(objectRef),
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field
|
|
});
|
|
}
|
|
nodes.push({
|
|
id: buildResolvedId(object, objectRef),
|
|
ref: objectRef,
|
|
label: buildDisplayName(object, objectRef),
|
|
object
|
|
});
|
|
}
|
|
if (duplicateCounts.size > 0) {
|
|
const summary = Array.from(duplicateCounts.values()).map((entry) => `${entry.displayRef} x${entry.count}`).join(", ");
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: `Duplicate object refs were merged: ${summary}`,
|
|
severity: "info",
|
|
path: diagram.path,
|
|
field
|
|
});
|
|
}
|
|
return {
|
|
nodes,
|
|
missingObjects,
|
|
warnings
|
|
};
|
|
}
|
|
function resolveEdges(diagram, index, presentObjectIds, warnings) {
|
|
const explicitEdges = diagram.edges.filter((edge) => {
|
|
const sourceObject = resolveObjectModelReference(edge.source, index);
|
|
const targetObject = resolveObjectModelReference(edge.target, index);
|
|
if (!sourceObject || !targetObject) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved relation endpoint in relation "${edge.id ?? `${edge.source}:${edge.target}`}"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "relations"
|
|
});
|
|
return false;
|
|
}
|
|
const sourceId = getObjectId2(sourceObject);
|
|
const targetId = getObjectId2(targetObject);
|
|
if (!presentObjectIds.has(sourceId) || !presentObjectIds.has(targetId)) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `relation "${edge.id ?? `${edge.source}:${edge.target}`}" is outside diagram scope`,
|
|
severity: "info",
|
|
path: diagram.path,
|
|
field: "relations"
|
|
});
|
|
return false;
|
|
}
|
|
edge.source = sourceId;
|
|
edge.target = targetId;
|
|
return true;
|
|
});
|
|
if (explicitEdges.length > 0) {
|
|
return explicitEdges;
|
|
}
|
|
const autoAggregatedEdges = resolveClassDiagramEdgesFromObjects(
|
|
diagram,
|
|
index,
|
|
presentObjectIds,
|
|
warnings
|
|
);
|
|
if (autoAggregatedEdges.length > 0) {
|
|
warnings.push({
|
|
code: "section-missing",
|
|
message: 'diagram relations are empty; using auto-collected class relations from "Objects"',
|
|
severity: "info",
|
|
path: diagram.path,
|
|
field: "relations"
|
|
});
|
|
return autoAggregatedEdges;
|
|
}
|
|
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 ?? buildRelationKey2(relation);
|
|
if (seenRelationIds.has(relationKey)) {
|
|
continue;
|
|
}
|
|
seenRelationIds.add(relationKey);
|
|
const sourceObject = resolveObjectModelReference(relation.source, index);
|
|
const targetObject = resolveObjectModelReference(relation.target, index);
|
|
if (!sourceObject || !targetObject) {
|
|
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(getObjectId2(sourceObject)) && presentObjectIds.has(getObjectId2(targetObject))) {
|
|
edges.push(toDiagramEdge(relation, sourceObject, targetObject));
|
|
}
|
|
}
|
|
}
|
|
return edges;
|
|
}
|
|
function resolveClassDiagramEdgesFromObjects(diagram, index, presentObjectIds, warnings) {
|
|
const edges = [];
|
|
const seenRelationIds = /* @__PURE__ */ new Set();
|
|
for (const objectId of presentObjectIds) {
|
|
const object = index.objectsById[objectId];
|
|
if (!object) {
|
|
continue;
|
|
}
|
|
for (const relation of object.relations) {
|
|
const sourceObject = resolveObjectModelReference(relation.sourceClass, index);
|
|
const targetObject = resolveObjectModelReference(relation.targetClass, index);
|
|
const relationKey = buildClassRelationKey2(relation);
|
|
if (seenRelationIds.has(relationKey)) {
|
|
continue;
|
|
}
|
|
if (!sourceObject || !targetObject) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved class relation endpoint in relation "${relation.id ?? relationKey}"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "relations"
|
|
});
|
|
continue;
|
|
}
|
|
const sourceId = getObjectId2(sourceObject);
|
|
const targetId = getObjectId2(targetObject);
|
|
if (!presentObjectIds.has(sourceId) || !presentObjectIds.has(targetId)) {
|
|
continue;
|
|
}
|
|
seenRelationIds.add(relationKey);
|
|
edges.push(toClassDiagramEdge(relation, sourceObject, targetObject));
|
|
}
|
|
}
|
|
return edges;
|
|
}
|
|
function toDiagramEdge(relation, sourceObject, targetObject) {
|
|
return {
|
|
id: relation.id,
|
|
source: getObjectId2(sourceObject),
|
|
target: getObjectId2(targetObject),
|
|
kind: relation.kind,
|
|
label: relation.label,
|
|
metadata: {
|
|
sourceCardinality: relation.sourceCardinality,
|
|
targetCardinality: relation.targetCardinality
|
|
}
|
|
};
|
|
}
|
|
function toClassDiagramEdge(relation, sourceObject, targetObject) {
|
|
return {
|
|
id: relation.id,
|
|
source: getObjectId2(sourceObject),
|
|
target: getObjectId2(targetObject),
|
|
kind: relation.kind,
|
|
label: relation.label,
|
|
metadata: {
|
|
notes: relation.notes,
|
|
sourceCardinality: relation.fromMultiplicity,
|
|
targetCardinality: relation.toMultiplicity
|
|
}
|
|
};
|
|
}
|
|
function buildRelationKey2(relation) {
|
|
return `${relation.source}:${relation.kind}:${relation.target}:${relation.label ?? ""}`;
|
|
}
|
|
function buildClassRelationKey2(relation) {
|
|
return relation.id ?? `${relation.sourceClass}:${relation.targetClass}:${relation.kind}:${relation.label ?? ""}`;
|
|
}
|
|
function resolveErEdges(diagram, index, presentEntities, warnings) {
|
|
const edges = [];
|
|
const seenRelationIds = /* @__PURE__ */ new Set();
|
|
const presentEntityIds = new Set(presentEntities.map((entity) => entity.id));
|
|
const presentEntityKeys = /* @__PURE__ */ new Set();
|
|
for (const entity of presentEntities) {
|
|
for (const key of buildErEntityCanonicalKeys(entity)) {
|
|
presentEntityKeys.add(key);
|
|
}
|
|
}
|
|
for (const entity of presentEntities) {
|
|
for (const relation of entity.outboundRelations) {
|
|
const relationId = relation.id ?? `${entity.id}:${relation.targetEntity}:${relation.kind}`;
|
|
if (seenRelationIds.has(relationId)) {
|
|
continue;
|
|
}
|
|
seenRelationIds.add(relationId);
|
|
const targetEntity = resolveErEntityReference(relation.targetEntity, index);
|
|
if (!targetEntity) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved ER relation endpoint in relation "${relation.id ?? relationId}"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "relations"
|
|
});
|
|
continue;
|
|
}
|
|
const targetIsPresent = presentEntityIds.has(targetEntity.id) || buildErEntityCanonicalKeys(targetEntity).some((key) => presentEntityKeys.has(key));
|
|
if (!targetIsPresent) {
|
|
continue;
|
|
}
|
|
edges.push(toErDiagramEdge(entity, targetEntity, relation));
|
|
}
|
|
}
|
|
return edges;
|
|
}
|
|
function getObjectId2(object) {
|
|
const explicitId = object.frontmatter.id;
|
|
if (typeof explicitId === "string" && explicitId.trim()) {
|
|
return explicitId.trim();
|
|
}
|
|
return object.name;
|
|
}
|
|
function getClassDiagramNodeDisplayName(reference, object) {
|
|
if (object) {
|
|
return object.name || getObjectId2(object);
|
|
}
|
|
const parsed = parseReferenceValue(reference);
|
|
if (parsed?.target) {
|
|
return parsed.target.split("/").pop() ?? parsed.target;
|
|
}
|
|
return parsed?.display || parsed?.raw || reference.trim();
|
|
}
|
|
function getErDiagramNodeDisplayName(reference, entity) {
|
|
const parsed = parseReferenceValue(reference);
|
|
if (parsed?.display) {
|
|
return parsed.display;
|
|
}
|
|
if (entity) {
|
|
return entity.logicalName || entity.physicalName || entity.id;
|
|
}
|
|
if (parsed?.target) {
|
|
return parsed.target.split("/").pop() ?? parsed.target;
|
|
}
|
|
return parsed?.raw || reference.trim();
|
|
}
|
|
function buildErEntityCanonicalKeys(entity) {
|
|
const keys = /* @__PURE__ */ new Set();
|
|
if (entity.id?.trim()) {
|
|
keys.add(`id:${entity.id.trim()}`);
|
|
}
|
|
if (entity.physicalName?.trim()) {
|
|
keys.add(`physical:${entity.physicalName.trim()}`);
|
|
}
|
|
if (entity.path?.trim()) {
|
|
const normalizedPath = entity.path.replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
keys.add(`path:${normalizedPath}`);
|
|
const basename = normalizedPath.split("/").pop();
|
|
if (basename) {
|
|
keys.add(`basename:${basename}`);
|
|
}
|
|
}
|
|
return Array.from(keys);
|
|
}
|
|
function getDfdDiagramNodeDisplayName(entry, object) {
|
|
if (entry.label?.trim()) {
|
|
return entry.label.trim();
|
|
}
|
|
if (object) {
|
|
return object.name || object.id;
|
|
}
|
|
if (entry.id?.trim()) {
|
|
return entry.id.trim();
|
|
}
|
|
const reference = entry.ref?.trim() ?? "";
|
|
const parsed = parseReferenceValue(reference);
|
|
if (parsed?.target) {
|
|
return parsed.target.split("/").pop() ?? parsed.target;
|
|
}
|
|
return parsed?.raw || reference.trim();
|
|
}
|
|
function toErDiagramEdge(sourceEntity, targetEntity, relation) {
|
|
const mappingSummary = relation.mappings.map((mapping) => `${mapping.localColumn} -> ${mapping.targetColumn}`).join(" / ");
|
|
return {
|
|
id: relation.id,
|
|
source: sourceEntity.id,
|
|
target: targetEntity.id,
|
|
kind: "association",
|
|
label: relation.label,
|
|
metadata: {
|
|
cardinality: relation.cardinality,
|
|
sourceColumn: relation.mappings[0]?.localColumn,
|
|
targetColumn: relation.mappings[0]?.targetColumn,
|
|
logicalName: relation.label,
|
|
physicalName: relation.id,
|
|
kind: relation.kind,
|
|
mappingSummary,
|
|
mappings: relation.mappings
|
|
}
|
|
};
|
|
}
|
|
function createDfdFlowShapeWarning(path2, context, shape) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message: `DFD flow shape "${shape}" may be unusual`,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "Flows",
|
|
context
|
|
};
|
|
}
|
|
|
|
// src/core/render-mode.ts
|
|
var VALID_RENDER_MODES = /* @__PURE__ */ new Set([
|
|
"custom",
|
|
"mermaid",
|
|
"mermaid-detail"
|
|
]);
|
|
var TABLE_TEXT_FORMATS = /* @__PURE__ */ new Set([
|
|
"data-object",
|
|
"app-process",
|
|
"rule",
|
|
"codeset",
|
|
"message",
|
|
"mapping"
|
|
]);
|
|
function resolveRenderMode(input) {
|
|
const diagnostics = [];
|
|
const toolbarMode = normalizeRenderMode(input.toolbarOverride);
|
|
const frontmatterMode = normalizeFrontmatterRenderMode(
|
|
input.frontmatterRenderMode,
|
|
input.filePath,
|
|
diagnostics
|
|
);
|
|
const settingsMode = normalizeRenderMode(input.settingsDefaultRenderMode);
|
|
const formatDefaultMode = getFormatDefaultRenderMode(input.formatType);
|
|
const supportedModes = getSupportedRenderModes(input.formatType, input.modelKind);
|
|
const fallbackMode = getFallbackRenderMode(input.formatType, input.modelKind);
|
|
const toolbarResult = selectSupportedRenderMode(
|
|
toolbarMode,
|
|
"toolbar",
|
|
supportedModes
|
|
);
|
|
if (toolbarResult) {
|
|
return buildResolvedRenderMode(input, toolbarResult.mode, toolbarResult.source, diagnostics);
|
|
}
|
|
const frontmatterResult = selectSupportedRenderMode(
|
|
frontmatterMode,
|
|
"frontmatter",
|
|
supportedModes
|
|
);
|
|
if (frontmatterResult) {
|
|
return buildResolvedRenderMode(
|
|
input,
|
|
frontmatterResult.mode,
|
|
frontmatterResult.source,
|
|
diagnostics
|
|
);
|
|
}
|
|
if (frontmatterMode) {
|
|
diagnostics.push(
|
|
createRenderModeWarning(
|
|
input.filePath,
|
|
`${capitalizeRenderMode(frontmatterMode)} renderer is not supported for ${input.formatType}. Using the format default renderer.`,
|
|
"render_mode"
|
|
)
|
|
);
|
|
}
|
|
const settingsResult = selectSupportedRenderMode(
|
|
settingsMode,
|
|
"settings",
|
|
supportedModes
|
|
);
|
|
if (settingsResult) {
|
|
return buildResolvedRenderMode(input, settingsResult.mode, settingsResult.source, diagnostics);
|
|
}
|
|
const defaultResult = selectSupportedRenderMode(
|
|
formatDefaultMode,
|
|
"format_default",
|
|
supportedModes
|
|
);
|
|
if (defaultResult) {
|
|
return buildResolvedRenderMode(input, defaultResult.mode, defaultResult.source, diagnostics);
|
|
}
|
|
return buildResolvedRenderMode(
|
|
input,
|
|
fallbackMode,
|
|
"fallback",
|
|
diagnostics,
|
|
settingsMode ? `unsupported:${settingsMode}` : void 0
|
|
);
|
|
}
|
|
function getFormatDefaultRenderMode(formatType) {
|
|
switch (formatType) {
|
|
case "dfd-diagram":
|
|
return "mermaid";
|
|
default:
|
|
return "custom";
|
|
}
|
|
}
|
|
function getSupportedRenderModes(formatType, modelKind) {
|
|
return getForcedRenderModes(formatType, modelKind);
|
|
}
|
|
function getForcedRenderModes(formatType, modelKind) {
|
|
switch (formatType) {
|
|
case "diagram":
|
|
if (modelKind === "class") {
|
|
return ["custom", "mermaid", "mermaid-detail"];
|
|
}
|
|
return modelKind === "er" ? ["custom", "mermaid", "mermaid-detail"] : ["custom"];
|
|
case "object":
|
|
return ["custom", "mermaid", "mermaid-detail"];
|
|
case "er-entity":
|
|
return ["custom", "mermaid", "mermaid-detail"];
|
|
case "dfd-diagram":
|
|
return ["mermaid"];
|
|
case "dfd-object":
|
|
return [];
|
|
case "screen":
|
|
return [];
|
|
case "data-object":
|
|
case "app-process":
|
|
case "rule":
|
|
case "codeset":
|
|
case "message":
|
|
case "mapping":
|
|
return [];
|
|
case "markdown":
|
|
return [];
|
|
default:
|
|
return ["custom"];
|
|
}
|
|
}
|
|
function normalizeRenderMode(value) {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
const normalized = value.trim().toLowerCase();
|
|
return VALID_RENDER_MODES.has(normalized) ? normalized : null;
|
|
}
|
|
function normalizeFrontmatterRenderMode(value, filePath, diagnostics) {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
const normalized = value.trim().toLowerCase();
|
|
if (!normalized) {
|
|
return null;
|
|
}
|
|
if (normalized === "auto") {
|
|
diagnostics.push(
|
|
createRenderModeWarning(
|
|
filePath,
|
|
'Deprecated render_mode value "auto". Using the format default renderer.',
|
|
"render_mode"
|
|
)
|
|
);
|
|
return null;
|
|
}
|
|
const mode = normalizeRenderMode(normalized);
|
|
if (!mode) {
|
|
diagnostics.push(
|
|
createRenderModeWarning(
|
|
filePath,
|
|
`Unknown render_mode value "${value}". Using the format default renderer.`,
|
|
"render_mode"
|
|
)
|
|
);
|
|
}
|
|
return mode;
|
|
}
|
|
function selectSupportedRenderMode(mode, source, supportedModes) {
|
|
if (!mode || !supportedModes.includes(mode)) {
|
|
return null;
|
|
}
|
|
return { mode, source };
|
|
}
|
|
function buildResolvedRenderMode(input, mode, source, diagnostics, fallbackReason) {
|
|
return {
|
|
selectedMode: mode,
|
|
effectiveMode: mode,
|
|
actualRenderer: getRendererImplementation(
|
|
input.formatType,
|
|
mode,
|
|
input.modelKind
|
|
),
|
|
source,
|
|
fallbackReason,
|
|
diagnostics: appendReducedOverviewNote(
|
|
diagnostics,
|
|
input.formatType,
|
|
input.modelKind,
|
|
mode,
|
|
input.filePath
|
|
)
|
|
};
|
|
}
|
|
function getFallbackRenderMode(formatType, modelKind) {
|
|
const supported = getForcedRenderModes(formatType, modelKind);
|
|
if (supported.includes(getFormatDefaultRenderMode(formatType))) {
|
|
return getFormatDefaultRenderMode(formatType);
|
|
}
|
|
return supported[0] ?? "custom";
|
|
}
|
|
function getRendererImplementation(formatType, mode, modelKind) {
|
|
if ((mode === "mermaid" || mode === "mermaid-detail") && (formatType === "dfd-diagram" || formatType === "object" || formatType === "er-entity" || formatType === "diagram" && (modelKind === "class" || modelKind === "er"))) {
|
|
return "mermaid";
|
|
}
|
|
if (TABLE_TEXT_FORMATS.has(formatType)) {
|
|
return "table-text";
|
|
}
|
|
return "custom";
|
|
}
|
|
function createRenderModeWarning(filePath, message, field) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
filePath,
|
|
field,
|
|
section: "frontmatter"
|
|
};
|
|
}
|
|
function capitalizeRenderMode(value) {
|
|
return value.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
}
|
|
function appendReducedOverviewNote(diagnostics, formatType, modelKind, effectiveMode, filePath) {
|
|
if (effectiveMode !== "mermaid" || !(formatType === "object" || formatType === "er-entity" || formatType === "diagram" && (modelKind === "class" || modelKind === "er"))) {
|
|
return diagnostics;
|
|
}
|
|
return [
|
|
...diagnostics,
|
|
{
|
|
code: "invalid-structure",
|
|
message: "Mermaid mode shows reduced overview only.",
|
|
severity: "info",
|
|
filePath,
|
|
section: "frontmatter",
|
|
field: "render_mode"
|
|
}
|
|
];
|
|
}
|
|
|
|
// src/core/schema-detector.ts
|
|
var SCHEMA_TO_FILE_TYPE = {
|
|
model_object_v1: "object",
|
|
model_relations_v1: "relations"
|
|
};
|
|
var TYPE_TO_FILE_TYPE = {
|
|
class: "object",
|
|
data_object: "data-object",
|
|
app_process: "app-process",
|
|
screen: "screen",
|
|
rule: "rule",
|
|
codeset: "codeset",
|
|
message: "message",
|
|
mapping: "mapping",
|
|
dfd_object: "dfd-object",
|
|
dfd_diagram: "dfd-diagram",
|
|
er_entity: "er-entity",
|
|
er_diagram: "diagram",
|
|
class_diagram: "diagram"
|
|
};
|
|
function detectFileType(value) {
|
|
const schema = typeof value === "string" ? value : value?.schema;
|
|
if (!schema) {
|
|
if (typeof value !== "string") {
|
|
const type = typeof value?.type === "string" ? value.type.trim() : "";
|
|
if (type && TYPE_TO_FILE_TYPE[type]) {
|
|
return TYPE_TO_FILE_TYPE[type];
|
|
}
|
|
}
|
|
return "markdown";
|
|
}
|
|
return SCHEMA_TO_FILE_TYPE[schema] ?? "markdown";
|
|
}
|
|
|
|
// src/editor/model-weave-editor-suggest.ts
|
|
var import_obsidian2 = require("obsidian");
|
|
|
|
// 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/core/internal-edge-adapters.ts
|
|
function toClassRelationEdge(relation, sourceClass = relation.source, targetClass = relation.target) {
|
|
return {
|
|
domain: "class",
|
|
id: relation.id,
|
|
source: sourceClass,
|
|
target: targetClass,
|
|
sourceClass,
|
|
targetClass,
|
|
kind: relation.kind,
|
|
label: relation.label,
|
|
notes: relation.description,
|
|
fromMultiplicity: relation.sourceCardinality,
|
|
toMultiplicity: relation.targetCardinality
|
|
};
|
|
}
|
|
function erRelationBlockToInternalEdge(relationBlock, sourceEntity) {
|
|
const sourceName = typeof sourceEntity === "string" ? sourceEntity : sourceEntity.id;
|
|
return {
|
|
domain: "er",
|
|
id: relationBlock.id,
|
|
source: sourceName,
|
|
target: relationBlock.targetTable ?? "",
|
|
sourceEntity: sourceName,
|
|
targetEntity: relationBlock.targetTable ?? "",
|
|
kind: relationBlock.kind ?? "fk",
|
|
label: relationBlock.id,
|
|
notes: relationBlock.notes ?? void 0,
|
|
cardinality: relationBlock.cardinality ?? void 0,
|
|
mappings: relationBlock.mappings.map((mapping) => ({
|
|
localColumn: mapping.localColumn,
|
|
targetColumn: mapping.targetColumn,
|
|
notes: mapping.notes ?? void 0
|
|
}))
|
|
};
|
|
}
|
|
function classDiagramEdgeToInternalEdge(edge) {
|
|
return {
|
|
domain: "class",
|
|
id: edge.id,
|
|
source: edge.source,
|
|
target: edge.target,
|
|
sourceClass: edge.source,
|
|
targetClass: edge.target,
|
|
kind: edge.kind ?? "association",
|
|
label: edge.label,
|
|
notes: typeof edge.metadata?.notes === "string" ? edge.metadata.notes : void 0,
|
|
fromMultiplicity: typeof edge.metadata?.sourceCardinality === "string" ? edge.metadata.sourceCardinality : void 0,
|
|
toMultiplicity: typeof edge.metadata?.targetCardinality === "string" ? edge.metadata.targetCardinality : void 0
|
|
};
|
|
}
|
|
function erDiagramEdgeToInternalEdge(edge) {
|
|
const mappings = [];
|
|
if (Array.isArray(edge.metadata?.mappings)) {
|
|
for (const mapping of edge.metadata.mappings) {
|
|
if (!mapping || typeof mapping !== "object") {
|
|
continue;
|
|
}
|
|
const candidate = mapping;
|
|
const localColumn = candidate.localColumn;
|
|
const targetColumn = candidate.targetColumn;
|
|
if (typeof localColumn !== "string" || typeof targetColumn !== "string") {
|
|
continue;
|
|
}
|
|
mappings.push({
|
|
localColumn,
|
|
targetColumn,
|
|
notes: typeof candidate.notes === "string" ? candidate.notes : void 0
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
domain: "er",
|
|
id: edge.id,
|
|
source: edge.source,
|
|
target: edge.target,
|
|
sourceEntity: edge.source,
|
|
targetEntity: edge.target,
|
|
kind: edge.kind ?? "association",
|
|
label: getErEdgeLabel(edge),
|
|
notes: typeof edge.metadata?.notes === "string" ? edge.metadata.notes : void 0,
|
|
cardinality: typeof edge.metadata?.cardinality === "string" ? edge.metadata.cardinality : void 0,
|
|
mappings: mappings.length > 0 ? mappings : hasColumnMapping(edge) ? [
|
|
{
|
|
localColumn: String(edge.metadata?.sourceColumn),
|
|
targetColumn: String(edge.metadata?.targetColumn),
|
|
notes: typeof edge.metadata?.mappingNotes === "string" ? edge.metadata.mappingNotes : void 0
|
|
}
|
|
] : []
|
|
};
|
|
}
|
|
function getErEdgeLabel(edge) {
|
|
if (typeof edge.metadata?.logicalName === "string") {
|
|
return edge.metadata.logicalName;
|
|
}
|
|
if (typeof edge.metadata?.physicalName === "string") {
|
|
return edge.metadata.physicalName;
|
|
}
|
|
return edge.label;
|
|
}
|
|
function hasColumnMapping(edge) {
|
|
return typeof edge.metadata?.sourceColumn === "string" && typeof edge.metadata?.targetColumn === "string";
|
|
}
|
|
|
|
// src/parsers/markdown-sections.ts
|
|
var SECTION_HEADINGS = {
|
|
"# Summary": "Summary",
|
|
"## Summary": "Summary",
|
|
"## Overview": "Overview",
|
|
"## Attributes": "Attributes",
|
|
"## Methods": "Methods",
|
|
"## Layout": "Layout",
|
|
"## Fields": "Fields",
|
|
"## Actions": "Actions",
|
|
"## Messages": "Messages",
|
|
"## Format": "Format",
|
|
"## Records": "Records",
|
|
"## References": "References",
|
|
"## Conditions": "Conditions",
|
|
"## Values": "Values",
|
|
"## Scope": "Scope",
|
|
"## Mappings": "Mappings",
|
|
"## Rules": "Rules",
|
|
"## Triggers": "Triggers",
|
|
"## Inputs": "Inputs",
|
|
"## Steps": "Steps",
|
|
"## Outputs": "Outputs",
|
|
"## Transitions": "Transitions",
|
|
"## Errors": "Errors",
|
|
"## Local Processes": "Local Processes",
|
|
"## Notes": "Notes",
|
|
"## Relations": "Relations",
|
|
"## Source Links": "Source Links",
|
|
"## Flows": "Flows",
|
|
"## Objects": "Objects",
|
|
"## Columns": "Columns",
|
|
"## Indexes": "Indexes"
|
|
};
|
|
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/parsers/markdown-table.ts
|
|
function parseMarkdownTable(lines, expectedHeaders, path2, sectionName) {
|
|
if (!lines) {
|
|
return { rows: [], warnings: [] };
|
|
}
|
|
const normalizedLines = lines.map((line) => line.trim()).filter((line) => line.startsWith("|"));
|
|
if (normalizedLines.length < 2) {
|
|
return {
|
|
rows: [],
|
|
warnings: normalizedLines.length === 0 ? [] : [
|
|
createWarning2(
|
|
"invalid-table-row",
|
|
`table in section "${sectionName}" is incomplete`,
|
|
path2,
|
|
sectionName
|
|
)
|
|
]
|
|
};
|
|
}
|
|
const headers = splitMarkdownTableRow(normalizedLines[0]) ?? [];
|
|
const warnings = [];
|
|
if (!sameHeaders(headers, expectedHeaders)) {
|
|
warnings.push(
|
|
createWarning2(
|
|
"invalid-table-column",
|
|
`table columns in section "${sectionName}" do not match expected headers`,
|
|
path2,
|
|
sectionName
|
|
)
|
|
);
|
|
}
|
|
const rows = [];
|
|
for (const rowLine of normalizedLines.slice(2)) {
|
|
const values = splitMarkdownTableRow(rowLine) ?? [];
|
|
if (values.length !== headers.length) {
|
|
warnings.push(
|
|
createWarning2(
|
|
"invalid-table-row",
|
|
`table row in section "${sectionName}" has ${values.length} columns, expected ${headers.length}`,
|
|
path2,
|
|
sectionName
|
|
)
|
|
);
|
|
continue;
|
|
}
|
|
const row = {};
|
|
for (const [index, header] of headers.entries()) {
|
|
row[header] = values[index] ?? "";
|
|
}
|
|
rows.push(row);
|
|
}
|
|
return { rows, warnings };
|
|
}
|
|
function splitMarkdownTableRow(line) {
|
|
const ranges = getMarkdownTableCellRanges(line);
|
|
if (!ranges) {
|
|
return null;
|
|
}
|
|
return ranges.map((range) => line.slice(range.contentStart, range.contentEnd).trim());
|
|
}
|
|
function getMarkdownTableCellRanges(line) {
|
|
const trimmedLine = line.trim();
|
|
if (!trimmedLine.startsWith("|")) {
|
|
return null;
|
|
}
|
|
const separatorIndexes = findMarkdownTableSeparators(line);
|
|
if (separatorIndexes.length === 0) {
|
|
return null;
|
|
}
|
|
const trailingPipeIndex = separatorIndexes[separatorIndexes.length - 1];
|
|
const effectiveSeparators = trailingPipeIndex === line.length - 1 ? separatorIndexes : [...separatorIndexes, line.length];
|
|
if (effectiveSeparators.length < 2) {
|
|
return null;
|
|
}
|
|
const cells = [];
|
|
for (let columnIndex = 0; columnIndex < effectiveSeparators.length - 1; columnIndex += 1) {
|
|
const rawStart = effectiveSeparators[columnIndex] + 1;
|
|
const rawEnd = effectiveSeparators[columnIndex + 1];
|
|
let contentStart = rawStart;
|
|
let contentEnd = rawEnd;
|
|
while (contentStart < rawEnd && /\s/.test(line[contentStart] ?? "")) {
|
|
contentStart += 1;
|
|
}
|
|
while (contentEnd > rawStart && /\s/.test(line[contentEnd - 1] ?? "")) {
|
|
contentEnd -= 1;
|
|
}
|
|
if (contentStart > contentEnd) {
|
|
contentStart = rawStart;
|
|
contentEnd = rawStart;
|
|
}
|
|
cells.push({
|
|
columnIndex,
|
|
rawStart,
|
|
rawEnd,
|
|
contentStart,
|
|
contentEnd
|
|
});
|
|
}
|
|
return cells;
|
|
}
|
|
function findMarkdownTableSeparators(line) {
|
|
const separators = [];
|
|
let escaped = false;
|
|
let wikilinkDepth = 0;
|
|
let markdownLinkTextDepth = 0;
|
|
let markdownLinkTargetDepth = 0;
|
|
for (let index = 0; index < line.length; index += 1) {
|
|
const char = line[index];
|
|
const next = line[index + 1];
|
|
if (escaped) {
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (char === "\\") {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (char === "[" && next === "[") {
|
|
wikilinkDepth += 1;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (char === "]" && next === "]" && wikilinkDepth > 0) {
|
|
wikilinkDepth -= 1;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (wikilinkDepth === 0 && markdownLinkTargetDepth === 0 && char === "[") {
|
|
markdownLinkTextDepth += 1;
|
|
continue;
|
|
}
|
|
if (markdownLinkTextDepth > 0 && char === "]" && next === "(") {
|
|
markdownLinkTextDepth -= 1;
|
|
markdownLinkTargetDepth = 1;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (markdownLinkTargetDepth > 0) {
|
|
if (char === "(") {
|
|
markdownLinkTargetDepth += 1;
|
|
continue;
|
|
}
|
|
if (char === ")") {
|
|
markdownLinkTargetDepth -= 1;
|
|
continue;
|
|
}
|
|
}
|
|
if (char === "|" && wikilinkDepth === 0 && markdownLinkTextDepth === 0 && markdownLinkTargetDepth === 0) {
|
|
separators.push(index);
|
|
continue;
|
|
}
|
|
}
|
|
return separators;
|
|
}
|
|
function sameHeaders(actual, expected) {
|
|
if (actual.length !== expected.length) {
|
|
return false;
|
|
}
|
|
return actual.every((header, index) => header === expected[index]);
|
|
}
|
|
function createWarning2(code, message, path2, field) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/parsers/source-links-parser.ts
|
|
function parseSourceLinks(lines) {
|
|
if (!lines) {
|
|
return [];
|
|
}
|
|
const tableLinks = parseSourceLinksTable(lines);
|
|
if (tableLinks) {
|
|
return tableLinks;
|
|
}
|
|
return lines.map((line) => parseSourceLinkLine(line)).filter((link) => Boolean(link));
|
|
}
|
|
function parseSourceLinksTable(lines) {
|
|
const tableLines = lines.map((line) => line.trim()).filter((line) => line.startsWith("|"));
|
|
if (tableLines.length < 2) {
|
|
return null;
|
|
}
|
|
const headers = splitMarkdownTableRow(tableLines[0])?.map(
|
|
(header) => normalizeHeader(header)
|
|
);
|
|
if (!headers || headers.length === 0) {
|
|
return [];
|
|
}
|
|
const pathIndex = findHeaderIndex(headers, ["path", "source", "source_path", "file"]);
|
|
if (pathIndex < 0) {
|
|
return [];
|
|
}
|
|
const labelIndex = findHeaderIndex(headers, ["label", "name", "title"]);
|
|
const notesIndex = findHeaderIndex(headers, ["notes", "note", "description"]);
|
|
return tableLines.slice(2).map((line) => splitMarkdownTableRow(line) ?? []).map((cells) => ({
|
|
path: cleanSourcePath(cells[pathIndex]),
|
|
label: labelIndex >= 0 ? cleanOptionalValue(cells[labelIndex]) : void 0,
|
|
notes: notesIndex >= 0 ? cleanOptionalValue(cells[notesIndex]) : void 0
|
|
})).filter((link) => Boolean(link.path));
|
|
}
|
|
function parseSourceLinkLine(line) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
const withoutBullet = trimmed.replace(/^[-*]\s+/, "").trim();
|
|
if (!withoutBullet) {
|
|
return null;
|
|
}
|
|
const markdownLink = withoutBullet.match(/^\[([^\]]+)\]\(([^)]+)\)(?:\s*[-:]\s*(.+))?$/);
|
|
if (markdownLink) {
|
|
return {
|
|
path: cleanSourcePath(markdownLink[2]),
|
|
label: cleanOptionalValue(markdownLink[1]),
|
|
notes: cleanOptionalValue(markdownLink[3])
|
|
};
|
|
}
|
|
const [pathValue, notes] = splitPathAndNotes(withoutBullet);
|
|
const path2 = cleanSourcePath(pathValue);
|
|
return path2 ? {
|
|
path: path2,
|
|
notes: cleanOptionalValue(notes)
|
|
} : null;
|
|
}
|
|
function splitPathAndNotes(value) {
|
|
const separator = value.match(/\s+-\s+|\s+:\s+/);
|
|
if (!separator || separator.index === void 0) {
|
|
return [value, void 0];
|
|
}
|
|
return [
|
|
value.slice(0, separator.index),
|
|
value.slice(separator.index + separator[0].length)
|
|
];
|
|
}
|
|
function cleanSourcePath(value) {
|
|
return cleanOptionalValue(value)?.replace(/^`|`$/g, "").trim() ?? "";
|
|
}
|
|
function cleanOptionalValue(value) {
|
|
const cleaned = value?.trim();
|
|
return cleaned ? cleaned : void 0;
|
|
}
|
|
function normalizeHeader(value) {
|
|
return value.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
}
|
|
function findHeaderIndex(headers, candidates) {
|
|
return headers.findIndex((header) => candidates.includes(header));
|
|
}
|
|
|
|
// src/parsers/er-entity-parser.ts
|
|
var COLUMN_HEADERS = [
|
|
"logical_name",
|
|
"physical_name",
|
|
"data_type",
|
|
"length",
|
|
"scale",
|
|
"not_null",
|
|
"pk",
|
|
"encrypted",
|
|
"default_value",
|
|
"notes"
|
|
];
|
|
var INDEX_HEADERS = [
|
|
"index_name",
|
|
"index_type",
|
|
"unique",
|
|
"columns",
|
|
"notes"
|
|
];
|
|
var RELATION_MAPPING_HEADERS = [
|
|
"local_column",
|
|
"target_column",
|
|
"notes"
|
|
];
|
|
function parseErEntityFile(markdown, path2) {
|
|
const frontmatterResult = parseFrontmatter(markdown);
|
|
const warnings = [...frontmatterResult.warnings];
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
if (detectFileType(frontmatter) !== "er-entity") {
|
|
warnings.push(
|
|
createWarning3(
|
|
"invalid-structure",
|
|
'ER entity parser expected frontmatter type "er_entity"',
|
|
path2,
|
|
"type"
|
|
)
|
|
);
|
|
return { file: null, warnings };
|
|
}
|
|
const body = frontmatterResult.file.body;
|
|
const sections = extractMarkdownSections(body);
|
|
const id = getRequiredString(frontmatter, "id", warnings, path2);
|
|
const logicalName = getRequiredString(frontmatter, "logical_name", warnings, path2);
|
|
const physicalName = getRequiredString(frontmatter, "physical_name", warnings, path2);
|
|
if (!sections.Columns) {
|
|
warnings.push(
|
|
createInfoWarning("section-missing", 'section missing: "Columns"', path2, "Columns")
|
|
);
|
|
}
|
|
const columnTable = parseMarkdownTable(
|
|
sections.Columns,
|
|
[...COLUMN_HEADERS],
|
|
path2,
|
|
"Columns"
|
|
);
|
|
const indexTable = parseMarkdownTable(
|
|
sections.Indexes,
|
|
[...INDEX_HEADERS],
|
|
path2,
|
|
"Indexes"
|
|
);
|
|
warnings.push(...columnTable.warnings, ...indexTable.warnings);
|
|
const columns = columnTable.rows.map((row) => toErColumn(row, warnings, path2));
|
|
const indexes = indexTable.rows.map((row) => toErIndex(row));
|
|
const relationBlocks = parseRelationBlocks(body, warnings, path2);
|
|
const fallbackId = id || getFileStem(path2) || "UNTITLED-ER-ENTITY";
|
|
const fallbackLogicalName = logicalName || physicalName || fallbackId;
|
|
const fallbackPhysicalName = physicalName || logicalName || fallbackId;
|
|
const baseEntity = {
|
|
fileType: "er-entity",
|
|
path: path2,
|
|
filePath: path2,
|
|
title: buildTitle(fallbackLogicalName, fallbackPhysicalName),
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id: fallbackId,
|
|
logicalName: fallbackLogicalName,
|
|
physicalName: fallbackPhysicalName,
|
|
schemaName: getOptionalString(frontmatter, "schema_name"),
|
|
dbms: getOptionalString(frontmatter, "dbms"),
|
|
columns,
|
|
indexes,
|
|
relationBlocks,
|
|
outboundRelations: []
|
|
};
|
|
baseEntity.outboundRelations = relationBlocks.map(
|
|
(relationBlock) => erRelationBlockToInternalEdge(relationBlock, baseEntity)
|
|
);
|
|
return {
|
|
file: baseEntity,
|
|
warnings
|
|
};
|
|
}
|
|
function getFileStem(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
function parseRelationBlocks(body, warnings, path2) {
|
|
const lines = body.replace(/\r\n/g, "\n").split("\n");
|
|
const relationsSectionLines = extractRelationsSectionLines(lines);
|
|
if (relationsSectionLines.length === 0) {
|
|
return [];
|
|
}
|
|
const blocks = [];
|
|
let currentId = null;
|
|
let currentLines = [];
|
|
const seenIds = /* @__PURE__ */ new Set();
|
|
const flushBlock = () => {
|
|
if (!currentId) {
|
|
return;
|
|
}
|
|
if (isIncompleteErRelationId2(currentId)) {
|
|
warnings.push(
|
|
createWarning3(
|
|
"invalid-structure",
|
|
`ER relation id looks incomplete: ${currentId}`,
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
}
|
|
if (seenIds.has(currentId)) {
|
|
warnings.push(
|
|
createWarning3(
|
|
"invalid-structure",
|
|
`duplicate ER relation id: ${currentId}`,
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
} else {
|
|
seenIds.add(currentId);
|
|
}
|
|
blocks.push(parseRelationBlock(currentId, currentLines, warnings, path2));
|
|
};
|
|
for (const line of relationsSectionLines) {
|
|
const trimmed = line.trim();
|
|
const match = trimmed.match(/^###\s+(.+)$/);
|
|
if (match) {
|
|
flushBlock();
|
|
currentId = match[1].trim();
|
|
currentLines = [];
|
|
continue;
|
|
}
|
|
if (currentId) {
|
|
currentLines.push(line);
|
|
}
|
|
}
|
|
flushBlock();
|
|
return blocks;
|
|
}
|
|
function extractRelationsSectionLines(lines) {
|
|
let inRelations = false;
|
|
const collected = [];
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!inRelations) {
|
|
if (trimmed === "## Relations") {
|
|
inRelations = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (/^##\s+/.test(trimmed)) {
|
|
break;
|
|
}
|
|
collected.push(line);
|
|
}
|
|
return collected;
|
|
}
|
|
function isIncompleteErRelationId2(id) {
|
|
const trimmed = id.trim();
|
|
if (!trimmed) {
|
|
return true;
|
|
}
|
|
const normalized = trimmed.toUpperCase();
|
|
return normalized === "REL" || normalized === "REL-" || normalized === "REL--" || normalized === "REL-NEW" || normalized === "REL-TODO";
|
|
}
|
|
function parseRelationBlock(id, lines, warnings, path2) {
|
|
const metadata = {};
|
|
const tableLines = [];
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) {
|
|
continue;
|
|
}
|
|
const metadataMatch = trimmed.match(/^-\s+([a-zA-Z_]+)\s*:\s*(.+)$/);
|
|
if (metadataMatch) {
|
|
metadata[metadataMatch[1]] = metadataMatch[2].trim();
|
|
continue;
|
|
}
|
|
if (trimmed.startsWith("|")) {
|
|
tableLines.push(trimmed);
|
|
}
|
|
}
|
|
const targetTableRaw = metadata.target_table?.trim() ?? null;
|
|
const targetTable = targetTableRaw ? normalizeReferenceTarget(targetTableRaw) : null;
|
|
const kind = metadata.kind?.trim() ?? null;
|
|
const cardinality = metadata.cardinality?.trim() ?? null;
|
|
const notes = metadata.notes?.trim() ?? null;
|
|
if (!targetTableRaw) {
|
|
warnings.push(
|
|
createWarning3(
|
|
"invalid-structure",
|
|
`relation block "${id}" missing required field "target_table"`,
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
}
|
|
const mappingTable = parseMarkdownTable(
|
|
tableLines,
|
|
[...RELATION_MAPPING_HEADERS],
|
|
path2,
|
|
`Relations:${id}`
|
|
);
|
|
warnings.push(...mappingTable.warnings);
|
|
const mappings = mappingTable.rows.map((row) => toRelationMapping(row));
|
|
return {
|
|
id,
|
|
targetTable,
|
|
kind,
|
|
cardinality,
|
|
notes,
|
|
mappings
|
|
};
|
|
}
|
|
function toRelationMapping(row) {
|
|
return {
|
|
localColumn: row.local_column ?? "",
|
|
targetColumn: row.target_column ?? "",
|
|
notes: toNullableString(row.notes)
|
|
};
|
|
}
|
|
function toErColumn(row, warnings, path2) {
|
|
return {
|
|
logicalName: row.logical_name ?? "",
|
|
physicalName: row.physical_name ?? "",
|
|
dataType: row.data_type ?? "",
|
|
length: parseNullableNumber(row.length, warnings, path2, "length"),
|
|
scale: parseNullableNumber(row.scale, warnings, path2, "scale"),
|
|
notNull: parseYN(row.not_null),
|
|
pk: parseYN(row.pk),
|
|
encrypted: parseYN(row.encrypted),
|
|
defaultValue: toNullableString(row.default_value),
|
|
notes: toNullableString(row.notes)
|
|
};
|
|
}
|
|
function toErIndex(row) {
|
|
return {
|
|
indexName: row.index_name ?? "",
|
|
indexType: row.index_type ?? "",
|
|
unique: parseYN(row.unique),
|
|
columns: row.columns ?? "",
|
|
notes: toNullableString(row.notes)
|
|
};
|
|
}
|
|
function parseNullableNumber(value, warnings, path2, field) {
|
|
const normalized = toNullableString(value);
|
|
if (normalized === null) {
|
|
return null;
|
|
}
|
|
const parsed = Number(normalized);
|
|
if (Number.isFinite(parsed)) {
|
|
return parsed;
|
|
}
|
|
warnings.push(
|
|
createWarning3(
|
|
"invalid-numeric-value",
|
|
`failed to parse numeric value "${normalized}" for "${field}"`,
|
|
path2,
|
|
field
|
|
)
|
|
);
|
|
return null;
|
|
}
|
|
function parseYN(value) {
|
|
return (value ?? "").trim().toUpperCase() === "Y";
|
|
}
|
|
function buildTitle(logicalName, physicalName) {
|
|
return `${logicalName} / ${physicalName}`;
|
|
}
|
|
function getRequiredString(frontmatter, key, warnings, path2) {
|
|
const value = getOptionalString(frontmatter, key);
|
|
if (value) {
|
|
return value;
|
|
}
|
|
warnings.push(
|
|
createWarning3(
|
|
key === "id" ? "missing-name" : "invalid-structure",
|
|
`missing required field "${key}"`,
|
|
path2,
|
|
key
|
|
)
|
|
);
|
|
return null;
|
|
}
|
|
function getOptionalString(frontmatter, key) {
|
|
const value = frontmatter[key];
|
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
}
|
|
function toNullableString(value) {
|
|
const normalized = value?.trim() ?? "";
|
|
return normalized ? normalized : null;
|
|
}
|
|
function createWarning3(code, message, path2, field) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
function createInfoWarning(code, message, path2, field) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "info",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/editor/model-weave-editor-suggest.ts
|
|
var NO_COMPLETION_NOTICE = modelWeaveText(
|
|
"No Model Weave completion is available at the current cursor position.",
|
|
"\u73FE\u5728\u306E\u30AB\u30FC\u30BD\u30EB\u4F4D\u7F6E\u3067\u5229\u7528\u3067\u304D\u308B Model Weave \u88DC\u5B8C\u306F\u3042\u308A\u307E\u305B\u3093\u3002"
|
|
);
|
|
var MARKDOWN_ONLY_NOTICE = modelWeaveText(
|
|
"Model Weave completion is available only in Markdown editors.",
|
|
"Model Weave \u88DC\u5B8C\u306F Markdown \u30A8\u30C7\u30A3\u30BF\u3067\u306E\u307F\u5229\u7528\u3067\u304D\u307E\u3059\u3002"
|
|
);
|
|
var TARGET_TABLE_NOT_RESOLVED_NOTICE = modelWeaveText(
|
|
"Target table is not resolved for the current relation block.",
|
|
"\u73FE\u5728\u306E relation block \u306E target_table \u304C\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002"
|
|
);
|
|
var CLASS_RELATION_KIND_OPTIONS = [
|
|
"association",
|
|
"dependency",
|
|
"inheritance",
|
|
"implementation",
|
|
"aggregation",
|
|
"composition"
|
|
];
|
|
var SCREEN_ACTION_KIND_OPTIONS = [
|
|
"ui_action",
|
|
"field_event",
|
|
"screen_event",
|
|
"form_event",
|
|
"system_event",
|
|
"shortcut",
|
|
"auto",
|
|
"other"
|
|
];
|
|
var SCREEN_ACTION_EVENT_OPTIONS = [
|
|
"load",
|
|
"unload",
|
|
"click",
|
|
"change",
|
|
"input",
|
|
"focus",
|
|
"blur",
|
|
"submit",
|
|
"select",
|
|
"keydown",
|
|
"timer",
|
|
"message",
|
|
"other"
|
|
];
|
|
var SCREEN_TYPE_OPTIONS = [
|
|
"entry",
|
|
"list",
|
|
"detail",
|
|
"confirm",
|
|
"complete",
|
|
"dialog",
|
|
"dashboard",
|
|
"admin",
|
|
"other"
|
|
];
|
|
var SCREEN_LAYOUT_KIND_OPTIONS = [
|
|
"header",
|
|
"body",
|
|
"detail",
|
|
"footer",
|
|
"section",
|
|
"form_area",
|
|
"table_area",
|
|
"action_area",
|
|
"search_area",
|
|
"result_area",
|
|
"message_area",
|
|
"other"
|
|
];
|
|
var SCREEN_FIELD_KIND_OPTIONS = [
|
|
"window",
|
|
"form",
|
|
"panel",
|
|
"section",
|
|
"table",
|
|
"list",
|
|
"input",
|
|
"textarea",
|
|
"select",
|
|
"checkbox",
|
|
"radio",
|
|
"button",
|
|
"link",
|
|
"label",
|
|
"hidden",
|
|
"computed",
|
|
"table_input",
|
|
"table_select",
|
|
"other"
|
|
];
|
|
var SCREEN_FIELD_DATA_TYPE_OPTIONS = [
|
|
"string",
|
|
"number",
|
|
"integer",
|
|
"decimal",
|
|
"boolean",
|
|
"date",
|
|
"datetime",
|
|
"time",
|
|
"array",
|
|
"object",
|
|
"binary",
|
|
"other"
|
|
];
|
|
var SCREEN_REQUIRED_OPTIONS = ["Y", "N"];
|
|
var SCREEN_MESSAGE_SEVERITY_OPTIONS = [
|
|
"info",
|
|
"success",
|
|
"warning",
|
|
"error",
|
|
"confirm",
|
|
"other"
|
|
];
|
|
var DATA_OBJECT_KIND_OPTIONS = [
|
|
"data",
|
|
"dto",
|
|
"request",
|
|
"response",
|
|
"payload",
|
|
"file",
|
|
"form",
|
|
"query",
|
|
"result",
|
|
"report",
|
|
"message",
|
|
"other"
|
|
];
|
|
var DATA_OBJECT_FORMAT_OPTIONS = [
|
|
"object",
|
|
"json",
|
|
"xml",
|
|
"csv",
|
|
"tsv",
|
|
"fixed",
|
|
"delimited",
|
|
"excel",
|
|
"edi",
|
|
"binary",
|
|
"form",
|
|
"query",
|
|
"other"
|
|
];
|
|
var DATA_OBJECT_ENCODING_OPTIONS = ["UTF-8", "Shift_JIS", "EUC-JP", "ISO-8859-1"];
|
|
var DATA_OBJECT_LINE_ENDING_OPTIONS = ["LF", "CRLF", "CR"];
|
|
var DATA_OBJECT_HAS_HEADER_OPTIONS = ["true", "false"];
|
|
var DATA_OBJECT_FORMAT_KEY_OPTIONS = [
|
|
"data_format",
|
|
"encoding",
|
|
"delimiter",
|
|
"quote",
|
|
"escape",
|
|
"line_ending",
|
|
"has_header",
|
|
"record_length",
|
|
"record_type_position",
|
|
"padding",
|
|
"numeric_padding",
|
|
"sheet",
|
|
"template"
|
|
];
|
|
var DATA_OBJECT_FORMAT_VALUE_OPTIONS = {
|
|
data_format: DATA_OBJECT_FORMAT_OPTIONS,
|
|
encoding: DATA_OBJECT_ENCODING_OPTIONS,
|
|
line_ending: DATA_OBJECT_LINE_ENDING_OPTIONS,
|
|
has_header: DATA_OBJECT_HAS_HEADER_OPTIONS,
|
|
padding: ["space", "zero", "none"],
|
|
numeric_padding: ["zero", "space", "none"],
|
|
quote: ["double_quote", "single_quote", "none"],
|
|
escape: ["backslash", "double_quote", "none"]
|
|
};
|
|
var DATA_OBJECT_RECORD_OCCURRENCE_OPTIONS = ["1", "0..1", "1..*", "0..*"];
|
|
var DATA_OBJECT_REQUIRED_OPTIONS = ["Y", "N"];
|
|
var DATA_OBJECT_FIELD_TYPE_OPTIONS = [
|
|
"string",
|
|
"number",
|
|
"integer",
|
|
"decimal",
|
|
"boolean",
|
|
"date",
|
|
"datetime",
|
|
"time",
|
|
"array",
|
|
"object",
|
|
"binary",
|
|
"other"
|
|
];
|
|
var DATA_OBJECT_FIELD_FORMAT_OPTIONS = [
|
|
"yyyyMMdd",
|
|
"yyyy/MM/dd",
|
|
"zero_pad_left",
|
|
"space_pad_right",
|
|
"fixed:",
|
|
"decimal_0",
|
|
"decimal_2",
|
|
"half_width",
|
|
"half_width_kana",
|
|
"full_width"
|
|
];
|
|
var CODESET_KIND_OPTIONS = [
|
|
"enum",
|
|
"status",
|
|
"master_code",
|
|
"system_code",
|
|
"external_code",
|
|
"ui_options",
|
|
"other"
|
|
];
|
|
function openModelWeaveCompletion(app, getIndex) {
|
|
const activeView = app.workspace.getActiveViewOfType(import_obsidian2.MarkdownView);
|
|
const file = activeView?.file ?? null;
|
|
const editor = activeView?.editor;
|
|
if (!file || file.extension !== "md" || !editor) {
|
|
new import_obsidian2.Notice(MARKDOWN_ONLY_NOTICE);
|
|
return;
|
|
}
|
|
const request = resolveCompletionRequest(file, editor, getIndex());
|
|
if ("notice" in request) {
|
|
new import_obsidian2.Notice(request.notice);
|
|
return;
|
|
}
|
|
if (request.suggestions.length === 0) {
|
|
new import_obsidian2.Notice(NO_COMPLETION_NOTICE);
|
|
return;
|
|
}
|
|
const modal = new ModelWeaveCompletionModal(app, editor, request);
|
|
modal.open();
|
|
modal.applyInitialQuery();
|
|
}
|
|
var ModelWeaveCompletionModal = class extends import_obsidian2.FuzzySuggestModal {
|
|
constructor(app, editor, request) {
|
|
super(app);
|
|
this.editor = editor;
|
|
this.request = request;
|
|
this.setPlaceholder(request.placeholder);
|
|
this.emptyStateText = "No matching Model Weave candidates.";
|
|
}
|
|
getItems() {
|
|
return this.request.suggestions;
|
|
}
|
|
getItemText(item) {
|
|
return [item.label, item.insertText, item.resolveKey, item.detail].filter((value) => Boolean(value)).join(" ");
|
|
}
|
|
renderSuggestion(item, el) {
|
|
const suggestion = item.item;
|
|
el.createDiv({ text: suggestion.label });
|
|
if (suggestion.detail) {
|
|
el.createDiv({
|
|
text: suggestion.detail,
|
|
cls: "model-weave-editor-suggest-detail"
|
|
});
|
|
}
|
|
}
|
|
onChooseItem(item) {
|
|
const liveEditor = this.app.workspace.getActiveViewOfType(import_obsidian2.MarkdownView)?.editor ?? this.editor;
|
|
const cursor = replaceSuggestionText(liveEditor, this.request, item);
|
|
restoreCompletionCursor(liveEditor, cursor);
|
|
}
|
|
applyInitialQuery() {
|
|
if (!this.request.initialQuery) {
|
|
return;
|
|
}
|
|
this.inputEl.value = this.request.initialQuery;
|
|
this.inputEl.dispatchEvent(new Event("input"));
|
|
}
|
|
};
|
|
function resolveCompletionRequest(file, editor, index) {
|
|
const content = editor.getValue();
|
|
const type = getFrontmatterType(content);
|
|
if (!type) {
|
|
return { notice: NO_COMPLETION_NOTICE };
|
|
}
|
|
const cursor = editor.getCursor();
|
|
const lines = content.split(/\r?\n/);
|
|
const line = lines[cursor.line] ?? "";
|
|
if (type === "er_entity") {
|
|
const targetTableRequest = getTargetTableCompletion(cursor, line, index);
|
|
if (targetTableRequest) {
|
|
return targetTableRequest;
|
|
}
|
|
const mappingRequest = getErMappingCompletion(file, content, lines, cursor, line, index);
|
|
if (mappingRequest) {
|
|
return mappingRequest;
|
|
}
|
|
}
|
|
if (type === "er_diagram" || type === "class_diagram") {
|
|
if (type === "class_diagram") {
|
|
const relationPickerRequest = getClassDiagramRelationsCompletion(
|
|
lines,
|
|
cursor,
|
|
line,
|
|
content,
|
|
index
|
|
);
|
|
if (relationPickerRequest) {
|
|
return relationPickerRequest;
|
|
}
|
|
}
|
|
const request = getDiagramObjectsRefCompletion(lines, cursor, line, type, index);
|
|
if (request) {
|
|
return request;
|
|
}
|
|
}
|
|
if (type === "dfd_diagram") {
|
|
const objectRequest = getDfdDiagramObjectsRefCompletion(
|
|
lines,
|
|
cursor,
|
|
line,
|
|
index
|
|
);
|
|
if (objectRequest) {
|
|
return objectRequest;
|
|
}
|
|
const flowRequest = getDfdDiagramFlowCompletion(lines, cursor, line, index);
|
|
if (flowRequest) {
|
|
return flowRequest;
|
|
}
|
|
}
|
|
if (type === "data_object") {
|
|
const request = getDataObjectCompletion(content, lines, cursor, line, index);
|
|
if (request) {
|
|
return request;
|
|
}
|
|
}
|
|
if (type === "screen") {
|
|
const request = getScreenCompletion(content, lines, cursor, line, index);
|
|
if (request) {
|
|
return request;
|
|
}
|
|
}
|
|
if (type === "app_process") {
|
|
const request = getAppProcessCompletion(lines, cursor, line, index);
|
|
if (request) {
|
|
return request;
|
|
}
|
|
}
|
|
if (type === "rule") {
|
|
const request = getRuleCompletion(lines, cursor, line, index);
|
|
if (request) {
|
|
return request;
|
|
}
|
|
}
|
|
if (type === "mapping") {
|
|
const request = getMappingCompletion(lines, cursor, line, index);
|
|
if (request) {
|
|
return request;
|
|
}
|
|
}
|
|
if (type === "codeset") {
|
|
const request = getCodeSetCompletion(content, lines, cursor, line);
|
|
if (request) {
|
|
return request;
|
|
}
|
|
}
|
|
if (type === "class") {
|
|
const request = getClassRelationsCompletion(lines, cursor, line, index);
|
|
if (request) {
|
|
return request;
|
|
}
|
|
}
|
|
return { notice: NO_COMPLETION_NOTICE };
|
|
}
|
|
function getTargetTableCompletion(cursor, line, index) {
|
|
const match = line.match(/^(\s*-\s*target_table\s*:\s*)(.*)$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
const prefixLength = match[1].length;
|
|
if (cursor.ch < prefixLength || !index) {
|
|
return null;
|
|
}
|
|
const suggestions = Object.values(index.erEntitiesById).sort((left, right) => left.physicalName.localeCompare(right.physicalName)).map((entity) => toErEntitySuggestion(entity));
|
|
return {
|
|
kind: "er-target-table",
|
|
replaceFrom: { line: cursor.line, ch: prefixLength },
|
|
replaceTo: { line: cursor.line, ch: line.length },
|
|
suggestions,
|
|
placeholder: "Complete ER target_table",
|
|
initialQuery: normalizeCompletionQuery(line.slice(prefixLength))
|
|
};
|
|
}
|
|
function getErMappingCompletion(file, content, lines, cursor, line, index) {
|
|
if (!line.trim().startsWith("|")) {
|
|
return null;
|
|
}
|
|
if (getSectionNameAtLine(lines, cursor.line) !== "Relations") {
|
|
return null;
|
|
}
|
|
const tableHeaderIndex = findNearestLine(lines, cursor.line, (candidate) => {
|
|
const row = parseMarkdownTableRow(candidate);
|
|
return row !== null && row.length >= 3 && row[0] === "local_column" && row[1] === "target_column" && row[2] === "notes";
|
|
});
|
|
if (tableHeaderIndex < 0 || cursor.line <= tableHeaderIndex + 1) {
|
|
return null;
|
|
}
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell || cell.columnIndex > 1) {
|
|
return null;
|
|
}
|
|
const currentEntity = parseErEntityFile(content, file.path).file;
|
|
if (!currentEntity) {
|
|
return { notice: NO_COMPLETION_NOTICE };
|
|
}
|
|
if (cell.columnIndex === 0) {
|
|
return {
|
|
kind: "er-local-column",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: currentEntity.columns.map((column) => column.physicalName).filter((value) => Boolean(value)).filter(onlyUnique).sort().map((physicalName) => ({
|
|
label: physicalName,
|
|
insertText: physicalName,
|
|
resolveKey: physicalName,
|
|
kind: "column"
|
|
})),
|
|
placeholder: "Complete local column",
|
|
initialQuery: extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
};
|
|
}
|
|
const targetTableRef = findCurrentRelationTargetTable(lines, cursor.line);
|
|
if (!targetTableRef || !index) {
|
|
return { notice: TARGET_TABLE_NOT_RESOLVED_NOTICE };
|
|
}
|
|
const targetEntity = resolveErEntityReference(targetTableRef, index);
|
|
if (!targetEntity) {
|
|
return { notice: TARGET_TABLE_NOT_RESOLVED_NOTICE };
|
|
}
|
|
return {
|
|
kind: "er-target-column",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: targetEntity.columns.map((column) => column.physicalName).filter((value) => Boolean(value)).filter(onlyUnique).sort().map((physicalName) => ({
|
|
label: physicalName,
|
|
insertText: physicalName,
|
|
resolveKey: physicalName,
|
|
kind: "column"
|
|
})),
|
|
placeholder: "Complete target column",
|
|
initialQuery: extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
};
|
|
}
|
|
function getClassDiagramRelationsCompletion(lines, cursor, line, content, index) {
|
|
if (!line.trim().startsWith("|") || !index) {
|
|
return null;
|
|
}
|
|
if (getSectionNameAtLine(lines, cursor.line) !== "Relations") {
|
|
return null;
|
|
}
|
|
const tableHeaderIndex = findNearestLine(lines, cursor.line, (candidate) => {
|
|
const row = parseMarkdownTableRow(candidate);
|
|
return row !== null && row.length >= 8 && row[0] === "id" && row[1] === "from" && row[2] === "to" && row[3] === "kind";
|
|
});
|
|
if (tableHeaderIndex < 0 || cursor.line <= tableHeaderIndex + 1) {
|
|
return null;
|
|
}
|
|
if (isMarkdownTableSeparator(line)) {
|
|
return null;
|
|
}
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell) {
|
|
return null;
|
|
}
|
|
const suggestions = getClassDiagramRelationSuggestions(content, index);
|
|
if (suggestions.length === 0) {
|
|
return {
|
|
notice: modelWeaveText(
|
|
"No class relations are available for the current diagram.",
|
|
"\u73FE\u5728\u306E diagram \u3067\u5229\u7528\u3067\u304D\u308B class relation \u306F\u3042\u308A\u307E\u305B\u3093\u3002"
|
|
)
|
|
};
|
|
}
|
|
return {
|
|
kind: "class-diagram-relation-picker",
|
|
replaceFrom: { line: cursor.line, ch: 0 },
|
|
replaceTo: { line: cursor.line, ch: line.length },
|
|
suggestions,
|
|
placeholder: "Pick a class relation for this diagram row"
|
|
};
|
|
}
|
|
function getDiagramObjectsRefCompletion(lines, cursor, line, type, index) {
|
|
if (!line.trim().startsWith("|") || !index) {
|
|
return null;
|
|
}
|
|
if (getSectionNameAtLine(lines, cursor.line) !== "Objects") {
|
|
return null;
|
|
}
|
|
const tableHeaderIndex = findNearestLine(lines, cursor.line, (candidate) => {
|
|
const row = parseMarkdownTableRow(candidate);
|
|
return row !== null && row.length >= 2 && row[0] === "ref" && row[1] === "notes";
|
|
});
|
|
if (tableHeaderIndex < 0 || cursor.line <= tableHeaderIndex + 1) {
|
|
return null;
|
|
}
|
|
if (isMarkdownTableSeparator(line)) {
|
|
return null;
|
|
}
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell || cell.columnIndex !== 0) {
|
|
return null;
|
|
}
|
|
if (type === "er_diagram") {
|
|
return {
|
|
kind: "er-diagram-object",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: Object.values(index.erEntitiesById).sort((left, right) => left.physicalName.localeCompare(right.physicalName)).map((entity) => toErEntitySuggestion(entity)),
|
|
placeholder: "Complete ER diagram object",
|
|
initialQuery: normalizeCompletionQuery(
|
|
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
),
|
|
tableColumnIndex: 0
|
|
};
|
|
}
|
|
return {
|
|
kind: "class-diagram-object",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: Object.values(index.objectsById).sort((left, right) => getObjectId3(left).localeCompare(getObjectId3(right))).map((object) => toClassObjectSuggestion(object)),
|
|
placeholder: "Complete class diagram object",
|
|
initialQuery: normalizeCompletionQuery(
|
|
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
),
|
|
tableColumnIndex: 0
|
|
};
|
|
}
|
|
function getDfdDiagramObjectsRefCompletion(lines, cursor, line, index) {
|
|
if (!line.trim().startsWith("|") || !index) {
|
|
return null;
|
|
}
|
|
if (getSectionNameAtLine(lines, cursor.line) !== "Objects") {
|
|
return null;
|
|
}
|
|
const tableHeaderIndex = findNearestLine(lines, cursor.line, (candidate) => {
|
|
const row = parseMarkdownTableRow(candidate);
|
|
return row !== null && row.length >= 2 && row[0] === "ref" && row[1] === "notes";
|
|
});
|
|
if (tableHeaderIndex < 0 || cursor.line <= tableHeaderIndex + 1) {
|
|
return null;
|
|
}
|
|
if (isMarkdownTableSeparator(line)) {
|
|
return null;
|
|
}
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell || cell.columnIndex !== 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
kind: "dfd-diagram-object",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: Object.values(index.dfdObjectsById).sort((left, right) => left.id.localeCompare(right.id)).map((object) => toDfdObjectSuggestion(object)),
|
|
placeholder: "Complete DFD diagram object",
|
|
initialQuery: normalizeCompletionQuery(
|
|
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
),
|
|
tableColumnIndex: 0
|
|
};
|
|
}
|
|
function getDfdDiagramFlowCompletion(lines, cursor, line, index) {
|
|
if (!line.trim().startsWith("|") || !index) {
|
|
return null;
|
|
}
|
|
if (getSectionNameAtLine(lines, cursor.line) !== "Flows") {
|
|
return null;
|
|
}
|
|
const tableHeaderIndex = findNearestLine(lines, cursor.line, (candidate) => {
|
|
const row = parseMarkdownTableRow(candidate);
|
|
return row !== null && row.length >= 5 && row[0] === "id" && row[1] === "from" && row[2] === "to";
|
|
});
|
|
if (tableHeaderIndex < 0 || cursor.line <= tableHeaderIndex + 1) {
|
|
return null;
|
|
}
|
|
if (isMarkdownTableSeparator(line)) {
|
|
return null;
|
|
}
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell || cell.columnIndex !== 1 && cell.columnIndex !== 2 && cell.columnIndex !== 3) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 3) {
|
|
return {
|
|
kind: "dfd-diagram-flow-data",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: Object.values(index.dataObjectsById).sort((left, right) => left.id.localeCompare(right.id)).map((object) => toDataObjectSuggestion(object)),
|
|
placeholder: "Complete DFD flow data",
|
|
initialQuery: normalizeCompletionQuery(
|
|
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
),
|
|
tableColumnIndex: 3
|
|
};
|
|
}
|
|
const preferredObjects = getDiagramObjectRefs(lines.join("\n")).map((ref) => resolveDfdObjectReference(ref, index)).filter((object) => Boolean(object));
|
|
const preferredIds = new Set(preferredObjects.map((object) => object.id));
|
|
const remainingObjects = Object.values(index.dfdObjectsById).filter(
|
|
(object) => !preferredIds.has(object.id)
|
|
);
|
|
const orderedSuggestions = [
|
|
...preferredObjects.sort((left, right) => left.id.localeCompare(right.id)).map((object) => toDfdObjectSuggestion(object, "in diagram")),
|
|
...remainingObjects.sort((left, right) => left.id.localeCompare(right.id)).map((object) => toDfdObjectSuggestion(object, "in vault"))
|
|
];
|
|
return {
|
|
kind: cell.columnIndex === 1 ? "dfd-diagram-flow-from" : "dfd-diagram-flow-to",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: orderedSuggestions,
|
|
placeholder: cell.columnIndex === 1 ? "Complete DFD flow source" : "Complete DFD flow target",
|
|
initialQuery: normalizeCompletionQuery(
|
|
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
),
|
|
tableColumnIndex: cell.columnIndex
|
|
};
|
|
}
|
|
function getDataObjectCompletion(content, lines, cursor, line, index) {
|
|
const frontmatterRequest = getDataObjectFrontmatterCompletion(content, cursor, line);
|
|
if (frontmatterRequest) {
|
|
return frontmatterRequest;
|
|
}
|
|
if (!line.trim().startsWith("|") || !index || isMarkdownTableSeparator(line)) {
|
|
return null;
|
|
}
|
|
const section = getSectionNameAtLine(lines, cursor.line);
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell) {
|
|
return null;
|
|
}
|
|
if (section === "Format") {
|
|
if (!hasTableHeader(lines, cursor.line, ["key", "value", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 0) {
|
|
return buildOptionCompletionRequest(
|
|
"data-object-option",
|
|
cell,
|
|
line,
|
|
DATA_OBJECT_FORMAT_KEY_OPTIONS,
|
|
"Complete data object format key",
|
|
0
|
|
);
|
|
}
|
|
if (cell.columnIndex === 1) {
|
|
const row = parseMarkdownTableRow(line);
|
|
const key = row?.[0]?.trim() ?? "";
|
|
const options = DATA_OBJECT_FORMAT_VALUE_OPTIONS[key];
|
|
if (!options || options.length === 0) {
|
|
return null;
|
|
}
|
|
return buildOptionCompletionRequest(
|
|
"data-object-option",
|
|
cell,
|
|
line,
|
|
options,
|
|
`Complete data object format value for ${key}`,
|
|
1
|
|
);
|
|
}
|
|
}
|
|
if (section === "Records") {
|
|
if (!hasTableHeader(lines, cursor.line, ["record_type", "name", "occurrence", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 2) {
|
|
return buildOptionCompletionRequest(
|
|
"data-object-option",
|
|
cell,
|
|
line,
|
|
DATA_OBJECT_RECORD_OCCURRENCE_OPTIONS,
|
|
"Complete data object occurrence",
|
|
2
|
|
);
|
|
}
|
|
}
|
|
if (section === "Fields") {
|
|
const header = getNearestTableHeader(lines, cursor.line);
|
|
if (!header) {
|
|
return null;
|
|
}
|
|
const headerIndex = new Map(header.map((column, index2) => [column, index2]));
|
|
const isFileLayout = headerIndex.has("record_type") || headerIndex.has("no") || headerIndex.has("position") || headerIndex.has("field_format");
|
|
const refColumnIndex = headerIndex.get("ref");
|
|
if (typeof refColumnIndex === "number" && cell.columnIndex === refColumnIndex) {
|
|
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
|
const qualifiedMemberRequest = getQualifiedMemberCompletionRequest(
|
|
cursor,
|
|
cell,
|
|
cellValue,
|
|
index,
|
|
"Complete data object field reference"
|
|
);
|
|
if (qualifiedMemberRequest) {
|
|
return qualifiedMemberRequest;
|
|
}
|
|
return {
|
|
kind: "data-object-field-ref",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildDataObjectReferenceSuggestions(index),
|
|
placeholder: "Complete data object field reference",
|
|
initialQuery: normalizeCompletionQuery(cellValue),
|
|
tableColumnIndex: refColumnIndex
|
|
};
|
|
}
|
|
const requiredColumnIndex = headerIndex.get("required");
|
|
if (typeof requiredColumnIndex === "number" && cell.columnIndex === requiredColumnIndex) {
|
|
return buildOptionCompletionRequest(
|
|
"data-object-option",
|
|
cell,
|
|
line,
|
|
DATA_OBJECT_REQUIRED_OPTIONS,
|
|
"Complete data object required flag",
|
|
requiredColumnIndex
|
|
);
|
|
}
|
|
const typeColumnIndex = headerIndex.get("type");
|
|
if (typeof typeColumnIndex === "number" && cell.columnIndex === typeColumnIndex) {
|
|
return buildOptionCompletionRequest(
|
|
"data-object-option",
|
|
cell,
|
|
line,
|
|
DATA_OBJECT_FIELD_TYPE_OPTIONS,
|
|
"Complete data object field type",
|
|
typeColumnIndex
|
|
);
|
|
}
|
|
const fieldFormatColumnIndex = headerIndex.get("field_format");
|
|
if (isFileLayout && typeof fieldFormatColumnIndex === "number" && cell.columnIndex === fieldFormatColumnIndex) {
|
|
return buildOptionCompletionRequest(
|
|
"data-object-option",
|
|
cell,
|
|
line,
|
|
DATA_OBJECT_FIELD_FORMAT_OPTIONS,
|
|
"Complete data object field format",
|
|
fieldFormatColumnIndex
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function getScreenCompletion(content, lines, cursor, line, index) {
|
|
const frontmatterRequest = getScreenFrontmatterCompletion(content, lines, cursor, line);
|
|
if (frontmatterRequest) {
|
|
return frontmatterRequest;
|
|
}
|
|
if (!line.trim().startsWith("|") || !index) {
|
|
return null;
|
|
}
|
|
const section = getSectionNameAtLine(lines, cursor.line);
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell || isMarkdownTableSeparator(line)) {
|
|
return null;
|
|
}
|
|
if (section === "Layout") {
|
|
if (!hasTableHeader(lines, cursor.line, ["id", "label", "kind", "purpose", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 2) {
|
|
return buildOptionCompletionRequest(
|
|
"screen-option",
|
|
cell,
|
|
line,
|
|
SCREEN_LAYOUT_KIND_OPTIONS,
|
|
"Complete screen layout kind",
|
|
2
|
|
);
|
|
}
|
|
}
|
|
if (section === "Fields") {
|
|
if (!hasTableHeader(lines, cursor.line, [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"layout",
|
|
"data_type",
|
|
"required",
|
|
"ref",
|
|
"rule",
|
|
"notes"
|
|
])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 2) {
|
|
return buildOptionCompletionRequest(
|
|
"screen-field-kind",
|
|
cell,
|
|
line,
|
|
SCREEN_FIELD_KIND_OPTIONS,
|
|
"Complete screen field kind",
|
|
2
|
|
);
|
|
}
|
|
if (cell.columnIndex === 3) {
|
|
return {
|
|
kind: "screen-field-layout",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: getScreenLayoutSuggestions(lines),
|
|
placeholder: "Complete screen layout",
|
|
initialQuery: extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch),
|
|
tableColumnIndex: 3
|
|
};
|
|
}
|
|
if (cell.columnIndex === 4) {
|
|
return buildOptionCompletionRequest(
|
|
"screen-field-data-type",
|
|
cell,
|
|
line,
|
|
SCREEN_FIELD_DATA_TYPE_OPTIONS,
|
|
"Complete screen field data type",
|
|
4
|
|
);
|
|
}
|
|
if (cell.columnIndex === 5) {
|
|
return buildOptionCompletionRequest(
|
|
"screen-field-required",
|
|
cell,
|
|
line,
|
|
SCREEN_REQUIRED_OPTIONS,
|
|
"Complete screen field required flag",
|
|
5
|
|
);
|
|
}
|
|
if (cell.columnIndex === 6) {
|
|
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
|
const qualifiedMemberRequest = getQualifiedMemberCompletionRequest(
|
|
cursor,
|
|
cell,
|
|
cellValue,
|
|
index,
|
|
"Complete screen field reference"
|
|
);
|
|
if (qualifiedMemberRequest) {
|
|
return qualifiedMemberRequest;
|
|
}
|
|
return {
|
|
kind: "screen-field-ref",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildStructuredReferenceSuggestions(index),
|
|
placeholder: "Complete screen field ref",
|
|
initialQuery: normalizeCompletionQuery(cellValue),
|
|
tableColumnIndex: 6
|
|
};
|
|
}
|
|
if (cell.columnIndex === 7) {
|
|
return {
|
|
kind: "screen-rule-ref",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildRuleReferenceSuggestions(index),
|
|
placeholder: "Complete screen field rule",
|
|
initialQuery: normalizeCompletionQuery(
|
|
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
),
|
|
tableColumnIndex: 7
|
|
};
|
|
}
|
|
}
|
|
if (section === "Actions") {
|
|
if (!hasTableHeader(lines, cursor.line, [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"target",
|
|
"event",
|
|
"invoke",
|
|
"transition",
|
|
"rule",
|
|
"notes"
|
|
])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 2) {
|
|
return buildOptionCompletionRequest(
|
|
"screen-action-kind",
|
|
cell,
|
|
line,
|
|
SCREEN_ACTION_KIND_OPTIONS,
|
|
"Complete screen action kind",
|
|
2
|
|
);
|
|
}
|
|
if (cell.columnIndex === 3) {
|
|
return {
|
|
kind: "screen-action-target",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: getScreenFieldTargetSuggestions(lines),
|
|
placeholder: "Complete screen action target",
|
|
initialQuery: extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch),
|
|
tableColumnIndex: 3
|
|
};
|
|
}
|
|
if (cell.columnIndex === 4) {
|
|
return buildOptionCompletionRequest(
|
|
"screen-action-event",
|
|
cell,
|
|
line,
|
|
SCREEN_ACTION_EVENT_OPTIONS,
|
|
"Complete screen action event",
|
|
4
|
|
);
|
|
}
|
|
if (cell.columnIndex === 5) {
|
|
const appProcessSuggestions = Object.values(index.appProcessesById).sort((left, right) => left.id.localeCompare(right.id)).map((process) => toAppProcessSuggestion(process));
|
|
return {
|
|
kind: "screen-action-invoke",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: [...getScreenLocalProcessSuggestions(lines), ...appProcessSuggestions],
|
|
placeholder: "Complete screen invoke reference",
|
|
initialQuery: normalizeCompletionQuery(
|
|
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
),
|
|
tableColumnIndex: 5
|
|
};
|
|
}
|
|
if (cell.columnIndex === 6) {
|
|
return {
|
|
kind: "screen-action-transition",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: Object.values(index.screensById).sort((left, right) => left.id.localeCompare(right.id)).map((screen) => toScreenSuggestion(screen)),
|
|
placeholder: "Complete screen transition reference",
|
|
initialQuery: normalizeCompletionQuery(
|
|
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
),
|
|
tableColumnIndex: 6
|
|
};
|
|
}
|
|
if (cell.columnIndex === 7) {
|
|
return {
|
|
kind: "screen-rule-ref",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildRuleReferenceSuggestions(index),
|
|
placeholder: "Complete screen action rule",
|
|
initialQuery: normalizeCompletionQuery(
|
|
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
),
|
|
tableColumnIndex: 7
|
|
};
|
|
}
|
|
}
|
|
if (section === "Messages") {
|
|
if (!hasTableHeader(lines, cursor.line, ["id", "text", "severity", "timing", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 1) {
|
|
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
|
return {
|
|
kind: "rule-message-ref",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: Object.values(index.messagesById).sort((left, right) => left.id.localeCompare(right.id)).map((messageSet) => toMessageSuggestion(messageSet)),
|
|
placeholder: "Complete screen message reference",
|
|
initialQuery: normalizeCompletionQuery(cellValue),
|
|
tableColumnIndex: 1
|
|
};
|
|
}
|
|
if (cell.columnIndex === 2) {
|
|
return buildOptionCompletionRequest(
|
|
"screen-message-severity",
|
|
cell,
|
|
line,
|
|
SCREEN_MESSAGE_SEVERITY_OPTIONS,
|
|
"Complete message severity",
|
|
2
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function getScreenFrontmatterCompletion(content, lines, cursor, line) {
|
|
if (!isLineInsideFrontmatter(content, cursor.line)) {
|
|
return null;
|
|
}
|
|
const frontmatterKey = getFrontmatterKeyAtLine(line);
|
|
if (frontmatterKey !== "screen_type") {
|
|
return null;
|
|
}
|
|
const separatorIndex = line.indexOf(":");
|
|
if (separatorIndex < 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
kind: "screen-frontmatter",
|
|
replaceFrom: { line: cursor.line, ch: separatorIndex + 1 },
|
|
replaceTo: { line: cursor.line, ch: lines[cursor.line]?.length ?? line.length },
|
|
suggestions: SCREEN_TYPE_OPTIONS.map((option) => ({
|
|
label: option,
|
|
insertText: option,
|
|
resolveKey: option,
|
|
kind: "kind"
|
|
})),
|
|
placeholder: "Complete screen screen_type",
|
|
initialQuery: line.slice(separatorIndex + 1).trim()
|
|
};
|
|
}
|
|
function getScreenLayoutSuggestions(lines) {
|
|
const layouts = /* @__PURE__ */ new Map();
|
|
let inLayout = false;
|
|
let headerSeen = false;
|
|
for (const rawLine of lines) {
|
|
const trimmed = rawLine.trim();
|
|
const headingMatch = trimmed.match(/^##\s+(.+)$/);
|
|
if (headingMatch) {
|
|
inLayout = headingMatch[1].trim() === "Layout";
|
|
headerSeen = false;
|
|
continue;
|
|
}
|
|
if (!inLayout || !trimmed.startsWith("|") || isMarkdownTableSeparator(trimmed)) {
|
|
continue;
|
|
}
|
|
const row = parseMarkdownTableRow(trimmed);
|
|
if (!row || row.length < 2) {
|
|
continue;
|
|
}
|
|
if (!headerSeen) {
|
|
headerSeen = row[0] === "id" && row[1] === "label";
|
|
continue;
|
|
}
|
|
const id = row[0]?.trim();
|
|
const label = row[1]?.trim();
|
|
if (id) {
|
|
layouts.set(id, label || id);
|
|
}
|
|
}
|
|
return [...layouts.entries()].map(([id, label]) => ({
|
|
label: `${id} / ${label}`,
|
|
insertText: id,
|
|
resolveKey: id,
|
|
detail: "screen layout",
|
|
kind: "reference"
|
|
}));
|
|
}
|
|
function getScreenLocalProcessSuggestions(lines) {
|
|
const suggestions = [];
|
|
let inLocalProcesses = false;
|
|
for (const rawLine of lines) {
|
|
const trimmed = rawLine.trim();
|
|
const headingMatch = trimmed.match(/^##\s+(.+)$/);
|
|
if (headingMatch) {
|
|
inLocalProcesses = headingMatch[1].trim() === "Local Processes";
|
|
continue;
|
|
}
|
|
if (!inLocalProcesses) {
|
|
continue;
|
|
}
|
|
const localProcessMatch = trimmed.match(/^###\s+(.+)$/);
|
|
if (!localProcessMatch) {
|
|
continue;
|
|
}
|
|
const heading = localProcessMatch[1].trim();
|
|
suggestions.push({
|
|
label: heading,
|
|
insertText: buildAliasedWikilink(`#${heading}`, heading),
|
|
resolveKey: `#${heading}`,
|
|
detail: "screen local process",
|
|
kind: "reference"
|
|
});
|
|
}
|
|
return suggestions;
|
|
}
|
|
function getAppProcessCompletion(lines, cursor, line, index) {
|
|
if (!line.trim().startsWith("|") || !index) {
|
|
return null;
|
|
}
|
|
const section = getSectionNameAtLine(lines, cursor.line);
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell || isMarkdownTableSeparator(line)) {
|
|
return null;
|
|
}
|
|
if (section === "Inputs") {
|
|
if (!hasTableHeader(lines, cursor.line, ["id", "data", "source", "required", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 1 || cell.columnIndex === 2) {
|
|
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
|
if (cell.columnIndex === 2) {
|
|
const qualifiedMemberRequest = getQualifiedMemberCompletionRequest(
|
|
cursor,
|
|
cell,
|
|
cellValue,
|
|
index,
|
|
"Complete app_process input source"
|
|
);
|
|
if (qualifiedMemberRequest) {
|
|
return qualifiedMemberRequest;
|
|
}
|
|
}
|
|
return {
|
|
kind: cell.columnIndex === 1 ? "app-process-input-data" : "app-process-input-source",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildStructuredReferenceSuggestions(index),
|
|
placeholder: cell.columnIndex === 1 ? "Complete app_process input data" : "Complete app_process input source",
|
|
initialQuery: normalizeCompletionQuery(cellValue),
|
|
tableColumnIndex: cell.columnIndex
|
|
};
|
|
}
|
|
}
|
|
if (section === "Outputs") {
|
|
if (!hasTableHeader(lines, cursor.line, ["id", "data", "target", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 1 || cell.columnIndex === 2) {
|
|
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
|
if (cell.columnIndex === 2) {
|
|
const qualifiedMemberRequest = getQualifiedMemberCompletionRequest(
|
|
cursor,
|
|
cell,
|
|
cellValue,
|
|
index,
|
|
"Complete app_process output target"
|
|
);
|
|
if (qualifiedMemberRequest) {
|
|
return qualifiedMemberRequest;
|
|
}
|
|
}
|
|
return {
|
|
kind: cell.columnIndex === 1 ? "app-process-output-data" : "app-process-output-target",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildStructuredReferenceSuggestions(index),
|
|
placeholder: cell.columnIndex === 1 ? "Complete app_process output data" : "Complete app_process output target",
|
|
initialQuery: normalizeCompletionQuery(cellValue),
|
|
tableColumnIndex: cell.columnIndex
|
|
};
|
|
}
|
|
}
|
|
if (section === "Triggers") {
|
|
if (!hasTableHeader(lines, cursor.line, ["id", "kind", "source", "event", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 2) {
|
|
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
|
const qualifiedMemberRequest = getQualifiedMemberCompletionRequest(
|
|
cursor,
|
|
cell,
|
|
cellValue,
|
|
index,
|
|
"Complete app_process trigger source"
|
|
);
|
|
if (qualifiedMemberRequest) {
|
|
return qualifiedMemberRequest;
|
|
}
|
|
return {
|
|
kind: "app-process-trigger-source",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildStructuredReferenceSuggestions(index),
|
|
placeholder: "Complete app_process trigger source",
|
|
initialQuery: normalizeCompletionQuery(cellValue),
|
|
tableColumnIndex: 2
|
|
};
|
|
}
|
|
}
|
|
if (section === "Transitions") {
|
|
if (!hasTableHeader(lines, cursor.line, ["id", "event", "to", "condition", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 2) {
|
|
return {
|
|
kind: "app-process-transition-to",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: Object.values(index.screensById).sort((left, right) => left.id.localeCompare(right.id)).map((screen) => toScreenSuggestion(screen)),
|
|
placeholder: "Complete transition screen reference",
|
|
initialQuery: normalizeCompletionQuery(
|
|
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
),
|
|
tableColumnIndex: 2
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function getRuleCompletion(lines, cursor, line, index) {
|
|
if (!line.trim().startsWith("|") || !index) {
|
|
return null;
|
|
}
|
|
const section = getSectionNameAtLine(lines, cursor.line);
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell || isMarkdownTableSeparator(line)) {
|
|
return null;
|
|
}
|
|
if (section === "Inputs") {
|
|
if (!hasTableHeader(lines, cursor.line, ["id", "data", "source", "required", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 1 || cell.columnIndex === 2) {
|
|
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
|
const qualifiedMemberRequest = getQualifiedMemberCompletionRequest(
|
|
cursor,
|
|
cell,
|
|
cellValue,
|
|
index,
|
|
cell.columnIndex === 1 ? "Complete rule input data" : "Complete rule input source"
|
|
);
|
|
if (qualifiedMemberRequest) {
|
|
return qualifiedMemberRequest;
|
|
}
|
|
return {
|
|
kind: cell.columnIndex === 1 ? "rule-input-data" : "rule-input-source",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildStructuredReferenceSuggestions(index),
|
|
placeholder: cell.columnIndex === 1 ? "Complete rule input data" : "Complete rule input source",
|
|
initialQuery: normalizeCompletionQuery(cellValue),
|
|
tableColumnIndex: cell.columnIndex
|
|
};
|
|
}
|
|
}
|
|
if (section === "References") {
|
|
if (!hasTableHeader(lines, cursor.line, ["ref", "usage", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 0) {
|
|
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
|
const qualifiedMemberRequest = getQualifiedMemberCompletionRequest(
|
|
cursor,
|
|
cell,
|
|
cellValue,
|
|
index,
|
|
"Complete rule reference"
|
|
);
|
|
if (qualifiedMemberRequest) {
|
|
return qualifiedMemberRequest;
|
|
}
|
|
return {
|
|
kind: "rule-reference-ref",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildRuleReferenceableSuggestions(index),
|
|
placeholder: "Complete rule reference",
|
|
initialQuery: normalizeCompletionQuery(cellValue),
|
|
tableColumnIndex: 0
|
|
};
|
|
}
|
|
}
|
|
if (section === "Messages") {
|
|
if (!hasTableHeader(lines, cursor.line, ["condition", "message", "severity", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 2) {
|
|
return buildOptionCompletionRequest(
|
|
"rule-message-ref",
|
|
cell,
|
|
line,
|
|
["error", "warning", "info", "confirm"],
|
|
"Complete rule message severity",
|
|
2
|
|
);
|
|
}
|
|
if (cell.columnIndex === 1) {
|
|
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
|
return {
|
|
kind: "rule-message-ref",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildGenericFileSuggestions(index),
|
|
placeholder: "Complete rule message reference",
|
|
initialQuery: normalizeCompletionQuery(cellValue),
|
|
tableColumnIndex: 1
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function getMappingCompletion(lines, cursor, line, index) {
|
|
if (!line.trim().startsWith("|") || !index) {
|
|
return null;
|
|
}
|
|
const section = getSectionNameAtLine(lines, cursor.line);
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell || isMarkdownTableSeparator(line)) {
|
|
return null;
|
|
}
|
|
if (section === "Scope") {
|
|
if (!hasTableHeader(lines, cursor.line, ["role", "ref", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 0) {
|
|
return buildOptionCompletionRequest(
|
|
"mapping-scope-ref",
|
|
cell,
|
|
line,
|
|
["source", "target", "intermediate", "reference", "rule", "process"],
|
|
"Complete mapping scope role",
|
|
0
|
|
);
|
|
}
|
|
if (cell.columnIndex === 1) {
|
|
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
|
const qualifiedMemberRequest = getQualifiedMemberCompletionRequest(
|
|
cursor,
|
|
cell,
|
|
cellValue,
|
|
index,
|
|
"Complete mapping scope ref"
|
|
);
|
|
if (qualifiedMemberRequest) {
|
|
return qualifiedMemberRequest;
|
|
}
|
|
return {
|
|
kind: "mapping-scope-ref",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildRuleReferenceableSuggestions(index),
|
|
placeholder: "Complete mapping scope ref",
|
|
initialQuery: normalizeCompletionQuery(cellValue),
|
|
tableColumnIndex: 1
|
|
};
|
|
}
|
|
}
|
|
if (section === "Mappings") {
|
|
if (!hasTableHeader(lines, cursor.line, ["source_ref", "target_ref", "transform", "rule", "required", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 0 || cell.columnIndex === 1) {
|
|
const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch);
|
|
const qualifiedMemberRequest = getQualifiedMemberCompletionRequest(
|
|
cursor,
|
|
cell,
|
|
cellValue,
|
|
index,
|
|
cell.columnIndex === 0 ? "Complete mapping source_ref" : "Complete mapping target_ref"
|
|
);
|
|
if (qualifiedMemberRequest) {
|
|
return qualifiedMemberRequest;
|
|
}
|
|
return {
|
|
kind: cell.columnIndex === 0 ? "mapping-source-ref" : "mapping-target-ref",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: buildStructuredReferenceSuggestions(index),
|
|
placeholder: cell.columnIndex === 0 ? "Complete mapping source_ref" : "Complete mapping target_ref",
|
|
initialQuery: normalizeCompletionQuery(cellValue),
|
|
tableColumnIndex: cell.columnIndex
|
|
};
|
|
}
|
|
if (cell.columnIndex === 3) {
|
|
return {
|
|
kind: "mapping-rule-ref",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: [
|
|
...buildRuleReferenceSuggestions(index),
|
|
...Object.values(index.codesetsById).sort((left, right) => left.id.localeCompare(right.id)).map((codeset) => toCodeSetSuggestion(codeset))
|
|
],
|
|
placeholder: "Complete mapping rule",
|
|
initialQuery: normalizeCompletionQuery(
|
|
extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch)
|
|
),
|
|
tableColumnIndex: 3
|
|
};
|
|
}
|
|
if (cell.columnIndex === 4) {
|
|
return buildOptionCompletionRequest(
|
|
"mapping-rule-ref",
|
|
cell,
|
|
line,
|
|
["Y", "N"],
|
|
"Complete mapping required",
|
|
4
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function getCodeSetCompletion(content, lines, cursor, line) {
|
|
const frontmatterRequest = getCodeSetFrontmatterCompletion(content, cursor, line);
|
|
if (frontmatterRequest) {
|
|
return frontmatterRequest;
|
|
}
|
|
if (!line.trim().startsWith("|")) {
|
|
return null;
|
|
}
|
|
const section = getSectionNameAtLine(lines, cursor.line);
|
|
if (section !== "Values") {
|
|
return null;
|
|
}
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell || isMarkdownTableSeparator(line)) {
|
|
return null;
|
|
}
|
|
if (!hasTableHeader(lines, cursor.line, ["code", "label", "sort_order", "active", "notes"])) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 3) {
|
|
return buildOptionCompletionRequest(
|
|
"codeset-active",
|
|
cell,
|
|
line,
|
|
["Y", "N"],
|
|
"Complete codeset active",
|
|
3
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
function getCodeSetFrontmatterCompletion(content, cursor, line) {
|
|
if (!isLineInsideFrontmatter(content, cursor.line)) {
|
|
return null;
|
|
}
|
|
const frontmatterKey = getFrontmatterKeyAtLine(line);
|
|
if (frontmatterKey !== "kind") {
|
|
return null;
|
|
}
|
|
const separatorIndex = line.indexOf(":");
|
|
if (separatorIndex < 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
kind: "codeset-active",
|
|
replaceFrom: { line: cursor.line, ch: separatorIndex + 1 },
|
|
replaceTo: { line: cursor.line, ch: line.length },
|
|
suggestions: CODESET_KIND_OPTIONS.map((option) => ({
|
|
label: option,
|
|
insertText: option,
|
|
resolveKey: option,
|
|
kind: "kind"
|
|
})),
|
|
placeholder: "Complete codeset kind",
|
|
initialQuery: line.slice(separatorIndex + 1).trim()
|
|
};
|
|
}
|
|
function buildStructuredReferenceSuggestions(index) {
|
|
return [
|
|
...Object.values(index.dataObjectsById).sort((left, right) => left.id.localeCompare(right.id)).map((object) => toDataObjectSuggestion(object)),
|
|
...Object.values(index.codesetsById).sort((left, right) => left.id.localeCompare(right.id)).map((codeset) => toCodeSetSuggestion(codeset)),
|
|
...Object.values(index.rulesById).sort((left, right) => left.id.localeCompare(right.id)).map((rule) => toRuleSuggestion(rule)),
|
|
...Object.values(index.mappingsById).sort((left, right) => left.id.localeCompare(right.id)).map((mapping) => toMappingSuggestion(mapping)),
|
|
...Object.values(index.screensById).sort((left, right) => left.id.localeCompare(right.id)).map((screen) => toScreenSuggestion(screen)),
|
|
...Object.values(index.appProcessesById).sort((left, right) => left.id.localeCompare(right.id)).map((process) => toAppProcessSuggestion(process)),
|
|
...Object.values(index.erEntitiesById).sort((left, right) => left.logicalName.localeCompare(right.logicalName)).map((entity) => toLinkedReferenceSuggestionForEntity(entity)),
|
|
...Object.values(index.objectsById).sort((left, right) => getObjectId3(left).localeCompare(getObjectId3(right))).map((object) => toLinkedReferenceSuggestionForClass(object))
|
|
];
|
|
}
|
|
function buildRuleReferenceSuggestions(index) {
|
|
return Object.values(index.rulesById).sort((left, right) => left.id.localeCompare(right.id)).map((rule) => toRuleSuggestion(rule));
|
|
}
|
|
function buildRuleReferenceableSuggestions(index) {
|
|
return [
|
|
...buildRuleReferenceSuggestions(index),
|
|
...Object.values(index.codesetsById).sort((left, right) => left.id.localeCompare(right.id)).map((codeset) => toCodeSetSuggestion(codeset)),
|
|
...Object.values(index.messagesById).sort((left, right) => left.id.localeCompare(right.id)).map((messageSet) => toMessageSuggestion(messageSet)),
|
|
...Object.values(index.dataObjectsById).sort((left, right) => left.id.localeCompare(right.id)).map((object) => toDataObjectSuggestion(object)),
|
|
...Object.values(index.screensById).sort((left, right) => left.id.localeCompare(right.id)).map((screen) => toScreenSuggestion(screen)),
|
|
...Object.values(index.appProcessesById).sort((left, right) => left.id.localeCompare(right.id)).map((process) => toAppProcessSuggestion(process)),
|
|
...Object.values(index.mappingsById).sort((left, right) => left.id.localeCompare(right.id)).map((mapping) => toMappingSuggestion(mapping)),
|
|
...Object.values(index.erEntitiesById).sort((left, right) => left.logicalName.localeCompare(right.logicalName)).map((entity) => toLinkedReferenceSuggestionForEntity(entity)),
|
|
...Object.values(index.objectsById).sort((left, right) => getObjectId3(left).localeCompare(getObjectId3(right))).map((object) => toLinkedReferenceSuggestionForClass(object))
|
|
];
|
|
}
|
|
function buildDataObjectReferenceSuggestions(index) {
|
|
return [
|
|
...Object.values(index.erEntitiesById).sort((left, right) => left.logicalName.localeCompare(right.logicalName)).map((entity) => toLinkedReferenceSuggestionForEntity(entity)),
|
|
...Object.values(index.objectsById).sort((left, right) => getObjectId3(left).localeCompare(getObjectId3(right))).map((object) => toLinkedReferenceSuggestionForClass(object)),
|
|
...Object.values(index.dataObjectsById).sort((left, right) => left.id.localeCompare(right.id)).map((object) => toDataObjectSuggestion(object)),
|
|
...Object.values(index.codesetsById).sort((left, right) => left.id.localeCompare(right.id)).map((codeset) => toCodeSetSuggestion(codeset)),
|
|
...Object.values(index.screensById).sort((left, right) => left.id.localeCompare(right.id)).map((screen) => toScreenSuggestion(screen)),
|
|
...Object.values(index.appProcessesById).sort((left, right) => left.id.localeCompare(right.id)).map((process) => toAppProcessSuggestion(process)),
|
|
...Object.values(index.dfdObjectsById).sort((left, right) => left.id.localeCompare(right.id)).map((object) => toDfdObjectSuggestion(object))
|
|
];
|
|
}
|
|
function buildGenericFileSuggestions(index) {
|
|
return buildRuleReferenceableSuggestions(index);
|
|
}
|
|
function getScreenFieldTargetSuggestions(lines) {
|
|
const fields = /* @__PURE__ */ new Map();
|
|
let inFields = false;
|
|
let headerSeen = false;
|
|
for (const rawLine of lines) {
|
|
const trimmed = rawLine.trim();
|
|
const headingMatch = trimmed.match(/^##\s+(.+)$/);
|
|
if (headingMatch) {
|
|
inFields = headingMatch[1].trim() === "Fields";
|
|
headerSeen = false;
|
|
continue;
|
|
}
|
|
if (!inFields || !trimmed.startsWith("|") || isMarkdownTableSeparator(trimmed)) {
|
|
continue;
|
|
}
|
|
const row = parseMarkdownTableRow(trimmed);
|
|
if (!row || row.length < 2) {
|
|
continue;
|
|
}
|
|
if (!headerSeen) {
|
|
headerSeen = row[0] === "id" && row[1] === "label";
|
|
continue;
|
|
}
|
|
const id = row[0]?.trim();
|
|
const label = row[1]?.trim();
|
|
if (id) {
|
|
fields.set(id, label || id);
|
|
}
|
|
}
|
|
return [...fields.entries()].map(([id, label]) => ({
|
|
label: `${id} / ${label}`,
|
|
insertText: id,
|
|
resolveKey: id,
|
|
detail: "screen field target",
|
|
kind: "reference"
|
|
}));
|
|
}
|
|
function hasTableHeader(lines, cursorLine, expectedHeader) {
|
|
const tableHeaderIndex = findNearestLine(lines, cursorLine, (candidate) => {
|
|
const row = parseMarkdownTableRow(candidate);
|
|
return row !== null && expectedHeader.every((header, index) => row[index] === header);
|
|
});
|
|
return tableHeaderIndex >= 0 && cursorLine > tableHeaderIndex + 1;
|
|
}
|
|
function getNearestTableHeader(lines, cursorLine) {
|
|
const tableHeaderIndex = findNearestLine(lines, cursorLine, (candidate) => {
|
|
const row = parseMarkdownTableRow(candidate);
|
|
return row !== null && row.length > 0;
|
|
});
|
|
if (tableHeaderIndex < 0 || cursorLine <= tableHeaderIndex + 1) {
|
|
return null;
|
|
}
|
|
const header = parseMarkdownTableRow(lines[tableHeaderIndex] ?? "");
|
|
return header && header.length > 0 ? header : null;
|
|
}
|
|
function buildOptionCompletionRequest(kind, cell, line, options, placeholder, tableColumnIndex) {
|
|
return {
|
|
kind,
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: options.map((option) => ({
|
|
label: option,
|
|
insertText: option,
|
|
resolveKey: option,
|
|
kind: "kind"
|
|
})),
|
|
placeholder,
|
|
initialQuery: extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch),
|
|
tableColumnIndex
|
|
};
|
|
}
|
|
function getQualifiedMemberCompletionRequest(cursor, cell, cellValue, index, placeholder) {
|
|
const qualified = parseQualifiedRef(cellValue);
|
|
if (!qualified) {
|
|
return null;
|
|
}
|
|
const normalizedCellValue = cellValue.trim();
|
|
const dotIndex = normalizedCellValue.lastIndexOf(".");
|
|
if (dotIndex < 0) {
|
|
return null;
|
|
}
|
|
const memberStartInCell = normalizedCellValue.slice(0, dotIndex + 1).length;
|
|
const memberQuery = normalizedCellValue.slice(dotIndex + 1).trim();
|
|
const memberCandidates = getQualifiedMemberCandidates(qualified.baseRefRaw, index);
|
|
if (memberCandidates.length === 0) {
|
|
return null;
|
|
}
|
|
const rawCellStart = cell.replaceFrom.ch;
|
|
const rawTrimmedStart = lineTrimmedOffset(cellValue);
|
|
const replaceFromCh = rawCellStart + rawTrimmedStart + memberStartInCell;
|
|
const replaceToCh = cell.replaceTo.ch;
|
|
if (cursor.ch < replaceFromCh - 1) {
|
|
return null;
|
|
}
|
|
return {
|
|
kind: "data-object-field-ref",
|
|
replaceFrom: { line: cursor.line, ch: replaceFromCh },
|
|
replaceTo: { line: cursor.line, ch: replaceToCh },
|
|
suggestions: memberCandidates.sort((left, right) => left.memberId.localeCompare(right.memberId)).map((candidate) => ({
|
|
label: candidate.displayName ? `${candidate.memberId} \u2014 ${candidate.displayName}` : candidate.memberId,
|
|
insertText: candidate.memberId,
|
|
resolveKey: `${candidate.ownerId}.${candidate.memberId}`,
|
|
detail: `${candidate.memberKind} \xB7 ${candidate.sourceSection}`,
|
|
kind: "reference"
|
|
})),
|
|
placeholder,
|
|
initialQuery: memberQuery
|
|
};
|
|
}
|
|
function getClassDiagramRelationSuggestions(content, index) {
|
|
const objectRefs = getDiagramObjectRefs(content);
|
|
const diagramObjects = objectRefs.map((ref) => resolveObjectModelReference(ref, index)).filter((object) => Boolean(object));
|
|
const diagramObjectIds = new Set(diagramObjects.map((object) => getObjectId3(object)));
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const suggestions = [];
|
|
for (const object of diagramObjects) {
|
|
for (const relation of object.relations) {
|
|
const targetObject = index.objectsById[relation.targetClass] ?? resolveObjectModelReference(relation.targetClass, index);
|
|
const key = [
|
|
relation.id ?? "",
|
|
relation.sourceClass,
|
|
relation.targetClass,
|
|
relation.kind,
|
|
relation.label ?? "",
|
|
relation.fromMultiplicity ?? "",
|
|
relation.toMultiplicity ?? ""
|
|
].join("|");
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
const insideDiagram = diagramObjectIds.has(relation.targetClass);
|
|
suggestions.push(
|
|
toClassDiagramRelationSuggestion(relation, object, targetObject, insideDiagram)
|
|
);
|
|
}
|
|
}
|
|
return suggestions.sort((left, right) => {
|
|
const leftPriority = left.detail?.includes("in diagram") ? 0 : 1;
|
|
const rightPriority = right.detail?.includes("in diagram") ? 0 : 1;
|
|
if (leftPriority !== rightPriority) {
|
|
return leftPriority - rightPriority;
|
|
}
|
|
return left.label.localeCompare(right.label);
|
|
});
|
|
}
|
|
function getClassRelationsCompletion(lines, cursor, line, index) {
|
|
if (!line.trim().startsWith("|") || !index) {
|
|
return null;
|
|
}
|
|
if (getSectionNameAtLine(lines, cursor.line) !== "Relations") {
|
|
return null;
|
|
}
|
|
const tableHeaderIndex = findNearestLine(lines, cursor.line, (candidate) => {
|
|
const row = parseMarkdownTableRow(candidate);
|
|
return row !== null && row.length >= 7 && row[0] === "id" && row[1] === "to" && row[2] === "kind";
|
|
});
|
|
if (tableHeaderIndex < 0 || cursor.line <= tableHeaderIndex + 1) {
|
|
return null;
|
|
}
|
|
if (isMarkdownTableSeparator(line)) {
|
|
return null;
|
|
}
|
|
const cell = getTableCellContext(line, cursor.line, cursor.ch);
|
|
if (!cell) {
|
|
return null;
|
|
}
|
|
if (cell.columnIndex === 1) {
|
|
const suggestions = Object.values(index.objectsById).sort((left, right) => getObjectId3(left).localeCompare(getObjectId3(right))).map((object) => ({
|
|
label: `${getObjectId3(object)} \u2014 ${object.name}`,
|
|
insertText: buildAliasedWikilink(
|
|
toFileLinkTarget(object.path),
|
|
object.name || getObjectId3(object)
|
|
),
|
|
resolveKey: getObjectId3(object),
|
|
detail: object.kind,
|
|
kind: "class"
|
|
}));
|
|
return {
|
|
kind: "class-relation-to",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions,
|
|
placeholder: "Complete class relation to",
|
|
initialQuery: extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch),
|
|
tableColumnIndex: 1
|
|
};
|
|
}
|
|
if (cell.columnIndex === 2) {
|
|
return {
|
|
kind: "class-relation-kind",
|
|
replaceFrom: cell.replaceFrom,
|
|
replaceTo: cell.replaceTo,
|
|
suggestions: CLASS_RELATION_KIND_OPTIONS.map((kind) => ({
|
|
label: kind,
|
|
insertText: kind,
|
|
resolveKey: kind,
|
|
kind: "kind"
|
|
})),
|
|
placeholder: "Complete class relation kind",
|
|
initialQuery: extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch),
|
|
tableColumnIndex: 2
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
function getFrontmatterType(content) {
|
|
const parsed = parseFrontmatter(content);
|
|
const type = parsed.file.frontmatter?.type;
|
|
return typeof type === "string" && type.trim() ? type.trim() : void 0;
|
|
}
|
|
function getDataObjectFrontmatterCompletion(content, cursor, line) {
|
|
if (!isLineInsideFrontmatter(content, cursor.line)) {
|
|
return null;
|
|
}
|
|
const frontmatterKey = getFrontmatterKeyAtLine(line);
|
|
if (!frontmatterKey) {
|
|
return null;
|
|
}
|
|
const optionsByKey = {
|
|
data_format: DATA_OBJECT_FORMAT_OPTIONS,
|
|
kind: DATA_OBJECT_KIND_OPTIONS,
|
|
encoding: DATA_OBJECT_ENCODING_OPTIONS,
|
|
line_ending: DATA_OBJECT_LINE_ENDING_OPTIONS,
|
|
has_header: DATA_OBJECT_HAS_HEADER_OPTIONS
|
|
};
|
|
const options = optionsByKey[frontmatterKey];
|
|
if (!options) {
|
|
return null;
|
|
}
|
|
const separatorIndex = line.indexOf(":");
|
|
if (separatorIndex < 0) {
|
|
return null;
|
|
}
|
|
const replaceFrom = { line: cursor.line, ch: separatorIndex + 1 };
|
|
const replaceTo = { line: cursor.line, ch: line.length };
|
|
return {
|
|
kind: "data-object-frontmatter",
|
|
replaceFrom,
|
|
replaceTo,
|
|
suggestions: options.map((option) => ({
|
|
label: option,
|
|
insertText: option,
|
|
resolveKey: option,
|
|
kind: "kind"
|
|
})),
|
|
placeholder: `Complete data_object ${frontmatterKey}`,
|
|
initialQuery: line.slice(separatorIndex + 1).trim()
|
|
};
|
|
}
|
|
function isLineInsideFrontmatter(content, lineIndex) {
|
|
const lines = content.split(/\r?\n/);
|
|
if ((lines[0] ?? "").trim() !== "---") {
|
|
return false;
|
|
}
|
|
for (let index = 1; index < lines.length; index += 1) {
|
|
if (lines[index].trim() === "---") {
|
|
return lineIndex > 0 && lineIndex < index;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function getFrontmatterKeyAtLine(line) {
|
|
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_-]*)\s*:/);
|
|
return match ? match[1] : null;
|
|
}
|
|
function getSectionNameAtLine(lines, lineIndex) {
|
|
for (let index = lineIndex; index >= 0; index -= 1) {
|
|
const trimmed = lines[index].trim();
|
|
const match = trimmed.match(/^##\s+(.+)$/);
|
|
if (match) {
|
|
return match[1].trim();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function findNearestLine(lines, startIndex, predicate) {
|
|
for (let index = startIndex; index >= 0; index -= 1) {
|
|
const candidate = lines[index] ?? "";
|
|
if (predicate(candidate)) {
|
|
return index;
|
|
}
|
|
if (index !== startIndex && /^##\s+/.test(candidate.trim())) {
|
|
break;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
function findCurrentRelationTargetTable(lines, startIndex) {
|
|
const blockStartIndex = findCurrentRelationBlockStart(lines, startIndex);
|
|
if (blockStartIndex < 0) {
|
|
return null;
|
|
}
|
|
for (let index = blockStartIndex + 1; index <= startIndex; index += 1) {
|
|
const trimmed = (lines[index] ?? "").trim();
|
|
if (/^###\s+/.test(trimmed)) {
|
|
break;
|
|
}
|
|
const match = trimmed.match(/^-\s*target_table\s*:\s*(.*)$/);
|
|
if (match) {
|
|
const value = match[1].trim();
|
|
return value ? normalizeReferenceTarget(value) : null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function findCurrentRelationBlockStart(lines, startIndex) {
|
|
for (let index = startIndex; index >= 0; index -= 1) {
|
|
const trimmed = (lines[index] ?? "").trim();
|
|
if (/^###\s+/.test(trimmed)) {
|
|
return index;
|
|
}
|
|
if (/^##\s+/.test(trimmed) && index !== startIndex) {
|
|
break;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
function getTableCellContext(line, lineNumber, cursorCh) {
|
|
const ranges = getMarkdownTableCellRanges(line);
|
|
if (!ranges || ranges.length === 0) {
|
|
return null;
|
|
}
|
|
for (const range of ranges) {
|
|
const inCell = cursorCh >= range.rawStart && cursorCh < range.rawEnd || cursorCh === range.rawEnd || cursorCh === range.rawStart - 1 && range.columnIndex > 0;
|
|
if (!inCell) {
|
|
continue;
|
|
}
|
|
return {
|
|
columnIndex: range.columnIndex,
|
|
replaceFrom: { line: lineNumber, ch: range.contentStart },
|
|
replaceTo: { line: lineNumber, ch: range.contentEnd }
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
function parseMarkdownTableRow(line) {
|
|
return splitMarkdownTableRow(line);
|
|
}
|
|
function isMarkdownTableSeparator(line) {
|
|
const cells = parseMarkdownTableRow(line);
|
|
if (!cells || cells.length === 0) {
|
|
return false;
|
|
}
|
|
return cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
}
|
|
function getObjectId3(object) {
|
|
const explicitId = object.frontmatter.id;
|
|
if (typeof explicitId === "string" && explicitId.trim()) {
|
|
return explicitId.trim();
|
|
}
|
|
return object.name;
|
|
}
|
|
function onlyUnique(value, index, array) {
|
|
return array.indexOf(value) === index;
|
|
}
|
|
function toErEntitySuggestion(entity) {
|
|
const linkTarget = toFileLinkTarget(entity.path);
|
|
const displayName = entity.logicalName || entity.physicalName || entity.id;
|
|
return {
|
|
label: `${entity.logicalName} / ${entity.physicalName}`,
|
|
insertText: buildAliasedWikilink(linkTarget, displayName),
|
|
resolveKey: linkTarget,
|
|
detail: `${entity.id} \xB7 ${entity.path}`,
|
|
kind: "er_entity"
|
|
};
|
|
}
|
|
function toLinkedReferenceSuggestionForEntity(entity) {
|
|
const linkTarget = toFileLinkTarget(entity.path);
|
|
const displayName = entity.logicalName || entity.physicalName || entity.id;
|
|
return {
|
|
label: `${displayName} / ${entity.physicalName}`,
|
|
insertText: buildAliasedWikilink(linkTarget, displayName),
|
|
resolveKey: linkTarget,
|
|
detail: `er_entity \xB7 ${entity.id} \xB7 ${entity.path}`,
|
|
kind: "er_entity"
|
|
};
|
|
}
|
|
function toClassObjectSuggestion(object) {
|
|
const linkTarget = toFileLinkTarget(object.path);
|
|
const displayName = object.name || getObjectId3(object);
|
|
return {
|
|
label: `${getObjectId3(object)} / ${object.name}`,
|
|
insertText: buildAliasedWikilink(linkTarget, displayName),
|
|
resolveKey: linkTarget,
|
|
detail: object.kind,
|
|
kind: "class"
|
|
};
|
|
}
|
|
function toLinkedReferenceSuggestionForClass(object) {
|
|
const linkTarget = toFileLinkTarget(object.path);
|
|
const displayName = object.name || getObjectId3(object);
|
|
return {
|
|
label: `${displayName} / ${getObjectId3(object)}`,
|
|
insertText: buildAliasedWikilink(linkTarget, displayName),
|
|
resolveKey: linkTarget,
|
|
detail: `class \xB7 ${object.kind} \xB7 ${object.path}`,
|
|
kind: "class"
|
|
};
|
|
}
|
|
function toDfdObjectSuggestion(object, scopeDetail) {
|
|
const linkTarget = toFileLinkTarget(object.path);
|
|
return {
|
|
label: `${object.id} / ${object.name}`,
|
|
insertText: buildAliasedWikilink(linkTarget, object.name || object.id),
|
|
resolveKey: linkTarget,
|
|
detail: scopeDetail ? `${object.kind} \xB7 ${scopeDetail}` : object.kind,
|
|
kind: "dfd_object"
|
|
};
|
|
}
|
|
function toDataObjectSuggestion(object) {
|
|
const linkTarget = toFileLinkTarget(object.path);
|
|
return {
|
|
label: `${object.id} / ${object.name}`,
|
|
insertText: buildAliasedWikilink(linkTarget, object.name || object.id),
|
|
resolveKey: linkTarget,
|
|
detail: object.kind ?? "data_object",
|
|
kind: "data_object"
|
|
};
|
|
}
|
|
function toScreenSuggestion(screen) {
|
|
const linkTarget = toFileLinkTarget(screen.path);
|
|
return {
|
|
label: `${screen.id} / ${screen.name}`,
|
|
insertText: buildAliasedWikilink(linkTarget, screen.name || screen.id),
|
|
resolveKey: linkTarget,
|
|
detail: screen.screenType ?? "screen",
|
|
kind: "reference"
|
|
};
|
|
}
|
|
function toAppProcessSuggestion(process) {
|
|
const linkTarget = toFileLinkTarget(process.path);
|
|
return {
|
|
label: `${process.id} / ${process.name}`,
|
|
insertText: buildAliasedWikilink(linkTarget, process.name || process.id),
|
|
resolveKey: linkTarget,
|
|
detail: process.kind ?? "app_process",
|
|
kind: "reference"
|
|
};
|
|
}
|
|
function toCodeSetSuggestion(codeset) {
|
|
const linkTarget = toFileLinkTarget(codeset.path);
|
|
return {
|
|
label: `${codeset.id} / ${codeset.name}`,
|
|
insertText: buildAliasedWikilink(linkTarget, codeset.name || codeset.id),
|
|
resolveKey: linkTarget,
|
|
detail: codeset.kind ?? "codeset",
|
|
kind: "reference"
|
|
};
|
|
}
|
|
function toMessageSuggestion(messageSet) {
|
|
const linkTarget = toFileLinkTarget(messageSet.path);
|
|
return {
|
|
label: `${messageSet.id} / ${messageSet.name}`,
|
|
insertText: buildAliasedWikilink(linkTarget, messageSet.name || messageSet.id),
|
|
resolveKey: linkTarget,
|
|
detail: messageSet.kind ?? "message",
|
|
kind: "reference"
|
|
};
|
|
}
|
|
function toRuleSuggestion(rule) {
|
|
const linkTarget = toFileLinkTarget(rule.path);
|
|
return {
|
|
label: `${rule.id} / ${rule.name}`,
|
|
insertText: buildAliasedWikilink(linkTarget, rule.name || rule.id),
|
|
resolveKey: linkTarget,
|
|
detail: rule.kind ?? "rule",
|
|
kind: "reference"
|
|
};
|
|
}
|
|
function toMappingSuggestion(mapping) {
|
|
const linkTarget = toFileLinkTarget(mapping.path);
|
|
return {
|
|
label: `${mapping.id} / ${mapping.name}`,
|
|
insertText: buildAliasedWikilink(linkTarget, mapping.name || mapping.id),
|
|
resolveKey: linkTarget,
|
|
detail: mapping.kind ?? "mapping",
|
|
kind: "reference"
|
|
};
|
|
}
|
|
function toFileLinkTarget(path2) {
|
|
return path2.replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
}
|
|
function normalizeCompletionQuery(value) {
|
|
const trimmed = value.trim();
|
|
const withoutOpening = trimmed.startsWith("[[") ? trimmed.slice(2) : trimmed;
|
|
const withoutClosing = withoutOpening.endsWith("]]") ? withoutOpening.slice(0, -2) : withoutOpening;
|
|
const normalized = withoutClosing.replace(/\\\|/g, "|");
|
|
let escaped = false;
|
|
for (let index = 0; index < normalized.length; index += 1) {
|
|
const char = normalized[index];
|
|
if (escaped) {
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (char === "\\") {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (char === "|") {
|
|
return normalized.slice(0, index).trim();
|
|
}
|
|
}
|
|
return normalized.trim();
|
|
}
|
|
function lineTrimmedOffset(value) {
|
|
const match = value.match(/^\s*/);
|
|
return match ? match[0].length : 0;
|
|
}
|
|
function getDiagramObjectRefs(content) {
|
|
const lines = content.split(/\r?\n/);
|
|
const refs = [];
|
|
let inObjectsSection = false;
|
|
let headerSeen = false;
|
|
for (const rawLine of lines) {
|
|
const trimmed = rawLine.trim();
|
|
const headingMatch = trimmed.match(/^##\s+(.+)$/);
|
|
if (headingMatch) {
|
|
inObjectsSection = headingMatch[1].trim() === "Objects";
|
|
headerSeen = false;
|
|
continue;
|
|
}
|
|
if (!inObjectsSection || !trimmed.startsWith("|")) {
|
|
continue;
|
|
}
|
|
if (isMarkdownTableSeparator(trimmed)) {
|
|
continue;
|
|
}
|
|
const row = parseMarkdownTableRow(trimmed);
|
|
if (!row || row.length < 2) {
|
|
continue;
|
|
}
|
|
if (!headerSeen) {
|
|
headerSeen = row[0] === "ref" && row[1] === "notes";
|
|
continue;
|
|
}
|
|
if (row[0]) {
|
|
refs.push(row[0]);
|
|
}
|
|
}
|
|
return refs;
|
|
}
|
|
function extractLineText(line, from, to) {
|
|
return line.slice(from, to).trim();
|
|
}
|
|
function replaceSuggestionText(editor, request, suggestion) {
|
|
const insertText = suggestion.insertText;
|
|
if (request.tableColumnIndex !== void 0 && (request.kind === "er-diagram-object" || request.kind === "dfd-diagram-object" || request.kind === "dfd-diagram-flow-from" || request.kind === "dfd-diagram-flow-to" || request.kind === "dfd-diagram-flow-data" || request.kind === "data-object-field-ref" || request.kind === "data-object-option" || request.kind === "screen-option" || request.kind === "class-diagram-object" || request.kind === "class-relation-to" || request.kind === "class-relation-kind" || request.kind === "screen-field-ref" || request.kind === "screen-field-layout" || request.kind === "screen-field-kind" || request.kind === "screen-field-data-type" || request.kind === "screen-field-required" || request.kind === "screen-action-target" || request.kind === "screen-action-kind" || request.kind === "screen-action-event" || request.kind === "screen-action-invoke" || request.kind === "screen-action-transition" || request.kind === "screen-rule-ref" || request.kind === "screen-message-severity" || request.kind === "app-process-input-data" || request.kind === "app-process-input-source" || request.kind === "app-process-output-data" || request.kind === "app-process-output-target" || request.kind === "app-process-trigger-source" || request.kind === "app-process-transition-to" || request.kind === "rule-input-data" || request.kind === "rule-input-source" || request.kind === "rule-reference-ref" || request.kind === "rule-message-ref" || request.kind === "mapping-scope-ref" || request.kind === "mapping-source-ref" || request.kind === "mapping-target-ref" || request.kind === "mapping-rule-ref" || request.kind === "codeset-active")) {
|
|
return replaceMarkdownTableCell(editor, request, insertText);
|
|
}
|
|
if (request.kind === "class-diagram-relation-picker") {
|
|
if (suggestion.rowValues) {
|
|
return replaceClassDiagramRelationRow(
|
|
editor,
|
|
request.replaceFrom.line,
|
|
suggestion.rowValues
|
|
);
|
|
}
|
|
}
|
|
editor.replaceRange(insertText, request.replaceFrom, request.replaceTo);
|
|
return {
|
|
line: request.replaceFrom.line,
|
|
ch: request.replaceFrom.ch + insertText.length
|
|
};
|
|
}
|
|
function restoreCompletionCursor(editor, cursor) {
|
|
focusMarkdownEditor(editor);
|
|
editor.setSelection(cursor, cursor);
|
|
editor.setCursor(cursor);
|
|
const defer = typeof window !== "undefined" && typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : (callback) => window.setTimeout(() => callback(0), 0);
|
|
defer(() => {
|
|
focusMarkdownEditor(editor);
|
|
editor.setSelection(cursor, cursor);
|
|
editor.setCursor(cursor);
|
|
});
|
|
}
|
|
function focusMarkdownEditor(editor) {
|
|
const editorWithFocus = editor;
|
|
editorWithFocus.focus?.();
|
|
editorWithFocus.cm?.focus?.();
|
|
}
|
|
function replaceClassDiagramRelationRow(editor, lineNumber, rowValues) {
|
|
const line = editor.getLine(lineNumber);
|
|
const existingCells = parseMarkdownTableRow(line) ?? [];
|
|
const cells = new Array(8).fill("");
|
|
for (let index = 0; index < Math.min(existingCells.length, cells.length); index += 1) {
|
|
cells[index] = existingCells[index] ?? "";
|
|
}
|
|
const existingId = cells[0].trim();
|
|
if (!existingId) {
|
|
const preferredId = normalizeRelationId(rowValues.id) ?? buildFallbackRelationId(rowValues.from ?? "", rowValues.to ?? "", rowValues.kind ?? "");
|
|
cells[0] = ensureUniqueClassDiagramRelationId(editor, lineNumber, preferredId);
|
|
}
|
|
cells[1] = rowValues.from ?? cells[1];
|
|
cells[2] = rowValues.to ?? cells[2];
|
|
cells[3] = rowValues.kind ?? cells[3];
|
|
cells[4] = rowValues.label ?? cells[4];
|
|
cells[5] = rowValues.from_multiplicity ?? cells[5];
|
|
cells[6] = rowValues.to_multiplicity ?? cells[6];
|
|
const nextLine = `| ${cells.join(" | ")} |`;
|
|
editor.replaceRange(
|
|
nextLine,
|
|
{ line: lineNumber, ch: 0 },
|
|
{ line: lineNumber, ch: line.length }
|
|
);
|
|
return {
|
|
line: lineNumber,
|
|
ch: nextLine.length
|
|
};
|
|
}
|
|
function toClassDiagramRelationSuggestion(relation, sourceObject, targetObject, insideDiagram) {
|
|
const labelPart = relation.label ? ` | ${relation.label}` : "";
|
|
const multiplicityPart = relation.fromMultiplicity || relation.toMultiplicity ? ` [${relation.fromMultiplicity ?? ""} -> ${relation.toMultiplicity ?? ""}]` : "";
|
|
return {
|
|
label: `${relation.sourceClass} -> ${relation.targetClass} | ${relation.kind}${labelPart}${multiplicityPart}`,
|
|
insertText: relation.id ?? `${relation.sourceClass}->${relation.targetClass}`,
|
|
detail: insideDiagram ? "target in diagram" : "target outside diagram",
|
|
kind: "class",
|
|
rowValues: {
|
|
id: relation.id ?? "",
|
|
from: toObjectDiagramWikilink(sourceObject),
|
|
to: targetObject ? toObjectDiagramWikilink(targetObject) : toReferenceWikilink(relation.targetClass),
|
|
kind: relation.kind,
|
|
label: relation.label ?? "",
|
|
from_multiplicity: relation.fromMultiplicity ?? "",
|
|
to_multiplicity: relation.toMultiplicity ?? ""
|
|
}
|
|
};
|
|
}
|
|
function toObjectDiagramWikilink(object) {
|
|
const displayName = object.name || typeof object.frontmatter?.id === "string" && object.frontmatter.id.trim() || getFileStem2(object.path);
|
|
return buildAliasedWikilink(toFileLinkTarget(object.path), displayName);
|
|
}
|
|
function toReferenceWikilink(reference) {
|
|
return buildAliasedWikilink(normalizeReferenceTarget(reference), normalizeReferenceTarget(reference).split("/").pop() ?? normalizeReferenceTarget(reference));
|
|
}
|
|
function normalizeRelationId(value) {
|
|
const trimmed = value?.trim();
|
|
return trimmed ? trimmed : null;
|
|
}
|
|
function buildFallbackRelationId(from, to, kind) {
|
|
const source = sanitizeRelationIdPart(from) || "FROM";
|
|
const target = sanitizeRelationIdPart(to) || "TO";
|
|
const relationKind = sanitizeRelationIdPart(kind);
|
|
return relationKind ? `REL-${source}-${relationKind}-${target}` : `REL-${source}-${target}`;
|
|
}
|
|
function sanitizeRelationIdPart(value) {
|
|
return value.trim().replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toUpperCase();
|
|
}
|
|
function ensureUniqueClassDiagramRelationId(editor, lineNumber, preferredId) {
|
|
const existingIds = collectExistingClassDiagramRelationIds(editor, lineNumber);
|
|
if (!existingIds.has(preferredId)) {
|
|
return preferredId;
|
|
}
|
|
let suffix = 2;
|
|
while (existingIds.has(`${preferredId}-${suffix}`)) {
|
|
suffix += 1;
|
|
}
|
|
return `${preferredId}-${suffix}`;
|
|
}
|
|
function collectExistingClassDiagramRelationIds(editor, lineNumber) {
|
|
const lines = editor.getValue().split(/\r?\n/);
|
|
const headerRowIndex = findNearestLine(lines, lineNumber, (candidate) => {
|
|
const row = parseMarkdownTableRow(candidate);
|
|
return row !== null && row.length >= 8 && row[0] === "id" && row[1] === "from" && row[2] === "to" && row[3] === "kind";
|
|
});
|
|
if (headerRowIndex < 0) {
|
|
return /* @__PURE__ */ new Set();
|
|
}
|
|
const ids = /* @__PURE__ */ new Set();
|
|
for (let index = headerRowIndex + 2; index < lines.length; index += 1) {
|
|
const candidate = lines[index] ?? "";
|
|
const trimmed = candidate.trim();
|
|
if (/^##\s+/.test(trimmed)) {
|
|
break;
|
|
}
|
|
if (!trimmed.startsWith("|") || isMarkdownTableSeparator(trimmed)) {
|
|
continue;
|
|
}
|
|
if (index === lineNumber) {
|
|
continue;
|
|
}
|
|
const row = parseMarkdownTableRow(trimmed);
|
|
const id = row?.[0]?.trim();
|
|
if (id) {
|
|
ids.add(id);
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
function replaceMarkdownTableCell(editor, request, insertText) {
|
|
const lineNumber = request.replaceFrom.line;
|
|
const line = editor.getLine(lineNumber);
|
|
const ranges = getMarkdownTableCellRanges(line);
|
|
const columnIndex = request.tableColumnIndex ?? 0;
|
|
if (!ranges || columnIndex >= ranges.length) {
|
|
editor.replaceRange(insertText, request.replaceFrom, request.replaceTo);
|
|
return {
|
|
line: request.replaceFrom.line,
|
|
ch: request.replaceFrom.ch + insertText.length
|
|
};
|
|
}
|
|
const range = ranges[columnIndex];
|
|
const nextLine = `${line.slice(0, range.rawStart)} ${insertText} ${line.slice(range.rawEnd)}`;
|
|
editor.replaceRange(
|
|
nextLine,
|
|
{ line: lineNumber, ch: 0 },
|
|
{ line: lineNumber, ch: line.length }
|
|
);
|
|
return {
|
|
line: lineNumber,
|
|
ch: range.rawStart + 1 + insertText.length
|
|
};
|
|
}
|
|
function buildAliasedWikilink(target, displayName) {
|
|
return `[[${target}\\|${escapeWikilinkAlias(displayName)}]]`;
|
|
}
|
|
function escapeWikilinkAlias(value) {
|
|
return value.replace(/\|/g, "\\|");
|
|
}
|
|
function getFileStem2(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? path2;
|
|
}
|
|
|
|
// src/export/png-export.ts
|
|
var import_obsidian4 = require("obsidian");
|
|
|
|
// src/adapters/obsidian-mermaid.ts
|
|
var import_obsidian3 = require("obsidian");
|
|
async function loadMermaidAdapter() {
|
|
return (0, import_obsidian3.loadMermaid)();
|
|
}
|
|
|
|
// src/i18n/localized-messages.ts
|
|
function formatMermaidRenderStatusMessage(status, language) {
|
|
const statusText = modelWeaveText(
|
|
status,
|
|
status === "generated" ? "\u751F\u6210\u6E08\u307F" : status === "rendered" ? "\u63CF\u753B\u6E08\u307F" : "\u5931\u6557",
|
|
language
|
|
);
|
|
return modelWeaveText(
|
|
`Render status: ${statusText}`,
|
|
`\u63CF\u753B\u30B9\u30C6\u30FC\u30BF\u30B9: ${statusText}`,
|
|
language
|
|
);
|
|
}
|
|
function formatMermaidRenderErrorMessage(error, language) {
|
|
return modelWeaveText(
|
|
`Render error: ${error}`,
|
|
`\u63CF\u753B\u30A8\u30E9\u30FC: ${error}`,
|
|
language
|
|
);
|
|
}
|
|
function formatMermaidSvgNotRenderedMessage(language) {
|
|
return modelWeaveText("SVG: not rendered", "SVG: \u672A\u63CF\u753B", language);
|
|
}
|
|
|
|
// src/renderers/graph-view-shared.ts
|
|
var DEFAULT_FIT_PADDING_PX = 16;
|
|
function resetGraphViewportState(state, initialZoom = 1) {
|
|
state.zoom = initialZoom;
|
|
state.panX = 0;
|
|
state.panY = 0;
|
|
state.viewMode = "fit";
|
|
state.hasAutoFitted = false;
|
|
state.hasUserInteracted = false;
|
|
}
|
|
function getConnectionPoints(source, target) {
|
|
const sourceCenterX = source.x + source.width / 2;
|
|
const sourceCenterY = source.y + source.height / 2;
|
|
const targetCenterX = target.x + target.width / 2;
|
|
const targetCenterY = target.y + target.height / 2;
|
|
const horizontal = Math.abs(targetCenterX - sourceCenterX) >= Math.abs(targetCenterY - sourceCenterY);
|
|
const startX = horizontal ? sourceCenterX < targetCenterX ? source.x + source.width : source.x : sourceCenterX;
|
|
const startY = horizontal ? sourceCenterY : sourceCenterY < targetCenterY ? source.y + source.height : source.y;
|
|
const endX = horizontal ? sourceCenterX < targetCenterX ? target.x : target.x + target.width : targetCenterX;
|
|
const endY = horizontal ? targetCenterY : sourceCenterY < targetCenterY ? target.y : target.y + target.height;
|
|
return {
|
|
startX,
|
|
startY,
|
|
endX,
|
|
endY,
|
|
midX: (startX + endX) / 2,
|
|
midY: (startY + endY) / 2
|
|
};
|
|
}
|
|
function computeSceneBounds(nodes, labels, padding = 24) {
|
|
const allRects = [...nodes, ...labels];
|
|
if (allRects.length === 0) {
|
|
return {
|
|
minX: 0,
|
|
minY: 0,
|
|
maxX: padding * 2,
|
|
maxY: padding * 2,
|
|
width: padding * 2,
|
|
height: padding * 2
|
|
};
|
|
}
|
|
const minX = Math.min(...allRects.map((rect) => rect.x)) - padding;
|
|
const minY = Math.min(...allRects.map((rect) => rect.y)) - padding;
|
|
const maxX = Math.max(...allRects.map((rect) => rect.x + rect.width)) + padding;
|
|
const maxY = Math.max(...allRects.map((rect) => rect.y + rect.height)) + padding;
|
|
return {
|
|
minX,
|
|
minY,
|
|
maxX,
|
|
maxY,
|
|
width: maxX - minX,
|
|
height: maxY - minY
|
|
};
|
|
}
|
|
function estimateBadgeBounds(centerX, centerY, value, options) {
|
|
const charWidth = options?.charWidth ?? 8;
|
|
const minWidth = options?.minWidth ?? 52;
|
|
const height = options?.height ?? 20;
|
|
const width = Math.max(minWidth, value.length * charWidth + 12);
|
|
return {
|
|
x: centerX - width / 2,
|
|
y: centerY - height / 2,
|
|
width,
|
|
height
|
|
};
|
|
}
|
|
function estimateEdgeLabelBounds(edges, layoutById, getLabel, labelOffsetY = -8) {
|
|
const bounds = [];
|
|
for (const edge of edges) {
|
|
const source = layoutById[edge.source];
|
|
const target = layoutById[edge.target];
|
|
if (!source || !target) {
|
|
continue;
|
|
}
|
|
const label = getLabel(edge);
|
|
if (!label) {
|
|
continue;
|
|
}
|
|
const points = getConnectionPoints(source, target);
|
|
bounds.push(estimateBadgeBounds(points.midX, points.midY + labelOffsetY, label));
|
|
}
|
|
return bounds;
|
|
}
|
|
function attachGraphViewportInteractions(canvas, surface, toolbar, scene, options) {
|
|
const minZoom = options?.minZoom ?? 0.45;
|
|
const maxZoom = options?.maxZoom ?? 2.4;
|
|
const initialZoom = options?.initialZoom ?? 1;
|
|
const minFitScale = options?.minFitScale ?? 0;
|
|
const fitPaddingPx = Math.max(0, options?.fitPaddingPx ?? DEFAULT_FIT_PADDING_PX);
|
|
const nodeSelector = options?.nodeSelector;
|
|
const fitHorizontalAlign = options?.fitHorizontalAlign ?? "center";
|
|
const fitVerticalAlign = options?.fitVerticalAlign ?? "center";
|
|
const fitContentBounds = options?.fitContentBounds;
|
|
const state = options?.viewportState ?? {
|
|
zoom: initialZoom,
|
|
panX: 0,
|
|
panY: 0,
|
|
viewMode: "fit",
|
|
hasAutoFitted: false,
|
|
hasUserInteracted: false
|
|
};
|
|
let isPanning = false;
|
|
let pointerId = null;
|
|
let startClientX = 0;
|
|
let startClientY = 0;
|
|
let startPanX = 0;
|
|
let startPanY = 0;
|
|
const applyTransform = () => {
|
|
surface.setCssProps({
|
|
"--mw-preview-transform": `translate(${state.panX}px, ${state.panY}px) scale(${state.zoom})`
|
|
});
|
|
toolbar.zoomLabel.textContent = `${Math.round(state.zoom * 100)}%`;
|
|
};
|
|
const notifyViewportStateChange = () => {
|
|
options?.onViewportStateChange?.(state);
|
|
};
|
|
const fitToView = () => {
|
|
const viewportWidth = canvas.clientWidth;
|
|
const viewportHeight = canvas.clientHeight;
|
|
if (viewportWidth <= 0 || viewportHeight <= 0) {
|
|
return false;
|
|
}
|
|
const boundsSource = scene.source ?? "scene";
|
|
const boundsX = scene.minX ?? 0;
|
|
const boundsY = scene.minY ?? 0;
|
|
const boundsWidth = scene.width;
|
|
const boundsHeight = scene.height;
|
|
const effectiveViewportWidth = Math.max(1, viewportWidth - fitPaddingPx * 2);
|
|
const effectiveViewportHeight = Math.max(1, viewportHeight - fitPaddingPx * 2);
|
|
if (!isValidFitBounds(boundsWidth, boundsHeight)) {
|
|
state.zoom = initialZoom;
|
|
state.panX = 0;
|
|
state.panY = 0;
|
|
applyTransform();
|
|
notifyViewportStateChange();
|
|
options?.onFitMetrics?.({
|
|
viewportWidth,
|
|
viewportHeight,
|
|
boundsX,
|
|
boundsY,
|
|
boundsWidth,
|
|
boundsHeight,
|
|
fitPaddingPx,
|
|
boundsSource,
|
|
computedScale: 0,
|
|
appliedScale: state.zoom,
|
|
panX: state.panX,
|
|
panY: state.panY,
|
|
warning: "fit skipped: invalid bounds"
|
|
});
|
|
return false;
|
|
}
|
|
const scaleX = effectiveViewportWidth / scene.width;
|
|
const scaleY = effectiveViewportHeight / scene.height;
|
|
const computedScale = Math.min(scaleX, scaleY);
|
|
if (computedScale < minFitScale) {
|
|
state.zoom = initialZoom;
|
|
state.panX = 0;
|
|
state.panY = 0;
|
|
applyTransform();
|
|
notifyViewportStateChange();
|
|
options?.onFitMetrics?.({
|
|
viewportWidth,
|
|
viewportHeight,
|
|
boundsX,
|
|
boundsY,
|
|
boundsWidth,
|
|
boundsHeight,
|
|
fitPaddingPx,
|
|
boundsSource,
|
|
computedScale,
|
|
appliedScale: state.zoom,
|
|
panX: state.panX,
|
|
panY: state.panY,
|
|
warning: "fit skipped: computed scale is below the safe threshold"
|
|
});
|
|
return false;
|
|
}
|
|
const nextZoom = clamp(computedScale, minZoom, maxZoom);
|
|
state.zoom = nextZoom;
|
|
state.panX = fitHorizontalAlign === "left" ? resolveLeftAlignedFitPan(viewportWidth, nextZoom, scene) : Math.max(0, (viewportWidth - scene.width * nextZoom) / 2);
|
|
state.panY = fitVerticalAlign === "top" ? resolveTopAlignedFitPan(viewportHeight, nextZoom, scene, fitContentBounds) : Math.max(0, (viewportHeight - scene.height * nextZoom) / 2);
|
|
state.viewMode = "fit";
|
|
applyTransform();
|
|
notifyViewportStateChange();
|
|
options?.onFitMetrics?.({
|
|
viewportWidth,
|
|
viewportHeight,
|
|
boundsX,
|
|
boundsY,
|
|
boundsWidth,
|
|
boundsHeight,
|
|
fitPaddingPx,
|
|
boundsSource,
|
|
computedScale,
|
|
appliedScale: nextZoom,
|
|
panX: state.panX,
|
|
panY: state.panY
|
|
});
|
|
return true;
|
|
};
|
|
const autoFitToView = () => {
|
|
if (state.hasUserInteracted) {
|
|
return;
|
|
}
|
|
const didFit = fitToView();
|
|
if (didFit) {
|
|
state.hasAutoFitted = true;
|
|
}
|
|
};
|
|
const zoomAtPoint = (nextZoom, clientX, clientY) => {
|
|
const clampedZoom = clamp(nextZoom, minZoom, maxZoom);
|
|
const rect = canvas.getBoundingClientRect();
|
|
const localX = clientX - rect.left;
|
|
const localY = clientY - rect.top;
|
|
const worldX = (localX - state.panX) / state.zoom;
|
|
const worldY = (localY - state.panY) / state.zoom;
|
|
state.zoom = clampedZoom;
|
|
state.panX = localX - worldX * clampedZoom;
|
|
state.panY = localY - worldY * clampedZoom;
|
|
applyTransform();
|
|
notifyViewportStateChange();
|
|
};
|
|
canvas.addEventListener(
|
|
"wheel",
|
|
(event) => {
|
|
event.preventDefault();
|
|
state.hasUserInteracted = true;
|
|
state.hasAutoFitted = true;
|
|
state.viewMode = "manual";
|
|
const delta = event.deltaY < 0 ? 1.12 : 1 / 1.12;
|
|
zoomAtPoint(state.zoom * delta, event.clientX, event.clientY);
|
|
},
|
|
{ passive: false }
|
|
);
|
|
canvas.addEventListener("pointerdown", (event) => {
|
|
const target = event.target;
|
|
if (nodeSelector && target instanceof Element && target.closest(nodeSelector)) {
|
|
return;
|
|
}
|
|
state.hasUserInteracted = true;
|
|
state.hasAutoFitted = true;
|
|
state.viewMode = "manual";
|
|
isPanning = true;
|
|
pointerId = event.pointerId;
|
|
startClientX = event.clientX;
|
|
startClientY = event.clientY;
|
|
startPanX = state.panX;
|
|
startPanY = state.panY;
|
|
canvas.toggleClass("model-weave-is-grabbing", true);
|
|
canvas.setPointerCapture(event.pointerId);
|
|
});
|
|
canvas.addEventListener("pointermove", (event) => {
|
|
if (!isPanning || pointerId !== event.pointerId) {
|
|
return;
|
|
}
|
|
const dx = event.clientX - startClientX;
|
|
const dy = event.clientY - startClientY;
|
|
state.panX = startPanX + dx;
|
|
state.panY = startPanY + dy;
|
|
applyTransform();
|
|
notifyViewportStateChange();
|
|
});
|
|
const stopPanning = (event) => {
|
|
if (!isPanning || pointerId !== event.pointerId) {
|
|
return;
|
|
}
|
|
isPanning = false;
|
|
pointerId = null;
|
|
canvas.toggleClass("model-weave-is-grabbing", false);
|
|
if (canvas.hasPointerCapture(event.pointerId)) {
|
|
canvas.releasePointerCapture(event.pointerId);
|
|
}
|
|
};
|
|
canvas.addEventListener("pointerup", stopPanning);
|
|
canvas.addEventListener("pointercancel", stopPanning);
|
|
canvas.addEventListener("pointerleave", (event) => {
|
|
if (isPanning && pointerId === event.pointerId) {
|
|
stopPanning(event);
|
|
}
|
|
});
|
|
toolbar.zoomOutButton.addEventListener("click", () => {
|
|
state.hasUserInteracted = true;
|
|
state.hasAutoFitted = true;
|
|
state.viewMode = "manual";
|
|
state.zoom = clamp(state.zoom / 1.12, minZoom, maxZoom);
|
|
applyTransform();
|
|
notifyViewportStateChange();
|
|
});
|
|
toolbar.zoomInButton.addEventListener("click", () => {
|
|
state.hasUserInteracted = true;
|
|
state.hasAutoFitted = true;
|
|
state.viewMode = "manual";
|
|
state.zoom = clamp(state.zoom * 1.12, minZoom, maxZoom);
|
|
applyTransform();
|
|
notifyViewportStateChange();
|
|
});
|
|
toolbar.fitButton.addEventListener("click", () => {
|
|
state.hasUserInteracted = false;
|
|
if (fitToView()) {
|
|
state.hasAutoFitted = true;
|
|
}
|
|
});
|
|
toolbar.resetButton.addEventListener("click", () => {
|
|
state.hasUserInteracted = true;
|
|
state.hasAutoFitted = true;
|
|
state.viewMode = "manual";
|
|
state.zoom = initialZoom;
|
|
state.panX = 0;
|
|
state.panY = 0;
|
|
applyTransform();
|
|
notifyViewportStateChange();
|
|
});
|
|
requestAnimationFrame(() => {
|
|
if (!state.hasAutoFitted) {
|
|
autoFitToView();
|
|
} else {
|
|
applyTransform();
|
|
notifyViewportStateChange();
|
|
}
|
|
});
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
if (!state.hasAutoFitted || state.viewMode === "fit") {
|
|
autoFitToView();
|
|
return;
|
|
}
|
|
applyTransform();
|
|
notifyViewportStateChange();
|
|
});
|
|
resizeObserver.observe(canvas);
|
|
}
|
|
function clamp(value, min, max) {
|
|
return Math.min(max, Math.max(min, value));
|
|
}
|
|
function isValidFitBounds(width, height) {
|
|
return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0 && width < 1e5 && height < 1e5;
|
|
}
|
|
function resolveTopAlignedFitPan(viewportHeight, zoom, scene, fitContentBounds) {
|
|
const contentTop = fitContentBounds?.top ?? 0;
|
|
const contentBottom = fitContentBounds?.bottom ?? scene.height;
|
|
const contentHeight = Math.max(0, contentBottom - contentTop) * zoom;
|
|
const spareHeight = viewportHeight - contentHeight;
|
|
const topPadding = resolveAdaptiveEdgePadding(spareHeight);
|
|
return topPadding - contentTop * zoom;
|
|
}
|
|
function resolveLeftAlignedFitPan(viewportWidth, zoom, scene) {
|
|
const contentWidth = scene.width * zoom;
|
|
const spareWidth = viewportWidth - contentWidth;
|
|
const leftPadding = resolveAdaptiveEdgePadding(spareWidth);
|
|
return leftPadding;
|
|
}
|
|
function resolveAdaptiveEdgePadding(spareSpace) {
|
|
if (spareSpace >= 56) {
|
|
return 20;
|
|
}
|
|
if (spareSpace >= 16) {
|
|
return 8;
|
|
}
|
|
return 4;
|
|
}
|
|
|
|
// src/renderers/zoom-toolbar.ts
|
|
function createZoomToolbar(helpText) {
|
|
const toolbar = document.createElement("div");
|
|
toolbar.className = "mdspec-zoom-toolbar model-weave-zoom-toolbar";
|
|
const help = document.createElement("div");
|
|
help.addClass("model-weave-zoom-toolbar-help");
|
|
help.textContent = helpText;
|
|
const controls = document.createElement("div");
|
|
controls.addClass("model-weave-zoom-toolbar-controls");
|
|
const zoomOutButton = createToolbarButton("\u2212");
|
|
const fitButton = createToolbarButton("Fit");
|
|
const zoomLabel = document.createElement("span");
|
|
zoomLabel.addClass("model-weave-zoom-toolbar-label");
|
|
zoomLabel.textContent = "100%";
|
|
const zoomInButton = createToolbarButton("+");
|
|
const resetButton = createToolbarButton("100%");
|
|
controls.append(
|
|
zoomOutButton,
|
|
fitButton,
|
|
zoomLabel,
|
|
zoomInButton,
|
|
resetButton
|
|
);
|
|
toolbar.append(help, controls);
|
|
return {
|
|
root: toolbar,
|
|
zoomOutButton,
|
|
fitButton,
|
|
zoomLabel,
|
|
zoomInButton,
|
|
resetButton
|
|
};
|
|
}
|
|
function createToolbarButton(label) {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.textContent = label;
|
|
button.addClass("model-weave-zoom-toolbar-button");
|
|
return button;
|
|
}
|
|
|
|
// src/renderers/mermaid-shared.ts
|
|
var MODEL_WEAVE_MERMAID_RENDER_FLAG = "__modelWeaveRenderReady";
|
|
var MIN_ZOOM = 0.4;
|
|
var MAX_ZOOM = 2.25;
|
|
var INITIAL_ZOOM = 1;
|
|
function createMermaidShell(options) {
|
|
const root = document.createElement("section");
|
|
root.className = `${options.className} model-weave-mermaid-shell`;
|
|
if (options.title) {
|
|
const title = document.createElement("h2");
|
|
title.textContent = options.title;
|
|
title.addClass("model-weave-mermaid-title");
|
|
root.appendChild(title);
|
|
}
|
|
const canvas = document.createElement("div");
|
|
canvas.addClass("model-weave-graph-canvas");
|
|
if (!options.forExport) {
|
|
canvas.addClass("model-weave-graph-canvas-interactive");
|
|
}
|
|
const toolbar = options.forExport ? null : createZoomToolbar("Wheel: zoom / Drag background: pan");
|
|
if (toolbar) {
|
|
root.appendChild(toolbar.root);
|
|
}
|
|
const viewport = document.createElement("div");
|
|
viewport.addClass("model-weave-graph-viewport");
|
|
const surface = document.createElement("div");
|
|
surface.addClass("model-weave-graph-surface");
|
|
surface.dataset.modelWeaveExportSurface = "true";
|
|
viewport.appendChild(surface);
|
|
canvas.appendChild(viewport);
|
|
root.appendChild(canvas);
|
|
return { root, canvas, surface, toolbar };
|
|
}
|
|
async function renderMermaidSourceIntoShell(shell, options) {
|
|
if (options.showSourcePanel !== false) {
|
|
appendMermaidSourcePanel(
|
|
options.sourcePanelContainer ?? shell.root,
|
|
options.source,
|
|
options.sourcePanelPlacement
|
|
);
|
|
}
|
|
const debug = options.showRenderDebug ? appendMermaidRenderDebugPanel(
|
|
options.renderDebugContainer ?? shell.root,
|
|
options.renderDebugPlacement
|
|
) : null;
|
|
updateMermaidRenderDebug(debug, { status: "generated" });
|
|
try {
|
|
const mermaid = await loadMermaidAdapter();
|
|
const renderId = `${options.renderIdPrefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
const rendered = await mermaid.render(
|
|
renderId,
|
|
withModelWeaveMermaidTheme(options.source)
|
|
);
|
|
const { canvas, surface, toolbar } = shell;
|
|
surface.empty();
|
|
const svg = appendRenderedSvg(surface, rendered.svg);
|
|
surface.dataset.modelWeaveRenderer = "mermaid";
|
|
if (typeof rendered.bindFunctions === "function") {
|
|
rendered.bindFunctions(surface);
|
|
}
|
|
const sceneSize = readMermaidSceneSize(svg);
|
|
if (!sceneSize) {
|
|
throw new Error("Mermaid SVG has no measurable bounds.");
|
|
}
|
|
surface.dataset.modelWeaveSceneWidth = `${sceneSize.width}`;
|
|
surface.dataset.modelWeaveSceneHeight = `${sceneSize.height}`;
|
|
surface.setCssProps({
|
|
"--mw-scene-width": `${sceneSize.width}px`,
|
|
"--mw-scene-height": `${sceneSize.height}px`
|
|
});
|
|
svg.setAttribute("width", `${sceneSize.width}`);
|
|
svg.setAttribute("height", `${sceneSize.height}`);
|
|
svg.classList.add("model-weave-mermaid-svg");
|
|
updateMermaidRenderDebug(debug, {
|
|
status: "rendered",
|
|
svg: readMermaidSvgInfo(surface)
|
|
});
|
|
if (options.staticRender) {
|
|
canvas.addClass("model-weave-graph-canvas-static");
|
|
surface.addClass("model-weave-graph-surface-static");
|
|
svg.classList.add("model-weave-mermaid-svg-static");
|
|
return;
|
|
}
|
|
if (toolbar) {
|
|
attachGraphViewportInteractions(canvas, surface, toolbar, sceneSize, {
|
|
minZoom: options.minZoom ?? MIN_ZOOM,
|
|
maxZoom: options.maxZoom ?? MAX_ZOOM,
|
|
initialZoom: options.initialZoom ?? INITIAL_ZOOM,
|
|
minFitScale: options.minFitScale,
|
|
nodeSelector: options.nodeSelector ?? ".node, g.node, foreignObject",
|
|
fitHorizontalAlign: options.fitHorizontalAlign,
|
|
fitVerticalAlign: options.fitVerticalAlign,
|
|
viewportState: options.viewportState,
|
|
onViewportStateChange: options.onViewportStateChange,
|
|
onFitMetrics: (metrics) => {
|
|
updateMermaidRenderDebug(debug, { fit: metrics });
|
|
options.onFitMetrics?.(metrics);
|
|
}
|
|
});
|
|
}
|
|
} catch (error) {
|
|
updateMermaidRenderDebug(debug, {
|
|
status: "failed",
|
|
error: error instanceof Error ? error.message : String(error),
|
|
svg: readMermaidSvgInfo(shell.surface)
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
function appendMermaidSourcePanel(container, source, placement = "append") {
|
|
const fencedSource = `\`\`\`mermaid
|
|
${source}
|
|
\`\`\``;
|
|
const root = document.createElement("details");
|
|
root.addClass("model-weave-preview-section");
|
|
root.addClass("model-weave-mermaid-source-panel");
|
|
const summary = document.createElement("summary");
|
|
summary.textContent = "Mermaid Source";
|
|
summary.addClass("model-weave-preview-section-title");
|
|
root.appendChild(summary);
|
|
const actions = document.createElement("div");
|
|
actions.addClass("model-weave-mermaid-source-actions");
|
|
const copyButton = document.createElement("button");
|
|
copyButton.type = "button";
|
|
copyButton.textContent = "Copy Mermaid";
|
|
copyButton.addClass("model-weave-secondary-button");
|
|
copyButton.addEventListener("click", async (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
await navigator.clipboard?.writeText(fencedSource);
|
|
});
|
|
actions.appendChild(copyButton);
|
|
root.appendChild(actions);
|
|
const pre = document.createElement("pre");
|
|
pre.addClass("model-weave-mermaid-source-code");
|
|
const code = document.createElement("code");
|
|
code.textContent = fencedSource;
|
|
pre.appendChild(code);
|
|
root.appendChild(pre);
|
|
placePanel(container, root, placement);
|
|
}
|
|
function appendMermaidRenderDebugPanel(container, placement = "append") {
|
|
const root = document.createElement("details");
|
|
root.addClass("model-weave-preview-section");
|
|
root.addClass("model-weave-mermaid-render-debug");
|
|
const summary = document.createElement("summary");
|
|
summary.textContent = "Mermaid Render Debug";
|
|
summary.addClass("model-weave-preview-section-title");
|
|
root.appendChild(summary);
|
|
const status = root.createEl("p", {
|
|
text: formatMermaidRenderStatusMessage("generated"),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
const error = root.createEl("p", {
|
|
text: formatMermaidRenderErrorMessage("-"),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
const svgInfo = root.createEl("p", {
|
|
text: formatMermaidSvgNotRenderedMessage(),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
const fitInfo = root.createEl("p", {
|
|
text: "Fit: not measured",
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
placePanel(container, root, placement);
|
|
return { root, status, error, svgInfo, fitInfo };
|
|
}
|
|
function placePanel(container, panel, placement) {
|
|
if (placement === "prepend") {
|
|
container.prepend(panel);
|
|
return;
|
|
}
|
|
container.appendChild(panel);
|
|
}
|
|
function updateMermaidRenderDebug(debug, update) {
|
|
if (!debug) {
|
|
return;
|
|
}
|
|
if (update.status) {
|
|
debug.status.textContent = formatMermaidRenderStatusMessage(update.status);
|
|
}
|
|
if (update.error) {
|
|
debug.error.textContent = formatMermaidRenderErrorMessage(update.error);
|
|
}
|
|
if (update.svg) {
|
|
debug.svgInfo.textContent = [
|
|
`SVG exists: ${update.svg.exists ? "yes" : "no"}`,
|
|
`width: ${update.svg.width || "-"}`,
|
|
`height: ${update.svg.height || "-"}`,
|
|
`viewBox: ${update.svg.viewBox || "-"}`,
|
|
`child elements: ${update.svg.childElementCount}`
|
|
].join(" / ");
|
|
}
|
|
if (update.fit) {
|
|
debug.fitInfo.textContent = [
|
|
modelWeaveText(
|
|
`Fit bounds source: ${update.fit.boundsSource}`,
|
|
`fit bounds source: ${update.fit.boundsSource}`
|
|
),
|
|
`viewport: ${formatFitNumber(update.fit.viewportWidth)}x${formatFitNumber(update.fit.viewportHeight)}`,
|
|
`bounds: ${formatFitNumber(update.fit.boundsX)},${formatFitNumber(update.fit.boundsY)} ${formatFitNumber(update.fit.boundsWidth)}x${formatFitNumber(update.fit.boundsHeight)}`,
|
|
`computed scale: ${formatFitPercent(update.fit.computedScale)}`,
|
|
`applied scale: ${formatFitPercent(update.fit.appliedScale)}`,
|
|
`pan: ${formatFitNumber(update.fit.panX)},${formatFitNumber(update.fit.panY)}`,
|
|
update.fit.warning ? modelWeaveText(`warning: ${update.fit.warning}`, `\u8B66\u544A: ${update.fit.warning}`) : null
|
|
].filter((part) => Boolean(part)).join(" / ");
|
|
}
|
|
}
|
|
function readMermaidSvgInfo(surface) {
|
|
const svg = surface.querySelector("svg");
|
|
return {
|
|
exists: Boolean(svg),
|
|
width: svg?.getAttribute("width") ?? "",
|
|
height: svg?.getAttribute("height") ?? "",
|
|
viewBox: svg?.getAttribute("viewBox") ?? "",
|
|
childElementCount: svg?.childElementCount ?? 0
|
|
};
|
|
}
|
|
function formatFitNumber(value) {
|
|
if (!Number.isFinite(value)) {
|
|
return "-";
|
|
}
|
|
return Math.round(value * 100) / 100 + "";
|
|
}
|
|
function formatFitPercent(value) {
|
|
if (!Number.isFinite(value)) {
|
|
return "-";
|
|
}
|
|
return `${Math.round(value * 1e3) / 10}%`;
|
|
}
|
|
function appendRenderedSvg(surface, svgMarkup) {
|
|
const parsedSvg = parseMermaidSvgMarkup(svgMarkup);
|
|
if (!parsedSvg) {
|
|
throw new Error("Mermaid SVG was not generated.");
|
|
}
|
|
scrubSvgElementTree(parsedSvg);
|
|
const importedSvg = surface.ownerDocument.importNode(
|
|
parsedSvg,
|
|
true
|
|
);
|
|
surface.appendChild(importedSvg);
|
|
return importedSvg;
|
|
}
|
|
function parseMermaidSvgMarkup(svgMarkup) {
|
|
const parser = new DOMParser();
|
|
const svgDocument = parser.parseFromString(svgMarkup, "image/svg+xml");
|
|
const svgParseError = svgDocument.querySelector("parsererror");
|
|
if (!svgParseError) {
|
|
const parsedSvg = svgDocument.documentElement;
|
|
if (parsedSvg && parsedSvg.tagName.toLowerCase() === "svg") {
|
|
return parsedSvg;
|
|
}
|
|
}
|
|
const htmlDocument = parser.parseFromString(svgMarkup, "text/html");
|
|
const htmlSvg = htmlDocument.body.querySelector("svg");
|
|
if (!htmlSvg) {
|
|
return null;
|
|
}
|
|
return htmlSvg;
|
|
}
|
|
function scrubSvgElementTree(root) {
|
|
const elements = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
for (const element of elements) {
|
|
if (element.tagName.toLowerCase() === "script") {
|
|
element.remove();
|
|
continue;
|
|
}
|
|
for (const attribute of Array.from(element.attributes)) {
|
|
const attributeName = attribute.name.toLowerCase();
|
|
const attributeValue = attribute.value.trim().toLowerCase();
|
|
if (attributeName.startsWith("on")) {
|
|
element.removeAttribute(attribute.name);
|
|
continue;
|
|
}
|
|
if ((attributeName === "href" || attributeName === "xlink:href") && attributeValue.startsWith("javascript:")) {
|
|
element.removeAttribute(attribute.name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function setMermaidRenderReadyPromise(element, ready) {
|
|
element[MODEL_WEAVE_MERMAID_RENDER_FLAG] = ready;
|
|
}
|
|
function getMermaidRenderReadyPromise(element) {
|
|
return element[MODEL_WEAVE_MERMAID_RENDER_FLAG] ?? null;
|
|
}
|
|
function createMermaidFallbackNotice(message) {
|
|
const notice = document.createElement("div");
|
|
notice.addClass("model-weave-mermaid-fallback");
|
|
notice.textContent = message;
|
|
return notice;
|
|
}
|
|
function getModelWeaveMermaidPalette() {
|
|
const isDark = isModelWeaveDarkTheme();
|
|
if (isDark) {
|
|
return {
|
|
background: "#1f2329",
|
|
nodeFill: "#273241",
|
|
nodeBorder: "#6f8fb8",
|
|
nodeText: "#e6edf3",
|
|
line: "#9aa7b8",
|
|
labelBackground: "#2a3038",
|
|
subgraphFill: "#242a33",
|
|
subgraphBorder: "#5f6f82",
|
|
classFill: "#273241",
|
|
classBorder: "#6f8fb8",
|
|
erFill: "#243629",
|
|
erBorder: "#70a57d",
|
|
dfdExternalFill: "#3a3325",
|
|
dfdExternalBorder: "#b69a58",
|
|
dfdProcessFill: "#253349",
|
|
dfdProcessBorder: "#6f8fb8",
|
|
dfdDatastoreFill: "#253728",
|
|
dfdDatastoreBorder: "#78a984",
|
|
dfdOtherFill: "#2b3038",
|
|
dfdOtherBorder: "#7a8797"
|
|
};
|
|
}
|
|
return {
|
|
background: "#ffffff",
|
|
nodeFill: "#f4f7fb",
|
|
nodeBorder: "#7a8da8",
|
|
nodeText: "#1f2937",
|
|
line: "#64748b",
|
|
labelBackground: "#f8fafc",
|
|
subgraphFill: "#f5f7fa",
|
|
subgraphBorder: "#c5ceda",
|
|
classFill: "#eef4ff",
|
|
classBorder: "#4a6fa3",
|
|
erFill: "#eef8ef",
|
|
erBorder: "#467454",
|
|
dfdExternalFill: "#f8f1df",
|
|
dfdExternalBorder: "#8b6a17",
|
|
dfdProcessFill: "#e9f2ff",
|
|
dfdProcessBorder: "#2f5b9a",
|
|
dfdDatastoreFill: "#eef7ee",
|
|
dfdDatastoreBorder: "#3b6b47",
|
|
dfdOtherFill: "#f5f7fb",
|
|
dfdOtherBorder: "#5f6b7a"
|
|
};
|
|
}
|
|
function buildModelWeaveMermaidClassDef(className, fill, stroke, options) {
|
|
const palette = getModelWeaveMermaidPalette();
|
|
const text = options?.text ?? palette.nodeText;
|
|
const strokeWidth = options?.strokeWidth ?? 1.4;
|
|
const extra = options?.extra ? `,${options.extra}` : "";
|
|
return `classDef ${className} fill:${fill},stroke:${stroke},color:${text},stroke-width:${strokeWidth}px${extra}`;
|
|
}
|
|
function withModelWeaveMermaidTheme(source) {
|
|
if (/^\s*%%\{init:/u.test(source)) {
|
|
return source;
|
|
}
|
|
return `${buildModelWeaveMermaidInitDirective()}
|
|
${source}`;
|
|
}
|
|
function buildModelWeaveMermaidInitDirective() {
|
|
const palette = getModelWeaveMermaidPalette();
|
|
return `%%{init: ${JSON.stringify({
|
|
theme: "base",
|
|
themeVariables: {
|
|
background: palette.background,
|
|
mainBkg: palette.nodeFill,
|
|
secondBkg: palette.subgraphFill,
|
|
primaryColor: palette.nodeFill,
|
|
primaryBorderColor: palette.nodeBorder,
|
|
primaryTextColor: palette.nodeText,
|
|
secondaryColor: palette.subgraphFill,
|
|
secondaryBorderColor: palette.subgraphBorder,
|
|
secondaryTextColor: palette.nodeText,
|
|
tertiaryColor: palette.labelBackground,
|
|
tertiaryBorderColor: palette.subgraphBorder,
|
|
tertiaryTextColor: palette.nodeText,
|
|
lineColor: palette.line,
|
|
textColor: palette.nodeText,
|
|
edgeLabelBackground: palette.labelBackground,
|
|
clusterBkg: palette.subgraphFill,
|
|
clusterBorder: palette.subgraphBorder,
|
|
titleColor: palette.nodeText,
|
|
nodeBorder: palette.nodeBorder
|
|
}
|
|
})}}%%`;
|
|
}
|
|
function isModelWeaveDarkTheme() {
|
|
return document.body.classList.contains("theme-dark");
|
|
}
|
|
function readMermaidSceneSize(svg) {
|
|
const viewBox = svg.viewBox?.baseVal;
|
|
if (viewBox && Number.isFinite(viewBox.width) && Number.isFinite(viewBox.height)) {
|
|
return {
|
|
minX: viewBox.x,
|
|
minY: viewBox.y,
|
|
maxX: viewBox.x + Math.max(1, viewBox.width),
|
|
maxY: viewBox.y + Math.max(1, viewBox.height),
|
|
width: Math.max(1, viewBox.width),
|
|
height: Math.max(1, viewBox.height),
|
|
source: "viewBox"
|
|
};
|
|
}
|
|
const bbox = safeReadSvgBBox(svg);
|
|
if (bbox) {
|
|
return {
|
|
minX: bbox.x,
|
|
minY: bbox.y,
|
|
maxX: bbox.x + bbox.width,
|
|
maxY: bbox.y + bbox.height,
|
|
width: bbox.width,
|
|
height: bbox.height,
|
|
source: "getBBox"
|
|
};
|
|
}
|
|
const width = parseFloat(svg.getAttribute("width") ?? "");
|
|
const height = parseFloat(svg.getAttribute("height") ?? "");
|
|
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
minX: 0,
|
|
minY: 0,
|
|
maxX: width,
|
|
maxY: height,
|
|
width,
|
|
height,
|
|
source: "fallback"
|
|
};
|
|
}
|
|
function safeReadSvgBBox(svg) {
|
|
try {
|
|
const bbox = svg.getBBox();
|
|
if (Number.isFinite(bbox.x) && Number.isFinite(bbox.y) && Number.isFinite(bbox.width) && Number.isFinite(bbox.height) && bbox.width > 0 && bbox.height > 0) {
|
|
return {
|
|
x: bbox.x,
|
|
y: bbox.y,
|
|
width: bbox.width,
|
|
height: bbox.height
|
|
};
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// src/export/png-export.ts
|
|
var EXPORT_FOLDER = "exports";
|
|
var EXPORT_PADDING = 32;
|
|
var EXPORT_SCALE = 2;
|
|
var OFFSCREEN_ROOT_ID = "model-weave-export-root";
|
|
var DiagramExportError = class extends Error {
|
|
constructor(message, code) {
|
|
super(message);
|
|
this.code = code;
|
|
this.name = "DiagramExportError";
|
|
}
|
|
};
|
|
async function exportDiagramRenderableAsPng(app, renderable) {
|
|
const rendered = await Promise.resolve(renderable.render());
|
|
const mounted = mountOffscreenExportRoot(rendered);
|
|
try {
|
|
const mermaidReady = getMermaidRenderReadyPromise(rendered);
|
|
if (mermaidReady) {
|
|
await mermaidReady;
|
|
}
|
|
await waitForAnimationFrame();
|
|
const snapshot = buildDomDiagramExportSnapshot(
|
|
mounted.mount,
|
|
renderable.filePath,
|
|
renderable.renderer
|
|
);
|
|
if (!snapshot) {
|
|
throw new DiagramExportError(
|
|
"The current diagram has no measurable export bounds.",
|
|
"bounds-invalid"
|
|
);
|
|
}
|
|
return exportDiagramSnapshotAsPng(app, snapshot);
|
|
} finally {
|
|
mounted.dispose();
|
|
}
|
|
}
|
|
function buildDomDiagramExportSnapshot(container, filePath, renderer) {
|
|
const surface = container.querySelector(
|
|
'[data-model-weave-export-surface="true"]'
|
|
);
|
|
if (!surface) {
|
|
return null;
|
|
}
|
|
const surfaceComputedStyle = window.getComputedStyle(surface);
|
|
const sceneWidth = readSceneSize(
|
|
surface.dataset.modelWeaveSceneWidth,
|
|
surfaceComputedStyle.width
|
|
);
|
|
const sceneHeight = readSceneSize(
|
|
surface.dataset.modelWeaveSceneHeight,
|
|
surfaceComputedStyle.height
|
|
);
|
|
if (!sceneWidth || !sceneHeight) {
|
|
return null;
|
|
}
|
|
return {
|
|
filePath,
|
|
surface,
|
|
sceneWidth,
|
|
sceneHeight,
|
|
renderer: surface.dataset.modelWeaveRenderer,
|
|
filenameRenderer: renderer ?? surface.dataset.modelWeaveRenderer
|
|
};
|
|
}
|
|
async function exportDiagramSnapshotAsPng(app, snapshot) {
|
|
const arrayBuffer = await renderSnapshotToPng(snapshot);
|
|
try {
|
|
await ensureFolder(app, EXPORT_FOLDER);
|
|
const exportPath = `${EXPORT_FOLDER}/${toExportFileName(
|
|
snapshot.filePath,
|
|
snapshot.filenameRenderer ?? snapshot.renderer
|
|
)}.png`;
|
|
const existing = app.vault.getAbstractFileByPath(exportPath);
|
|
if (existing instanceof import_obsidian4.TFile) {
|
|
await app.vault.modifyBinary(existing, arrayBuffer);
|
|
} else {
|
|
await app.vault.createBinary(exportPath, arrayBuffer);
|
|
}
|
|
return exportPath;
|
|
} catch {
|
|
throw new DiagramExportError("Failed to save PNG export.", "save-failed");
|
|
}
|
|
}
|
|
async function renderSnapshotToPng(snapshot) {
|
|
if (snapshot.renderer === "mermaid") {
|
|
return renderMermaidSnapshotToPng(snapshot);
|
|
}
|
|
const exportWidth = snapshot.sceneWidth + EXPORT_PADDING * 2;
|
|
const exportHeight = snapshot.sceneHeight + EXPORT_PADDING * 2;
|
|
if (!Number.isFinite(exportWidth) || !Number.isFinite(exportHeight) || exportWidth <= 0 || exportHeight <= 0) {
|
|
throw new DiagramExportError(
|
|
"The current diagram has no measurable export bounds.",
|
|
"bounds-invalid"
|
|
);
|
|
}
|
|
const wrapper = createDiv();
|
|
wrapper.addClass("model-weave-export-wrapper");
|
|
wrapper.setCssProps({
|
|
"--mw-export-width": `${exportWidth}px`,
|
|
"--mw-export-height": `${exportHeight}px`
|
|
});
|
|
const clone = snapshot.surface.cloneNode(true);
|
|
if (!clone.instanceOf(HTMLElement)) {
|
|
throw new DiagramExportError("Failed to clone the diagram surface.", "render-failed");
|
|
}
|
|
prepareSurfaceClone(clone, snapshot, exportWidth, exportHeight);
|
|
wrapper.appendChild(clone);
|
|
const svg = buildExportSvg(wrapper, exportWidth, exportHeight);
|
|
const serialized = new XMLSerializer().serializeToString(svg);
|
|
const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(serialized)}`;
|
|
try {
|
|
const image = await loadImage(svgUrl);
|
|
const canvas = createEl("canvas");
|
|
canvas.width = Math.ceil(exportWidth * EXPORT_SCALE);
|
|
canvas.height = Math.ceil(exportHeight * EXPORT_SCALE);
|
|
const context = canvas.getContext("2d");
|
|
if (!context) {
|
|
throw new DiagramExportError(
|
|
modelWeaveText(
|
|
"Canvas rendering context is not available.",
|
|
"Canvas rendering context \u304C\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002"
|
|
),
|
|
"render-failed"
|
|
);
|
|
}
|
|
context.fillStyle = "#ffffff";
|
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
context.setTransform(EXPORT_SCALE, 0, 0, EXPORT_SCALE, 0, 0);
|
|
context.drawImage(image, 0, 0, exportWidth, exportHeight);
|
|
const pngBlob = await canvasToBlob(canvas);
|
|
const arrayBuffer = await pngBlob.arrayBuffer();
|
|
if (arrayBuffer.byteLength <= 0) {
|
|
throw new DiagramExportError("Failed to encode PNG image.", "encode-failed");
|
|
}
|
|
return arrayBuffer;
|
|
} catch (error) {
|
|
if (error instanceof DiagramExportError) {
|
|
throw error;
|
|
}
|
|
throw new DiagramExportError(
|
|
modelWeaveText("Failed to render diagram PNG.", "diagram PNG \u306E\u63CF\u753B\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002"),
|
|
"render-failed"
|
|
);
|
|
}
|
|
}
|
|
async function renderMermaidSnapshotToPng(snapshot) {
|
|
const svg = snapshot.surface.querySelector("svg");
|
|
if (!svg) {
|
|
throw new DiagramExportError(
|
|
modelWeaveText(
|
|
"Mermaid SVG export source was not found.",
|
|
"Mermaid SVG export source \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002"
|
|
),
|
|
"render-failed"
|
|
);
|
|
}
|
|
const contentBounds = measureMermaidContentBounds(svg);
|
|
if (!contentBounds) {
|
|
return renderSnapshotToPng({
|
|
...snapshot,
|
|
renderer: "custom"
|
|
});
|
|
}
|
|
const exportWidth = contentBounds.width + EXPORT_PADDING * 2;
|
|
const exportHeight = contentBounds.height + EXPORT_PADDING * 2;
|
|
const viewBoxX = contentBounds.x - EXPORT_PADDING;
|
|
const viewBoxY = contentBounds.y - EXPORT_PADDING;
|
|
const palette = getModelWeaveMermaidPalette();
|
|
const clone = svg.cloneNode(true);
|
|
if (!clone.instanceOf(SVGSVGElement)) {
|
|
throw new DiagramExportError("Failed to clone the Mermaid SVG.", "render-failed");
|
|
}
|
|
inlineSvgStyles(svg, clone);
|
|
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
clone.setAttribute("width", `${exportWidth}`);
|
|
clone.setAttribute("height", `${exportHeight}`);
|
|
clone.setAttribute(
|
|
"viewBox",
|
|
`${viewBoxX} ${viewBoxY} ${exportWidth} ${exportHeight}`
|
|
);
|
|
clone.addClass("model-weave-export-mermaid-clone");
|
|
clone.setCssProps({
|
|
"--mw-export-width": `${exportWidth}px`,
|
|
"--mw-export-height": `${exportHeight}px`
|
|
});
|
|
const background = createSvg("rect");
|
|
background.setAttribute("x", String(viewBoxX));
|
|
background.setAttribute("y", String(viewBoxY));
|
|
background.setAttribute("width", String(exportWidth));
|
|
background.setAttribute("height", String(exportHeight));
|
|
background.setAttribute("fill", palette.background);
|
|
clone.insertBefore(background, clone.firstChild);
|
|
const serialized = new XMLSerializer().serializeToString(clone);
|
|
const svgUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(serialized)}`;
|
|
try {
|
|
const image = await loadImage(svgUrl);
|
|
const canvas = createEl("canvas");
|
|
canvas.width = Math.ceil(exportWidth * EXPORT_SCALE);
|
|
canvas.height = Math.ceil(exportHeight * EXPORT_SCALE);
|
|
const context = canvas.getContext("2d");
|
|
if (!context) {
|
|
throw new DiagramExportError(
|
|
modelWeaveText(
|
|
"Canvas rendering context is not available.",
|
|
"Canvas rendering context \u304C\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002"
|
|
),
|
|
"render-failed"
|
|
);
|
|
}
|
|
context.fillStyle = palette.background;
|
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
context.setTransform(EXPORT_SCALE, 0, 0, EXPORT_SCALE, 0, 0);
|
|
context.drawImage(image, 0, 0, exportWidth, exportHeight);
|
|
const pngBlob = await canvasToBlob(canvas);
|
|
const arrayBuffer = await pngBlob.arrayBuffer();
|
|
if (arrayBuffer.byteLength <= 0) {
|
|
throw new DiagramExportError("Failed to encode PNG image.", "encode-failed");
|
|
}
|
|
return arrayBuffer;
|
|
} catch (error) {
|
|
if (error instanceof DiagramExportError) {
|
|
throw error;
|
|
}
|
|
throw new DiagramExportError(
|
|
modelWeaveText(
|
|
"Failed to render Mermaid diagram PNG.",
|
|
"Mermaid diagram PNG \u306E\u63CF\u753B\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002"
|
|
),
|
|
"render-failed"
|
|
);
|
|
}
|
|
}
|
|
function mountOffscreenExportRoot(root) {
|
|
const exportDocument = root.ownerDocument ?? activeDocument;
|
|
const host = exportDocument.createElement("div");
|
|
host.id = OFFSCREEN_ROOT_ID;
|
|
host.addClass("model-weave-export-offscreen-root");
|
|
host.appendChild(root);
|
|
exportDocument.body.appendChild(host);
|
|
return {
|
|
mount: host,
|
|
dispose: () => host.remove()
|
|
};
|
|
}
|
|
function prepareSurfaceClone(clone, snapshot, exportWidth, exportHeight) {
|
|
inlineComputedStyles(snapshot.surface, clone);
|
|
clone.addClass("model-weave-export-surface-clone");
|
|
clone.setCssProps({
|
|
"--mw-export-left": `${EXPORT_PADDING}px`,
|
|
"--mw-export-top": `${EXPORT_PADDING}px`,
|
|
"--mw-export-width": `${snapshot.sceneWidth}px`,
|
|
"--mw-export-height": `${snapshot.sceneHeight}px`,
|
|
"--mw-export-min-width": `${snapshot.sceneWidth}px`,
|
|
"--mw-export-min-height": `${snapshot.sceneHeight}px`,
|
|
"--mw-export-transform": "none"
|
|
});
|
|
for (const toolbar of Array.from(clone.querySelectorAll(".mdspec-zoom-toolbar"))) {
|
|
toolbar.remove();
|
|
}
|
|
for (const details of Array.from(
|
|
clone.querySelectorAll(
|
|
".mdspec-related-list, .mdspec-connections, .mdspec-relations-table"
|
|
)
|
|
)) {
|
|
details.remove();
|
|
}
|
|
const root = clone.closest("section");
|
|
if (root?.instanceOf(HTMLElement)) {
|
|
root.addClass("model-weave-export-root-background");
|
|
}
|
|
const svgs = Array.from(clone.querySelectorAll("svg"));
|
|
for (const svg of svgs) {
|
|
svg.setAttribute("width", `${exportWidth}`);
|
|
svg.setAttribute("height", `${exportHeight}`);
|
|
}
|
|
}
|
|
function buildExportSvg(wrapper, exportWidth, exportHeight) {
|
|
const svg = createSvg("svg");
|
|
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
svg.setAttribute("width", `${exportWidth}`);
|
|
svg.setAttribute("height", `${exportHeight}`);
|
|
svg.setAttribute("viewBox", `0 0 ${exportWidth} ${exportHeight}`);
|
|
const foreignObject = createSvg("foreignObject");
|
|
foreignObject.setAttribute("x", "0");
|
|
foreignObject.setAttribute("y", "0");
|
|
foreignObject.setAttribute("width", `${exportWidth}`);
|
|
foreignObject.setAttribute("height", `${exportHeight}`);
|
|
foreignObject.appendChild(wrapper);
|
|
svg.appendChild(foreignObject);
|
|
return svg;
|
|
}
|
|
function readSceneSize(datasetValue, styleValue) {
|
|
const preferred = Number.parseFloat(datasetValue ?? "");
|
|
if (Number.isFinite(preferred) && preferred > 0) {
|
|
return preferred;
|
|
}
|
|
const fallback = Number.parseFloat(styleValue);
|
|
return Number.isFinite(fallback) && fallback > 0 ? fallback : null;
|
|
}
|
|
function measureMermaidContentBounds(svg) {
|
|
const candidates = [
|
|
svg.querySelector("g.output"),
|
|
svg.querySelector("g.root"),
|
|
svg.querySelector("g.flowchart"),
|
|
svg.querySelector("svg > g"),
|
|
svg.querySelector("g")
|
|
].filter((value) => Boolean(value));
|
|
for (const candidate of candidates) {
|
|
const bbox = safeGetBBox(candidate);
|
|
if (bbox) {
|
|
return bbox;
|
|
}
|
|
}
|
|
const svgBox = safeGetBBox(svg);
|
|
if (svgBox) {
|
|
return svgBox;
|
|
}
|
|
const rect = svg.getBoundingClientRect();
|
|
if (Number.isFinite(rect.width) && Number.isFinite(rect.height) && rect.width > 0 && rect.height > 0) {
|
|
return {
|
|
x: 0,
|
|
y: 0,
|
|
width: rect.width,
|
|
height: rect.height
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
function safeGetBBox(element) {
|
|
try {
|
|
const bbox = element.getBBox();
|
|
if (Number.isFinite(bbox.width) && Number.isFinite(bbox.height) && bbox.width > 0 && bbox.height > 0) {
|
|
return {
|
|
x: bbox.x,
|
|
y: bbox.y,
|
|
width: bbox.width,
|
|
height: bbox.height
|
|
};
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
async function ensureFolder(app, folderPath) {
|
|
const existing = app.vault.getAbstractFileByPath(folderPath);
|
|
if (existing) {
|
|
return;
|
|
}
|
|
await app.vault.createFolder(folderPath);
|
|
}
|
|
function toExportFileName(filePath, renderer) {
|
|
const normalized = filePath.replace(/\\/g, "/");
|
|
const basename = normalized.split("/").pop() ?? normalized;
|
|
const modelName = sanitizeExportFileNamePart(
|
|
basename.replace(/\.md$/i, "") || "diagram"
|
|
);
|
|
const rendererName = sanitizeExportFileNamePart(renderer || "default");
|
|
return `${modelName}__${rendererName}`;
|
|
}
|
|
function sanitizeExportFileNamePart(value) {
|
|
const sanitized = value.replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, " ").trim();
|
|
return sanitized || "default";
|
|
}
|
|
function loadImage(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const image = new Image();
|
|
image.onload = () => resolve(image);
|
|
image.onerror = () => reject(new DiagramExportError(
|
|
modelWeaveText(
|
|
"Failed to render diagram image.",
|
|
"diagram image \u306E\u63CF\u753B\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002"
|
|
),
|
|
"render-failed"
|
|
));
|
|
image.src = url;
|
|
});
|
|
}
|
|
function canvasToBlob(canvas) {
|
|
return new Promise((resolve, reject) => {
|
|
canvas.toBlob((blob) => {
|
|
if (blob) {
|
|
resolve(blob);
|
|
return;
|
|
}
|
|
reject(new DiagramExportError("Failed to encode PNG image.", "encode-failed"));
|
|
}, "image/png");
|
|
});
|
|
}
|
|
function inlineComputedStyles(source, target) {
|
|
const computed = window.getComputedStyle(source);
|
|
applyComputedStyle(target, computed);
|
|
const sourceChildren = Array.from(source.children);
|
|
const targetChildren = Array.from(target.children);
|
|
for (let index = 0; index < sourceChildren.length; index += 1) {
|
|
const sourceChild = sourceChildren[index];
|
|
const targetChild = targetChildren[index];
|
|
if (sourceChild.instanceOf(HTMLElement) && targetChild.instanceOf(HTMLElement)) {
|
|
inlineComputedStyles(sourceChild, targetChild);
|
|
continue;
|
|
}
|
|
if (sourceChild.instanceOf(SVGElement) && targetChild.instanceOf(SVGElement)) {
|
|
inlineSvgStyles(sourceChild, targetChild);
|
|
}
|
|
}
|
|
}
|
|
function inlineSvgStyles(source, target) {
|
|
const computed = window.getComputedStyle(source);
|
|
applyComputedStyle(target, computed);
|
|
const sourceChildren = Array.from(source.children);
|
|
const targetChildren = Array.from(target.children);
|
|
for (let index = 0; index < sourceChildren.length; index += 1) {
|
|
const sourceChild = sourceChildren[index];
|
|
const targetChild = targetChildren[index];
|
|
if (sourceChild.instanceOf(SVGElement) && targetChild.instanceOf(SVGElement)) {
|
|
inlineSvgStyles(sourceChild, targetChild);
|
|
} else if (sourceChild.instanceOf(HTMLElement) && targetChild.instanceOf(HTMLElement)) {
|
|
inlineComputedStyles(sourceChild, targetChild);
|
|
}
|
|
}
|
|
}
|
|
function applyComputedStyle(target, computed) {
|
|
const style = Reflect.get(target, "style");
|
|
if (!(style instanceof CSSStyleDeclaration)) {
|
|
return;
|
|
}
|
|
for (let index = 0; index < computed.length; index += 1) {
|
|
const property = computed.item(index);
|
|
const value = computed.getPropertyValue(property);
|
|
const priority = computed.getPropertyPriority(property);
|
|
if (value) {
|
|
style.setProperty(property, value, priority);
|
|
}
|
|
}
|
|
}
|
|
function waitForAnimationFrame() {
|
|
return new Promise((resolve) => {
|
|
window.requestAnimationFrame(() => resolve());
|
|
});
|
|
}
|
|
|
|
// src/settings/model-weave-settings.ts
|
|
var DEFAULT_MODEL_WEAVE_SETTINGS = {
|
|
defaultClassRenderMode: "custom",
|
|
defaultErRenderMode: "custom",
|
|
defaultDfdRenderMode: "mermaid",
|
|
defaultProcessRenderMode: "custom",
|
|
defaultScreenRenderMode: "custom",
|
|
defaultZoom: "fit",
|
|
fontSize: "normal",
|
|
nodeDensity: "normal",
|
|
localSourceRoot: "",
|
|
enableRelationshipView: true,
|
|
showMermaidRenderDebug: false,
|
|
uiLanguage: "auto"
|
|
};
|
|
var VALID_DEFAULT_ZOOMS = /* @__PURE__ */ new Set(["fit", "100"]);
|
|
var VALID_FONT_SIZES = /* @__PURE__ */ new Set([
|
|
"small",
|
|
"normal",
|
|
"large"
|
|
]);
|
|
var VALID_NODE_DENSITIES = /* @__PURE__ */ new Set([
|
|
"compact",
|
|
"normal",
|
|
"relaxed"
|
|
]);
|
|
var VALID_RENDER_MODES2 = /* @__PURE__ */ new Set([
|
|
"custom",
|
|
"mermaid",
|
|
"mermaid-detail"
|
|
]);
|
|
var CLASS_RENDER_MODES = /* @__PURE__ */ new Set([
|
|
"custom",
|
|
"mermaid",
|
|
"mermaid-detail"
|
|
]);
|
|
var ER_RENDER_MODES = /* @__PURE__ */ new Set([
|
|
"custom",
|
|
"mermaid",
|
|
"mermaid-detail"
|
|
]);
|
|
var DFD_RENDER_MODES = /* @__PURE__ */ new Set(["mermaid"]);
|
|
var PROCESS_RENDER_MODES = /* @__PURE__ */ new Set(["custom"]);
|
|
var SCREEN_RENDER_MODES = /* @__PURE__ */ new Set(["custom"]);
|
|
var VALID_UI_LANGUAGES = /* @__PURE__ */ new Set(["auto", "en", "ja"]);
|
|
function normalizeModelWeaveSettings(value) {
|
|
const raw = isRecord(value) ? value : {};
|
|
const legacyDefaultRenderMode = normalizeEnumValue(
|
|
raw.defaultRenderMode,
|
|
VALID_RENDER_MODES2,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.defaultClassRenderMode
|
|
);
|
|
return {
|
|
defaultClassRenderMode: normalizeEnumValue(
|
|
raw.defaultClassRenderMode ?? legacyDefaultRenderMode,
|
|
CLASS_RENDER_MODES,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.defaultClassRenderMode
|
|
),
|
|
defaultErRenderMode: normalizeEnumValue(
|
|
raw.defaultErRenderMode ?? legacyDefaultRenderMode,
|
|
ER_RENDER_MODES,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.defaultErRenderMode
|
|
),
|
|
defaultDfdRenderMode: normalizeEnumValue(
|
|
raw.defaultDfdRenderMode,
|
|
DFD_RENDER_MODES,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.defaultDfdRenderMode
|
|
),
|
|
defaultProcessRenderMode: normalizeEnumValue(
|
|
raw.defaultProcessRenderMode,
|
|
PROCESS_RENDER_MODES,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.defaultProcessRenderMode
|
|
),
|
|
defaultScreenRenderMode: normalizeEnumValue(
|
|
raw.defaultScreenRenderMode,
|
|
SCREEN_RENDER_MODES,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.defaultScreenRenderMode
|
|
),
|
|
defaultZoom: normalizeEnumValue(
|
|
raw.defaultZoom,
|
|
VALID_DEFAULT_ZOOMS,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.defaultZoom
|
|
),
|
|
fontSize: normalizeEnumValue(
|
|
raw.fontSize,
|
|
VALID_FONT_SIZES,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.fontSize
|
|
),
|
|
nodeDensity: normalizeEnumValue(
|
|
raw.nodeDensity,
|
|
VALID_NODE_DENSITIES,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.nodeDensity
|
|
),
|
|
localSourceRoot: normalizeStringValue(raw.localSourceRoot ?? raw.sourceRoot),
|
|
enableRelationshipView: normalizeBooleanValue(
|
|
raw.enableRelationshipView,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.enableRelationshipView
|
|
),
|
|
showMermaidRenderDebug: normalizeBooleanValue(
|
|
raw.showMermaidRenderDebug,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.showMermaidRenderDebug
|
|
),
|
|
uiLanguage: normalizeEnumValue(
|
|
raw.uiLanguage,
|
|
VALID_UI_LANGUAGES,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.uiLanguage
|
|
)
|
|
};
|
|
}
|
|
function normalizeStringValue(value) {
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|
|
function normalizeBooleanValue(value, fallback) {
|
|
return typeof value === "boolean" ? value : fallback;
|
|
}
|
|
function normalizeEnumValue(value, allowed, fallback) {
|
|
if (typeof value !== "string") {
|
|
return fallback;
|
|
}
|
|
return allowed.has(value) ? value : fallback;
|
|
}
|
|
function isRecord(value) {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
// src/templates/model-weave-templates.ts
|
|
var MODEL_WEAVE_TEMPLATES = {
|
|
class: `---
|
|
type: class
|
|
id: CLS-
|
|
name:
|
|
kind: class
|
|
package:
|
|
stereotype:
|
|
tags:
|
|
- Class
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
|
|
|
|
## Attributes
|
|
|
|
| name | type | visibility | static | notes |
|
|
|---|---|---|---|---|
|
|
|
|
## Methods
|
|
|
|
| name | parameters | returns | visibility | static | notes |
|
|
|---|---|---|---|---|---|
|
|
|
|
## Relations
|
|
|
|
| id | to | kind | label | from_multiplicity | to_multiplicity | notes |
|
|
|---|---|---|---|---|---|---|
|
|
|
|
## Notes
|
|
|
|
- `,
|
|
classDiagram: `---
|
|
type: class_diagram
|
|
id: CLASSD-
|
|
name:
|
|
tags:
|
|
- Class
|
|
- Diagram
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
|
|
|
|
## Objects
|
|
|
|
| ref | notes |
|
|
|---|---|
|
|
|
|
## Relations
|
|
|
|
| id | from | to | kind | label | from_multiplicity | to_multiplicity | notes |
|
|
|---|---|---|---|---|---|---|---|
|
|
|
|
## Notes
|
|
|
|
- `,
|
|
erEntity: `---
|
|
type: er_entity
|
|
id: ENT-
|
|
logical_name:
|
|
physical_name:
|
|
schema_name:
|
|
dbms:
|
|
tags:
|
|
- ER
|
|
- Entity
|
|
---
|
|
|
|
# /
|
|
|
|
## Overview
|
|
|
|
- purpose:
|
|
- notes:
|
|
|
|
## Columns
|
|
|
|
| logical_name | physical_name | data_type | length | scale | not_null | pk | encrypted | default_value | notes |
|
|
|---|---|---|---:|---:|---|---|---|---|---|
|
|
|
|
## Indexes
|
|
|
|
| index_name | index_type | unique | columns | notes |
|
|
|---|---|---|---|---|
|
|
|
|
## Relations
|
|
|
|
### REL-
|
|
- target_table: [[]]
|
|
- kind: fk
|
|
- cardinality:
|
|
- notes:
|
|
|
|
| local_column | target_column | notes |
|
|
|---|---|---|
|
|
|
|
## Notes
|
|
|
|
- `,
|
|
erDiagram: `---
|
|
type: er_diagram
|
|
id: ERD-
|
|
name:
|
|
tags:
|
|
- ER
|
|
- Diagram
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
|
|
|
|
## Objects
|
|
|
|
| ref | notes |
|
|
|---|---|
|
|
|
|
## Notes
|
|
|
|
- `,
|
|
dfdObject: `---
|
|
type: dfd_object
|
|
id: DFD-
|
|
name:
|
|
kind: process
|
|
tags:
|
|
- DFD
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
## Notes
|
|
`,
|
|
dfdDiagram: `---
|
|
type: dfd_diagram
|
|
id: DFD-
|
|
name:
|
|
level: 0
|
|
tags:
|
|
- DFD
|
|
- Diagram
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
## Objects
|
|
|
|
| ref | notes |
|
|
|---|---|
|
|
| | |
|
|
|
|
## Flows
|
|
|
|
| id | from | to | data | notes |
|
|
|---|---|---|---|---|
|
|
| | | | | |
|
|
|
|
## Notes
|
|
`,
|
|
dataObject: `---
|
|
type: data_object
|
|
id:
|
|
name:
|
|
kind:
|
|
data_format: object
|
|
tags:
|
|
- DataObject
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
## Fields
|
|
|
|
| name | label | type | length | required | path | ref | notes |
|
|
|---|---|---|---:|---|---|---|---|
|
|
| | | | | | | | |
|
|
|
|
## Notes
|
|
`,
|
|
dataObjectFileLayout: `---
|
|
type: data_object
|
|
id:
|
|
name:
|
|
kind: file
|
|
data_format:
|
|
encoding:
|
|
delimiter:
|
|
line_ending:
|
|
has_header:
|
|
record_length:
|
|
tags:
|
|
- DataObject
|
|
- File
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
## Format
|
|
|
|
| key | value | notes |
|
|
|---|---|---|
|
|
| | | |
|
|
|
|
## Records
|
|
|
|
| record_type | name | occurrence | notes |
|
|
|---|---|---|---|
|
|
| | | | |
|
|
|
|
## Fields
|
|
|
|
| record_type | no | name | label | type | length | required | position | field_format | ref | notes |
|
|
|---|---:|---|---|---|---:|---|---|---|---|---|
|
|
| | | | | | | | | | | |
|
|
|
|
## Notes
|
|
`,
|
|
appProcess: `---
|
|
type: app_process
|
|
id: PROC-
|
|
name:
|
|
kind:
|
|
tags:
|
|
- AppProcess
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
## Source Links
|
|
|
|
| path | notes |
|
|
|---|---|
|
|
| src/app/processes/ExampleProcess.ts | Example implementation |
|
|
|
|
## Triggers
|
|
|
|
| id | kind | source | event | notes |
|
|
|---|---|---|---|---|
|
|
| | | | | |
|
|
|
|
## Inputs
|
|
|
|
| id | data | source | required | notes |
|
|
|---|---|---|---|---|
|
|
| | | | | |
|
|
|
|
## Outputs
|
|
|
|
| id | data | target | notes |
|
|
|---|---|---|---|
|
|
| | | | |
|
|
|
|
## Steps
|
|
|
|
| id | lane | label | kind | input | output | rule | invoke | screen | notes |
|
|
|---|---|---|---|---|---|---|---|---|---|
|
|
| step1 | User | Submit request | start | IN-REQUEST | | | | SCR-REQUEST | User starts the process |
|
|
| step2 | System | Validate request | process | IN-REQUEST | VALIDATED-REQUEST | RULE-VALIDATE | | | Check required values |
|
|
| step3 | Screen | Show result | end | VALIDATED-REQUEST | OUT-RESULT | | | SCR-RESULT | Present the result |
|
|
|
|
## Flows
|
|
|
|
| from | to | condition | label | notes |
|
|
|---|---|---|---|---|
|
|
| step1 | step2 | | submit | |
|
|
| step2 | step3 | [[CODE-INVENTORY-STATUS]].available | show result | Flows.from/to are internal step ids; Flows.condition may contain structured references |
|
|
|
|
## Transitions
|
|
|
|
| id | event | to | condition | notes |
|
|
|---|---|---|---|---|
|
|
| | | | | |
|
|
|
|
## Errors
|
|
|
|
## Notes
|
|
`,
|
|
screen: `---
|
|
type: screen
|
|
id: SCR-
|
|
name:
|
|
screen_type:
|
|
tags:
|
|
- Screen
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
## Layout
|
|
|
|
| id | label | kind | purpose | notes |
|
|
|---|---|---|---|---|
|
|
| | | | | |
|
|
|
|
## Fields
|
|
|
|
| id | label | kind | layout | data_type | required | ref | condition | rule | notes |
|
|
|---|---|---|---|---|---|---|---|---|---|
|
|
| | | | | | | | | | |
|
|
|
|
## Actions
|
|
|
|
| id | label | kind | target | event | condition | invoke | transition | rule | notes |
|
|
|---|---|---|---|---|---|---|---|---|---|
|
|
| | | | | | | | | | |
|
|
|
|
## Messages
|
|
|
|
| id | text | severity | timing | condition | notes |
|
|
|---|---|---|---|---|---|
|
|
| | | | | | |
|
|
|
|
## Transitions
|
|
|
|
| id | event | to | condition | notes |
|
|
|---|---|---|---|---|
|
|
| | | | | |
|
|
|
|
## Notes
|
|
|
|
## Local Processes
|
|
|
|
### PROC-CLEAR
|
|
|
|
#### Summary
|
|
|
|
#### Inputs
|
|
|
|
| id | data | source | required | notes |
|
|
|---|---|---|---|---|
|
|
| | | | | |
|
|
|
|
#### Steps
|
|
|
|
| id | label | kind | condition | input | output | rule | invoke | screen | notes |
|
|
|---|---|---|---|---|---|---|---|---|---|
|
|
| | | | | | | | | | |
|
|
|
|
#### Outputs
|
|
|
|
| id | data | target | notes |
|
|
|---|---|---|---|
|
|
| | | | |
|
|
|
|
#### Errors
|
|
|
|
| id | condition | message | notes |
|
|
|---|---|---|---|
|
|
| | | | |
|
|
`,
|
|
codeSet: `---
|
|
type: codeset
|
|
id:
|
|
name:
|
|
kind:
|
|
tags:
|
|
- CodeSet
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
## Values
|
|
|
|
| code | label | sort_order | active | notes |
|
|
|---|---|---:|---|---|
|
|
|
|
## Notes
|
|
`,
|
|
message: `---
|
|
type: message
|
|
id:
|
|
name:
|
|
kind:
|
|
tags:
|
|
- Message
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
## Messages
|
|
|
|
| message_id | text | severity | timing | audience | active | notes |
|
|
|---|---|---|---|---|---|---|
|
|
|
|
## Notes
|
|
`,
|
|
rule: `---
|
|
type: rule
|
|
id:
|
|
name:
|
|
kind:
|
|
tags:
|
|
- Rule
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
## Inputs
|
|
|
|
| id | data | source | required | notes |
|
|
|---|---|---|---|---|
|
|
|
|
## References
|
|
|
|
| ref | usage | notes |
|
|
|---|---|---|
|
|
|
|
## Conditions
|
|
|
|
| id | condition | ref | value | notes |
|
|
|---|---|---|---|---|
|
|
| CND-001 | [[CODE-INVENTORY-STATUS]].available | [[CODE-INVENTORY-STATUS]] | available | \u826F\u54C1\u5229\u7528\u53EF\u306E\u5728\u5EAB\u306E\u307F\u5BFE\u8C61 |
|
|
|
|
Prose Conditions are human-readable. Table Conditions are analyzer-readable. \`condition\` may contain \`[[CODE-ID]].value\`; \`ref + value\` may also express a codeset value reference.
|
|
|
|
## Messages
|
|
|
|
| condition | message | severity | notes |
|
|
|---|---|---|---|
|
|
|
|
## Notes
|
|
`,
|
|
mapping: `---
|
|
type: mapping
|
|
id:
|
|
name:
|
|
kind:
|
|
source:
|
|
target:
|
|
tags:
|
|
- Mapping
|
|
---
|
|
|
|
#
|
|
|
|
## Summary
|
|
|
|
## Scope
|
|
|
|
| role | ref | notes |
|
|
|---|---|---|
|
|
|
|
## Mappings
|
|
|
|
| source_ref | target_ref | transform | rule | required | notes |
|
|
|---|---|---|---|---|---|
|
|
|
|
## Rules
|
|
|
|
## Notes
|
|
`
|
|
};
|
|
var MODEL_WEAVE_RELATION_TEMPLATES = {
|
|
erRelationBlock: [
|
|
"### REL-",
|
|
"- target_table: [[]]",
|
|
"- kind: fk",
|
|
"- cardinality:",
|
|
"- notes:",
|
|
"",
|
|
"| local_column | target_column | notes |",
|
|
"|---|---|---|"
|
|
]
|
|
};
|
|
|
|
// 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"
|
|
];
|
|
|
|
// src/parsers/object-parser.ts
|
|
var ATTRIBUTE_TABLE_HEADERS = [
|
|
"name",
|
|
"type",
|
|
"visibility",
|
|
"static",
|
|
"notes"
|
|
];
|
|
var METHOD_TABLE_HEADERS = [
|
|
"name",
|
|
"parameters",
|
|
"returns",
|
|
"visibility",
|
|
"static",
|
|
"notes"
|
|
];
|
|
var LEGACY_RELATION_TABLE_HEADERS = [
|
|
"id",
|
|
"from",
|
|
"to",
|
|
"kind",
|
|
"label",
|
|
"from_multiplicity",
|
|
"to_multiplicity",
|
|
"notes"
|
|
];
|
|
var SPEC04_RELATION_TABLE_HEADERS = [
|
|
"id",
|
|
"to",
|
|
"kind",
|
|
"label",
|
|
"from_multiplicity",
|
|
"to_multiplicity",
|
|
"notes"
|
|
];
|
|
function parseObjectFile(markdown, path2) {
|
|
const frontmatterResult = parseFrontmatter(markdown);
|
|
const warnings = [...frontmatterResult.warnings];
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
const schema = getString(frontmatter, "schema");
|
|
const type = getString(frontmatter, "type");
|
|
const acceptsClassType = type === "class";
|
|
if (detectFileType(frontmatter) !== "object" || !acceptsClassType && schema !== "model_object_v1") {
|
|
warnings.push(
|
|
createWarning4(
|
|
"unknown-schema",
|
|
`object parser expected schema "model_object_v1" or type "class" but received schema "${schema ?? "none"}" / type "${type ?? "none"}"`,
|
|
path2,
|
|
acceptsClassType ? "type" : "schema"
|
|
)
|
|
);
|
|
return {
|
|
file: null,
|
|
warnings
|
|
};
|
|
}
|
|
const sections = extractMarkdownSections(frontmatterResult.file.body);
|
|
const name = getString(frontmatter, "name");
|
|
const rawKind = getString(frontmatter, "kind") ?? (acceptsClassType ? "class" : void 0);
|
|
const summary = joinSectionLines(sections.Summary);
|
|
const attributes = acceptsClassType ? parseAttributeTable(sections.Attributes, warnings, path2) : parseAttributes(sections.Attributes, warnings, path2);
|
|
const methods = acceptsClassType ? parseMethodTable(sections.Methods, warnings, path2) : parseMethods(sections.Methods, warnings, path2);
|
|
const relations = parseRelationsTable(
|
|
sections.Relations,
|
|
warnings,
|
|
path2,
|
|
getClassObjectId(frontmatter, name)
|
|
);
|
|
if (!name) {
|
|
warnings.push(
|
|
createWarning4("missing-name", 'missing required field "name"', path2, "name")
|
|
);
|
|
}
|
|
if (!rawKind) {
|
|
warnings.push(
|
|
createWarning4("missing-kind", 'missing required field "kind"', path2, "kind")
|
|
);
|
|
} else if (isReservedObjectKind(rawKind)) {
|
|
warnings.push(
|
|
createInfoWarning2(
|
|
"reserved-kind-used",
|
|
`reserved kind used: "${rawKind}"`,
|
|
path2,
|
|
"kind"
|
|
)
|
|
);
|
|
} else if (!isCoreObjectKind(rawKind)) {
|
|
warnings.push(
|
|
createWarning4("invalid-kind", `invalid kind "${rawKind}"`, path2, "kind")
|
|
);
|
|
}
|
|
const file = {
|
|
fileType: "object",
|
|
schema: "model_object_v1",
|
|
path: path2,
|
|
title: getString(frontmatter, "title"),
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
name: name ?? getString(frontmatter, "id") ?? "unknown",
|
|
kind: normalizeObjectKind(rawKind),
|
|
description: summary || void 0,
|
|
attributes,
|
|
methods,
|
|
relations
|
|
};
|
|
return {
|
|
file,
|
|
warnings
|
|
};
|
|
}
|
|
function parseAttributes(lines, warnings, path2) {
|
|
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(
|
|
createWarning4(
|
|
"invalid-attribute-line",
|
|
`malformed attribute line: "${trimmed}"`,
|
|
path2,
|
|
"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, path2) {
|
|
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(
|
|
createWarning4(
|
|
"invalid-method-line",
|
|
`malformed method line: "${trimmed}"`,
|
|
path2,
|
|
"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 parseAttributeTable(lines, warnings, path2) {
|
|
const table = parseMarkdownTable(
|
|
lines,
|
|
[...ATTRIBUTE_TABLE_HEADERS],
|
|
path2,
|
|
"Attributes"
|
|
);
|
|
warnings.push(...table.warnings);
|
|
return table.rows.map((row) => ({
|
|
name: getTableValue(row, "name"),
|
|
type: optionalTableValue(row, "type"),
|
|
visibility: normalizeVisibility(optionalTableValue(row, "visibility")),
|
|
description: optionalTableValue(row, "notes"),
|
|
raw: JSON.stringify(row)
|
|
}));
|
|
}
|
|
function parseMethodTable(lines, warnings, path2) {
|
|
const table = parseMarkdownTable(
|
|
lines,
|
|
[...METHOD_TABLE_HEADERS],
|
|
path2,
|
|
"Methods"
|
|
);
|
|
warnings.push(...table.warnings);
|
|
return table.rows.map((row) => ({
|
|
name: getTableValue(row, "name"),
|
|
parameters: parseMethodParameters(getTableValue(row, "parameters")),
|
|
returnType: optionalTableValue(row, "returns"),
|
|
visibility: normalizeVisibility(optionalTableValue(row, "visibility")),
|
|
isStatic: normalizeBoolean(optionalTableValue(row, "static")),
|
|
description: optionalTableValue(row, "notes"),
|
|
raw: JSON.stringify(row)
|
|
}));
|
|
}
|
|
function parseRelationsTable(lines, warnings, path2, currentClassId) {
|
|
const relations = [];
|
|
const table = parseClassRelationsTable(lines, path2);
|
|
warnings.push(...table.warnings);
|
|
for (const row of table.rows) {
|
|
const id = getTableValue(row, "id");
|
|
const to = normalizeReferenceTarget(getTableValue(row, "to"));
|
|
const kind = getTableValue(row, "kind");
|
|
const from = table.format === "legacy" ? normalizeReferenceTarget(getTableValue(row, "from")) : normalizeReferenceTarget(currentClassId);
|
|
if (!id || !from || !to || !kind) {
|
|
warnings.push(
|
|
createWarning4(
|
|
"invalid-table-row",
|
|
`Relations row is missing required values: ${JSON.stringify(row)}`,
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
continue;
|
|
}
|
|
if (table.format === "legacy") {
|
|
if (from === normalizeReferenceTarget(currentClassId)) {
|
|
warnings.push(
|
|
createInfoWarning2(
|
|
"legacy-class-relation-format",
|
|
`Legacy class relation format with explicit "from" was accepted for relation "${id}".`,
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
} else {
|
|
warnings.push(
|
|
createWarning4(
|
|
"legacy-class-relation-from-mismatch",
|
|
`Legacy class relation "from" does not match the current class id for relation "${id}".`,
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
}
|
|
}
|
|
relations.push({
|
|
domain: "class",
|
|
id,
|
|
source: from,
|
|
target: to,
|
|
sourceClass: from,
|
|
targetClass: to,
|
|
kind,
|
|
label: optionalTableValue(row, "label"),
|
|
fromMultiplicity: optionalTableValue(row, "from_multiplicity"),
|
|
toMultiplicity: optionalTableValue(row, "to_multiplicity"),
|
|
notes: optionalTableValue(row, "notes")
|
|
});
|
|
}
|
|
return relations;
|
|
}
|
|
function parseClassRelationsTable(lines, path2) {
|
|
if (!lines) {
|
|
return { rows: [], warnings: [], format: "spec04" };
|
|
}
|
|
const normalizedLines = lines.map((line) => line.trim()).filter((line) => line.startsWith("|"));
|
|
if (normalizedLines.length < 2) {
|
|
return {
|
|
rows: [],
|
|
warnings: normalizedLines.length === 0 ? [] : [
|
|
createWarning4(
|
|
"invalid-table-row",
|
|
'table in section "Relations" is incomplete',
|
|
path2,
|
|
"Relations"
|
|
)
|
|
],
|
|
format: "spec04"
|
|
};
|
|
}
|
|
const headers = splitMarkdownTableRow(normalizedLines[0]) ?? [];
|
|
const format = sameHeaders2(headers, [...SPEC04_RELATION_TABLE_HEADERS]) ? "spec04" : sameHeaders2(headers, [...LEGACY_RELATION_TABLE_HEADERS]) ? "legacy" : "spec04";
|
|
const warnings = [];
|
|
if (!sameHeaders2(headers, [...SPEC04_RELATION_TABLE_HEADERS]) && !sameHeaders2(headers, [...LEGACY_RELATION_TABLE_HEADERS])) {
|
|
warnings.push(
|
|
createWarning4(
|
|
"invalid-table-column",
|
|
'table columns in section "Relations" do not match supported class relation headers',
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
}
|
|
const rows = [];
|
|
for (const rowLine of normalizedLines.slice(2)) {
|
|
const values = splitMarkdownTableRow(rowLine) ?? [];
|
|
if (values.every((value) => !value.trim())) {
|
|
continue;
|
|
}
|
|
if (values.length !== headers.length) {
|
|
warnings.push(
|
|
createWarning4(
|
|
"invalid-table-row",
|
|
`table row in section "Relations" has ${values.length} columns, expected ${headers.length}`,
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
continue;
|
|
}
|
|
const row = {};
|
|
for (const [index, header] of headers.entries()) {
|
|
row[header] = values[index] ?? "";
|
|
}
|
|
rows.push(row);
|
|
}
|
|
return { rows, warnings, format };
|
|
}
|
|
function sameHeaders2(actual, expected) {
|
|
if (actual.length !== expected.length) {
|
|
return false;
|
|
}
|
|
return actual.every((header, index) => header === expected[index]);
|
|
}
|
|
function getTableValue(row, key) {
|
|
return row[key]?.trim() ?? "";
|
|
}
|
|
function optionalTableValue(row, key) {
|
|
const value = getTableValue(row, key);
|
|
return value || void 0;
|
|
}
|
|
function normalizeVisibility(value) {
|
|
switch (value) {
|
|
case "public":
|
|
case "protected":
|
|
case "private":
|
|
case "package":
|
|
return value;
|
|
default:
|
|
return void 0;
|
|
}
|
|
}
|
|
function normalizeBoolean(value) {
|
|
if (!value) {
|
|
return void 0;
|
|
}
|
|
const normalized = value.trim().toLowerCase();
|
|
if (normalized === "true" || normalized === "y" || normalized === "yes") {
|
|
return true;
|
|
}
|
|
if (normalized === "false" || normalized === "n" || normalized === "no") {
|
|
return false;
|
|
}
|
|
return void 0;
|
|
}
|
|
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 getClassObjectId(frontmatter, name) {
|
|
return getString(frontmatter, "id") ?? name ?? "unknown";
|
|
}
|
|
function isCoreObjectKind(kind) {
|
|
return CORE_OBJECT_KINDS.some((candidate) => candidate === kind);
|
|
}
|
|
function isReservedObjectKind(kind) {
|
|
return RESERVED_OBJECT_KINDS.some((candidate) => candidate === kind);
|
|
}
|
|
function createWarning4(code, message, path2, field) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
function createInfoWarning2(code, message, path2, field) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "info",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/parsers/relations-parser.ts
|
|
function parseRelationsFile(markdown, path2) {
|
|
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(
|
|
createWarning5(
|
|
"unknown-schema",
|
|
`relations parser expected schema "model_relations_v1" but received "${schema ?? "none"}"`,
|
|
path2,
|
|
"schema"
|
|
)
|
|
);
|
|
return {
|
|
file: null,
|
|
warnings
|
|
};
|
|
}
|
|
const sections = extractMarkdownSections(frontmatterResult.file.body);
|
|
if (!sections.Relations) {
|
|
warnings.push(
|
|
createInfoWarning3(
|
|
"section-missing",
|
|
'section missing: "Relations"',
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
}
|
|
const relations = parseRelationsSection(sections.Relations, warnings, path2);
|
|
return {
|
|
file: {
|
|
fileType: "relations",
|
|
schema: "model_relations_v1",
|
|
path: path2,
|
|
title: getString2(frontmatter, "title"),
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
relations
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function parseRelationsSection(lines, warnings, path2) {
|
|
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(
|
|
createWarning5(
|
|
"invalid-relation-record",
|
|
`malformed relation record: "${trimmed}"`,
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
continue;
|
|
}
|
|
const missingFields = ["id", "from", "to", "kind"].filter(
|
|
(field) => !record[field]
|
|
);
|
|
if (missingFields.length > 0) {
|
|
warnings.push(
|
|
createWarning5(
|
|
"invalid-relation-record",
|
|
`malformed relation record: missing ${missingFields.join(", ")}`,
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
continue;
|
|
}
|
|
const rawKind = record.kind;
|
|
if (isReservedRelationKind(rawKind)) {
|
|
warnings.push(
|
|
createInfoWarning3(
|
|
"reserved-relation-kind-used",
|
|
`reserved kind used: "${rawKind}"`,
|
|
path2,
|
|
"kind"
|
|
)
|
|
);
|
|
} else if (!isCoreRelationKind(rawKind)) {
|
|
warnings.push(
|
|
createWarning5(
|
|
"invalid-relation-kind",
|
|
`invalid relation kind "${rawKind}"`,
|
|
path2,
|
|
"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.some((candidate) => candidate === kind);
|
|
}
|
|
function isReservedRelationKind(kind) {
|
|
return RESERVED_RELATION_KINDS.some((candidate) => candidate === kind);
|
|
}
|
|
function normalizeRelationKind(kind) {
|
|
if (isCoreRelationKind(kind) || isReservedRelationKind(kind)) {
|
|
return kind;
|
|
}
|
|
return "association";
|
|
}
|
|
function createWarning5(code, message, path2, field) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
function createInfoWarning3(code, message, path2, field) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "info",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/parsers/diagram-parser.ts
|
|
var ER_DIAGRAM_OBJECT_HEADERS = ["ref", "notes"];
|
|
var CLASS_DIAGRAM_OBJECT_HEADERS = ["ref", "notes"];
|
|
var CLASS_DIAGRAM_RELATION_HEADERS = [
|
|
"id",
|
|
"from",
|
|
"to",
|
|
"kind",
|
|
"label",
|
|
"from_multiplicity",
|
|
"to_multiplicity",
|
|
"notes"
|
|
];
|
|
function parseDiagramFile(markdown, path2) {
|
|
const frontmatterResult = parseFrontmatter(markdown);
|
|
const warnings = [...frontmatterResult.warnings];
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
const type = getString3(frontmatter, "type");
|
|
const acceptsErDiagramType = type === "er_diagram";
|
|
const acceptsClassDiagramType = type === "class_diagram";
|
|
if (detectFileType(frontmatter) !== "diagram" || !acceptsErDiagramType && !acceptsClassDiagramType) {
|
|
warnings.push(
|
|
createWarning6(
|
|
"unknown-schema",
|
|
`diagram parser expected type "er_diagram" or "class_diagram" but received type "${type ?? "none"}"`,
|
|
path2,
|
|
"type"
|
|
)
|
|
);
|
|
return {
|
|
file: null,
|
|
warnings
|
|
};
|
|
}
|
|
const sections = extractMarkdownSections(frontmatterResult.file.body);
|
|
const name = getString3(frontmatter, "name");
|
|
const objectRows = acceptsErDiagramType ? parseErDiagramObjects(sections.Objects, warnings, path2) : acceptsClassDiagramType ? parseClassDiagramObjects(sections.Objects, warnings, path2) : null;
|
|
const objectRefs = objectRows ? objectRows.map((row) => row.ref) : [];
|
|
const classDiagramRelations = acceptsClassDiagramType ? parseClassDiagramRelations(sections.Relations, warnings, path2) : [];
|
|
const nodes = objectRows ? objectRows.map(
|
|
(row) => ({
|
|
id: row.ref,
|
|
ref: row.ref,
|
|
metadata: row.notes ? { notes: row.notes } : void 0
|
|
})
|
|
) : objectRefs.map((ref) => ({
|
|
id: normalizeReferenceTarget(ref),
|
|
ref
|
|
}));
|
|
if (!name) {
|
|
warnings.push(
|
|
createWarning6("missing-name", 'missing required field "name"', path2, "name")
|
|
);
|
|
}
|
|
if (!sections.Objects) {
|
|
warnings.push(
|
|
createInfoWarning4(
|
|
"section-missing",
|
|
'section missing: "Objects"',
|
|
path2,
|
|
"Objects"
|
|
)
|
|
);
|
|
}
|
|
return {
|
|
file: {
|
|
fileType: "diagram",
|
|
schema: acceptsErDiagramType ? "er_diagram" : "class_diagram",
|
|
path: path2,
|
|
title: getString3(frontmatter, "title"),
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
name: name ?? getString3(frontmatter, "id") ?? "unknown",
|
|
kind: acceptsErDiagramType ? "er" : "class",
|
|
objectRefs,
|
|
nodes,
|
|
edges: acceptsClassDiagramType ? classDiagramRelations.map(classRelationToDiagramEdge) : []
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function parseErDiagramObjects(lines, warnings, path2) {
|
|
const table = parseMarkdownTable(
|
|
lines,
|
|
[...ER_DIAGRAM_OBJECT_HEADERS],
|
|
path2,
|
|
"Objects"
|
|
);
|
|
warnings.push(...table.warnings);
|
|
const objects = [];
|
|
for (const row of table.rows) {
|
|
const ref = row.ref?.trim();
|
|
if (!ref) {
|
|
warnings.push(
|
|
createWarning6(
|
|
"invalid-object-ref",
|
|
'table row in section "Objects" is missing "ref"',
|
|
path2,
|
|
"Objects"
|
|
)
|
|
);
|
|
continue;
|
|
}
|
|
const notes = row.notes?.trim();
|
|
objects.push({
|
|
ref,
|
|
notes: notes ? notes : void 0
|
|
});
|
|
}
|
|
return objects;
|
|
}
|
|
function parseClassDiagramObjects(lines, warnings, path2) {
|
|
const table = parseMarkdownTable(
|
|
lines,
|
|
[...CLASS_DIAGRAM_OBJECT_HEADERS],
|
|
path2,
|
|
"Objects"
|
|
);
|
|
warnings.push(...table.warnings);
|
|
const objects = [];
|
|
for (const row of table.rows) {
|
|
const rawRef = row.ref?.trim();
|
|
if (!rawRef) {
|
|
warnings.push(
|
|
createWarning6(
|
|
"invalid-object-ref",
|
|
'table row in section "Objects" is missing "ref"',
|
|
path2,
|
|
"Objects"
|
|
)
|
|
);
|
|
continue;
|
|
}
|
|
objects.push({
|
|
ref: normalizeReferenceTarget(rawRef),
|
|
notes: row.notes?.trim() || void 0
|
|
});
|
|
}
|
|
return objects;
|
|
}
|
|
function parseClassDiagramRelations(lines, warnings, path2) {
|
|
if (!hasNonEmptyTableDataRows(lines)) {
|
|
return [];
|
|
}
|
|
const table = parseMarkdownTable(
|
|
lines,
|
|
[...CLASS_DIAGRAM_RELATION_HEADERS],
|
|
path2,
|
|
"Relations"
|
|
);
|
|
warnings.push(...table.warnings);
|
|
const relations = [];
|
|
for (const row of table.rows) {
|
|
const id = row.id?.trim();
|
|
const from = normalizeReferenceTarget(row.from?.trim() ?? "");
|
|
const to = normalizeReferenceTarget(row.to?.trim() ?? "");
|
|
const kind = row.kind?.trim();
|
|
if (!id || !from || !to || !kind) {
|
|
warnings.push(
|
|
createWarning6(
|
|
"invalid-table-row",
|
|
`table row in section "Relations" is missing required values`,
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
continue;
|
|
}
|
|
relations.push({
|
|
domain: "class",
|
|
id,
|
|
source: from,
|
|
target: to,
|
|
sourceClass: from,
|
|
targetClass: to,
|
|
kind,
|
|
label: row.label?.trim() || void 0,
|
|
fromMultiplicity: row.from_multiplicity?.trim() || void 0,
|
|
toMultiplicity: row.to_multiplicity?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
});
|
|
}
|
|
return relations;
|
|
}
|
|
function hasNonEmptyTableDataRows(lines) {
|
|
if (!lines) {
|
|
return false;
|
|
}
|
|
const tableLines = lines.map((line) => line.trim()).filter((line) => line.startsWith("|"));
|
|
if (tableLines.length <= 2) {
|
|
return false;
|
|
}
|
|
return tableLines.slice(2).some(
|
|
(line) => line.replace(/^\|/, "").replace(/\|$/, "").split("|").some((cell) => cell.trim().length > 0)
|
|
);
|
|
}
|
|
function classRelationToDiagramEdge(relation) {
|
|
return {
|
|
id: relation.id,
|
|
source: relation.sourceClass,
|
|
target: relation.targetClass,
|
|
kind: relation.kind,
|
|
label: relation.label,
|
|
metadata: {
|
|
notes: relation.notes,
|
|
sourceCardinality: relation.fromMultiplicity,
|
|
targetCardinality: relation.toMultiplicity
|
|
}
|
|
};
|
|
}
|
|
function getString3(frontmatter, key) {
|
|
const value = frontmatter[key];
|
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
}
|
|
function createWarning6(code, message, path2, field) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
function createInfoWarning4(code, message, path2, field) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "info",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/parsers/dfd-diagram-parser.ts
|
|
var FLOW_HEADERS = ["id", "from", "to", "data", "notes"];
|
|
var LEGACY_OBJECT_HEADERS = ["ref", "notes"];
|
|
function parseDfdDiagramFile(markdown, path2) {
|
|
const frontmatterResult = parseFrontmatter(markdown);
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
const sections = extractMarkdownSections(frontmatterResult.file.body);
|
|
const warnings = frontmatterResult.warnings.map((warning) => ({
|
|
...warning,
|
|
path: warning.path ?? path2
|
|
}));
|
|
const id = typeof frontmatter.id === "string" ? frontmatter.id.trim() : "";
|
|
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
const level = typeof frontmatter.level === "string" || typeof frontmatter.level === "number" ? String(frontmatter.level).trim() : void 0;
|
|
if (frontmatter.type !== "dfd_diagram") {
|
|
warnings.push(createWarning7(path2, "type", 'expected type "dfd_diagram"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning7(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning7(path2, "name", 'required frontmatter "name" is missing'));
|
|
}
|
|
const objectsTable = parseDfdObjectsTable(sections.Objects, path2);
|
|
const flowsTable = parseMarkdownTable(sections.Flows, FLOW_HEADERS, path2, "Flows");
|
|
warnings.push(...objectsTable.warnings, ...flowsTable.warnings);
|
|
const fallbackTitle = name || id || getFileStem3(path2) || "Untitled DFD Diagram";
|
|
const objectEntries = objectsTable.rows;
|
|
const objectRefs = objectEntries.map((row) => row.id?.trim() || row.ref?.trim() || "").filter(Boolean);
|
|
const nodes = objectEntries.map((entry) => ({
|
|
id: entry.id?.trim() || entry.ref?.trim() || `object-${entry.rowIndex + 1}`,
|
|
ref: entry.ref?.trim() || void 0,
|
|
label: entry.label?.trim() || void 0,
|
|
kind: entry.kind
|
|
}));
|
|
const flows = [];
|
|
const edges = [];
|
|
flowsTable.rows.forEach((row, rowIndex) => {
|
|
const from = row.from?.trim() ?? "";
|
|
const to = row.to?.trim() ?? "";
|
|
const data = row.data?.trim() ?? "";
|
|
const notes = row.notes?.trim() ?? "";
|
|
const flowId = row.id?.trim() ?? "";
|
|
flows.push({
|
|
id: flowId || void 0,
|
|
from,
|
|
to,
|
|
data: data || void 0,
|
|
dataRef: data ? parseReferenceValue(data) ?? void 0 : void 0,
|
|
notes: notes || void 0,
|
|
rowIndex
|
|
});
|
|
edges.push({
|
|
id: flowId || void 0,
|
|
source: from,
|
|
target: to,
|
|
kind: "flow",
|
|
label: data || void 0,
|
|
metadata: {
|
|
notes: notes || void 0,
|
|
rowIndex
|
|
}
|
|
});
|
|
});
|
|
return {
|
|
file: {
|
|
fileType: "dfd-diagram",
|
|
schema: "dfd_diagram",
|
|
path: path2,
|
|
title: fallbackTitle,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: name || fallbackTitle,
|
|
kind: "dfd",
|
|
level,
|
|
description: joinSectionLines2(sections.Summary),
|
|
objectRefs,
|
|
objectEntries,
|
|
nodes,
|
|
edges,
|
|
flows
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function getFileStem3(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
function joinSectionLines2(lines) {
|
|
const value = (lines ?? []).join("\n").trim();
|
|
return value || void 0;
|
|
}
|
|
function createWarning7(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
function parseDfdObjectsTable(lines, path2) {
|
|
if (!lines) {
|
|
return { rows: [], warnings: [] };
|
|
}
|
|
const normalizedLines = lines.map((line) => line.trim()).filter((line) => line.startsWith("|"));
|
|
if (normalizedLines.length < 2) {
|
|
return {
|
|
rows: [],
|
|
warnings: normalizedLines.length === 0 ? [] : [createWarning7(path2, "Objects", 'table in section "Objects" is incomplete')]
|
|
};
|
|
}
|
|
const headers = splitMarkdownTableRow(normalizedLines[0]) ?? [];
|
|
const warnings = [];
|
|
const hasLegacyHeaders = sameHeaders3(headers, LEGACY_OBJECT_HEADERS);
|
|
const hasLocalHeaders = headers.includes("id") && headers.includes("label") && headers.includes("kind") && headers.includes("ref");
|
|
if (!hasLegacyHeaders && !hasLocalHeaders) {
|
|
warnings.push(
|
|
createWarning7(
|
|
path2,
|
|
"Objects",
|
|
'table columns in section "Objects" do not match supported DFD object headers'
|
|
)
|
|
);
|
|
}
|
|
if (hasLegacyHeaders) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: "Old ref-only DFD Objects format detected; compatibility mode used.",
|
|
severity: "info",
|
|
path: path2,
|
|
field: "Objects"
|
|
});
|
|
}
|
|
const rows = [];
|
|
const seenIds = /* @__PURE__ */ new Set();
|
|
normalizedLines.slice(2).forEach((rowLine, rowIndex) => {
|
|
const values = splitMarkdownTableRow(rowLine) ?? [];
|
|
if (values.length !== headers.length) {
|
|
warnings.push(
|
|
createWarning7(
|
|
path2,
|
|
"Objects",
|
|
`table row in section "Objects" has ${values.length} columns, expected ${headers.length}`
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
const row = {};
|
|
for (const [headerIndex, header] of headers.entries()) {
|
|
row[header] = values[headerIndex] ?? "";
|
|
}
|
|
const id = row.id?.trim() || "";
|
|
const label = row.label?.trim() || "";
|
|
const kind = row.kind?.trim() || "";
|
|
const ref = row.ref?.trim() || "";
|
|
const notes = row.notes?.trim() || "";
|
|
if (!id && !ref) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: 'DFD Objects row must have "id" or "ref".',
|
|
severity: "error",
|
|
path: path2,
|
|
field: "Objects",
|
|
context: { rowIndex: rowIndex + 1 }
|
|
});
|
|
return;
|
|
}
|
|
if (id) {
|
|
if (seenIds.has(id)) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: `duplicate DFD Objects.id "${id}"`,
|
|
severity: "error",
|
|
path: path2,
|
|
field: "Objects",
|
|
context: { rowIndex: rowIndex + 1 }
|
|
});
|
|
} else {
|
|
seenIds.add(id);
|
|
}
|
|
}
|
|
if (kind && !isSupportedDfdDiagramObjectKind(kind)) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: `unknown DFD object kind "${kind}"`,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "Objects",
|
|
context: { rowIndex: rowIndex + 1 }
|
|
});
|
|
}
|
|
rows.push({
|
|
id: id || void 0,
|
|
label: label || void 0,
|
|
kind: kind ? normalizeDfdDiagramObjectKind(kind) : void 0,
|
|
ref: ref || void 0,
|
|
notes: notes || void 0,
|
|
rowIndex,
|
|
compatibilityMode: hasLegacyHeaders ? "legacy_ref_only" : "explicit"
|
|
});
|
|
});
|
|
return { rows, warnings };
|
|
}
|
|
function normalizeDfdDiagramObjectKind(value) {
|
|
switch (value) {
|
|
case "external":
|
|
case "process":
|
|
case "datastore":
|
|
return value;
|
|
default:
|
|
return "other";
|
|
}
|
|
}
|
|
function isSupportedDfdDiagramObjectKind(value) {
|
|
return value === "external" || value === "process" || value === "datastore" || value === "other";
|
|
}
|
|
function sameHeaders3(actual, expected) {
|
|
return actual.length === expected.length && actual.every((header, index) => header === expected[index]);
|
|
}
|
|
|
|
// src/parsers/dfd-object-parser.ts
|
|
var DFD_OBJECT_KINDS = /* @__PURE__ */ new Set(["external", "process", "datastore"]);
|
|
function parseDfdObjectFile(markdown, path2) {
|
|
const frontmatterResult = parseFrontmatter(markdown);
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
const sections = extractMarkdownSections(frontmatterResult.file.body);
|
|
const warnings = frontmatterResult.warnings.map((warning) => ({
|
|
...warning,
|
|
path: warning.path ?? path2
|
|
}));
|
|
const id = typeof frontmatter.id === "string" ? frontmatter.id.trim() : "";
|
|
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
const rawKind = typeof frontmatter.kind === "string" ? frontmatter.kind.trim() : "";
|
|
if (frontmatter.type !== "dfd_object") {
|
|
warnings.push(createWarning8(path2, "type", 'expected type "dfd_object"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning8(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning8(path2, "name", 'required frontmatter "name" is missing'));
|
|
}
|
|
if (!rawKind) {
|
|
warnings.push(createWarning8(path2, "kind", 'required frontmatter "kind" is missing'));
|
|
} else if (!DFD_OBJECT_KINDS.has(rawKind)) {
|
|
warnings.push({
|
|
code: "invalid-kind",
|
|
message: `invalid dfd_object kind "${rawKind}"`,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "kind"
|
|
});
|
|
}
|
|
const fallbackName = name || id || getFileStem4(path2) || "Untitled DFD Object";
|
|
const normalizedKind = DFD_OBJECT_KINDS.has(rawKind) ? rawKind : "process";
|
|
return {
|
|
file: {
|
|
fileType: "dfd-object",
|
|
schema: "dfd_object",
|
|
path: path2,
|
|
title: fallbackName,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: fallbackName,
|
|
kind: normalizedKind,
|
|
summary: joinSectionLines3(sections.Summary),
|
|
notes: normalizeNotes(sections.Notes)
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function getFileStem4(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
function joinSectionLines3(lines) {
|
|
const value = (lines ?? []).join("\n").trim();
|
|
return value || void 0;
|
|
}
|
|
function normalizeNotes(lines) {
|
|
const notes = (lines ?? []).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^-\s+/, ""));
|
|
return notes.length > 0 ? notes : void 0;
|
|
}
|
|
function createWarning8(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/parsers/data-object-parser.ts
|
|
var FORMAT_HEADERS = ["key", "value", "notes"];
|
|
var RECORD_HEADERS = ["record_type", "name", "occurrence", "notes"];
|
|
var FILE_LAYOUT_HINTS = /* @__PURE__ */ new Set(["record_type", "no", "position", "field_format"]);
|
|
function parseDataObjectFile(markdown, path2) {
|
|
const frontmatterResult = parseFrontmatter(markdown);
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
const sections = extractMarkdownSections(frontmatterResult.file.body);
|
|
const warnings = frontmatterResult.warnings.map((warning) => ({
|
|
...warning,
|
|
path: warning.path ?? path2
|
|
}));
|
|
const id = typeof frontmatter.id === "string" ? frontmatter.id.trim() : "";
|
|
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
const kind = typeof frontmatter.kind === "string" ? frontmatter.kind.trim() : "";
|
|
const dataFormat = typeof frontmatter.data_format === "string" ? frontmatter.data_format.trim() : "";
|
|
const encoding = typeof frontmatter.encoding === "string" ? frontmatter.encoding.trim() : "";
|
|
const delimiter = typeof frontmatter.delimiter === "string" ? frontmatter.delimiter.trim() : "";
|
|
const lineEnding = typeof frontmatter.line_ending === "string" ? frontmatter.line_ending.trim() : "";
|
|
const hasHeader = typeof frontmatter.has_header === "string" || typeof frontmatter.has_header === "boolean" ? String(frontmatter.has_header).trim() : "";
|
|
const recordLength = typeof frontmatter.record_length === "string" || typeof frontmatter.record_length === "number" ? String(frontmatter.record_length).trim() : "";
|
|
if (frontmatter.type !== "data_object") {
|
|
warnings.push(createWarning9(path2, "type", 'expected type "data_object"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning9(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning9(path2, "name", 'required frontmatter "name" is missing'));
|
|
}
|
|
const lines = normalizeLines(markdown);
|
|
const bodyStartLine = getBodyStartLine(lines);
|
|
const sectionRanges = getSectionRanges(lines, bodyStartLine);
|
|
const formatTable = parseSectionTable(lines, sectionRanges.Format, path2, "Format");
|
|
const recordsTable = parseSectionTable(lines, sectionRanges.Records, path2, "Records");
|
|
const fieldsTable = parseSectionTable(lines, sectionRanges.Fields, path2, "Fields");
|
|
warnings.push(...formatTable.warnings, ...recordsTable.warnings, ...fieldsTable.warnings);
|
|
const formatEntries = formatTable.rows.map((row) => ({
|
|
key: row.record.key?.trim() ?? "",
|
|
value: row.record.value?.trim() || void 0,
|
|
notes: row.record.notes?.trim() || void 0,
|
|
rowLine: row.line
|
|
}));
|
|
const records = recordsTable.rows.map((row) => ({
|
|
recordType: row.record.record_type?.trim() ?? "",
|
|
name: row.record.name?.trim() || void 0,
|
|
occurrence: row.record.occurrence?.trim() || void 0,
|
|
notes: row.record.notes?.trim() || void 0,
|
|
rowLine: row.line
|
|
}));
|
|
const fieldMode = detectFieldMode(fieldsTable.header);
|
|
if (fieldMode === "file_layout" && hasStandardAndFileLayoutColumns(fieldsTable.header)) {
|
|
warnings.push(
|
|
createSectionWarning2(
|
|
path2,
|
|
"Fields",
|
|
"Fields table mixes standard and file layout columns; parsed as file_layout"
|
|
)
|
|
);
|
|
}
|
|
const fields = fieldsTable.rows.map(
|
|
(row) => fieldMode === "file_layout" ? {
|
|
fieldMode,
|
|
recordType: row.record.record_type?.trim() || void 0,
|
|
no: row.record.no?.trim() || void 0,
|
|
name: row.record.name?.trim() ?? "",
|
|
label: row.record.label?.trim() || void 0,
|
|
type: row.record.type?.trim() || void 0,
|
|
length: row.record.length?.trim() || void 0,
|
|
required: row.record.required?.trim() || void 0,
|
|
position: row.record.position?.trim() || void 0,
|
|
fieldFormat: row.record.field_format?.trim() || void 0,
|
|
ref: row.record.ref?.trim() || void 0,
|
|
notes: row.record.notes?.trim() || void 0,
|
|
rowLine: row.line
|
|
} : {
|
|
fieldMode,
|
|
name: row.record.name?.trim() ?? "",
|
|
label: row.record.label?.trim() || void 0,
|
|
type: row.record.type?.trim() || void 0,
|
|
length: row.record.length?.trim() || void 0,
|
|
required: row.record.required?.trim() || void 0,
|
|
path: row.record.path?.trim() || void 0,
|
|
ref: row.record.ref?.trim() || void 0,
|
|
notes: row.record.notes?.trim() || void 0,
|
|
rowLine: row.line
|
|
}
|
|
);
|
|
const fallbackName = name || id || getFileStem5(path2) || "Untitled Data Object";
|
|
const sectionLines = Object.fromEntries(
|
|
Object.entries(sectionRanges).filter(([, range]) => range).map(([key, range]) => [key, range?.headingLine ?? bodyStartLine])
|
|
);
|
|
return {
|
|
file: {
|
|
fileType: "data-object",
|
|
schema: "data_object",
|
|
path: path2,
|
|
title: fallbackName,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: fallbackName,
|
|
kind: kind || void 0,
|
|
dataFormat: dataFormat || void 0,
|
|
encoding: encoding || void 0,
|
|
delimiter: delimiter || void 0,
|
|
lineEnding: lineEnding || void 0,
|
|
hasHeader: hasHeader || void 0,
|
|
recordLength: recordLength || void 0,
|
|
summary: joinSectionLines4(sections.Summary),
|
|
notes: normalizeNotes2(sections.Notes),
|
|
formatEntries,
|
|
records,
|
|
fields,
|
|
fieldMode,
|
|
sectionLines
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function normalizeLines(markdown) {
|
|
return markdown.replace(/\r\n/g, "\n").split("\n");
|
|
}
|
|
function getBodyStartLine(lines) {
|
|
if ((lines[0] ?? "").trim() !== "---") {
|
|
return 0;
|
|
}
|
|
for (let index = 1; index < lines.length; index += 1) {
|
|
if ((lines[index] ?? "").trim() === "---") {
|
|
return index + 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
function getSectionRanges(lines, bodyStartLine) {
|
|
const sectionNames = [
|
|
"Summary",
|
|
"Format",
|
|
"Records",
|
|
"Fields",
|
|
"Notes"
|
|
];
|
|
const boundarySectionNames = [...sectionNames, "Source Links"];
|
|
const ranges = {
|
|
Summary: null,
|
|
Format: null,
|
|
Records: null,
|
|
Fields: null,
|
|
Notes: null
|
|
};
|
|
const headings = [];
|
|
let inFence = false;
|
|
for (let index = bodyStartLine; index < lines.length; index += 1) {
|
|
const trimmed = (lines[index] ?? "").trim();
|
|
if (/^(```|~~~)/.test(trimmed)) {
|
|
inFence = !inFence;
|
|
continue;
|
|
}
|
|
if (inFence) {
|
|
continue;
|
|
}
|
|
const match = trimmed.match(/^##\s+(.+)$/);
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
const name = match[1].trim();
|
|
if (boundarySectionNames.includes(name)) {
|
|
headings.push({ name, line: index });
|
|
}
|
|
}
|
|
for (let index = 0; index < headings.length; index += 1) {
|
|
const heading = headings[index];
|
|
const nextLine = headings[index + 1]?.line ?? lines.length;
|
|
if (!sectionNames.includes(heading.name)) {
|
|
continue;
|
|
}
|
|
ranges[heading.name] = {
|
|
headingLine: heading.line,
|
|
endLine: nextLine
|
|
};
|
|
}
|
|
return ranges;
|
|
}
|
|
function parseSectionTable(lines, range, path2, section) {
|
|
const warnings = [];
|
|
if (!range) {
|
|
return { header: [], rows: [], warnings };
|
|
}
|
|
let header = [];
|
|
const rows = [];
|
|
for (let index = range.headingLine + 1; index < range.endLine; index += 1) {
|
|
const line = lines[index] ?? "";
|
|
const trimmed = line.trim();
|
|
if (!trimmed.startsWith("|")) {
|
|
continue;
|
|
}
|
|
const cells = splitMarkdownTableRow(line);
|
|
if (!cells || cells.length === 0) {
|
|
continue;
|
|
}
|
|
if (isSeparatorRow(cells)) {
|
|
continue;
|
|
}
|
|
if (header.length === 0) {
|
|
header = cells.map((cell) => cell.trim());
|
|
continue;
|
|
}
|
|
const record = {};
|
|
for (let columnIndex = 0; columnIndex < header.length; columnIndex += 1) {
|
|
record[header[columnIndex]] = cells[columnIndex] ?? "";
|
|
}
|
|
if (Object.values(record).every((value) => !value.trim())) {
|
|
continue;
|
|
}
|
|
if (cells.length > header.length) {
|
|
warnings.push(
|
|
createSectionWarning2(
|
|
path2,
|
|
section,
|
|
`table row in section "${section}" has ${cells.length} columns, expected ${header.length}`
|
|
)
|
|
);
|
|
}
|
|
rows.push({ record, line: index });
|
|
}
|
|
if (section === "Format" && header.length > 0 && !matchesHeader(header, FORMAT_HEADERS)) {
|
|
warnings.push(
|
|
createSectionWarning2(path2, section, "Format table should use: key | value | notes")
|
|
);
|
|
}
|
|
if (section === "Records" && header.length > 0 && !matchesHeader(header, RECORD_HEADERS)) {
|
|
warnings.push(
|
|
createSectionWarning2(
|
|
path2,
|
|
section,
|
|
"Records table should use: record_type | name | occurrence | notes"
|
|
)
|
|
);
|
|
}
|
|
return { header, rows, warnings };
|
|
}
|
|
function detectFieldMode(header) {
|
|
return header.some((cell) => FILE_LAYOUT_HINTS.has(cell)) ? "file_layout" : "standard";
|
|
}
|
|
function hasStandardAndFileLayoutColumns(header) {
|
|
const hasFileLayout = header.some((cell) => FILE_LAYOUT_HINTS.has(cell));
|
|
const hasStandardOnly = header.some((cell) => ["path"].includes(cell));
|
|
return hasFileLayout && hasStandardOnly;
|
|
}
|
|
function matchesHeader(header, expected) {
|
|
return expected.every((value, index) => header[index] === value);
|
|
}
|
|
function isSeparatorRow(cells) {
|
|
return cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
|
|
}
|
|
function getFileStem5(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
function joinSectionLines4(lines) {
|
|
const value = (lines ?? []).join("\n").trim();
|
|
return value || void 0;
|
|
}
|
|
function normalizeNotes2(lines) {
|
|
const notes = (lines ?? []).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^-\s+/, ""));
|
|
return notes.length > 0 ? notes : void 0;
|
|
}
|
|
function createWarning9(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
function createSectionWarning2(path2, section, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: section,
|
|
context: {
|
|
section
|
|
}
|
|
};
|
|
}
|
|
|
|
// src/parsers/app-process-parser.ts
|
|
var INPUT_HEADERS = ["id", "data", "source", "required", "notes"];
|
|
var OUTPUT_HEADERS = ["id", "data", "target", "notes"];
|
|
var TRIGGER_HEADERS = ["id", "kind", "source", "event", "notes"];
|
|
var TRANSITION_HEADERS = ["id", "event", "to", "condition", "notes"];
|
|
var STEP_HEADERS = ["id", "lane", "label", "kind", "input", "output", "rule", "invoke", "screen", "notes"];
|
|
var FLOW_HEADERS2 = ["from", "to", "condition", "label", "notes"];
|
|
function parseAppProcessFile(markdown, path2) {
|
|
const frontmatterResult = parseFrontmatter(markdown);
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
const sections = extractMarkdownSections(frontmatterResult.file.body);
|
|
const warnings = frontmatterResult.warnings.map((warning) => ({
|
|
...warning,
|
|
path: warning.path ?? path2
|
|
}));
|
|
const id = typeof frontmatter.id === "string" ? frontmatter.id.trim() : "";
|
|
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
const kind = typeof frontmatter.kind === "string" ? frontmatter.kind.trim() : "";
|
|
if (frontmatter.type !== "app_process") {
|
|
warnings.push(createWarning10(path2, "type", 'expected type "app_process"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning10(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning10(path2, "name", 'required frontmatter "name" is missing'));
|
|
}
|
|
const inputsTable = parseMarkdownTable(sections.Inputs, INPUT_HEADERS, path2, "Inputs");
|
|
const outputsTable = parseMarkdownTable(sections.Outputs, OUTPUT_HEADERS, path2, "Outputs");
|
|
const triggersTable = parseMarkdownTable(
|
|
sections.Triggers,
|
|
TRIGGER_HEADERS,
|
|
path2,
|
|
"Triggers"
|
|
);
|
|
const transitionsTable = parseMarkdownTable(
|
|
sections.Transitions,
|
|
TRANSITION_HEADERS,
|
|
path2,
|
|
"Transitions"
|
|
);
|
|
const hasStructuredSteps = hasMarkdownTable(sections.Steps);
|
|
const stepsTable = hasStructuredSteps ? parseMarkdownTable(sections.Steps, STEP_HEADERS, path2, "Steps") : { rows: [], warnings: [] };
|
|
const flowsTable = hasMarkdownTable(sections.Flows) ? parseMarkdownTable(sections.Flows, FLOW_HEADERS2, path2, "Flows") : { rows: [], warnings: [] };
|
|
const steps = stepsTable.rows.map((row) => ({
|
|
id: row.id?.trim() ?? "",
|
|
lane: row.lane?.trim() || void 0,
|
|
label: row.label?.trim() || void 0,
|
|
kind: row.kind?.trim() || void 0,
|
|
input: row.input?.trim() || void 0,
|
|
output: row.output?.trim() || void 0,
|
|
rule: row.rule?.trim() || void 0,
|
|
invoke: row.invoke?.trim() || void 0,
|
|
screen: row.screen?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow(Object.values(row)));
|
|
const flows = flowsTable.rows.map((row) => ({
|
|
from: row.from?.trim() ?? "",
|
|
to: row.to?.trim() ?? "",
|
|
condition: row.condition?.trim() || void 0,
|
|
label: row.label?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow(Object.values(row)));
|
|
warnings.push(
|
|
...inputsTable.warnings,
|
|
...outputsTable.warnings,
|
|
...triggersTable.warnings,
|
|
...transitionsTable.warnings,
|
|
...stepsTable.warnings,
|
|
...flowsTable.warnings
|
|
);
|
|
const fallbackName = name || id || getFileStem6(path2) || "Untitled App Process";
|
|
return {
|
|
file: {
|
|
fileType: "app-process",
|
|
schema: "app_process",
|
|
path: path2,
|
|
title: fallbackName,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: fallbackName,
|
|
kind: kind || void 0,
|
|
summary: joinSectionLines5(sections.Summary),
|
|
inputs: inputsTable.rows.map((row) => ({
|
|
id: row.id?.trim() ?? "",
|
|
data: row.data?.trim() || void 0,
|
|
source: row.source?.trim() || void 0,
|
|
required: row.required?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow(Object.values(row))),
|
|
outputs: outputsTable.rows.map((row) => ({
|
|
id: row.id?.trim() ?? "",
|
|
data: row.data?.trim() || void 0,
|
|
target: row.target?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow(Object.values(row))),
|
|
triggers: triggersTable.rows.map((row) => ({
|
|
id: row.id?.trim() ?? "",
|
|
kind: row.kind?.trim() || void 0,
|
|
source: row.source?.trim() || void 0,
|
|
event: row.event?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow(Object.values(row))),
|
|
transitions: transitionsTable.rows.map((row) => ({
|
|
id: row.id?.trim() ?? "",
|
|
event: row.event?.trim() || void 0,
|
|
to: row.to?.trim() || void 0,
|
|
condition: row.condition?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow(Object.values(row))),
|
|
steps,
|
|
flows,
|
|
hasExplicitFlows: hasStructuredSteps && flows.length > 0,
|
|
notes: normalizeNotes3(sections.Notes)
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function getFileStem6(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
function joinSectionLines5(lines) {
|
|
const value = (lines ?? []).join("\n").trim();
|
|
return value || void 0;
|
|
}
|
|
function normalizeNotes3(lines) {
|
|
const notes = (lines ?? []).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^-\s+/, ""));
|
|
return notes.length > 0 ? notes : void 0;
|
|
}
|
|
function hasMarkdownTable(lines) {
|
|
const tableLines = (lines ?? []).map((line) => line.trim()).filter((line) => line.startsWith("|"));
|
|
if (tableLines.length < 2) {
|
|
return false;
|
|
}
|
|
return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(tableLines[1]);
|
|
}
|
|
function isEmptyRow(values) {
|
|
return values.every((value) => !value?.trim());
|
|
}
|
|
function createWarning10(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/parsers/screen-parser.ts
|
|
var LAYOUT_HEADERS = ["id", "label", "kind", "purpose", "notes"];
|
|
var FIELD_HEADERS = [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"layout",
|
|
"data_type",
|
|
"required",
|
|
"ref",
|
|
"rule",
|
|
"notes"
|
|
];
|
|
var FIELD_HEADERS_WITH_CONDITION = [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"layout",
|
|
"data_type",
|
|
"required",
|
|
"ref",
|
|
"condition",
|
|
"rule",
|
|
"notes"
|
|
];
|
|
var FIELD_HEADERS_WITH_RULE_BEFORE_CONDITION = [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"layout",
|
|
"data_type",
|
|
"required",
|
|
"ref",
|
|
"rule",
|
|
"condition",
|
|
"notes"
|
|
];
|
|
var LEGACY_FIELD_HEADERS = [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"data_type",
|
|
"required",
|
|
"ref",
|
|
"rule",
|
|
"notes"
|
|
];
|
|
var LEGACY_FIELD_HEADERS_WITH_CONDITION = [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"data_type",
|
|
"required",
|
|
"ref",
|
|
"condition",
|
|
"rule",
|
|
"notes"
|
|
];
|
|
var LEGACY_FIELD_HEADERS_WITH_RULE_BEFORE_CONDITION = [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"data_type",
|
|
"required",
|
|
"ref",
|
|
"rule",
|
|
"condition",
|
|
"notes"
|
|
];
|
|
var ACTION_HEADERS = [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"target",
|
|
"event",
|
|
"invoke",
|
|
"transition",
|
|
"rule",
|
|
"notes"
|
|
];
|
|
var ACTION_HEADERS_WITH_CONDITION = [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"target",
|
|
"event",
|
|
"condition",
|
|
"invoke",
|
|
"transition",
|
|
"rule",
|
|
"notes"
|
|
];
|
|
var ACTION_HEADERS_WITH_CONDITION_AFTER_RULE = [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"target",
|
|
"event",
|
|
"invoke",
|
|
"transition",
|
|
"rule",
|
|
"condition",
|
|
"notes"
|
|
];
|
|
var MESSAGE_HEADERS = ["id", "text", "severity", "timing", "notes"];
|
|
var MESSAGE_HEADERS_WITH_CONDITION = [
|
|
"id",
|
|
"text",
|
|
"severity",
|
|
"timing",
|
|
"condition",
|
|
"notes"
|
|
];
|
|
var LEGACY_MESSAGE_HEADERS = ["ref", "timing", "notes"];
|
|
var LEGACY_TRANSITION_HEADERS = ["id", "event", "to", "condition", "notes"];
|
|
var LOCAL_PROCESS_STEP_HEADERS = [
|
|
"id",
|
|
"label",
|
|
"kind",
|
|
"condition",
|
|
"input",
|
|
"output",
|
|
"rule",
|
|
"invoke",
|
|
"screen",
|
|
"notes"
|
|
];
|
|
var LOCAL_PROCESS_ERROR_HEADERS = [
|
|
"id",
|
|
"condition",
|
|
"message",
|
|
"notes"
|
|
];
|
|
function parseScreenFile(markdown, path2) {
|
|
const normalizedMarkdown = markdown.replace(/\r\n/g, "\n");
|
|
const frontmatterResult = parseFrontmatter(normalizedMarkdown);
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
const body = frontmatterResult.file.body;
|
|
const sections = extractMarkdownSections(body);
|
|
const warnings = frontmatterResult.warnings.map((warning) => ({
|
|
...warning,
|
|
path: warning.path ?? path2
|
|
}));
|
|
const id = typeof frontmatter.id === "string" ? frontmatter.id.trim() : "";
|
|
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
const screenType = typeof frontmatter.screen_type === "string" ? frontmatter.screen_type.trim() : "";
|
|
if (frontmatter.type !== "screen") {
|
|
warnings.push(createWarning11(path2, "type", 'expected type "screen"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning11(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning11(path2, "name", 'required frontmatter "name" is missing'));
|
|
}
|
|
const bodyLines = body.split("\n");
|
|
const bodyStartLine = getBodyStartLine2(normalizedMarkdown);
|
|
const sectionLines = getSectionLineNumbers(bodyLines, bodyStartLine);
|
|
const layoutTable = readSectionTable(bodyLines, bodyStartLine, "Layout");
|
|
const fieldsTable = readSectionTable(bodyLines, bodyStartLine, "Fields");
|
|
const actionsTable = readSectionTable(bodyLines, bodyStartLine, "Actions");
|
|
const messagesTable = readSectionTable(bodyLines, bodyStartLine, "Messages");
|
|
const transitionsTable = readSectionTable(bodyLines, bodyStartLine, "Transitions");
|
|
const localProcesses = collectLocalProcesses(bodyLines, bodyStartLine);
|
|
const layoutHeaders = layoutTable.headers;
|
|
if (layoutHeaders.length > 0 && !sameHeaders4(layoutHeaders, LAYOUT_HEADERS)) {
|
|
warnings.push(createWarning11(path2, "Layout", 'table columns in section "Layout" do not match expected headers'));
|
|
}
|
|
const fieldHeaders = fieldsTable.headers;
|
|
const isCanonicalFields = sameHeaders4(fieldHeaders, FIELD_HEADERS);
|
|
const isCanonicalFieldsWithCondition = sameHeaders4(fieldHeaders, FIELD_HEADERS_WITH_CONDITION);
|
|
const isCanonicalFieldsWithRuleBeforeCondition = sameHeaders4(fieldHeaders, FIELD_HEADERS_WITH_RULE_BEFORE_CONDITION);
|
|
const isLegacyFields = sameHeaders4(fieldHeaders, LEGACY_FIELD_HEADERS);
|
|
const isLegacyFieldsWithCondition = sameHeaders4(fieldHeaders, LEGACY_FIELD_HEADERS_WITH_CONDITION);
|
|
const isLegacyFieldsWithRuleBeforeCondition = sameHeaders4(fieldHeaders, LEGACY_FIELD_HEADERS_WITH_RULE_BEFORE_CONDITION);
|
|
if (fieldHeaders.length > 0 && !isCanonicalFields && !isCanonicalFieldsWithCondition && !isCanonicalFieldsWithRuleBeforeCondition && !isLegacyFields && !isLegacyFieldsWithCondition && !isLegacyFieldsWithRuleBeforeCondition) {
|
|
warnings.push(createWarning11(path2, "Fields", 'table columns in section "Fields" do not match expected screen field headers'));
|
|
}
|
|
const actionHeaders = actionsTable.headers;
|
|
if (actionHeaders.length > 0 && !sameHeaders4(actionHeaders, ACTION_HEADERS) && !sameHeaders4(actionHeaders, ACTION_HEADERS_WITH_CONDITION) && !sameHeaders4(actionHeaders, ACTION_HEADERS_WITH_CONDITION_AFTER_RULE)) {
|
|
warnings.push(createWarning11(path2, "Actions", 'table columns in section "Actions" do not match expected headers'));
|
|
}
|
|
const messageHeaders = messagesTable.headers;
|
|
const isCanonicalMessages = sameHeaders4(messageHeaders, MESSAGE_HEADERS);
|
|
const isCanonicalMessagesWithCondition = sameHeaders4(messageHeaders, MESSAGE_HEADERS_WITH_CONDITION);
|
|
const isLegacyMessages = sameHeaders4(messageHeaders, LEGACY_MESSAGE_HEADERS);
|
|
if (messageHeaders.length > 0 && !isCanonicalMessages && !isCanonicalMessagesWithCondition && !isLegacyMessages) {
|
|
warnings.push(createWarning11(path2, "Messages", 'table columns in section "Messages" do not match expected headers'));
|
|
}
|
|
const transitionHeaders = transitionsTable.headers;
|
|
if (transitionHeaders.length > 0 && !sameHeaders4(transitionHeaders, LEGACY_TRANSITION_HEADERS)) {
|
|
warnings.push(createWarning11(path2, "Transitions", 'table columns in section "Transitions" do not match expected legacy headers'));
|
|
}
|
|
const fallbackName = name || id || getFileStem7(path2) || "Untitled Screen";
|
|
return {
|
|
file: {
|
|
fileType: "screen",
|
|
schema: "screen",
|
|
path: path2,
|
|
title: fallbackName,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: fallbackName,
|
|
screenType: screenType || void 0,
|
|
summary: joinSectionLines6(sections.Summary),
|
|
layouts: layoutTable.rows.map((row) => ({
|
|
id: row.record.id?.trim() ?? "",
|
|
label: row.record.label?.trim() || void 0,
|
|
kind: row.record.kind?.trim() || void 0,
|
|
purpose: row.record.purpose?.trim() || void 0,
|
|
notes: row.record.notes?.trim() || void 0,
|
|
rowLine: row.rowLine
|
|
})).filter((row) => !isEmptyRow2(Object.values(row))),
|
|
fields: fieldsTable.rows.map((row) => {
|
|
const record = row.record;
|
|
return {
|
|
id: record.id?.trim() ?? "",
|
|
label: record.label?.trim() || void 0,
|
|
kind: record.kind?.trim() || void 0,
|
|
layout: record.layout?.trim() || void 0,
|
|
dataType: record.data_type?.trim() || void 0,
|
|
required: record.required?.trim() || void 0,
|
|
ref: record.ref?.trim() || void 0,
|
|
rule: record.rule?.trim() || void 0,
|
|
condition: record.condition?.trim() || void 0,
|
|
notes: record.notes?.trim() || void 0,
|
|
rowLine: row.rowLine
|
|
};
|
|
}).filter((row) => !isEmptyRow2(Object.values(row))),
|
|
actions: actionsTable.rows.map((row) => mapActionRow(row.record, row.rowLine)).filter((row) => !isEmptyRow2(Object.values(row))),
|
|
messages: messagesTable.rows.map((row) => mapMessageRow(row.record, row.rowLine, isLegacyMessages)).filter((row) => !isEmptyRow2(Object.values(row))),
|
|
localProcesses,
|
|
legacyTransitions: transitionsTable.rows.map((row) => mapLegacyTransitionRow(row.record, row.rowLine)).filter((row) => !isEmptyRow2(Object.values(row))),
|
|
notes: normalizeNotes4(sections.Notes),
|
|
sectionLines
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function getBodyStartLine2(markdown) {
|
|
if (!markdown.startsWith("---\n")) {
|
|
return 0;
|
|
}
|
|
const lines = markdown.split("\n");
|
|
for (let index = 1; index < lines.length; index += 1) {
|
|
if (lines[index].trim() === "---") {
|
|
return index + 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
function getSectionLineNumbers(bodyLines, bodyStartLine) {
|
|
const lines = {};
|
|
for (let index = 0; index < bodyLines.length; index += 1) {
|
|
const trimmed = bodyLines[index].trim();
|
|
if (trimmed === "# Summary") {
|
|
lines.Summary = bodyStartLine + index;
|
|
continue;
|
|
}
|
|
const match = trimmed.match(/^##\s+(.+)$/);
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
const sectionName = match[1].trim();
|
|
lines[sectionName] = bodyStartLine + index;
|
|
}
|
|
return lines;
|
|
}
|
|
function readSectionTable(bodyLines, bodyStartLine, sectionName) {
|
|
const sectionBody = getSectionBodyLines(bodyLines, sectionName);
|
|
const tableLines = sectionBody.map((entry) => ({ ...entry, trimmed: entry.text.trim() })).filter((entry) => entry.trimmed.startsWith("|"));
|
|
if (tableLines.length < 2) {
|
|
return { headers: [], rows: [] };
|
|
}
|
|
const headers = splitMarkdownTableRow(tableLines[0].trimmed) ?? [];
|
|
if (headers.length === 0) {
|
|
return { headers: [], rows: [] };
|
|
}
|
|
const rows = [];
|
|
for (const rowLine of tableLines.slice(2)) {
|
|
const values = splitMarkdownTableRow(rowLine.trimmed) ?? [];
|
|
if (values.length !== headers.length) {
|
|
continue;
|
|
}
|
|
const record = {};
|
|
for (const [index, header] of headers.entries()) {
|
|
record[header] = values[index] ?? "";
|
|
}
|
|
if (Object.values(record).every((value) => !value.trim())) {
|
|
continue;
|
|
}
|
|
rows.push({
|
|
record,
|
|
rowLine: bodyStartLine + rowLine.index
|
|
});
|
|
}
|
|
return { headers, rows };
|
|
}
|
|
function getSectionBodyLines(bodyLines, sectionName) {
|
|
const entries = [];
|
|
let inSection = false;
|
|
for (let index = 0; index < bodyLines.length; index += 1) {
|
|
const line = bodyLines[index];
|
|
const trimmed = line.trim();
|
|
if (sectionName === "Summary" && trimmed === "# Summary") {
|
|
inSection = true;
|
|
continue;
|
|
}
|
|
const topLevelHeading = trimmed.match(/^##\s+(.+)$/);
|
|
if (topLevelHeading) {
|
|
const current = topLevelHeading[1].trim();
|
|
if (inSection && current !== sectionName) {
|
|
break;
|
|
}
|
|
inSection = current === sectionName;
|
|
continue;
|
|
}
|
|
if (inSection) {
|
|
entries.push({ index, text: line });
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
function collectLocalProcesses(bodyLines, bodyStartLine) {
|
|
const localProcessLines = getSectionBodyLines(bodyLines, "Local Processes");
|
|
const processes = [];
|
|
for (let index = 0; index < localProcessLines.length; index += 1) {
|
|
const entry = localProcessLines[index];
|
|
const headingMatch = entry.text.trim().match(/^###\s+(.+)$/);
|
|
if (!headingMatch) {
|
|
continue;
|
|
}
|
|
const heading = headingMatch[1].trim();
|
|
let summary;
|
|
const processLines = getLocalProcessBodyLines(localProcessLines, index + 1);
|
|
const stepsTable = readLocalProcessSubsectionTable(
|
|
processLines,
|
|
bodyStartLine,
|
|
"Steps",
|
|
LOCAL_PROCESS_STEP_HEADERS
|
|
);
|
|
const errorsTable = readLocalProcessSubsectionTable(
|
|
processLines,
|
|
bodyStartLine,
|
|
"Errors",
|
|
LOCAL_PROCESS_ERROR_HEADERS
|
|
);
|
|
for (let nextIndex = 0; nextIndex < processLines.length; nextIndex += 1) {
|
|
const nextLine = processLines[nextIndex].text.trim();
|
|
if (/^####\s+Summary$/.test(nextLine)) {
|
|
const collected = [];
|
|
for (let bodyIndex = nextIndex + 1; bodyIndex < processLines.length; bodyIndex += 1) {
|
|
const bodyLine = processLines[bodyIndex].text.trim();
|
|
if (/^####\s+/.test(bodyLine)) {
|
|
break;
|
|
}
|
|
if (bodyLine) {
|
|
collected.push(bodyLine);
|
|
}
|
|
}
|
|
summary = collected.join(" ").trim() || void 0;
|
|
break;
|
|
}
|
|
}
|
|
processes.push({
|
|
id: heading,
|
|
heading,
|
|
summary,
|
|
steps: stepsTable.rows.map((row) => mapLocalProcessStepRow(row.record, row.rowLine)).filter((row) => !isEmptyRow2(Object.values(row))),
|
|
errors: errorsTable.rows.map((row) => mapLocalProcessErrorRow(row.record, row.rowLine)).filter((row) => !isEmptyRow2(Object.values(row))),
|
|
line: bodyStartLine + entry.index
|
|
});
|
|
}
|
|
return processes;
|
|
}
|
|
function getLocalProcessBodyLines(localProcessLines, startIndex) {
|
|
const entries = [];
|
|
for (let index = startIndex; index < localProcessLines.length; index += 1) {
|
|
const entry = localProcessLines[index];
|
|
if (/^###\s+/.test(entry.text.trim())) {
|
|
break;
|
|
}
|
|
entries.push(entry);
|
|
}
|
|
return entries;
|
|
}
|
|
function readLocalProcessSubsectionTable(processLines, bodyStartLine, subsectionName, expectedHeaders) {
|
|
const subsectionLines = [];
|
|
let inSubsection = false;
|
|
for (const entry of processLines) {
|
|
const trimmed = entry.text.trim();
|
|
const headingMatch = trimmed.match(/^####\s+(.+)$/);
|
|
if (headingMatch) {
|
|
if (inSubsection) {
|
|
break;
|
|
}
|
|
inSubsection = headingMatch[1].trim() === subsectionName;
|
|
continue;
|
|
}
|
|
if (inSubsection) {
|
|
subsectionLines.push(entry);
|
|
}
|
|
}
|
|
const table = readTableFromEntries(subsectionLines, bodyStartLine);
|
|
if (table.headers.length > 0 && !sameHeaders4(table.headers, expectedHeaders)) {
|
|
return { headers: table.headers, rows: [] };
|
|
}
|
|
return table;
|
|
}
|
|
function readTableFromEntries(entries, bodyStartLine) {
|
|
const tableLines = entries.map((entry) => ({ ...entry, trimmed: entry.text.trim() })).filter((entry) => entry.trimmed.startsWith("|"));
|
|
if (tableLines.length < 2) {
|
|
return { headers: [], rows: [] };
|
|
}
|
|
const headers = splitMarkdownTableRow(tableLines[0].trimmed) ?? [];
|
|
if (headers.length === 0) {
|
|
return { headers: [], rows: [] };
|
|
}
|
|
const rows = [];
|
|
for (const rowLine of tableLines.slice(2)) {
|
|
const values = splitMarkdownTableRow(rowLine.trimmed) ?? [];
|
|
if (values.length !== headers.length) {
|
|
continue;
|
|
}
|
|
const record = {};
|
|
for (const [index, header] of headers.entries()) {
|
|
record[header] = values[index] ?? "";
|
|
}
|
|
if (Object.values(record).every((value) => !value.trim())) {
|
|
continue;
|
|
}
|
|
rows.push({
|
|
record,
|
|
rowLine: bodyStartLine + rowLine.index
|
|
});
|
|
}
|
|
return { headers, rows };
|
|
}
|
|
function mapActionRow(record, rowLine) {
|
|
return {
|
|
id: record.id?.trim() || void 0,
|
|
label: record.label?.trim() || void 0,
|
|
kind: record.kind?.trim() || void 0,
|
|
target: record.target?.trim() || void 0,
|
|
event: record.event?.trim() || void 0,
|
|
invoke: record.invoke?.trim() || void 0,
|
|
transition: record.transition?.trim() || void 0,
|
|
rule: record.rule?.trim() || void 0,
|
|
condition: record.condition?.trim() || void 0,
|
|
notes: record.notes?.trim() || void 0,
|
|
rowLine
|
|
};
|
|
}
|
|
function mapMessageRow(record, rowLine, isLegacyMessages) {
|
|
if (isLegacyMessages) {
|
|
return {
|
|
id: void 0,
|
|
text: record.ref?.trim() || void 0,
|
|
severity: void 0,
|
|
timing: record.timing?.trim() || void 0,
|
|
notes: record.notes?.trim() || void 0,
|
|
rowLine
|
|
};
|
|
}
|
|
return {
|
|
id: record.id?.trim() || void 0,
|
|
text: record.text?.trim() || void 0,
|
|
severity: record.severity?.trim() || void 0,
|
|
timing: record.timing?.trim() || void 0,
|
|
condition: record.condition?.trim() || void 0,
|
|
notes: record.notes?.trim() || void 0,
|
|
rowLine
|
|
};
|
|
}
|
|
function mapLocalProcessStepRow(record, rowLine) {
|
|
return {
|
|
id: record.id?.trim() || void 0,
|
|
label: record.label?.trim() || void 0,
|
|
kind: record.kind?.trim() || void 0,
|
|
condition: record.condition?.trim() || void 0,
|
|
input: record.input?.trim() || void 0,
|
|
output: record.output?.trim() || void 0,
|
|
rule: record.rule?.trim() || void 0,
|
|
invoke: record.invoke?.trim() || void 0,
|
|
screen: record.screen?.trim() || void 0,
|
|
notes: record.notes?.trim() || void 0,
|
|
rowLine
|
|
};
|
|
}
|
|
function mapLocalProcessErrorRow(record, rowLine) {
|
|
return {
|
|
id: record.id?.trim() || void 0,
|
|
condition: record.condition?.trim() || void 0,
|
|
message: record.message?.trim() || void 0,
|
|
notes: record.notes?.trim() || void 0,
|
|
rowLine
|
|
};
|
|
}
|
|
function mapLegacyTransitionRow(record, rowLine) {
|
|
return {
|
|
id: record.id?.trim() || void 0,
|
|
event: record.event?.trim() || void 0,
|
|
to: record.to?.trim() || void 0,
|
|
condition: record.condition?.trim() || void 0,
|
|
notes: record.notes?.trim() || void 0,
|
|
rowLine
|
|
};
|
|
}
|
|
function sameHeaders4(actual, expected) {
|
|
if (actual.length !== expected.length) {
|
|
return false;
|
|
}
|
|
return actual.every((header, index) => header === expected[index]);
|
|
}
|
|
function getFileStem7(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
function joinSectionLines6(lines) {
|
|
const value = (lines ?? []).join("\n").trim();
|
|
return value || void 0;
|
|
}
|
|
function normalizeNotes4(lines) {
|
|
const notes = (lines ?? []).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^-\s+/, ""));
|
|
return notes.length > 0 ? notes : void 0;
|
|
}
|
|
function isEmptyRow2(values) {
|
|
return values.every((value) => !String(value ?? "").trim());
|
|
}
|
|
function createWarning11(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/parsers/codeset-parser.ts
|
|
var VALUE_HEADERS = ["code", "label", "sort_order", "active", "notes"];
|
|
function parseCodeSetFile(markdown, path2) {
|
|
const frontmatterResult = parseFrontmatter(markdown);
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
const sections = extractMarkdownSections(frontmatterResult.file.body);
|
|
const warnings = frontmatterResult.warnings.map((warning) => ({
|
|
...warning,
|
|
path: warning.path ?? path2
|
|
}));
|
|
const id = typeof frontmatter.id === "string" ? frontmatter.id.trim() : "";
|
|
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
const kind = typeof frontmatter.kind === "string" ? frontmatter.kind.trim() : "";
|
|
if (frontmatter.type !== "codeset") {
|
|
warnings.push(createWarning12(path2, "type", 'expected type "codeset"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning12(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning12(path2, "name", 'required frontmatter "name" is missing'));
|
|
}
|
|
const valuesTable = parseMarkdownTable(sections.Values, VALUE_HEADERS, path2, "Values");
|
|
warnings.push(...valuesTable.warnings);
|
|
const fallbackName = name || id || getFileStem8(path2) || "Untitled CodeSet";
|
|
return {
|
|
file: {
|
|
fileType: "codeset",
|
|
schema: "codeset",
|
|
path: path2,
|
|
title: fallbackName,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: fallbackName,
|
|
kind: kind || void 0,
|
|
summary: joinSectionLines7(sections.Summary),
|
|
values: valuesTable.rows.map((row) => ({
|
|
code: row.code?.trim() ?? "",
|
|
label: row.label?.trim() || void 0,
|
|
sortOrder: row.sort_order?.trim() || void 0,
|
|
active: row.active?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow3(Object.values(row))),
|
|
notes: normalizeNotes5(sections.Notes)
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function getFileStem8(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
function joinSectionLines7(lines) {
|
|
const value = (lines ?? []).join("\n").trim();
|
|
return value || void 0;
|
|
}
|
|
function normalizeNotes5(lines) {
|
|
const notes = (lines ?? []).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^-\s+/, ""));
|
|
return notes.length > 0 ? notes : void 0;
|
|
}
|
|
function isEmptyRow3(values) {
|
|
return values.every((value) => !value?.trim());
|
|
}
|
|
function createWarning12(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/parsers/message-parser.ts
|
|
var MESSAGE_HEADERS2 = [
|
|
"message_id",
|
|
"text",
|
|
"severity",
|
|
"timing",
|
|
"audience",
|
|
"active",
|
|
"notes"
|
|
];
|
|
function parseMessageFile(markdown, path2) {
|
|
const frontmatterResult = parseFrontmatter(markdown);
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
const sections = extractMarkdownSections(frontmatterResult.file.body);
|
|
const warnings = frontmatterResult.warnings.map((warning) => ({
|
|
...warning,
|
|
path: warning.path ?? path2
|
|
}));
|
|
const id = typeof frontmatter.id === "string" ? frontmatter.id.trim() : "";
|
|
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
const kind = typeof frontmatter.kind === "string" ? frontmatter.kind.trim() : "";
|
|
if (frontmatter.type !== "message") {
|
|
warnings.push(createWarning13(path2, "type", 'expected type "message"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning13(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning13(path2, "name", 'required frontmatter "name" is missing'));
|
|
}
|
|
const messagesTable = parseMarkdownTable(sections.Messages, MESSAGE_HEADERS2, path2, "Messages");
|
|
warnings.push(...messagesTable.warnings);
|
|
const fallbackName = name || id || getFileStem9(path2) || "Untitled Message Set";
|
|
return {
|
|
file: {
|
|
fileType: "message",
|
|
schema: "message",
|
|
path: path2,
|
|
title: fallbackName,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: fallbackName,
|
|
kind: kind || void 0,
|
|
summary: joinSectionLines8(sections.Summary),
|
|
messages: messagesTable.rows.map((row) => ({
|
|
messageId: row.message_id?.trim() ?? "",
|
|
text: row.text?.trim() || void 0,
|
|
severity: row.severity?.trim() || void 0,
|
|
timing: row.timing?.trim() || void 0,
|
|
audience: row.audience?.trim() || void 0,
|
|
active: row.active?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow4(Object.values(row))),
|
|
notes: normalizeNotes6(sections.Notes)
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function getFileStem9(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
function joinSectionLines8(lines) {
|
|
const value = (lines ?? []).join("\n").trim();
|
|
return value || void 0;
|
|
}
|
|
function normalizeNotes6(lines) {
|
|
const notes = (lines ?? []).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^-\s+/, ""));
|
|
return notes.length > 0 ? notes : void 0;
|
|
}
|
|
function isEmptyRow4(values) {
|
|
return values.every((value) => !value?.trim());
|
|
}
|
|
function createWarning13(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/parsers/rule-parser.ts
|
|
var INPUT_HEADERS2 = ["id", "data", "source", "required", "notes"];
|
|
var REFERENCE_HEADERS = ["ref", "usage", "notes"];
|
|
var MESSAGE_HEADERS3 = ["condition", "message", "severity", "notes"];
|
|
function parseRuleFile(markdown, path2) {
|
|
const frontmatterResult = parseFrontmatter(markdown);
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
const sections = extractMarkdownSections(frontmatterResult.file.body);
|
|
const warnings = frontmatterResult.warnings.map((warning) => ({
|
|
...warning,
|
|
path: warning.path ?? path2
|
|
}));
|
|
const id = typeof frontmatter.id === "string" ? frontmatter.id.trim() : "";
|
|
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
const kind = typeof frontmatter.kind === "string" ? frontmatter.kind.trim() : "";
|
|
if (frontmatter.type !== "rule") {
|
|
warnings.push(createWarning14(path2, "type", 'expected type "rule"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning14(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning14(path2, "name", 'required frontmatter "name" is missing'));
|
|
}
|
|
const inputsTable = parseMarkdownTable(sections.Inputs, INPUT_HEADERS2, path2, "Inputs");
|
|
const referencesTable = parseMarkdownTable(
|
|
sections.References,
|
|
REFERENCE_HEADERS,
|
|
path2,
|
|
"References"
|
|
);
|
|
const messagesTable = parseMarkdownTable(sections.Messages, MESSAGE_HEADERS3, path2, "Messages");
|
|
warnings.push(...inputsTable.warnings, ...referencesTable.warnings, ...messagesTable.warnings);
|
|
const fallbackName = name || id || getFileStem10(path2) || "Untitled Rule";
|
|
return {
|
|
file: {
|
|
fileType: "rule",
|
|
schema: "rule",
|
|
path: path2,
|
|
title: fallbackName,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: fallbackName,
|
|
kind: kind || void 0,
|
|
summary: joinSectionLines9(sections.Summary),
|
|
inputs: inputsTable.rows.map((row) => ({
|
|
id: row.id?.trim() ?? "",
|
|
data: row.data?.trim() || void 0,
|
|
source: row.source?.trim() || void 0,
|
|
required: row.required?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow5(Object.values(row))),
|
|
references: referencesTable.rows.map((row) => ({
|
|
ref: row.ref?.trim() || void 0,
|
|
usage: row.usage?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow5(Object.values(row))),
|
|
messages: messagesTable.rows.map((row) => ({
|
|
condition: row.condition?.trim() || void 0,
|
|
message: row.message?.trim() || void 0,
|
|
severity: row.severity?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow5(Object.values(row))),
|
|
notes: normalizeNotes7(sections.Notes)
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function getFileStem10(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
function joinSectionLines9(lines) {
|
|
const value = (lines ?? []).join("\n").trim();
|
|
return value || void 0;
|
|
}
|
|
function normalizeNotes7(lines) {
|
|
const notes = (lines ?? []).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^-\s+/, ""));
|
|
return notes.length > 0 ? notes : void 0;
|
|
}
|
|
function isEmptyRow5(values) {
|
|
return values.every((value) => !value?.trim());
|
|
}
|
|
function createWarning14(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/parsers/mapping-parser.ts
|
|
var SCOPE_HEADERS = ["role", "ref", "notes"];
|
|
var MAPPING_HEADERS = ["source_ref", "target_ref", "transform", "rule", "required", "notes"];
|
|
function parseMappingFile(markdown, path2) {
|
|
const frontmatterResult = parseFrontmatter(markdown);
|
|
const frontmatter = frontmatterResult.file.frontmatter ?? {};
|
|
const sections = extractMarkdownSections(frontmatterResult.file.body);
|
|
const warnings = frontmatterResult.warnings.map((warning) => ({
|
|
...warning,
|
|
path: warning.path ?? path2
|
|
}));
|
|
const id = typeof frontmatter.id === "string" ? frontmatter.id.trim() : "";
|
|
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
|
|
const kind = typeof frontmatter.kind === "string" ? frontmatter.kind.trim() : "";
|
|
const source = typeof frontmatter.source === "string" ? frontmatter.source.trim() : "";
|
|
const target = typeof frontmatter.target === "string" ? frontmatter.target.trim() : "";
|
|
if (frontmatter.type !== "mapping") {
|
|
warnings.push(createWarning15(path2, "type", 'expected type "mapping"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning15(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning15(path2, "name", 'required frontmatter "name" is missing'));
|
|
}
|
|
const scopeTable = parseMarkdownTable(sections.Scope, SCOPE_HEADERS, path2, "Scope");
|
|
const mappingsTable = parseMarkdownTable(sections.Mappings, MAPPING_HEADERS, path2, "Mappings");
|
|
warnings.push(...scopeTable.warnings, ...mappingsTable.warnings);
|
|
const fallbackName = name || id || getFileStem11(path2) || "Untitled Mapping";
|
|
return {
|
|
file: {
|
|
fileType: "mapping",
|
|
schema: "mapping",
|
|
path: path2,
|
|
title: fallbackName,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: fallbackName,
|
|
kind: kind || void 0,
|
|
source: source || void 0,
|
|
target: target || void 0,
|
|
summary: joinSectionLines10(sections.Summary),
|
|
scope: scopeTable.rows.map((row) => ({
|
|
role: row.role?.trim() || void 0,
|
|
ref: row.ref?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow6(Object.values(row))),
|
|
mappings: mappingsTable.rows.map((row) => ({
|
|
sourceRef: row.source_ref?.trim() || void 0,
|
|
targetRef: row.target_ref?.trim() || void 0,
|
|
transform: row.transform?.trim() || void 0,
|
|
rule: row.rule?.trim() || void 0,
|
|
required: row.required?.trim() || void 0,
|
|
notes: row.notes?.trim() || void 0
|
|
})).filter((row) => !isEmptyRow6(Object.values(row))),
|
|
notes: normalizeNotes8(sections.Notes)
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function getFileStem11(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
function joinSectionLines10(lines) {
|
|
const value = (lines ?? []).join("\n").trim();
|
|
return value || void 0;
|
|
}
|
|
function normalizeNotes8(lines) {
|
|
const notes = (lines ?? []).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^-\s+/, ""));
|
|
return notes.length > 0 ? notes : void 0;
|
|
}
|
|
function isEmptyRow6(values) {
|
|
return values.every((value) => !value?.trim());
|
|
}
|
|
function createWarning15(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
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_KINDS = /* @__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 [entityId, entity] of Object.entries(index.erEntitiesById)) {
|
|
registerId(idRegistry, entityId, entity.path, warnings);
|
|
validateFilenameMatchesId(entityId, entity.path, warnings);
|
|
}
|
|
validateErRelationIds(index, warnings);
|
|
for (const [dfdObjectId, dfdObject] of Object.entries(index.dfdObjectsById)) {
|
|
registerId(idRegistry, dfdObjectId, dfdObject.path, warnings);
|
|
validateFilenameMatchesId(dfdObjectId, dfdObject.path, warnings);
|
|
}
|
|
for (const [dataObjectId, dataObject] of Object.entries(index.dataObjectsById)) {
|
|
registerId(idRegistry, dataObjectId, dataObject.path, warnings);
|
|
validateFilenameMatchesId(dataObjectId, dataObject.path, warnings);
|
|
validateDataObject(dataObject, index, 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_KINDS.has(diagram.kind)) {
|
|
warnings.push({
|
|
code: "reserved-diagram-kind-used",
|
|
message: `reserved kind used: "${diagram.kind}"`,
|
|
severity: "info",
|
|
path: diagram.path,
|
|
field: "diagram_kind"
|
|
});
|
|
}
|
|
if (diagram.schema === "dfd_diagram") {
|
|
const dfdDiagram = diagram;
|
|
const objectEntries = dfdDiagram.objectEntries.length > 0 ? dfdDiagram.objectEntries : dfdDiagram.objectRefs.map((objectRef, rowIndex) => ({
|
|
ref: objectRef,
|
|
rowIndex,
|
|
compatibilityMode: "legacy_ref_only"
|
|
}));
|
|
const objectIdentityKeys = /* @__PURE__ */ new Set();
|
|
const objectIds = /* @__PURE__ */ new Set();
|
|
for (const entry of objectEntries) {
|
|
if (entry.id?.trim()) {
|
|
objectIds.add(entry.id.trim());
|
|
}
|
|
const ref = entry.ref?.trim();
|
|
if (!ref) {
|
|
continue;
|
|
}
|
|
const identity = resolveReferenceIdentity(ref, index);
|
|
if (!resolveDfdObjectReference(ref, index) || identity.resolvedModelType !== "dfd-object") {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved object ref "${ref}"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Objects"
|
|
});
|
|
continue;
|
|
}
|
|
for (const key of buildReferenceIdentityKeys(identity)) {
|
|
objectIdentityKeys.add(key);
|
|
}
|
|
}
|
|
for (const edge of dfdDiagram.edges) {
|
|
if (edge.source && objectIds.has(edge.source)) {
|
|
} else {
|
|
const sourceIdentity = edge.source ? resolveReferenceIdentity(edge.source, index) : null;
|
|
const sourceResolved = !!edge.source && !!resolveDfdObjectReference(edge.source, index) && sourceIdentity?.resolvedModelType === "dfd-object";
|
|
const sourceIdentityKeys = sourceIdentity ? buildReferenceIdentityKeys(sourceIdentity) : [];
|
|
if (!sourceResolved) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved flow source "${edge.source}"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Flows"
|
|
});
|
|
} else if (!sourceIdentityKeys.some((key) => objectIdentityKeys.has(key))) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `flow source "${edge.source}" is not listed in "Objects"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Flows"
|
|
});
|
|
}
|
|
}
|
|
if (edge.target && objectIds.has(edge.target)) {
|
|
continue;
|
|
}
|
|
const targetIdentity = edge.target ? resolveReferenceIdentity(edge.target, index) : null;
|
|
const targetResolved = !!edge.target && !!resolveDfdObjectReference(edge.target, index) && targetIdentity?.resolvedModelType === "dfd-object";
|
|
const targetIdentityKeys = targetIdentity ? buildReferenceIdentityKeys(targetIdentity) : [];
|
|
if (!targetResolved) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved flow target "${edge.target}"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Flows"
|
|
});
|
|
} else if (!targetIdentityKeys.some((key) => objectIdentityKeys.has(key))) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `flow target "${edge.target}" is not listed in "Objects"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Flows"
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
for (const objectRef of diagram.objectRefs) {
|
|
if (!resolveObjectModelReference(objectRef, index) && !resolveErEntityReference(objectRef, index)) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved object ref "${objectRef}"`,
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "objectRefs"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function validateDataObject(dataObject, index, warnings) {
|
|
for (const field of dataObject.fields) {
|
|
const ref = field.ref?.trim();
|
|
if (!ref) {
|
|
continue;
|
|
}
|
|
const parsed = parseReferenceValue(ref);
|
|
if (parsed?.isExternal || parsed?.kind === "raw") {
|
|
continue;
|
|
}
|
|
const resolved = resolveReferenceIdentity(ref, index);
|
|
if (resolved.resolvedModel) {
|
|
continue;
|
|
}
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved field reference "${ref}"`,
|
|
severity: "warning",
|
|
path: dataObject.path,
|
|
field: "Fields"
|
|
});
|
|
}
|
|
}
|
|
function validateErRelationIds(index, warnings) {
|
|
const relationIdRegistry = /* @__PURE__ */ new Map();
|
|
for (const entity of Object.values(index.erEntitiesById)) {
|
|
for (const relation of entity.relationBlocks) {
|
|
const relationId = relation.id?.trim() ?? "";
|
|
if (!relationId) {
|
|
continue;
|
|
}
|
|
if (isIncompleteErRelationId3(relationId)) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: `ER relation id looks incomplete: ${relationId}`,
|
|
severity: "warning",
|
|
path: entity.path,
|
|
field: "Relations"
|
|
});
|
|
}
|
|
const existing = relationIdRegistry.get(relationId);
|
|
if (existing && (existing.path !== entity.path || existing.ownerId !== entity.id)) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: `duplicate ER relation id: ${relationId}`,
|
|
severity: "warning",
|
|
path: entity.path,
|
|
field: "Relations"
|
|
});
|
|
continue;
|
|
}
|
|
relationIdRegistry.set(relationId, { path: entity.path, ownerId: entity.id });
|
|
}
|
|
}
|
|
}
|
|
function isIncompleteErRelationId3(id) {
|
|
const normalized = id.trim().toUpperCase();
|
|
return !normalized || normalized === "REL" || normalized === "REL-" || normalized === "REL--" || normalized === "REL-NEW" || normalized === "REL-TODO";
|
|
}
|
|
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, path2, index, warnings) {
|
|
if (!resolveObjectModelReference(source, index) || !resolveObjectModelReference(target, index)) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: `unresolved relation endpoint: "${source}" -> "${target}"`,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "relations"
|
|
});
|
|
}
|
|
}
|
|
function registerId(registry, id, path2, warnings) {
|
|
const existing = registry.get(id);
|
|
if (!existing) {
|
|
registry.set(id, path2);
|
|
return;
|
|
}
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: `duplicate id detected: "${id}"`,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "id"
|
|
});
|
|
}
|
|
function validateFilenameMatchesId(id, path2, warnings) {
|
|
const baseName = path2.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: path2,
|
|
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, options = {}) {
|
|
const parseMode = options.parseMode ?? "full";
|
|
const shouldResolveRelations = options.resolveRelations ?? true;
|
|
const shouldIndexMembers = options.indexMembers ?? true;
|
|
const shouldValidate = options.validate ?? true;
|
|
const index = {
|
|
sourceFilesByPath: {},
|
|
objectsById: {},
|
|
appProcessesById: {},
|
|
screensById: {},
|
|
codesetsById: {},
|
|
messagesById: {},
|
|
rulesById: {},
|
|
mappingsById: {},
|
|
dataObjectsById: {},
|
|
dfdObjectsById: {},
|
|
erEntitiesById: {},
|
|
erEntitiesByPhysicalName: {},
|
|
relationsFilesById: {},
|
|
diagramsById: {},
|
|
modelsByFilePath: {},
|
|
relationsById: {},
|
|
relationsByObjectId: {},
|
|
membersByOwnerId: {},
|
|
membersByOwnerPath: {},
|
|
warningsByFilePath: {},
|
|
state: {
|
|
relationLookupsBuilt: false,
|
|
memberLookupsBuilt: false,
|
|
vaultValidationBuilt: false,
|
|
fullParsedFilePaths: {}
|
|
}
|
|
};
|
|
for (const file of files) {
|
|
index.sourceFilesByPath[file.path] = file;
|
|
indexSingleFile(index, file, parseMode);
|
|
if (parseMode === "full") {
|
|
index.state.fullParsedFilePaths[file.path] = true;
|
|
}
|
|
}
|
|
recomputeDuplicateModelIdDiagnostics(index);
|
|
if (shouldResolveRelations) {
|
|
ensureRelationLookups(index);
|
|
}
|
|
if (shouldIndexMembers) {
|
|
ensureMemberLookups(index);
|
|
}
|
|
if (shouldValidate) {
|
|
ensureVaultValidation(index);
|
|
recomputeDuplicateModelIdDiagnostics(index);
|
|
}
|
|
return index;
|
|
}
|
|
function ensureRelationLookups(index) {
|
|
if (index.state.relationLookupsBuilt) {
|
|
return;
|
|
}
|
|
rebuildReferenceLookups(index);
|
|
index.state.relationLookupsBuilt = true;
|
|
}
|
|
function ensureMemberLookups(index) {
|
|
if (index.state.memberLookupsBuilt) {
|
|
return;
|
|
}
|
|
rebuildMemberLookups(index);
|
|
index.state.memberLookupsBuilt = true;
|
|
}
|
|
function ensureVaultValidation(index) {
|
|
if (index.state.vaultValidationBuilt) {
|
|
return;
|
|
}
|
|
for (const warning of validateVaultIndex(index)) {
|
|
pushWarning(index.warningsByFilePath, warning.path ?? "vault", warning);
|
|
}
|
|
index.state.vaultValidationBuilt = true;
|
|
recomputeDuplicateModelIdDiagnostics(index);
|
|
}
|
|
function replaceVaultIndexFile(index, file, parseMode) {
|
|
const previousModel = index.modelsByFilePath[file.path];
|
|
if (previousModel) {
|
|
removeModelFromIndexes(index, previousModel);
|
|
}
|
|
index.sourceFilesByPath[file.path] = file;
|
|
index.warningsByFilePath[file.path] = [];
|
|
indexSingleFile(index, file, parseMode);
|
|
if (parseMode === "full") {
|
|
index.state.fullParsedFilePaths[file.path] = true;
|
|
} else {
|
|
delete index.state.fullParsedFilePaths[file.path];
|
|
}
|
|
index.state.relationLookupsBuilt = false;
|
|
index.state.memberLookupsBuilt = false;
|
|
index.state.vaultValidationBuilt = false;
|
|
recomputeDuplicateModelIdDiagnostics(index);
|
|
}
|
|
function indexSingleFile(index, file, parseMode) {
|
|
const parseResult = parseVaultFile(file, parseMode);
|
|
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 = getModelId2(parseResult.file);
|
|
addModelById(
|
|
index.objectsById,
|
|
objectId,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "app-process": {
|
|
addModelById(
|
|
index.appProcessesById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "screen": {
|
|
addModelById(
|
|
index.screensById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "codeset": {
|
|
addModelById(
|
|
index.codesetsById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "message": {
|
|
addModelById(
|
|
index.messagesById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "rule": {
|
|
addModelById(
|
|
index.rulesById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "mapping": {
|
|
addModelById(
|
|
index.mappingsById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "relations": {
|
|
const relationsFileId = getModelId2(parseResult.file);
|
|
addModelById(
|
|
index.relationsFilesById,
|
|
relationsFileId,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
for (const relation of parseResult.file.relations) {
|
|
if (relation.id) {
|
|
addModelById(
|
|
index.relationsById,
|
|
relation.id,
|
|
relation,
|
|
index.warningsByFilePath,
|
|
file.path
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "diagram": {
|
|
const diagramId = getModelId2(parseResult.file);
|
|
addModelById(
|
|
index.diagramsById,
|
|
diagramId,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "dfd-object": {
|
|
addModelById(
|
|
index.dfdObjectsById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "data-object": {
|
|
addModelById(
|
|
index.dataObjectsById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "dfd-diagram": {
|
|
addModelById(
|
|
index.diagramsById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "er-entity": {
|
|
addModelById(
|
|
index.erEntitiesById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
addModelById(
|
|
index.erEntitiesByPhysicalName,
|
|
parseResult.file.physicalName,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path
|
|
);
|
|
break;
|
|
}
|
|
case "markdown":
|
|
break;
|
|
}
|
|
}
|
|
function rebuildReferenceLookups(index) {
|
|
index.relationsByObjectId = {};
|
|
for (const model of Object.values(index.modelsByFilePath)) {
|
|
if (model.fileType === "relations") {
|
|
for (const relation of model.relations) {
|
|
const sourceObject = resolveObjectModelReference(relation.source, index);
|
|
const targetObject = resolveObjectModelReference(relation.target, index);
|
|
addRelationForObject(
|
|
index.relationsByObjectId,
|
|
getRelationObjectKey(relation.source, sourceObject),
|
|
relation
|
|
);
|
|
addRelationForObject(
|
|
index.relationsByObjectId,
|
|
getRelationObjectKey(relation.target, targetObject),
|
|
relation
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function rebuildMemberLookups(index) {
|
|
index.membersByOwnerId = {};
|
|
index.membersByOwnerPath = {};
|
|
for (const model of Object.values(index.modelsByFilePath)) {
|
|
switch (model.fileType) {
|
|
case "data-object":
|
|
indexDataObjectMembers(index, model);
|
|
break;
|
|
case "app-process":
|
|
indexAppProcessMembers(index, model);
|
|
break;
|
|
case "screen":
|
|
indexScreenMembers(index, model);
|
|
break;
|
|
case "codeset":
|
|
indexCodeSetMembers(index, model);
|
|
break;
|
|
case "message":
|
|
indexMessageMembers(index, model);
|
|
break;
|
|
case "rule":
|
|
indexRuleMembers(index, model);
|
|
break;
|
|
case "er-entity":
|
|
indexErEntityMembers(index, model);
|
|
break;
|
|
case "object":
|
|
indexClassMembers(index, model);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
function recomputeDuplicateModelIdDiagnostics(index) {
|
|
clearDuplicateModelIdWarnings(index);
|
|
const entriesByKey = /* @__PURE__ */ new Map();
|
|
for (const model of Object.values(index.modelsByFilePath)) {
|
|
const duplicateKey = getDuplicateModelKey(model);
|
|
if (!duplicateKey) {
|
|
continue;
|
|
}
|
|
if (!entriesByKey.has(duplicateKey.key)) {
|
|
entriesByKey.set(duplicateKey.key, []);
|
|
}
|
|
entriesByKey.get(duplicateKey.key).push({
|
|
id: duplicateKey.id,
|
|
path: model.path,
|
|
fileType: model.fileType
|
|
});
|
|
}
|
|
for (const entries of entriesByKey.values()) {
|
|
if (entries.length < 2) {
|
|
continue;
|
|
}
|
|
const duplicatePaths = entries.map((entry) => entry.path).sort((left, right) => left.localeCompare(right));
|
|
const first = entries[0];
|
|
if (!first) {
|
|
continue;
|
|
}
|
|
for (const entry of entries) {
|
|
pushWarning(index.warningsByFilePath, entry.path, {
|
|
code: "invalid-structure",
|
|
message: `duplicate model id detected: "${entry.id}" in ${duplicatePaths.join(", ")}`,
|
|
severity: "warning",
|
|
path: entry.path,
|
|
field: "id"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
function clearDuplicateModelIdWarnings(index) {
|
|
for (const [path2, warnings] of Object.entries(index.warningsByFilePath)) {
|
|
index.warningsByFilePath[path2] = warnings.filter(
|
|
(warning) => !(warning.code === "invalid-structure" && warning.field === "id" && (warning.message.startsWith("duplicate model id detected:") || warning.message.startsWith("duplicate id detected:")))
|
|
);
|
|
}
|
|
}
|
|
function getDuplicateModelKey(model) {
|
|
if (model.fileType === "markdown") {
|
|
return null;
|
|
}
|
|
const id = getModelId2(model).trim();
|
|
if (!id) {
|
|
return null;
|
|
}
|
|
return {
|
|
key: id,
|
|
id
|
|
};
|
|
}
|
|
function parseVaultFile(file, parseMode) {
|
|
if (parseMode === "shallow") {
|
|
return parseShallowVaultFile(file);
|
|
}
|
|
const content = file.content ?? "";
|
|
const frontmatterResult = parseFrontmatter(content);
|
|
const frontmatter = frontmatterResult.file.frontmatter;
|
|
if (frontmatter?.type === "data_object") {
|
|
return parseDataObjectFile(content, file.path);
|
|
}
|
|
if (frontmatter?.type === "app_process") {
|
|
return parseAppProcessFile(content, file.path);
|
|
}
|
|
if (frontmatter?.type === "screen") {
|
|
return parseScreenFile(content, file.path);
|
|
}
|
|
if (frontmatter?.type === "codeset") {
|
|
return parseCodeSetFile(content, file.path);
|
|
}
|
|
if (frontmatter?.type === "message") {
|
|
return parseMessageFile(content, file.path);
|
|
}
|
|
if (frontmatter?.type === "rule") {
|
|
return parseRuleFile(content, file.path);
|
|
}
|
|
if (frontmatter?.type === "mapping") {
|
|
return parseMappingFile(content, file.path);
|
|
}
|
|
const fileType = detectFileType(frontmatter);
|
|
switch (fileType) {
|
|
case "object":
|
|
return parseObjectFile(content, file.path);
|
|
case "dfd-object":
|
|
return parseDfdObjectFile(content, file.path);
|
|
case "app-process":
|
|
return parseAppProcessFile(content, file.path);
|
|
case "screen":
|
|
return parseScreenFile(content, file.path);
|
|
case "codeset":
|
|
return parseCodeSetFile(content, file.path);
|
|
case "message":
|
|
return parseMessageFile(content, file.path);
|
|
case "rule":
|
|
return parseRuleFile(content, file.path);
|
|
case "mapping":
|
|
return parseMappingFile(content, file.path);
|
|
case "relations":
|
|
return parseRelationsFile(content, file.path);
|
|
case "diagram":
|
|
return parseDiagramFile(content, file.path);
|
|
case "dfd-diagram":
|
|
return parseDfdDiagramFile(content, file.path);
|
|
case "er-entity":
|
|
return parseErEntityFile(content, file.path);
|
|
case "markdown":
|
|
default:
|
|
return {
|
|
file: createMarkdownModel(file.path, frontmatterResult.file.body, frontmatter),
|
|
warnings: frontmatterResult.warnings
|
|
};
|
|
}
|
|
}
|
|
function parseShallowVaultFile(file) {
|
|
const frontmatterResult = file.frontmatter ? {
|
|
file: {
|
|
frontmatter: file.frontmatter,
|
|
body: ""
|
|
},
|
|
warnings: []
|
|
} : parseFrontmatter(file.content ?? "");
|
|
const frontmatter = frontmatterResult.file.frontmatter;
|
|
const fileType = detectFileType(frontmatter);
|
|
return {
|
|
file: createShallowModel(file.path, fileType, frontmatter),
|
|
warnings: frontmatterResult.warnings
|
|
};
|
|
}
|
|
function createShallowModel(path2, fileType, frontmatter) {
|
|
const common = createShallowBase(path2, frontmatter);
|
|
const id = getFrontmatterString(frontmatter, "id") ?? common.title ?? getBasename2(path2);
|
|
const name = getFrontmatterString(frontmatter, "name") ?? common.title ?? id;
|
|
const kind = getFrontmatterString(frontmatter, "kind");
|
|
switch (fileType) {
|
|
case "object":
|
|
return {
|
|
...common,
|
|
fileType: "object",
|
|
schema: "model_object_v1",
|
|
name,
|
|
kind: kind ?? "class",
|
|
attributes: [],
|
|
methods: [],
|
|
relations: []
|
|
};
|
|
case "data-object":
|
|
return {
|
|
...common,
|
|
fileType: "data-object",
|
|
schema: "data_object",
|
|
id,
|
|
name,
|
|
kind,
|
|
dataFormat: getFrontmatterString(frontmatter, "data_format"),
|
|
formatEntries: [],
|
|
records: [],
|
|
fields: [],
|
|
fieldMode: "standard"
|
|
};
|
|
case "app-process":
|
|
return {
|
|
...common,
|
|
fileType: "app-process",
|
|
schema: "app_process",
|
|
id,
|
|
name,
|
|
kind,
|
|
inputs: [],
|
|
outputs: [],
|
|
triggers: [],
|
|
transitions: []
|
|
};
|
|
case "screen":
|
|
return {
|
|
...common,
|
|
fileType: "screen",
|
|
schema: "screen",
|
|
id,
|
|
name,
|
|
screenType: getFrontmatterString(frontmatter, "screen_type"),
|
|
layouts: [],
|
|
fields: [],
|
|
actions: [],
|
|
messages: [],
|
|
localProcesses: [],
|
|
legacyTransitions: []
|
|
};
|
|
case "codeset":
|
|
return {
|
|
...common,
|
|
fileType: "codeset",
|
|
schema: "codeset",
|
|
id,
|
|
name,
|
|
kind,
|
|
values: []
|
|
};
|
|
case "message":
|
|
return {
|
|
...common,
|
|
fileType: "message",
|
|
schema: "message",
|
|
id,
|
|
name,
|
|
kind,
|
|
messages: []
|
|
};
|
|
case "rule":
|
|
return {
|
|
...common,
|
|
fileType: "rule",
|
|
schema: "rule",
|
|
id,
|
|
name,
|
|
kind,
|
|
inputs: [],
|
|
references: [],
|
|
messages: []
|
|
};
|
|
case "mapping":
|
|
return {
|
|
...common,
|
|
fileType: "mapping",
|
|
schema: "mapping",
|
|
id,
|
|
name,
|
|
kind,
|
|
source: getFrontmatterString(frontmatter, "source"),
|
|
target: getFrontmatterString(frontmatter, "target"),
|
|
scope: [],
|
|
mappings: []
|
|
};
|
|
case "dfd-object":
|
|
return {
|
|
...common,
|
|
fileType: "dfd-object",
|
|
schema: "dfd_object",
|
|
id,
|
|
name,
|
|
kind: kind ?? "process"
|
|
};
|
|
case "relations":
|
|
return {
|
|
...common,
|
|
fileType: "relations",
|
|
schema: "model_relations_v1",
|
|
relations: []
|
|
};
|
|
case "diagram": {
|
|
const diagramKind = kind === "er" ? "er" : "class";
|
|
return {
|
|
...common,
|
|
fileType: "diagram",
|
|
schema: diagramKind === "er" ? "er_diagram" : "class_diagram",
|
|
name,
|
|
kind: diagramKind,
|
|
objectRefs: [],
|
|
nodes: [],
|
|
edges: []
|
|
};
|
|
}
|
|
case "dfd-diagram":
|
|
return {
|
|
...common,
|
|
fileType: "dfd-diagram",
|
|
schema: "dfd_diagram",
|
|
id,
|
|
name,
|
|
kind: "dfd",
|
|
objectRefs: [],
|
|
objectEntries: [],
|
|
nodes: [],
|
|
edges: [],
|
|
flows: []
|
|
};
|
|
case "er-entity":
|
|
return {
|
|
...common,
|
|
fileType: "er-entity",
|
|
id,
|
|
filePath: path2,
|
|
logicalName: getFrontmatterString(frontmatter, "logical_name") ?? name,
|
|
physicalName: getFrontmatterString(frontmatter, "physical_name") ?? id,
|
|
schemaName: getFrontmatterString(frontmatter, "schema_name") ?? null,
|
|
dbms: getFrontmatterString(frontmatter, "dbms") ?? null,
|
|
columns: [],
|
|
indexes: [],
|
|
relationBlocks: [],
|
|
outboundRelations: []
|
|
};
|
|
case "markdown":
|
|
default:
|
|
return {
|
|
...common,
|
|
fileType: "markdown",
|
|
content: ""
|
|
};
|
|
}
|
|
}
|
|
function createShallowBase(path2, frontmatter) {
|
|
return {
|
|
path: path2,
|
|
title: getFrontmatterString(frontmatter, "title"),
|
|
frontmatter: frontmatter ?? {},
|
|
sections: {},
|
|
sourceLinks: []
|
|
};
|
|
}
|
|
function getFrontmatterString(frontmatter, key) {
|
|
const value = frontmatter?.[key];
|
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
}
|
|
function createMarkdownModel(path2, body, frontmatter) {
|
|
return {
|
|
fileType: "markdown",
|
|
path: path2,
|
|
title: typeof frontmatter?.title === "string" ? frontmatter.title : void 0,
|
|
frontmatter: frontmatter ?? {},
|
|
sections: extractMarkdownSections(body),
|
|
sourceLinks: [],
|
|
content: body
|
|
};
|
|
}
|
|
function indexDataObjectMembers(index, model) {
|
|
const ownerId = getModelId2(model);
|
|
for (const field of model.fields) {
|
|
const memberId = field.name?.trim();
|
|
if (!memberId) {
|
|
continue;
|
|
}
|
|
const displayName = field.recordType?.trim() ? `${field.label?.trim() || memberId} (${field.recordType.trim()})` : field.label?.trim() || memberId;
|
|
addMemberCandidate(index, {
|
|
ownerModelType: "data_object",
|
|
ownerId,
|
|
ownerPath: model.path,
|
|
memberKind: "field",
|
|
memberId,
|
|
displayName,
|
|
sourceSection: "Fields"
|
|
});
|
|
}
|
|
}
|
|
function indexAppProcessMembers(index, model) {
|
|
const ownerId = getModelId2(model);
|
|
for (const input of model.inputs) {
|
|
const memberId = input.id?.trim();
|
|
if (!memberId) {
|
|
continue;
|
|
}
|
|
addMemberCandidate(index, {
|
|
ownerModelType: "app_process",
|
|
ownerId,
|
|
ownerPath: model.path,
|
|
memberKind: "input",
|
|
memberId,
|
|
displayName: memberId,
|
|
sourceSection: "Inputs"
|
|
});
|
|
}
|
|
for (const output of model.outputs) {
|
|
const memberId = output.id?.trim();
|
|
if (!memberId) {
|
|
continue;
|
|
}
|
|
addMemberCandidate(index, {
|
|
ownerModelType: "app_process",
|
|
ownerId,
|
|
ownerPath: model.path,
|
|
memberKind: "output",
|
|
memberId,
|
|
displayName: memberId,
|
|
sourceSection: "Outputs"
|
|
});
|
|
}
|
|
}
|
|
function indexScreenMembers(index, model) {
|
|
const ownerId = getModelId2(model);
|
|
for (const field of model.fields) {
|
|
const memberId = field.id?.trim();
|
|
if (!memberId) {
|
|
continue;
|
|
}
|
|
addMemberCandidate(index, {
|
|
ownerModelType: "screen",
|
|
ownerId,
|
|
ownerPath: model.path,
|
|
memberKind: "field",
|
|
memberId,
|
|
displayName: field.label?.trim() || memberId,
|
|
sourceSection: "Fields"
|
|
});
|
|
}
|
|
for (const action of model.actions) {
|
|
const memberId = action.id?.trim();
|
|
if (!memberId) {
|
|
continue;
|
|
}
|
|
addMemberCandidate(index, {
|
|
ownerModelType: "screen",
|
|
ownerId,
|
|
ownerPath: model.path,
|
|
memberKind: "action",
|
|
memberId,
|
|
displayName: action.label?.trim() || memberId,
|
|
sourceSection: "Actions"
|
|
});
|
|
}
|
|
}
|
|
function indexCodeSetMembers(index, model) {
|
|
const ownerId = getModelId2(model);
|
|
for (const value of model.values) {
|
|
const memberId = value.code?.trim();
|
|
if (!memberId) {
|
|
continue;
|
|
}
|
|
addMemberCandidate(index, {
|
|
ownerModelType: "codeset",
|
|
ownerId,
|
|
ownerPath: model.path,
|
|
memberKind: "code",
|
|
memberId,
|
|
displayName: value.label?.trim() || memberId,
|
|
sourceSection: "Values"
|
|
});
|
|
}
|
|
}
|
|
function indexMessageMembers(index, model) {
|
|
const ownerId = getModelId2(model);
|
|
for (const message of model.messages) {
|
|
const memberId = message.messageId?.trim();
|
|
if (!memberId) {
|
|
continue;
|
|
}
|
|
addMemberCandidate(index, {
|
|
ownerModelType: "message",
|
|
ownerId,
|
|
ownerPath: model.path,
|
|
memberKind: "message",
|
|
memberId,
|
|
displayName: message.text?.trim() || memberId,
|
|
sourceSection: "Messages"
|
|
});
|
|
}
|
|
}
|
|
function indexRuleMembers(index, model) {
|
|
const ownerId = getModelId2(model);
|
|
for (const input of model.inputs) {
|
|
const memberId = input.id?.trim();
|
|
if (!memberId) {
|
|
continue;
|
|
}
|
|
addMemberCandidate(index, {
|
|
ownerModelType: "rule",
|
|
ownerId,
|
|
ownerPath: model.path,
|
|
memberKind: "input",
|
|
memberId,
|
|
displayName: memberId,
|
|
sourceSection: "Inputs"
|
|
});
|
|
}
|
|
}
|
|
function indexErEntityMembers(index, model) {
|
|
const ownerId = getModelId2(model);
|
|
for (const column of model.columns) {
|
|
const memberId = column.physicalName?.trim();
|
|
if (!memberId) {
|
|
continue;
|
|
}
|
|
addMemberCandidate(index, {
|
|
ownerModelType: "er_entity",
|
|
ownerId,
|
|
ownerPath: model.path,
|
|
memberKind: "column",
|
|
memberId,
|
|
displayName: column.logicalName?.trim() || memberId,
|
|
sourceSection: "Columns"
|
|
});
|
|
}
|
|
}
|
|
function indexClassMembers(index, model) {
|
|
const ownerId = getModelId2(model);
|
|
for (const attribute of model.attributes) {
|
|
const memberId = attribute.name?.trim();
|
|
if (!memberId) {
|
|
continue;
|
|
}
|
|
addMemberCandidate(index, {
|
|
ownerModelType: "class",
|
|
ownerId,
|
|
ownerPath: model.path,
|
|
memberKind: "attribute",
|
|
memberId,
|
|
displayName: memberId,
|
|
sourceSection: "Attributes"
|
|
});
|
|
}
|
|
for (const method of model.methods) {
|
|
const memberId = method.name?.trim();
|
|
if (!memberId) {
|
|
continue;
|
|
}
|
|
addMemberCandidate(index, {
|
|
ownerModelType: "class",
|
|
ownerId,
|
|
ownerPath: model.path,
|
|
memberKind: "method",
|
|
memberId,
|
|
displayName: memberId,
|
|
sourceSection: "Methods"
|
|
});
|
|
}
|
|
}
|
|
function addModelById(target, id, model, warningsByFilePath, path2, options = {}) {
|
|
if (!target[id]) {
|
|
target[id] = model;
|
|
return;
|
|
}
|
|
if (options.suppressDuplicateWarning) {
|
|
return;
|
|
}
|
|
pushWarning(warningsByFilePath, path2, {
|
|
code: "invalid-structure",
|
|
message: `duplicate id detected: "${id}"`,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "id"
|
|
});
|
|
}
|
|
function removeModelFromIndexes(index, model) {
|
|
delete index.modelsByFilePath[model.path];
|
|
switch (model.fileType) {
|
|
case "object":
|
|
delete index.objectsById[getModelId2(model)];
|
|
break;
|
|
case "app-process":
|
|
delete index.appProcessesById[model.id];
|
|
break;
|
|
case "screen":
|
|
delete index.screensById[model.id];
|
|
break;
|
|
case "codeset":
|
|
delete index.codesetsById[model.id];
|
|
break;
|
|
case "message":
|
|
delete index.messagesById[model.id];
|
|
break;
|
|
case "rule":
|
|
delete index.rulesById[model.id];
|
|
break;
|
|
case "mapping":
|
|
delete index.mappingsById[model.id];
|
|
break;
|
|
case "relations":
|
|
delete index.relationsFilesById[getModelId2(model)];
|
|
for (const relation of model.relations) {
|
|
if (relation.id) {
|
|
delete index.relationsById[relation.id];
|
|
}
|
|
}
|
|
break;
|
|
case "diagram":
|
|
delete index.diagramsById[getModelId2(model)];
|
|
break;
|
|
case "dfd-object":
|
|
delete index.dfdObjectsById[model.id];
|
|
break;
|
|
case "data-object":
|
|
delete index.dataObjectsById[model.id];
|
|
break;
|
|
case "dfd-diagram":
|
|
delete index.diagramsById[model.id];
|
|
break;
|
|
case "er-entity":
|
|
delete index.erEntitiesById[model.id];
|
|
delete index.erEntitiesByPhysicalName[model.physicalName];
|
|
break;
|
|
case "markdown":
|
|
break;
|
|
}
|
|
}
|
|
function addRelationForObject(relationsByObjectId, objectId, relation) {
|
|
if (!objectId.trim()) {
|
|
return;
|
|
}
|
|
if (!relationsByObjectId[objectId]) {
|
|
relationsByObjectId[objectId] = [];
|
|
}
|
|
relationsByObjectId[objectId].push(relation);
|
|
}
|
|
function addMemberCandidate(index, candidate) {
|
|
if (!candidate.ownerId.trim() || !candidate.ownerPath.trim() || !candidate.memberId.trim()) {
|
|
return;
|
|
}
|
|
pushMemberCandidate(index.membersByOwnerId, candidate.ownerId, candidate);
|
|
pushMemberCandidate(index.membersByOwnerPath, candidate.ownerPath, candidate);
|
|
}
|
|
function pushMemberCandidate(target, key, candidate) {
|
|
if (!target[key]) {
|
|
target[key] = [];
|
|
}
|
|
const exists = target[key].some(
|
|
(entry) => entry.ownerPath === candidate.ownerPath && entry.memberKind === candidate.memberKind && entry.memberId === candidate.memberId && entry.sourceSection === candidate.sourceSection && entry.displayName === candidate.displayName
|
|
);
|
|
if (!exists) {
|
|
target[key].push(candidate);
|
|
}
|
|
}
|
|
function getRelationObjectKey(rawReference, object) {
|
|
if (object) {
|
|
return getModelId2(object);
|
|
}
|
|
return rawReference.trim();
|
|
}
|
|
function getModelId2(model) {
|
|
if ("id" in model && typeof model.id === "string" && model.id.trim()) {
|
|
return model.id.trim();
|
|
}
|
|
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 getBasename2(model.path);
|
|
}
|
|
function getBasename2(path2) {
|
|
const slashNormalized = path2.replace(/\\/g, "/");
|
|
const rawName = slashNormalized.split("/").pop() ?? path2;
|
|
return rawName.replace(/\.md$/i, "");
|
|
}
|
|
function pushWarning(warningsByFilePath, path2, warning) {
|
|
if (!warningsByFilePath[path2]) {
|
|
warningsByFilePath[path2] = [];
|
|
}
|
|
const exists = warningsByFilePath[path2].some(
|
|
(entry) => entry.code === warning.code && entry.message === warning.message && entry.field === warning.field
|
|
);
|
|
if (!exists) {
|
|
warningsByFilePath[path2].push(warning);
|
|
}
|
|
}
|
|
|
|
// src/utils/model-navigation.ts
|
|
var import_obsidian5 = require("obsidian");
|
|
async function openModelObjectNote(app, index, objectId, options = {}) {
|
|
const model = index.objectsById[objectId] ?? index.erEntitiesById[objectId] ?? index.dfdObjectsById[objectId];
|
|
if (!model) {
|
|
return {
|
|
ok: false,
|
|
reason: `Object "${objectId}" was not found in the current index.`
|
|
};
|
|
}
|
|
const file = app.vault.getAbstractFileByPath(model.path);
|
|
if (!(file instanceof import_obsidian5.TFile)) {
|
|
return {
|
|
ok: false,
|
|
reason: `Note for object "${objectId}" could not be opened.`
|
|
};
|
|
}
|
|
const leaf = options.openInNewLeaf ? app.workspace.getLeaf(true) : findExistingMarkdownLeaf(app, options.sourcePath) ?? app.workspace.getMostRecentLeaf();
|
|
if (!leaf) {
|
|
return {
|
|
ok: false,
|
|
reason: `No target tab was available to open "${objectId}".`
|
|
};
|
|
}
|
|
await leaf.openFile(file);
|
|
return { ok: true };
|
|
}
|
|
function findExistingMarkdownLeaf(app, sourcePath) {
|
|
if (!sourcePath) {
|
|
return null;
|
|
}
|
|
const markdownLeaves = app.workspace.getLeavesOfType("markdown");
|
|
for (const leaf of markdownLeaves) {
|
|
const viewFile = leaf.view.file ?? null;
|
|
if (viewFile?.path === sourcePath) {
|
|
return leaf;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// src/views/modeling-preview-view.ts
|
|
var import_obsidian7 = require("obsidian");
|
|
|
|
// src/core/object-subgraph-builder.ts
|
|
function buildObjectSubgraphScene(context) {
|
|
const centerId = getFocusObjectId(context.object);
|
|
const nodes = /* @__PURE__ */ new Map();
|
|
const edges = /* @__PURE__ */ new Map();
|
|
nodes.set(centerId, {
|
|
id: centerId,
|
|
ref: centerId,
|
|
object: context.object
|
|
});
|
|
for (const entry of context.relatedObjects) {
|
|
if (entry.relatedObject) {
|
|
nodes.set(entry.relatedObjectId, {
|
|
id: entry.relatedObjectId,
|
|
ref: entry.relatedObjectId,
|
|
object: entry.relatedObject
|
|
});
|
|
}
|
|
const edge = toDiagramEdge2(entry, centerId);
|
|
if (!edge) {
|
|
continue;
|
|
}
|
|
const edgeKey = edge.id ?? `${edge.source}:${edge.target}:${edge.kind ?? ""}:${edge.label ?? ""}`;
|
|
edges.set(edgeKey, edge);
|
|
}
|
|
const kind = context.object.fileType === "er-entity" ? "er" : "class";
|
|
const diagram = {
|
|
fileType: "diagram",
|
|
schema: kind === "er" ? "er_diagram" : "class_diagram",
|
|
path: context.object.path,
|
|
title: `${getGraphTitle(context.object)} related`,
|
|
frontmatter: {
|
|
name: `${getGraphTitle(context.object)} related`
|
|
},
|
|
sections: {},
|
|
sourceLinks: [],
|
|
name: `${getGraphTitle(context.object)} related`,
|
|
kind,
|
|
objectRefs: Array.from(nodes.keys()),
|
|
nodes: Array.from(nodes.values()).map(({ object, ...node }) => node),
|
|
edges: Array.from(edges.values())
|
|
};
|
|
return {
|
|
diagram,
|
|
nodes: Array.from(nodes.values()),
|
|
edges: Array.from(edges.values()),
|
|
missingObjects: [],
|
|
warnings: []
|
|
};
|
|
}
|
|
function toDiagramEdge2(entry, centerId) {
|
|
const relatedId = entry.relatedObjectId;
|
|
if (entry.relation && "domain" in entry.relation && entry.relation.domain === "er") {
|
|
const relation2 = entry.relation;
|
|
const sourceId2 = entry.direction === "incoming" ? relatedId : centerId;
|
|
const targetId2 = entry.direction === "incoming" ? centerId : relatedId;
|
|
return {
|
|
id: relation2.id,
|
|
source: sourceId2,
|
|
target: targetId2,
|
|
kind: "association",
|
|
label: relation2.label,
|
|
metadata: {
|
|
cardinality: relation2.cardinality,
|
|
sourceColumn: relation2.mappings[0]?.localColumn,
|
|
targetColumn: relation2.mappings[0]?.targetColumn,
|
|
logicalName: relation2.label,
|
|
physicalName: relation2.id,
|
|
kind: relation2.kind,
|
|
mappingSummary: relation2.mappings.map((mapping) => `${mapping.localColumn} -> ${mapping.targetColumn}`).join(" / "),
|
|
mappings: relation2.mappings
|
|
}
|
|
};
|
|
}
|
|
const relation = normalizeClassRelation(entry.relation);
|
|
const sourceId = entry.direction === "incoming" ? relatedId : centerId;
|
|
const targetId = entry.direction === "incoming" ? centerId : relatedId;
|
|
return {
|
|
id: relation.id,
|
|
source: sourceId,
|
|
target: targetId,
|
|
kind: relation.kind,
|
|
label: relation.label,
|
|
metadata: {
|
|
notes: relation.notes,
|
|
sourceCardinality: relation.fromMultiplicity,
|
|
targetCardinality: relation.toMultiplicity
|
|
}
|
|
};
|
|
}
|
|
function normalizeClassRelation(relation) {
|
|
if ("domain" in relation) {
|
|
if (relation.domain === "class") {
|
|
return relation;
|
|
}
|
|
return toClassRelationEdge({
|
|
id: relation.id,
|
|
kind: "association",
|
|
source: relation.source,
|
|
target: relation.target,
|
|
label: relation.label
|
|
});
|
|
}
|
|
return toClassRelationEdge(relation);
|
|
}
|
|
function getGraphTitle(object) {
|
|
return object.fileType === "er-entity" ? object.logicalName : object.name;
|
|
}
|
|
function getObjectId4(object) {
|
|
const explicitId = object.frontmatter.id;
|
|
if (typeof explicitId === "string" && explicitId.trim()) {
|
|
return explicitId.trim();
|
|
}
|
|
return object.name;
|
|
}
|
|
function getFocusObjectId(object) {
|
|
return object.fileType === "er-entity" ? object.id : getObjectId4(object);
|
|
}
|
|
|
|
// src/renderers/mermaid-helpers.ts
|
|
function sanitizeMermaidId(input) {
|
|
const normalized = (input || "node").replace(/[^A-Za-z0-9_]/g, "_");
|
|
if (/^[A-Za-z_]/.test(normalized)) {
|
|
return normalized;
|
|
}
|
|
return `N_${normalized}`;
|
|
}
|
|
function ensureUniqueMermaidId(baseId, usedIds) {
|
|
let candidate = baseId || "node";
|
|
let index = 2;
|
|
while (usedIds.has(candidate)) {
|
|
candidate = `${baseId}_${index}`;
|
|
index += 1;
|
|
}
|
|
usedIds.add(candidate);
|
|
return candidate;
|
|
}
|
|
function escapeMermaidLabel(input) {
|
|
return splitMermaidTextLines(input).map(escapeMermaidTextSegment).join("<br/>");
|
|
}
|
|
function escapeMermaidEdgeLabel(input) {
|
|
return escapeMermaidTextSegment(input).replace(/[[\]{}()]/g, " ").replace(/\s+/g, " ").trim();
|
|
}
|
|
function toMermaidQuotedLabel(input) {
|
|
return `"${splitMermaidTextLines(input).map(escapeMermaidQuotedTextSegment).join("<br/>")}"`;
|
|
}
|
|
function formatMermaidMember(input) {
|
|
return escapeMermaidTextSegment(input).replace(/\s+/g, " ").trim();
|
|
}
|
|
function splitMermaidTextLines(input) {
|
|
return String(input).replace(/\r\n?/g, "\n").split("\n");
|
|
}
|
|
function escapeMermaidTextSegment(input) {
|
|
return String(input).replace(/&/g, "&").replace(/"/g, """).replace(/\|/g, "/").replace(/</g, "<").replace(/>/g, ">").replace(/\[/g, "[").replace(/\]/g, "]");
|
|
}
|
|
function escapeMermaidQuotedTextSegment(input) {
|
|
return String(input).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/</g, "<").replace(/>/g, ">").replace(/\[/g, "#91;").replace(/\]/g, "#93;");
|
|
}
|
|
|
|
// src/renderers/graph-layout.ts
|
|
function buildGraphLayout(nodes, edges, options) {
|
|
if (nodes.length === 0) {
|
|
return {
|
|
nodes: [],
|
|
byId: {},
|
|
width: options.canvasPadding * 2,
|
|
height: options.canvasPadding * 2
|
|
};
|
|
}
|
|
const nodeSizes = /* @__PURE__ */ new Map();
|
|
const originalIndex = /* @__PURE__ */ new Map();
|
|
for (const [index, node] of nodes.entries()) {
|
|
nodeSizes.set(node.id, {
|
|
width: options.getWidth(node),
|
|
height: options.getHeight(node)
|
|
});
|
|
originalIndex.set(node.id, index);
|
|
}
|
|
const maxWidth = Math.max(...nodes.map((node) => nodeSizes.get(node.id)?.width ?? 0));
|
|
const maxHeight = Math.max(...nodes.map((node) => nodeSizes.get(node.id)?.height ?? 0));
|
|
const columnCount = clamp2(
|
|
deriveColumnCount(nodes.length),
|
|
options.minColumns ?? 1,
|
|
options.maxColumns ?? 4
|
|
);
|
|
const rowCount = Math.ceil(nodes.length / columnCount);
|
|
const cellWidth = maxWidth + options.columnGap;
|
|
const cellHeight = maxHeight + options.rowGap;
|
|
const degrees = buildDegreeMap(nodes, edges);
|
|
const neighborMap = buildNeighborMap(nodes, edges, originalIndex);
|
|
const sortedNodes = [...nodes].sort((left, right) => {
|
|
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
if (degreeDelta !== 0) {
|
|
return degreeDelta;
|
|
}
|
|
const barycenterDelta = getNeighborBarycenter(left.id, neighborMap) - getNeighborBarycenter(right.id, neighborMap);
|
|
if (barycenterDelta !== 0) {
|
|
return barycenterDelta;
|
|
}
|
|
return (originalIndex.get(left.id) ?? 0) - (originalIndex.get(right.id) ?? 0);
|
|
});
|
|
const slots = buildSlots(columnCount, rowCount);
|
|
const slotAssignments = /* @__PURE__ */ new Map();
|
|
for (const [index, node] of sortedNodes.entries()) {
|
|
const slot = slots[index];
|
|
if (slot) {
|
|
slotAssignments.set(node.id, slot);
|
|
}
|
|
}
|
|
optimizeAssignments(slotAssignments, nodes, edges, columnCount);
|
|
const layouts = [];
|
|
const byId = {};
|
|
for (const node of nodes) {
|
|
const slot = slotAssignments.get(node.id);
|
|
if (!slot) {
|
|
continue;
|
|
}
|
|
const size = nodeSizes.get(node.id) ?? { width: maxWidth, height: maxHeight };
|
|
const x = options.canvasPadding + slot.col * cellWidth + Math.max(0, (maxWidth - size.width) / 2);
|
|
const y = options.canvasPadding + slot.row * cellHeight + Math.max(0, (maxHeight - size.height) / 2);
|
|
const layout = {
|
|
node,
|
|
x,
|
|
y,
|
|
width: size.width,
|
|
height: size.height
|
|
};
|
|
layouts.push(layout);
|
|
byId[node.id] = layout;
|
|
}
|
|
return {
|
|
nodes: layouts,
|
|
byId,
|
|
width: options.canvasPadding * 2 + columnCount * maxWidth + Math.max(0, columnCount - 1) * options.columnGap,
|
|
height: options.canvasPadding * 2 + rowCount * maxHeight + Math.max(0, rowCount - 1) * options.rowGap
|
|
};
|
|
}
|
|
function deriveColumnCount(nodeCount) {
|
|
if (nodeCount >= 10) {
|
|
return 4;
|
|
}
|
|
if (nodeCount >= 5) {
|
|
return 3;
|
|
}
|
|
if (nodeCount >= 2) {
|
|
return 2;
|
|
}
|
|
return 1;
|
|
}
|
|
function buildDegreeMap(nodes, edges) {
|
|
const degrees = /* @__PURE__ */ new Map();
|
|
for (const node of nodes) {
|
|
degrees.set(node.id, 0);
|
|
}
|
|
for (const edge of edges) {
|
|
degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + 1);
|
|
degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + 1);
|
|
}
|
|
return degrees;
|
|
}
|
|
function buildNeighborMap(nodes, edges, originalIndex) {
|
|
const neighborMap = /* @__PURE__ */ new Map();
|
|
for (const node of nodes) {
|
|
neighborMap.set(node.id, []);
|
|
}
|
|
for (const edge of edges) {
|
|
neighborMap.get(edge.source)?.push(originalIndex.get(edge.target) ?? 0);
|
|
neighborMap.get(edge.target)?.push(originalIndex.get(edge.source) ?? 0);
|
|
}
|
|
return neighborMap;
|
|
}
|
|
function getNeighborBarycenter(nodeId, neighborMap) {
|
|
const indices = neighborMap.get(nodeId) ?? [];
|
|
if (indices.length === 0) {
|
|
return Number.MAX_SAFE_INTEGER;
|
|
}
|
|
return indices.reduce((sum, value) => sum + value, 0) / indices.length;
|
|
}
|
|
function buildSlots(columnCount, rowCount) {
|
|
const centerColumn = (columnCount - 1) / 2;
|
|
const centerRow = (rowCount - 1) / 2;
|
|
const slots = [];
|
|
for (let row = 0; row < rowCount; row += 1) {
|
|
for (let col = 0; col < columnCount; col += 1) {
|
|
const centerDistance = Math.abs(col - centerColumn) * 1.2 + Math.abs(row - centerRow);
|
|
slots.push({ col, row, centerDistance });
|
|
}
|
|
}
|
|
return slots.sort((left, right) => {
|
|
if (left.centerDistance !== right.centerDistance) {
|
|
return left.centerDistance - right.centerDistance;
|
|
}
|
|
if (left.row !== right.row) {
|
|
return left.row - right.row;
|
|
}
|
|
return left.col - right.col;
|
|
});
|
|
}
|
|
function optimizeAssignments(assignments, nodes, edges, columnCount) {
|
|
const orderedIds = [...nodes.map((node) => node.id)].sort((left, right) => {
|
|
const leftSlot = assignments.get(left);
|
|
const rightSlot = assignments.get(right);
|
|
if (!leftSlot || !rightSlot) {
|
|
return 0;
|
|
}
|
|
if (leftSlot.row !== rightSlot.row) {
|
|
return leftSlot.row - rightSlot.row;
|
|
}
|
|
return leftSlot.col - rightSlot.col;
|
|
});
|
|
for (let pass = 0; pass < 2; pass += 1) {
|
|
for (let index = 0; index < orderedIds.length - 1; index += 1) {
|
|
const leftId = orderedIds[index];
|
|
const rightId = orderedIds[index + 1];
|
|
const leftSlot = assignments.get(leftId);
|
|
const rightSlot = assignments.get(rightId);
|
|
if (!leftSlot || !rightSlot) {
|
|
continue;
|
|
}
|
|
const rowGap = Math.abs(leftSlot.row - rightSlot.row);
|
|
const colGap = Math.abs(leftSlot.col - rightSlot.col);
|
|
if (rowGap + colGap !== 1) {
|
|
continue;
|
|
}
|
|
const currentCost = estimateLayoutCost(assignments, edges, columnCount);
|
|
assignments.set(leftId, rightSlot);
|
|
assignments.set(rightId, leftSlot);
|
|
const swappedCost = estimateLayoutCost(assignments, edges, columnCount);
|
|
if (swappedCost >= currentCost) {
|
|
assignments.set(leftId, leftSlot);
|
|
assignments.set(rightId, rightSlot);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function estimateLayoutCost(assignments, edges, columnCount) {
|
|
let cost = 0;
|
|
for (const edge of edges) {
|
|
const source = assignments.get(edge.source);
|
|
const target = assignments.get(edge.target);
|
|
if (!source || !target) {
|
|
continue;
|
|
}
|
|
const dx = Math.abs(source.col - target.col);
|
|
const dy = Math.abs(source.row - target.row);
|
|
cost += dx * 3 + dy * 2;
|
|
if (dx > Math.max(1, columnCount - 2)) {
|
|
cost += 2;
|
|
}
|
|
}
|
|
return cost;
|
|
}
|
|
function clamp2(value, min, max) {
|
|
return Math.min(max, Math.max(min, value));
|
|
}
|
|
|
|
// src/renderers/class-renderer.ts
|
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
var NODE_WIDTH = 300;
|
|
var HEADER_HEIGHT = 38;
|
|
var SECTION_TITLE_HEIGHT = 24;
|
|
var ROW_HEIGHT = 20;
|
|
var NODE_PADDING = 12;
|
|
var LIST_INDENT_WIDTH = 18;
|
|
var ESTIMATED_ROW_CHAR_WIDTH = 7;
|
|
var COLUMN_GAP = 96;
|
|
var ROW_GAP = 92;
|
|
var CANVAS_PADDING = 48;
|
|
var CLASS_CARD_DECORATION_BOUNDS_PADDING = 10;
|
|
var CLASS_ROW_AVAILABLE_CHARS = Math.max(
|
|
1,
|
|
Math.floor((NODE_WIDTH - NODE_PADDING * 2 - LIST_INDENT_WIDTH) / ESTIMATED_ROW_CHAR_WIDTH)
|
|
);
|
|
var DEFAULT_ATTRIBUTE_LIMIT = 5;
|
|
var DEFAULT_METHOD_LIMIT = 5;
|
|
var MIN_ZOOM2 = 0.45;
|
|
var MAX_ZOOM2 = 2.4;
|
|
var INITIAL_ZOOM2 = 1;
|
|
var DIAGRAM_LABEL_BG = "#ffffff";
|
|
var DIAGRAM_LABEL_BORDER = "#e5e7eb";
|
|
var DIAGRAM_LABEL_TEXT = "#111827";
|
|
var DIAGRAM_EDGE = "#374151";
|
|
function renderClassDiagram(diagram, options) {
|
|
const root = document.createElement("section");
|
|
root.addClass("model-weave-diagram-shell");
|
|
if (!options?.hideTitle) {
|
|
const title = document.createElement("h2");
|
|
title.textContent = `${diagram.diagram.name} (class)`;
|
|
title.addClass("model-weave-diagram-title");
|
|
root.appendChild(title);
|
|
}
|
|
const layout = createLayout(
|
|
diagram.nodes,
|
|
diagram.edges
|
|
);
|
|
const sceneBounds = createSceneBounds(diagram.edges, layout.byId);
|
|
const canvas = document.createElement("div");
|
|
canvas.addClass("model-weave-diagram-canvas");
|
|
if (!options?.forExport) {
|
|
canvas.addClass("model-weave-diagram-canvas-interactive");
|
|
}
|
|
const toolbar = options?.forExport ? null : createZoomToolbar("Wheel: zoom / Drag background: pan");
|
|
if (toolbar) {
|
|
root.appendChild(toolbar.root);
|
|
}
|
|
const viewport = document.createElement("div");
|
|
viewport.addClass("model-weave-diagram-viewport");
|
|
const surface = document.createElement("div");
|
|
surface.addClass("model-weave-diagram-surface");
|
|
surface.dataset.modelWeaveExportSurface = "true";
|
|
surface.dataset.modelWeaveSceneWidth = `${sceneBounds.width}`;
|
|
surface.dataset.modelWeaveSceneHeight = `${sceneBounds.height}`;
|
|
surface.setCssProps({
|
|
"--mw-scene-width": `${sceneBounds.width}px`,
|
|
"--mw-scene-height": `${sceneBounds.height}px`
|
|
});
|
|
const svg = createSvgSurface(sceneBounds.width, sceneBounds.height);
|
|
svg.appendChild(createMarkerDefinitions());
|
|
for (const edge of diagram.edges) {
|
|
const edgeGroup = renderEdge(edge, layout.byId);
|
|
if (edgeGroup) {
|
|
svg.appendChild(edgeGroup);
|
|
}
|
|
}
|
|
surface.appendChild(svg);
|
|
for (const box of layout.nodes) {
|
|
surface.appendChild(createNodeBox(box, options));
|
|
}
|
|
viewport.appendChild(surface);
|
|
canvas.appendChild(viewport);
|
|
root.appendChild(canvas);
|
|
if (toolbar) {
|
|
attachGraphViewportInteractions(canvas, surface, toolbar, sceneBounds, {
|
|
minZoom: MIN_ZOOM2,
|
|
maxZoom: MAX_ZOOM2,
|
|
initialZoom: INITIAL_ZOOM2,
|
|
nodeSelector: ".model-weave-node",
|
|
fitVerticalAlign: options?.fitVerticalAlign,
|
|
fitContentBounds: createFitContentBounds(layout.nodes),
|
|
viewportState: options?.viewportState,
|
|
onViewportStateChange: options?.onViewportStateChange
|
|
});
|
|
}
|
|
if (!options?.hideDetails) {
|
|
root.appendChild(createConnectionsTable(diagram));
|
|
}
|
|
return root;
|
|
}
|
|
function createLayout(nodes, edges) {
|
|
return buildGraphLayout(nodes, edges, {
|
|
getWidth: () => NODE_WIDTH,
|
|
getHeight: (node) => measureNodeHeight(node.object),
|
|
canvasPadding: CANVAS_PADDING,
|
|
columnGap: COLUMN_GAP,
|
|
rowGap: ROW_GAP,
|
|
maxColumns: 4
|
|
});
|
|
}
|
|
function createSceneBounds(edges, layoutById) {
|
|
const nodeBounds = Object.values(layoutById).map((layout) => ({
|
|
x: layout.x,
|
|
y: layout.y,
|
|
width: layout.width,
|
|
height: layout.height
|
|
}));
|
|
const labelBounds = estimateEdgeLabelBounds(
|
|
edges,
|
|
layoutById,
|
|
getMinimalEdgeLabel
|
|
);
|
|
return computeSceneBounds(
|
|
nodeBounds,
|
|
labelBounds,
|
|
CANVAS_PADDING + CLASS_CARD_DECORATION_BOUNDS_PADDING
|
|
);
|
|
}
|
|
function createFitContentBounds(nodes) {
|
|
if (nodes.length === 0) {
|
|
return void 0;
|
|
}
|
|
return {
|
|
top: Math.min(...nodes.map((node) => node.y)) - CLASS_CARD_DECORATION_BOUNDS_PADDING,
|
|
bottom: Math.max(...nodes.map((node) => node.y + node.height)) + CLASS_CARD_DECORATION_BOUNDS_PADDING
|
|
};
|
|
}
|
|
function measureNodeHeight(object) {
|
|
if (!object || object.fileType !== "object") {
|
|
return HEADER_HEIGHT + NODE_PADDING * 2 + ROW_HEIGHT;
|
|
}
|
|
const attributeRows = estimateVisibleRows(getVisibleAttributes(object));
|
|
const methodRows = estimateVisibleRows(getVisibleMethods(object));
|
|
return HEADER_HEIGHT + SECTION_TITLE_HEIGHT + attributeRows * ROW_HEIGHT + SECTION_TITLE_HEIGHT + methodRows * ROW_HEIGHT + NODE_PADDING * 2;
|
|
}
|
|
function estimateVisibleRows(items) {
|
|
if (items.length === 0) {
|
|
return 1;
|
|
}
|
|
return items.reduce(
|
|
(sum, item) => sum + estimateWrappedLineCount(item, CLASS_ROW_AVAILABLE_CHARS),
|
|
0
|
|
);
|
|
}
|
|
function estimateWrappedLineCount(text, availableCharsPerLine) {
|
|
const normalizedText = text.trim();
|
|
if (!normalizedText) {
|
|
return 1;
|
|
}
|
|
return Math.max(1, Math.ceil(normalizedText.length / availableCharsPerLine));
|
|
}
|
|
function createSvgSurface(width, height) {
|
|
const svg = document.createElementNS(SVG_NS, "svg");
|
|
svg.setAttribute("width", String(width));
|
|
svg.setAttribute("height", String(height));
|
|
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
svg.setAttribute("class", "model-weave-diagram-svg");
|
|
return svg;
|
|
}
|
|
function createMarkerDefinitions() {
|
|
const defs = document.createElementNS(SVG_NS, "defs");
|
|
defs.appendChild(
|
|
createTriangleMarker("mdspec-arrow-solid", DIAGRAM_EDGE, DIAGRAM_EDGE)
|
|
);
|
|
defs.appendChild(createTriangleMarker("mdspec-arrow-open", "none", DIAGRAM_EDGE));
|
|
defs.appendChild(
|
|
createDiamondMarker("mdspec-diamond-open", "none", DIAGRAM_EDGE)
|
|
);
|
|
defs.appendChild(
|
|
createDiamondMarker(
|
|
"mdspec-diamond-solid",
|
|
DIAGRAM_EDGE,
|
|
DIAGRAM_EDGE
|
|
)
|
|
);
|
|
return defs;
|
|
}
|
|
function createTriangleMarker(id, fill, stroke) {
|
|
const marker = document.createElementNS(SVG_NS, "marker");
|
|
marker.setAttribute("id", id);
|
|
marker.setAttribute("markerWidth", "12");
|
|
marker.setAttribute("markerHeight", "12");
|
|
marker.setAttribute("refX", "10");
|
|
marker.setAttribute("refY", "6");
|
|
marker.setAttribute("orient", "auto");
|
|
marker.setAttribute("markerUnits", "strokeWidth");
|
|
const path2 = document.createElementNS(SVG_NS, "path");
|
|
path2.setAttribute("d", "M 0 0 L 10 6 L 0 12 z");
|
|
path2.setAttribute("fill", fill);
|
|
path2.setAttribute("stroke", stroke);
|
|
path2.setAttribute("stroke-width", "1.2");
|
|
marker.appendChild(path2);
|
|
return marker;
|
|
}
|
|
function createDiamondMarker(id, fill, stroke) {
|
|
const marker = document.createElementNS(SVG_NS, "marker");
|
|
marker.setAttribute("id", id);
|
|
marker.setAttribute("markerWidth", "14");
|
|
marker.setAttribute("markerHeight", "14");
|
|
marker.setAttribute("refX", "12");
|
|
marker.setAttribute("refY", "7");
|
|
marker.setAttribute("orient", "auto");
|
|
marker.setAttribute("markerUnits", "strokeWidth");
|
|
const path2 = document.createElementNS(SVG_NS, "path");
|
|
path2.setAttribute("d", "M 0 7 L 4 0 L 12 7 L 4 14 z");
|
|
path2.setAttribute("fill", fill);
|
|
path2.setAttribute("stroke", stroke);
|
|
path2.setAttribute("stroke-width", "1.2");
|
|
marker.appendChild(path2);
|
|
return marker;
|
|
}
|
|
function renderEdge(edge, layoutById) {
|
|
const source = layoutById[edge.source];
|
|
const target = layoutById[edge.target];
|
|
if (!source || !target) {
|
|
return null;
|
|
}
|
|
const group = document.createElementNS(SVG_NS, "g");
|
|
const { startX, startY, endX, endY, midX, midY } = getConnectionPoints(
|
|
source,
|
|
target
|
|
);
|
|
const line = document.createElementNS(SVG_NS, "line");
|
|
line.setAttribute("x1", String(startX));
|
|
line.setAttribute("y1", String(startY));
|
|
line.setAttribute("x2", String(endX));
|
|
line.setAttribute("y2", String(endY));
|
|
line.setAttribute("stroke", DIAGRAM_EDGE);
|
|
line.setAttribute("stroke-width", "2");
|
|
line.setAttribute("stroke-dasharray", getDashPattern(edge.kind));
|
|
const markers = getMarkerAttributes(edge.kind);
|
|
if (markers.start) {
|
|
line.setAttribute("marker-start", markers.start);
|
|
}
|
|
if (markers.end) {
|
|
line.setAttribute("marker-end", markers.end);
|
|
}
|
|
group.appendChild(line);
|
|
const edgeLabel = getMinimalEdgeLabel(edge);
|
|
if (edgeLabel) {
|
|
group.appendChild(createEdgeBadge(midX, midY - 8, edgeLabel));
|
|
}
|
|
return group;
|
|
}
|
|
function getMinimalEdgeLabel(edge) {
|
|
const internalEdge = classDiagramEdgeToInternalEdge(edge);
|
|
switch (internalEdge.kind) {
|
|
case "inheritance":
|
|
return "inheritance";
|
|
case "implementation":
|
|
return "implementation";
|
|
case "dependency":
|
|
return "dependency";
|
|
case "composition":
|
|
return "composition";
|
|
case "aggregation":
|
|
return "aggregation";
|
|
case "association":
|
|
return "association";
|
|
default:
|
|
return internalEdge.kind ?? null;
|
|
}
|
|
}
|
|
function createEdgeBadge(x, y, value) {
|
|
const group = document.createElementNS(SVG_NS, "g");
|
|
const width = Math.max(52, value.length * 8 + 12);
|
|
const height = 20;
|
|
const rect = document.createElementNS(SVG_NS, "rect");
|
|
rect.setAttribute("x", String(x - width / 2));
|
|
rect.setAttribute("y", String(y - height / 2));
|
|
rect.setAttribute("width", String(width));
|
|
rect.setAttribute("height", String(height));
|
|
rect.setAttribute("rx", "10");
|
|
rect.setAttribute("fill", DIAGRAM_LABEL_BG);
|
|
rect.setAttribute("stroke", DIAGRAM_LABEL_BORDER);
|
|
group.appendChild(rect);
|
|
const text = document.createElementNS(SVG_NS, "text");
|
|
text.setAttribute("x", String(x));
|
|
text.setAttribute("y", String(y + 4));
|
|
text.setAttribute("text-anchor", "middle");
|
|
text.setAttribute("font-size", "11px");
|
|
text.setAttribute("font-weight", "600");
|
|
text.setAttribute("fill", DIAGRAM_LABEL_TEXT);
|
|
text.textContent = value;
|
|
group.appendChild(text);
|
|
return group;
|
|
}
|
|
function getDashPattern(kind) {
|
|
switch (kind) {
|
|
case "dependency":
|
|
case "implementation":
|
|
return "8 6";
|
|
default:
|
|
return "0";
|
|
}
|
|
}
|
|
function getMarkerAttributes(kind) {
|
|
switch (kind) {
|
|
case "inheritance":
|
|
return { end: "url(#mdspec-arrow-open)" };
|
|
case "implementation":
|
|
return { end: "url(#mdspec-arrow-open)" };
|
|
case "dependency":
|
|
return { end: "url(#mdspec-arrow-solid)" };
|
|
case "aggregation":
|
|
return { start: "url(#mdspec-diamond-open)" };
|
|
case "composition":
|
|
return { start: "url(#mdspec-diamond-solid)" };
|
|
case "association":
|
|
case "reference":
|
|
case "flow":
|
|
default:
|
|
return { end: "url(#mdspec-arrow-solid)" };
|
|
}
|
|
}
|
|
function createNodeBox(layout, options) {
|
|
const box = document.createElement("article");
|
|
box.addClass("model-weave-node");
|
|
box.addClass(
|
|
layout.node.object?.fileType === "object" && layout.node.object.kind === "interface" ? "model-weave-node-interface" : "model-weave-node-class"
|
|
);
|
|
box.setCssProps({
|
|
"--mw-node-x": `${layout.x}px`,
|
|
"--mw-node-y": `${layout.y}px`,
|
|
"--mw-node-width": `${layout.width}px`,
|
|
"--mw-node-height": `${layout.height}px`
|
|
});
|
|
if (!layout.node.object) {
|
|
box.appendChild(createFallbackNode(layout.node.label ?? layout.node.ref ?? layout.node.id));
|
|
return box;
|
|
}
|
|
if (options?.onOpenObject) {
|
|
box.addClass("model-weave-node-clickable");
|
|
box.setAttribute("role", "button");
|
|
box.setAttribute("tabindex", "0");
|
|
box.title = `Open ${layout.node.label ?? (layout.node.object.fileType === "object" ? layout.node.object.name : layout.node.object.logicalName)}`;
|
|
box.addEventListener("click", (event) => {
|
|
if (event.defaultPrevented) {
|
|
return;
|
|
}
|
|
options.onOpenObject?.(layout.node.id, {
|
|
openInNewLeaf: event.ctrlKey || event.metaKey
|
|
});
|
|
});
|
|
box.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
options.onOpenObject?.(layout.node.id, { openInNewLeaf: false });
|
|
}
|
|
});
|
|
box.addEventListener("pointerdown", (event) => {
|
|
event.stopPropagation();
|
|
});
|
|
}
|
|
const object = layout.node.object;
|
|
if (object.fileType !== "object") {
|
|
box.appendChild(createFallbackNode(layout.node.label ?? object.logicalName));
|
|
return box;
|
|
}
|
|
const header = document.createElement("header");
|
|
header.addClass("model-weave-node-header");
|
|
header.addClass(getHeaderModifierClass(object.kind));
|
|
const kind = document.createElement("div");
|
|
kind.addClass("model-weave-node-kind");
|
|
kind.textContent = object.kind;
|
|
const title = document.createElement("div");
|
|
title.addClass("model-weave-node-title");
|
|
title.textContent = layout.node.label ?? object.name;
|
|
header.append(kind, title);
|
|
box.appendChild(header);
|
|
box.appendChild(createNodeSection("Attributes", getVisibleAttributes(object)));
|
|
box.appendChild(createNodeSection("Methods", getVisibleMethods(object)));
|
|
return box;
|
|
}
|
|
function getVisibleAttributes(object) {
|
|
const visible = object.attributes.slice(0, DEFAULT_ATTRIBUTE_LIMIT).map((attribute) => {
|
|
const detail = attribute.type ? `: ${attribute.type}` : "";
|
|
return `${attribute.name}${detail}`;
|
|
});
|
|
if (object.attributes.length > DEFAULT_ATTRIBUTE_LIMIT) {
|
|
visible.push("...");
|
|
}
|
|
return visible;
|
|
}
|
|
function getVisibleMethods(object) {
|
|
const visible = object.methods.slice(0, DEFAULT_METHOD_LIMIT).map((method) => {
|
|
const parameters = method.parameters.map(
|
|
(parameter) => `${parameter.name}${parameter.type ? `: ${parameter.type}` : ""}`
|
|
).join(", ");
|
|
const returnType = method.returnType ? ` ${method.returnType}` : "";
|
|
return `${method.name}(${parameters})${returnType}`;
|
|
});
|
|
if (object.methods.length > DEFAULT_METHOD_LIMIT) {
|
|
visible.push("...");
|
|
}
|
|
return visible;
|
|
}
|
|
function createNodeSection(title, items) {
|
|
const section = document.createElement("section");
|
|
section.addClass("model-weave-node-section");
|
|
const heading = document.createElement("div");
|
|
heading.addClass("model-weave-node-section-heading");
|
|
heading.textContent = title;
|
|
section.appendChild(heading);
|
|
if (items.length === 0) {
|
|
const empty = document.createElement("div");
|
|
empty.addClass("model-weave-node-empty");
|
|
empty.textContent = "None";
|
|
section.appendChild(empty);
|
|
return section;
|
|
}
|
|
const list = document.createElement("ul");
|
|
list.addClass("model-weave-node-list");
|
|
for (const item of items) {
|
|
const entry = document.createElement("li");
|
|
entry.textContent = item;
|
|
list.appendChild(entry);
|
|
}
|
|
section.appendChild(list);
|
|
return section;
|
|
}
|
|
function getHeaderModifierClass(kind) {
|
|
switch (kind) {
|
|
case "interface":
|
|
return "model-weave-node-header-interface";
|
|
case "enum":
|
|
return "model-weave-node-header-enum";
|
|
case "component":
|
|
return "model-weave-node-header-component";
|
|
case "entity":
|
|
return "model-weave-node-header-entity";
|
|
case "class":
|
|
default:
|
|
return "model-weave-node-header-class";
|
|
}
|
|
}
|
|
function createConnectionsTable(diagram) {
|
|
const section = document.createElement("details");
|
|
section.addClass("model-weave-diagram-details");
|
|
section.open = false;
|
|
const summary = document.createElement("summary");
|
|
summary.textContent = `Displayed relations (${diagram.edges.length})`;
|
|
summary.addClass("model-weave-diagram-details-summary");
|
|
section.appendChild(summary);
|
|
if (diagram.edges.length === 0) {
|
|
const empty = document.createElement("p");
|
|
empty.textContent = modelWeaveText(
|
|
"No relations are currently used for rendering.",
|
|
"\u63CF\u753B\u306B\u4F7F\u308F\u308C\u3066\u3044\u308B relation \u306F\u3042\u308A\u307E\u305B\u3093\u3002"
|
|
);
|
|
empty.addClass("model-weave-diagram-details-empty");
|
|
section.appendChild(empty);
|
|
return section;
|
|
}
|
|
const list = document.createElement("ul");
|
|
list.addClass("model-weave-diagram-details-list");
|
|
const sortedEdges = [...diagram.edges].sort(compareClassEdges);
|
|
for (const edge of sortedEdges) {
|
|
const internalEdge = classDiagramEdgeToInternalEdge(edge);
|
|
const details = buildEdgeDetails(internalEdge);
|
|
const item = document.createElement("li");
|
|
item.addClass("model-weave-diagram-details-item");
|
|
item.textContent = `${internalEdge.id || "-"} / ${internalEdge.sourceClass} -> ${internalEdge.targetClass} / ${internalEdge.kind || "-"} / ${internalEdge.label || "-"}${details ? ` / ${details}` : ""}${internalEdge.notes ? ` / ${internalEdge.notes}` : ""}`;
|
|
list.appendChild(item);
|
|
}
|
|
section.appendChild(list);
|
|
return section;
|
|
}
|
|
function buildEdgeDetails(edge) {
|
|
const parts = [];
|
|
if (edge.fromMultiplicity) {
|
|
parts.push(`from: ${edge.fromMultiplicity}`);
|
|
}
|
|
if (edge.toMultiplicity) {
|
|
parts.push(`to: ${edge.toMultiplicity}`);
|
|
}
|
|
return parts.join(" / ");
|
|
}
|
|
function createFallbackNode(id) {
|
|
const box = document.createElement("div");
|
|
box.addClass("model-weave-node-empty");
|
|
box.textContent = `Unresolved object: ${id}`;
|
|
return box;
|
|
}
|
|
function compareClassEdges(left, right) {
|
|
const leftEdge = classDiagramEdgeToInternalEdge(left);
|
|
const rightEdge = classDiagramEdgeToInternalEdge(right);
|
|
const sourceCompare = leftEdge.sourceClass.localeCompare(rightEdge.sourceClass);
|
|
if (sourceCompare !== 0) {
|
|
return sourceCompare;
|
|
}
|
|
const targetCompare = leftEdge.targetClass.localeCompare(rightEdge.targetClass);
|
|
if (targetCompare !== 0) {
|
|
return targetCompare;
|
|
}
|
|
return (leftEdge.id || "").localeCompare(rightEdge.id || "");
|
|
}
|
|
|
|
// src/renderers/er-shared.ts
|
|
var SVG_NS2 = "http://www.w3.org/2000/svg";
|
|
var DEFAULT_COLUMN_LIMIT = 5;
|
|
var ER_LABEL_BG = "#ffffff";
|
|
var ER_LABEL_BORDER = "#e5e7eb";
|
|
var ER_LABEL_TEXT = "#111827";
|
|
function getVisibleErColumns(columns, options) {
|
|
const highlighted = new Set(
|
|
(options?.highlightedColumns ?? []).map((value) => value.trim()).filter(Boolean)
|
|
);
|
|
const limit = options?.limit ?? DEFAULT_COLUMN_LIMIT;
|
|
const prioritized = [...columns].sort((left, right) => {
|
|
const leftScore = getColumnPriority(left, highlighted);
|
|
const rightScore = getColumnPriority(right, highlighted);
|
|
if (leftScore !== rightScore) {
|
|
return rightScore - leftScore;
|
|
}
|
|
return columns.indexOf(left) - columns.indexOf(right);
|
|
});
|
|
const visible = prioritized.slice(0, limit).map(formatErColumnLabel);
|
|
if (columns.length > limit) {
|
|
visible.push("...");
|
|
}
|
|
return visible;
|
|
}
|
|
function createErCardinalityBadge(x, y, value) {
|
|
const group = document.createElementNS(SVG_NS2, "g");
|
|
const width = Math.max(34, value.length * 8 + 12);
|
|
const height = 20;
|
|
const rect = document.createElementNS(SVG_NS2, "rect");
|
|
rect.setAttribute("x", String(x - width / 2));
|
|
rect.setAttribute("y", String(y - height / 2));
|
|
rect.setAttribute("width", String(width));
|
|
rect.setAttribute("height", String(height));
|
|
rect.setAttribute("rx", "10");
|
|
rect.setAttribute("fill", ER_LABEL_BG);
|
|
rect.setAttribute("stroke", ER_LABEL_BORDER);
|
|
group.appendChild(rect);
|
|
const text = document.createElementNS(SVG_NS2, "text");
|
|
text.setAttribute("x", String(x));
|
|
text.setAttribute("y", String(y + 4));
|
|
text.setAttribute("text-anchor", "middle");
|
|
text.setAttribute("font-size", "11px");
|
|
text.setAttribute("font-weight", "600");
|
|
text.setAttribute("fill", ER_LABEL_TEXT);
|
|
text.textContent = value;
|
|
group.appendChild(text);
|
|
return group;
|
|
}
|
|
function formatErColumnLabel(column) {
|
|
const parts = [`${column.logicalName} / ${column.physicalName}`, `: ${column.dataType}`];
|
|
if (column.pk) {
|
|
parts.push(" [PK]");
|
|
}
|
|
return parts.join("");
|
|
}
|
|
function getColumnPriority(column, highlighted) {
|
|
if (highlighted.has(column.physicalName) || highlighted.has(column.logicalName)) {
|
|
return column.pk ? 5 : 4;
|
|
}
|
|
if (column.pk) {
|
|
return 3;
|
|
}
|
|
const name = `${column.logicalName} ${column.physicalName}`.toLowerCase();
|
|
if (name.includes("id") || name.includes("_cd") || name.includes("code")) {
|
|
return 2;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// src/renderers/er-renderer.ts
|
|
var SVG_NS3 = "http://www.w3.org/2000/svg";
|
|
var NODE_WIDTH2 = 280;
|
|
var HEADER_HEIGHT2 = 40;
|
|
var SECTION_TITLE_HEIGHT2 = 24;
|
|
var ROW_HEIGHT2 = 20;
|
|
var NODE_PADDING2 = 12;
|
|
var COLUMN_GAP2 = 96;
|
|
var ROW_GAP2 = 92;
|
|
var CANVAS_PADDING2 = 48;
|
|
var MIN_ZOOM3 = 0.45;
|
|
var MAX_ZOOM3 = 2.4;
|
|
var INITIAL_ZOOM3 = 1;
|
|
var DIAGRAM_EDGE2 = "#374151";
|
|
var ER_EDGE_STROKE_WIDTH = 2;
|
|
var ER_NODE_BORDER_WIDTH = 1;
|
|
var ER_ARROW_MARKER_WIDTH = 14;
|
|
var ER_ARROW_MARKER_HEIGHT = 14;
|
|
var ER_ARROW_TIP_X = 12;
|
|
var ER_ARROW_TIP_Y = 7;
|
|
var ER_ARROW_EXTRA_PADDING = 6;
|
|
var ER_DIAMOND_MARKER_WIDTH = 14;
|
|
var ER_DIAMOND_MARKER_HEIGHT = 14;
|
|
var ER_DIAMOND_TIP_X = 12;
|
|
var ER_DIAMOND_TIP_Y = 7;
|
|
var ER_DIAMOND_EXTRA_PADDING = 4;
|
|
var ER_MIN_EDGE_VISIBLE_LENGTH = 14;
|
|
function renderErDiagram(diagram, options) {
|
|
const root = document.createElement("section");
|
|
root.addClass("model-weave-diagram-shell");
|
|
if (!options?.hideTitle) {
|
|
const title = document.createElement("h2");
|
|
title.textContent = `${diagram.diagram.name} (ER)`;
|
|
title.addClass("model-weave-diagram-title");
|
|
root.appendChild(title);
|
|
}
|
|
const layout = createLayout2(
|
|
diagram.nodes,
|
|
diagram.edges
|
|
);
|
|
const sceneBounds = createSceneBounds2(diagram.edges, layout.byId);
|
|
const canvas = document.createElement("div");
|
|
canvas.addClass("model-weave-diagram-canvas");
|
|
if (!options?.forExport) {
|
|
canvas.addClass("model-weave-diagram-canvas-interactive");
|
|
}
|
|
const toolbar = options?.forExport ? null : createZoomToolbar("Wheel: zoom / Drag background: pan");
|
|
if (toolbar) {
|
|
root.appendChild(toolbar.root);
|
|
}
|
|
const viewport = document.createElement("div");
|
|
viewport.addClass("model-weave-diagram-viewport");
|
|
const surface = document.createElement("div");
|
|
surface.addClass("model-weave-diagram-surface");
|
|
surface.dataset.modelWeaveExportSurface = "true";
|
|
surface.dataset.modelWeaveSceneWidth = `${sceneBounds.width}`;
|
|
surface.dataset.modelWeaveSceneHeight = `${sceneBounds.height}`;
|
|
surface.setCssProps({
|
|
"--mw-scene-width": `${sceneBounds.width}px`,
|
|
"--mw-scene-height": `${sceneBounds.height}px`
|
|
});
|
|
const svg = createSvgSurface2(sceneBounds.width, sceneBounds.height);
|
|
svg.appendChild(createMarkerDefinitions2());
|
|
for (const edge of diagram.edges) {
|
|
const edgeGroup = renderEdge2(edge, layout.byId);
|
|
if (edgeGroup) {
|
|
svg.appendChild(edgeGroup);
|
|
}
|
|
}
|
|
surface.appendChild(svg);
|
|
for (const box of layout.nodes) {
|
|
surface.appendChild(createEntityBox(box, options));
|
|
}
|
|
viewport.appendChild(surface);
|
|
canvas.appendChild(viewport);
|
|
root.appendChild(canvas);
|
|
if (toolbar) {
|
|
attachGraphViewportInteractions(canvas, surface, toolbar, sceneBounds, {
|
|
minZoom: MIN_ZOOM3,
|
|
maxZoom: MAX_ZOOM3,
|
|
initialZoom: INITIAL_ZOOM3,
|
|
nodeSelector: ".model-weave-node",
|
|
fitVerticalAlign: options?.fitVerticalAlign,
|
|
fitContentBounds: createFitContentBounds2(layout.nodes),
|
|
viewportState: options?.viewportState,
|
|
onViewportStateChange: options?.onViewportStateChange
|
|
});
|
|
}
|
|
if (!options?.hideDetails) {
|
|
root.appendChild(createRelationTable(diagram));
|
|
}
|
|
return root;
|
|
}
|
|
function createLayout2(nodes, edges) {
|
|
return buildGraphLayout(nodes, edges, {
|
|
getWidth: () => NODE_WIDTH2,
|
|
getHeight: (node) => measureNodeHeight2(node.object),
|
|
canvasPadding: CANVAS_PADDING2,
|
|
columnGap: COLUMN_GAP2,
|
|
rowGap: ROW_GAP2,
|
|
maxColumns: 4
|
|
});
|
|
}
|
|
function createSceneBounds2(edges, layoutById) {
|
|
const nodeBounds = Object.values(layoutById).map((layout) => ({
|
|
x: layout.x,
|
|
y: layout.y,
|
|
width: layout.width,
|
|
height: layout.height
|
|
}));
|
|
const labelBounds = estimateEdgeLabelBounds(
|
|
edges,
|
|
layoutById,
|
|
(edge) => erDiagramEdgeToInternalEdge(edge).cardinality ?? null
|
|
);
|
|
return computeSceneBounds(nodeBounds, labelBounds, CANVAS_PADDING2);
|
|
}
|
|
function createFitContentBounds2(nodes) {
|
|
if (nodes.length === 0) {
|
|
return void 0;
|
|
}
|
|
return {
|
|
top: Math.min(...nodes.map((node) => node.y)),
|
|
bottom: Math.max(...nodes.map((node) => node.y + node.height))
|
|
};
|
|
}
|
|
function measureNodeHeight2(object) {
|
|
if (!object) {
|
|
return HEADER_HEIGHT2 + NODE_PADDING2 * 2 + ROW_HEIGHT2;
|
|
}
|
|
const attributeRows = object.fileType === "er-entity" ? Math.max(getVisibleErColumns(object.columns).length, 1) : Math.max(object.attributes.length, 1);
|
|
return HEADER_HEIGHT2 + SECTION_TITLE_HEIGHT2 + attributeRows * ROW_HEIGHT2 + NODE_PADDING2 * 2 + 16;
|
|
}
|
|
function createSvgSurface2(width, height) {
|
|
const svg = document.createElementNS(SVG_NS3, "svg");
|
|
svg.setAttribute("width", String(width));
|
|
svg.setAttribute("height", String(height));
|
|
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
svg.setAttribute("class", "model-weave-diagram-svg");
|
|
return svg;
|
|
}
|
|
function createMarkerDefinitions2() {
|
|
const defs = document.createElementNS(SVG_NS3, "defs");
|
|
defs.appendChild(
|
|
createTriangleMarker2("mdspec-er-arrow", DIAGRAM_EDGE2, DIAGRAM_EDGE2)
|
|
);
|
|
defs.appendChild(
|
|
createDiamondMarker2("mdspec-er-diamond-open", "none", DIAGRAM_EDGE2)
|
|
);
|
|
defs.appendChild(
|
|
createDiamondMarker2(
|
|
"mdspec-er-diamond-solid",
|
|
DIAGRAM_EDGE2,
|
|
DIAGRAM_EDGE2
|
|
)
|
|
);
|
|
return defs;
|
|
}
|
|
function createTriangleMarker2(id, fill, stroke) {
|
|
const marker = document.createElementNS(SVG_NS3, "marker");
|
|
marker.setAttribute("id", id);
|
|
marker.setAttribute("markerWidth", String(ER_ARROW_MARKER_WIDTH));
|
|
marker.setAttribute("markerHeight", String(ER_ARROW_MARKER_HEIGHT));
|
|
marker.setAttribute("refX", String(ER_ARROW_TIP_X));
|
|
marker.setAttribute("refY", String(ER_ARROW_TIP_Y));
|
|
marker.setAttribute("orient", "auto");
|
|
marker.setAttribute("markerUnits", "userSpaceOnUse");
|
|
const path2 = document.createElementNS(SVG_NS3, "path");
|
|
path2.setAttribute(
|
|
"d",
|
|
`M 0 0 L ${ER_ARROW_TIP_X} ${ER_ARROW_TIP_Y} L 0 ${ER_ARROW_MARKER_HEIGHT} z`
|
|
);
|
|
path2.setAttribute("fill", fill);
|
|
path2.setAttribute("stroke", stroke);
|
|
path2.setAttribute("stroke-width", "1.2");
|
|
marker.appendChild(path2);
|
|
return marker;
|
|
}
|
|
function createDiamondMarker2(id, fill, stroke) {
|
|
const marker = document.createElementNS(SVG_NS3, "marker");
|
|
marker.setAttribute("id", id);
|
|
marker.setAttribute("markerWidth", String(ER_DIAMOND_MARKER_WIDTH));
|
|
marker.setAttribute("markerHeight", String(ER_DIAMOND_MARKER_HEIGHT));
|
|
marker.setAttribute("refX", String(ER_DIAMOND_TIP_X));
|
|
marker.setAttribute("refY", String(ER_DIAMOND_TIP_Y));
|
|
marker.setAttribute("orient", "auto");
|
|
marker.setAttribute("markerUnits", "strokeWidth");
|
|
const path2 = document.createElementNS(SVG_NS3, "path");
|
|
path2.setAttribute(
|
|
"d",
|
|
`M 0 ${ER_DIAMOND_TIP_Y} L 4 0 L ${ER_DIAMOND_TIP_X} ${ER_DIAMOND_TIP_Y} L 4 ${ER_DIAMOND_MARKER_HEIGHT} z`
|
|
);
|
|
path2.setAttribute("fill", fill);
|
|
path2.setAttribute("stroke", stroke);
|
|
path2.setAttribute("stroke-width", "1.2");
|
|
marker.appendChild(path2);
|
|
return marker;
|
|
}
|
|
function renderEdge2(edge, layoutById) {
|
|
const source = layoutById[edge.source];
|
|
const target = layoutById[edge.target];
|
|
if (!source || !target) {
|
|
return null;
|
|
}
|
|
const group = document.createElementNS(SVG_NS3, "g");
|
|
const basePoints = getConnectionPoints(source, target);
|
|
const markers = getMarkerAttributes2(edge.kind);
|
|
const { startX, startY, endX, endY, midX, midY } = insetConnectionPoints(
|
|
basePoints,
|
|
markers
|
|
);
|
|
const line = document.createElementNS(SVG_NS3, "line");
|
|
line.setAttribute("x1", String(startX));
|
|
line.setAttribute("y1", String(startY));
|
|
line.setAttribute("x2", String(endX));
|
|
line.setAttribute("y2", String(endY));
|
|
line.setAttribute("stroke", DIAGRAM_EDGE2);
|
|
line.setAttribute("stroke-width", String(ER_EDGE_STROKE_WIDTH));
|
|
line.setAttribute("stroke-dasharray", getDashPattern2(edge.kind));
|
|
if (markers.start) {
|
|
line.setAttribute("marker-start", markers.start);
|
|
}
|
|
if (markers.end) {
|
|
line.setAttribute("marker-end", markers.end);
|
|
}
|
|
group.appendChild(line);
|
|
const internalEdge = erDiagramEdgeToInternalEdge(edge);
|
|
const cardinality = internalEdge.cardinality ?? null;
|
|
if (cardinality) {
|
|
group.appendChild(createErCardinalityBadge(midX, midY - 8, cardinality));
|
|
}
|
|
return group;
|
|
}
|
|
function insetConnectionPoints(points, markers) {
|
|
const dx = points.endX - points.startX;
|
|
const dy = points.endY - points.startY;
|
|
const length = Math.hypot(dx, dy);
|
|
if (length <= 1e-3) {
|
|
return points;
|
|
}
|
|
const ux = dx / length;
|
|
const uy = dy / length;
|
|
const desiredStartInset = getMarkerClearance(markers.start);
|
|
const desiredEndInset = getMarkerClearance(markers.end);
|
|
const maxInsetPerSide = Math.max(0, (length - ER_MIN_EDGE_VISIBLE_LENGTH) / 2);
|
|
const startInset = Math.min(desiredStartInset, maxInsetPerSide);
|
|
const endInset = Math.min(desiredEndInset, maxInsetPerSide);
|
|
const usableLength = length - startInset - endInset;
|
|
if (usableLength <= 8) {
|
|
return points;
|
|
}
|
|
const startX = points.startX + ux * startInset;
|
|
const startY = points.startY + uy * startInset;
|
|
const endX = points.endX - ux * endInset;
|
|
const endY = points.endY - uy * endInset;
|
|
return {
|
|
startX,
|
|
startY,
|
|
endX,
|
|
endY,
|
|
midX: (startX + endX) / 2,
|
|
midY: (startY + endY) / 2
|
|
};
|
|
}
|
|
function getMarkerClearance(markerRef) {
|
|
if (!markerRef) {
|
|
return 0;
|
|
}
|
|
if (markerRef.includes("mdspec-er-arrow")) {
|
|
return ER_ARROW_TIP_X + ER_EDGE_STROKE_WIDTH + ER_NODE_BORDER_WIDTH + ER_ARROW_EXTRA_PADDING;
|
|
}
|
|
if (markerRef.includes("mdspec-er-diamond-open") || markerRef.includes("mdspec-er-diamond-solid")) {
|
|
return ER_DIAMOND_TIP_X + ER_EDGE_STROKE_WIDTH + ER_NODE_BORDER_WIDTH + ER_DIAMOND_EXTRA_PADDING;
|
|
}
|
|
return ER_EDGE_STROKE_WIDTH + ER_NODE_BORDER_WIDTH + ER_ARROW_EXTRA_PADDING;
|
|
}
|
|
function getDashPattern2(kind) {
|
|
switch (kind) {
|
|
case "dependency":
|
|
case "implementation":
|
|
return "8 6";
|
|
default:
|
|
return "0";
|
|
}
|
|
}
|
|
function getMarkerAttributes2(kind) {
|
|
switch (kind) {
|
|
case "composition":
|
|
return { start: "url(#mdspec-er-diamond-solid)" };
|
|
case "aggregation":
|
|
return { start: "url(#mdspec-er-diamond-open)" };
|
|
case "association":
|
|
case "reference":
|
|
case "flow":
|
|
case "dependency":
|
|
case "implementation":
|
|
case "inheritance":
|
|
default:
|
|
return { end: "url(#mdspec-er-arrow)" };
|
|
}
|
|
}
|
|
function createEntityBox(layout, options) {
|
|
const box = document.createElement("article");
|
|
box.addClass("model-weave-node");
|
|
box.addClass("model-weave-node-er");
|
|
box.setCssProps({
|
|
"--mw-node-x": `${layout.x}px`,
|
|
"--mw-node-y": `${layout.y}px`,
|
|
"--mw-node-width": `${layout.width}px`,
|
|
"--mw-node-height": `${layout.height}px`
|
|
});
|
|
if (!layout.node.object) {
|
|
box.appendChild(createFallbackNode2(layout.node.label ?? layout.node.ref ?? layout.node.id));
|
|
return box;
|
|
}
|
|
if (options?.onOpenObject) {
|
|
box.addClass("model-weave-node-clickable");
|
|
box.setAttribute("role", "button");
|
|
box.setAttribute("tabindex", "0");
|
|
box.title = `Open ${layout.node.label ?? (layout.node.object.fileType === "er-entity" ? layout.node.object.logicalName : layout.node.object.name)}`;
|
|
box.addEventListener("click", (event) => {
|
|
if (event.defaultPrevented) {
|
|
return;
|
|
}
|
|
options.onOpenObject?.(layout.node.id, {
|
|
openInNewLeaf: event.ctrlKey || event.metaKey
|
|
});
|
|
});
|
|
box.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
options.onOpenObject?.(layout.node.id, { openInNewLeaf: false });
|
|
}
|
|
});
|
|
box.addEventListener("pointerdown", (event) => {
|
|
event.stopPropagation();
|
|
});
|
|
}
|
|
const object = layout.node.object;
|
|
const header = document.createElement("header");
|
|
header.addClass("model-weave-node-header");
|
|
header.addClass("model-weave-node-header-er");
|
|
const kind = document.createElement("div");
|
|
kind.addClass("model-weave-node-kind");
|
|
kind.textContent = object.fileType === "er-entity" ? "er_entity" : "entity";
|
|
const title = document.createElement("div");
|
|
title.addClass("model-weave-node-title");
|
|
title.addClass("model-weave-node-er-logical");
|
|
title.textContent = layout.node.label ?? (object.fileType === "er-entity" ? object.logicalName : object.name);
|
|
header.append(kind, title);
|
|
box.appendChild(header);
|
|
if (object.fileType === "er-entity") {
|
|
const physical = document.createElement("div");
|
|
physical.addClass("model-weave-node-er-physical");
|
|
physical.textContent = object.physicalName;
|
|
box.appendChild(physical);
|
|
box.appendChild(createAttributeSection(getVisibleErColumns(object.columns)));
|
|
return box;
|
|
}
|
|
box.appendChild(
|
|
createAttributeSection(
|
|
object.attributes.map((attribute) => {
|
|
const detail = attribute.type ? `: ${attribute.type}` : "";
|
|
return `${attribute.name}${detail}`;
|
|
})
|
|
)
|
|
);
|
|
return box;
|
|
}
|
|
function createAttributeSection(items) {
|
|
const section = document.createElement("section");
|
|
section.addClass("model-weave-node-section");
|
|
const heading = document.createElement("div");
|
|
heading.addClass("model-weave-node-section-heading");
|
|
heading.textContent = "Columns";
|
|
section.appendChild(heading);
|
|
if (items.length === 0) {
|
|
const empty = document.createElement("div");
|
|
empty.addClass("model-weave-node-empty");
|
|
empty.textContent = "None";
|
|
section.appendChild(empty);
|
|
return section;
|
|
}
|
|
const list = document.createElement("ul");
|
|
list.addClass("model-weave-node-list");
|
|
for (const item of items) {
|
|
const entry = document.createElement("li");
|
|
entry.textContent = item;
|
|
list.appendChild(entry);
|
|
}
|
|
section.appendChild(list);
|
|
return section;
|
|
}
|
|
function createRelationTable(diagram) {
|
|
const section = document.createElement("details");
|
|
section.addClass("model-weave-diagram-details");
|
|
section.open = false;
|
|
const summary = document.createElement("summary");
|
|
summary.textContent = `Resolved relations (${diagram.edges.length})`;
|
|
summary.addClass("model-weave-diagram-details-summary");
|
|
section.appendChild(summary);
|
|
if (diagram.edges.length === 0) {
|
|
const empty = document.createElement("p");
|
|
empty.textContent = modelWeaveText(
|
|
"No relations are currently used for display.",
|
|
"\u8868\u793A\u5BFE\u8C61\u306E relation \u306F\u3042\u308A\u307E\u305B\u3093\u3002"
|
|
);
|
|
empty.addClass("model-weave-diagram-details-empty");
|
|
section.appendChild(empty);
|
|
return section;
|
|
}
|
|
const list = document.createElement("ul");
|
|
list.addClass("model-weave-diagram-details-list");
|
|
const sortedEdges = [...diagram.edges].sort(compareErEdges);
|
|
for (const edge of sortedEdges) {
|
|
const internalEdge = erDiagramEdgeToInternalEdge(edge);
|
|
const columns = internalEdge.mappings.map((mapping) => `${mapping.localColumn} -> ${mapping.targetColumn}`).join(" / ");
|
|
const item = document.createElement("li");
|
|
item.addClass("model-weave-diagram-details-item");
|
|
item.textContent = `${internalEdge.id || "-"} / ${internalEdge.sourceEntity} -> ${internalEdge.targetEntity} / ${internalEdge.kind || "-"} / ${internalEdge.cardinality || "-"}${internalEdge.notes ? ` / ${internalEdge.notes}` : ""} / ${columns || "-"}`;
|
|
list.appendChild(item);
|
|
}
|
|
section.appendChild(list);
|
|
return section;
|
|
}
|
|
function compareErEdges(left, right) {
|
|
const leftEdge = erDiagramEdgeToInternalEdge(left);
|
|
const rightEdge = erDiagramEdgeToInternalEdge(right);
|
|
const sourceCompare = leftEdge.sourceEntity.localeCompare(rightEdge.sourceEntity);
|
|
if (sourceCompare !== 0) {
|
|
return sourceCompare;
|
|
}
|
|
const targetCompare = leftEdge.targetEntity.localeCompare(rightEdge.targetEntity);
|
|
if (targetCompare !== 0) {
|
|
return targetCompare;
|
|
}
|
|
return (leftEdge.id || "").localeCompare(rightEdge.id || "");
|
|
}
|
|
function createFallbackNode2(id) {
|
|
const box = document.createElement("div");
|
|
box.addClass("model-weave-node-empty");
|
|
box.textContent = `Unresolved entity: ${id}`;
|
|
return box;
|
|
}
|
|
|
|
// src/renderers/class-er-mermaid.ts
|
|
var CLASS_NODE_CLASS = "mwClass";
|
|
var ER_NODE_CLASS = "mwEntity";
|
|
var MERMAID_CLASS_ATTRIBUTE_LIMIT = 5;
|
|
var MERMAID_CLASS_METHOD_LIMIT = 5;
|
|
function renderClassMermaidDiagram(diagram, options) {
|
|
return renderReducedMermaidDiagram({
|
|
className: "mdspec-diagram mdspec-diagram--class",
|
|
title: options?.hideTitle ? void 0 : `${diagram.diagram.name} (class / mermaid)`,
|
|
renderIdPrefix: "model_weave_class",
|
|
source: buildClassOverviewMermaidSource(diagram),
|
|
options,
|
|
fallback: () => renderClassDiagram(diagram, options),
|
|
fallbackMessage: modelWeaveText(
|
|
"Mermaid class overview could not be rendered. Falling back to the custom class renderer.",
|
|
"Mermaid \u306E class overview \u3092\u63CF\u753B\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002custom class renderer \u306B\u5207\u308A\u66FF\u3048\u307E\u3059\u3002"
|
|
)
|
|
});
|
|
}
|
|
function renderClassMermaidDetailDiagram(diagram, options) {
|
|
return renderReducedMermaidDiagram({
|
|
className: "mdspec-diagram mdspec-diagram--class",
|
|
title: options?.hideTitle ? void 0 : `${diagram.diagram.name} (class / mermaid detail)`,
|
|
renderIdPrefix: "model_weave_class_detail",
|
|
source: buildClassDetailMermaidSource(diagram),
|
|
options,
|
|
fallback: () => renderClassDiagram(diagram, options),
|
|
fallbackMessage: modelWeaveText(
|
|
"Mermaid Detail class overview could not be rendered. Falling back to the custom class renderer.",
|
|
"Mermaid Detail \u306E class overview \u3092\u63CF\u753B\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002custom class renderer \u306B\u5207\u308A\u66FF\u3048\u307E\u3059\u3002"
|
|
)
|
|
});
|
|
}
|
|
function renderErMermaidDiagram(diagram, options) {
|
|
return renderReducedMermaidDiagram({
|
|
className: "mdspec-diagram mdspec-diagram--er",
|
|
title: options?.hideTitle ? void 0 : `${diagram.diagram.name} (er / mermaid)`,
|
|
renderIdPrefix: "model_weave_er",
|
|
source: buildErOverviewMermaidSource(diagram),
|
|
options,
|
|
fallback: () => renderErDiagram(diagram, options),
|
|
fallbackMessage: modelWeaveText(
|
|
"Mermaid ER overview could not be rendered. Falling back to the custom ER renderer.",
|
|
"Mermaid \u306E ER overview \u3092\u63CF\u753B\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002custom ER renderer \u306B\u5207\u308A\u66FF\u3048\u307E\u3059\u3002"
|
|
)
|
|
});
|
|
}
|
|
function renderErMermaidDetailDiagram(diagram, options) {
|
|
return renderReducedMermaidDiagram({
|
|
className: "mdspec-diagram mdspec-diagram--er mdspec-diagram--er-detail",
|
|
title: options?.hideTitle ? void 0 : `${diagram.diagram.name} (er / mermaid detail)`,
|
|
renderIdPrefix: "model_weave_er_detail",
|
|
source: buildErDetailMermaidSource(diagram),
|
|
options,
|
|
fallback: () => renderErDiagram(diagram, options),
|
|
fallbackMessage: modelWeaveText(
|
|
"Mermaid Detail ER overview could not be rendered. Falling back to the custom ER renderer.",
|
|
"Mermaid Detail \u306E ER overview \u3092\u63CF\u753B\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002custom ER renderer \u306B\u5207\u308A\u66FF\u3048\u307E\u3059\u3002"
|
|
)
|
|
});
|
|
}
|
|
function renderReducedMermaidDiagram(config) {
|
|
const shell = createMermaidShell({
|
|
className: config.className,
|
|
title: config.title,
|
|
forExport: config.options?.forExport
|
|
});
|
|
const ready = renderMermaidSourceIntoShell(shell, {
|
|
source: config.source,
|
|
renderIdPrefix: config.renderIdPrefix,
|
|
nodeSelector: ".node, g.node, foreignObject",
|
|
fitVerticalAlign: config.options?.fitVerticalAlign,
|
|
viewportState: config.options?.viewportState,
|
|
onViewportStateChange: config.options?.onViewportStateChange,
|
|
showSourcePanel: !config.options?.forExport,
|
|
sourcePanelContainer: config.options?.sourcePanelContainer,
|
|
sourcePanelPlacement: config.options?.sourcePanelPlacement,
|
|
showRenderDebug: !config.options?.forExport && config.options?.showMermaidRenderDebug === true
|
|
}).catch(() => {
|
|
const fallback = config.fallback();
|
|
const notice = createMermaidFallbackNotice(config.fallbackMessage);
|
|
shell.root.replaceChildren(notice, ...Array.from(fallback.childNodes));
|
|
});
|
|
setMermaidRenderReadyPromise(shell.root, ready);
|
|
return shell.root;
|
|
}
|
|
function buildClassOverviewMermaidSource(diagram) {
|
|
const palette = getModelWeaveMermaidPalette();
|
|
const lines = [
|
|
"flowchart LR",
|
|
` ${buildModelWeaveMermaidClassDef(CLASS_NODE_CLASS, palette.classFill, palette.classBorder)}`
|
|
];
|
|
const nodeIds = /* @__PURE__ */ new Map();
|
|
const usedNodeIds = /* @__PURE__ */ new Set();
|
|
for (const node of diagram.nodes) {
|
|
const object = node.object && node.object.fileType === "object" ? node.object : void 0;
|
|
const mermaidId = ensureUniqueMermaidId(sanitizeMermaidId(node.id), usedNodeIds);
|
|
nodeIds.set(node.id, mermaidId);
|
|
lines.push(` ${mermaidId}["${buildClassOverviewNodeLabel(node.label, object, node.id)}"]:::${CLASS_NODE_CLASS}`);
|
|
}
|
|
for (const edge of diagram.edges) {
|
|
const from = nodeIds.get(edge.source);
|
|
const to = nodeIds.get(edge.target);
|
|
if (!from || !to) {
|
|
continue;
|
|
}
|
|
const label = sanitizeEdgeLabel(buildClassEdgeLabel(edge));
|
|
lines.push(label ? ` ${from} -->|${label}| ${to}` : ` ${from} --> ${to}`);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function buildClassDetailMermaidSource(diagram) {
|
|
const lines = ["classDiagram"];
|
|
const nodeIds = /* @__PURE__ */ new Map();
|
|
const usedNodeIds = /* @__PURE__ */ new Set();
|
|
for (const node of diagram.nodes) {
|
|
const object = node.object && node.object.fileType === "object" ? node.object : void 0;
|
|
const mermaidId = ensureUniqueMermaidId(sanitizeMermaidId(node.id), usedNodeIds);
|
|
nodeIds.set(node.id, mermaidId);
|
|
lines.push(...buildClassDetailDeclaration(mermaidId, node.label, object, node.id));
|
|
}
|
|
for (const edge of diagram.edges) {
|
|
const from = nodeIds.get(edge.source);
|
|
const to = nodeIds.get(edge.target);
|
|
if (!from || !to) {
|
|
continue;
|
|
}
|
|
lines.push(buildClassDetailRelation(edge, from, to));
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function buildErOverviewMermaidSource(diagram) {
|
|
const palette = getModelWeaveMermaidPalette();
|
|
const lines = [
|
|
"flowchart LR",
|
|
` ${buildModelWeaveMermaidClassDef(ER_NODE_CLASS, palette.erFill, palette.erBorder)}`
|
|
];
|
|
const nodeIds = /* @__PURE__ */ new Map();
|
|
const usedNodeIds = /* @__PURE__ */ new Set();
|
|
for (const node of diagram.nodes) {
|
|
const entity = node.object && node.object.fileType === "er-entity" ? node.object : void 0;
|
|
const mermaidId = ensureUniqueMermaidId(sanitizeMermaidId(node.id), usedNodeIds);
|
|
nodeIds.set(node.id, mermaidId);
|
|
lines.push(` ${mermaidId}["${buildErNodeLabel(node.label, entity, node.id)}"]:::${ER_NODE_CLASS}`);
|
|
}
|
|
for (const edge of diagram.edges) {
|
|
const from = nodeIds.get(edge.source);
|
|
const to = nodeIds.get(edge.target);
|
|
if (!from || !to) {
|
|
continue;
|
|
}
|
|
const label = sanitizeEdgeLabel(buildErEdgeLabel(edge));
|
|
lines.push(label ? ` ${from} -->|${label}| ${to}` : ` ${from} --> ${to}`);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function buildErDetailMermaidSource(diagram) {
|
|
const lines = ["erDiagram"];
|
|
const nodeIds = /* @__PURE__ */ new Map();
|
|
const usedNodeIds = /* @__PURE__ */ new Set();
|
|
for (const node of diagram.nodes) {
|
|
const entity = node.object && node.object.fileType === "er-entity" ? node.object : void 0;
|
|
const mermaidId = ensureUniqueMermaidId(
|
|
sanitizeMermaidId(entity?.physicalName || entity?.id || node.id),
|
|
usedNodeIds
|
|
);
|
|
nodeIds.set(node.id, mermaidId);
|
|
lines.push(...buildErDetailDeclaration(mermaidId, entity));
|
|
}
|
|
for (const edge of diagram.edges) {
|
|
const from = nodeIds.get(edge.source);
|
|
const to = nodeIds.get(edge.target);
|
|
if (!from || !to) {
|
|
continue;
|
|
}
|
|
lines.push(buildErDetailRelation(edge, from, to));
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function buildClassOverviewNodeLabel(explicitLabel, object, fallbackId) {
|
|
return escapeMermaidLabel(explicitLabel?.trim() || object?.name || fallbackId);
|
|
}
|
|
function buildErNodeLabel(explicitLabel, entity, fallbackId) {
|
|
if (!entity) {
|
|
return escapeMermaidLabel(explicitLabel?.trim() || fallbackId);
|
|
}
|
|
const lines = [entity.logicalName || explicitLabel?.trim() || fallbackId];
|
|
if (entity.physicalName) {
|
|
lines.push(entity.physicalName);
|
|
}
|
|
return escapeMermaidLabel(lines.join("\n"));
|
|
}
|
|
function buildClassEdgeLabel(edge) {
|
|
const internal = classDiagramEdgeToInternalEdge(edge);
|
|
const base = internal.label?.trim() || internal.kind || null;
|
|
const multiplicity = internal.fromMultiplicity || internal.toMultiplicity ? `${internal.fromMultiplicity ?? "-"}\u2192${internal.toMultiplicity ?? "-"}` : null;
|
|
if (base && multiplicity) {
|
|
return `${base} (${multiplicity})`;
|
|
}
|
|
return base ?? multiplicity;
|
|
}
|
|
function buildErEdgeLabel(edge) {
|
|
const internal = erDiagramEdgeToInternalEdge(edge);
|
|
return internal.cardinality?.trim() || internal.label?.trim() || internal.id?.trim() || internal.kind?.trim() || null;
|
|
}
|
|
function sanitizeEdgeLabel(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
return escapeMermaidEdgeLabel(value);
|
|
}
|
|
function buildErDetailDeclaration(mermaidId, entity) {
|
|
const columns = entity ? buildErColumnLines(entity) : [];
|
|
if (columns.length === 0) {
|
|
return [` ${mermaidId} {`, " }"];
|
|
}
|
|
return [
|
|
` ${mermaidId} {`,
|
|
...columns.map((column) => ` ${column}`),
|
|
" }"
|
|
];
|
|
}
|
|
function buildErColumnLines(entity) {
|
|
const fkColumns = getErForeignKeyColumns(entity);
|
|
const columns = entity.columns.slice(0, MERMAID_CLASS_ATTRIBUTE_LIMIT).map((column) => formatErColumn(column, fkColumns)).filter((column) => Boolean(column));
|
|
if (entity.columns.length > MERMAID_CLASS_ATTRIBUTE_LIMIT) {
|
|
columns.push("string more_columns");
|
|
}
|
|
return columns;
|
|
}
|
|
function formatErColumn(column, fkColumns) {
|
|
const name = formatErIdentifierToken(column.physicalName || column.logicalName);
|
|
if (!name) {
|
|
return null;
|
|
}
|
|
const type = formatErTypeToken(column.dataType || "string");
|
|
const keys = [
|
|
column.pk ? "PK" : null,
|
|
isErForeignKeyColumn(column, fkColumns) ? "FK" : null
|
|
].filter((key) => Boolean(key));
|
|
return `${type} ${name}${keys.length > 0 ? ` ${keys.join(",")}` : ""}`;
|
|
}
|
|
function getErForeignKeyColumns(entity) {
|
|
const columns = /* @__PURE__ */ new Set();
|
|
for (const relation of entity.outboundRelations) {
|
|
for (const mapping of relation.mappings) {
|
|
if (mapping.localColumn.trim()) {
|
|
columns.add(mapping.localColumn.trim());
|
|
}
|
|
}
|
|
}
|
|
return columns;
|
|
}
|
|
function isErForeignKeyColumn(column, fkColumns) {
|
|
return fkColumns.has(column.physicalName) || fkColumns.has(column.logicalName);
|
|
}
|
|
function buildErDetailRelation(edge, from, to) {
|
|
const internal = erDiagramEdgeToInternalEdge(edge);
|
|
const markers = getErRelationshipMarkers(internal);
|
|
const label = sanitizeEdgeLabel(
|
|
internal.label?.trim() || internal.id?.trim() || internal.kind?.trim() || null
|
|
) ?? "relates";
|
|
return ` ${from} ${markers.left}--${markers.right} ${to} : ${label}`;
|
|
}
|
|
function getErRelationshipMarkers(edge) {
|
|
const cardinality = edge.cardinality?.trim();
|
|
if (cardinality) {
|
|
const normalized = cardinality.toLowerCase().replace(/\s+/g, "");
|
|
switch (normalized) {
|
|
case "many-to-one":
|
|
case "manytoone":
|
|
case "n-1":
|
|
case "*-1":
|
|
return { left: "}o", right: "||" };
|
|
case "one-to-many":
|
|
case "onetomany":
|
|
case "1-n":
|
|
case "1-*":
|
|
return { left: "||", right: "o{" };
|
|
case "one-to-one":
|
|
case "onetoone":
|
|
case "1-1":
|
|
return { left: "||", right: "||" };
|
|
default:
|
|
break;
|
|
}
|
|
const split = normalized.match(/^(.+?)(?:-|:|to)(.+)$/);
|
|
if (split) {
|
|
return {
|
|
left: getLeftErCardinalityMarker(split[1]),
|
|
right: getRightErCardinalityMarker(split[2])
|
|
};
|
|
}
|
|
}
|
|
return { left: "||", right: "o{" };
|
|
}
|
|
function getLeftErCardinalityMarker(value) {
|
|
switch (normalizeErCardinalityPart(value)) {
|
|
case "zero-or-one":
|
|
return "o|";
|
|
case "one-or-more":
|
|
return "}|";
|
|
case "many":
|
|
case "zero-or-more":
|
|
return "}o";
|
|
case "one":
|
|
default:
|
|
return "||";
|
|
}
|
|
}
|
|
function getRightErCardinalityMarker(value) {
|
|
switch (normalizeErCardinalityPart(value)) {
|
|
case "zero-or-one":
|
|
return "|o";
|
|
case "one-or-more":
|
|
return "|{";
|
|
case "many":
|
|
case "zero-or-more":
|
|
return "o{";
|
|
case "one":
|
|
default:
|
|
return "||";
|
|
}
|
|
}
|
|
function normalizeErCardinalityPart(value) {
|
|
const normalized = value?.trim().toLowerCase() ?? "";
|
|
switch (normalized) {
|
|
case "1":
|
|
case "one":
|
|
case "single":
|
|
return "one";
|
|
case "0..1":
|
|
case "0..one":
|
|
case "zero-or-one":
|
|
case "optional":
|
|
return "zero-or-one";
|
|
case "1..*":
|
|
case "1..n":
|
|
case "one-or-more":
|
|
return "one-or-more";
|
|
case "*":
|
|
case "n":
|
|
case "many":
|
|
return "many";
|
|
case "0..*":
|
|
case "0..n":
|
|
case "zero-or-more":
|
|
default:
|
|
return normalized.includes("many") || normalized === "*" || normalized === "n" ? "many" : normalized.includes("0") && (normalized.includes("*") || normalized.includes("n")) ? "zero-or-more" : normalized.includes("1") ? "one" : "many";
|
|
}
|
|
}
|
|
function formatErIdentifierToken(value) {
|
|
return sanitizeMermaidId(formatMermaidMember(value));
|
|
}
|
|
function formatErTypeToken(value) {
|
|
const formatted = formatMermaidMember(value) || "string";
|
|
const token = formatted.replace(/[^A-Za-z0-9_]/g, "_").replace(/^_+|_+$/g, "");
|
|
if (!token) {
|
|
return "string";
|
|
}
|
|
return /^[A-Za-z]/.test(token) ? token : `T_${token}`;
|
|
}
|
|
function buildClassDetailDeclaration(mermaidId, explicitLabel, object, fallbackId) {
|
|
const displayName = explicitLabel?.trim() || object?.name || fallbackId;
|
|
const label = escapeMermaidClassText(displayName);
|
|
const members = object ? buildClassDetailMemberLines(object, displayName) : [];
|
|
if (members.length === 0) {
|
|
return [` class ${mermaidId}["${label}"]`];
|
|
}
|
|
return [
|
|
` class ${mermaidId}["${label}"] {`,
|
|
...members.map((member) => ` ${member}`),
|
|
" }"
|
|
];
|
|
}
|
|
function buildClassDetailMemberLines(object, displayName) {
|
|
const lines = [];
|
|
const objectId = getClassObjectId2(object);
|
|
if (objectId && objectId !== displayName) {
|
|
lines.push(`id: ${formatMermaidMember(objectId)}`);
|
|
}
|
|
lines.push(...buildClassMemberLines(object));
|
|
return lines;
|
|
}
|
|
function buildClassDetailRelation(edge, from, to) {
|
|
const internal = classDiagramEdgeToInternalEdge(edge);
|
|
const arrow = getClassDiagramArrow(internal.kind);
|
|
const fromMultiplicity = formatClassMultiplicity(internal.fromMultiplicity);
|
|
const toMultiplicity = formatClassMultiplicity(internal.toMultiplicity);
|
|
const label = sanitizeEdgeLabel(buildClassEdgeLabel(edge));
|
|
const multiplicities = fromMultiplicity || toMultiplicity ? ` "${fromMultiplicity ?? ""}" ${arrow} "${toMultiplicity ?? ""}" ` : ` ${arrow} `;
|
|
return label ? ` ${from}${multiplicities}${to} : ${label}` : ` ${from}${multiplicities}${to}`;
|
|
}
|
|
function getClassDiagramArrow(kind) {
|
|
switch (kind) {
|
|
case "inheritance":
|
|
return "--|>";
|
|
case "implementation":
|
|
return "..|>";
|
|
case "dependency":
|
|
return "..>";
|
|
case "composition":
|
|
return "*--";
|
|
case "aggregation":
|
|
return "o--";
|
|
case "association":
|
|
case "reference":
|
|
case "flow":
|
|
default:
|
|
return "-->";
|
|
}
|
|
}
|
|
function formatClassMultiplicity(value) {
|
|
if (!value?.trim()) {
|
|
return null;
|
|
}
|
|
return escapeMermaidClassText(value);
|
|
}
|
|
function getClassObjectId2(object) {
|
|
const rawId = object.frontmatter.id;
|
|
if (typeof rawId !== "string") {
|
|
return null;
|
|
}
|
|
const id = rawId.trim();
|
|
return id || null;
|
|
}
|
|
function buildClassMemberLines(object) {
|
|
const lines = object.attributes.slice(0, MERMAID_CLASS_ATTRIBUTE_LIMIT).map(formatClassAttribute).filter((line) => Boolean(line));
|
|
if (object.attributes.length > MERMAID_CLASS_ATTRIBUTE_LIMIT) {
|
|
lines.push("...");
|
|
}
|
|
lines.push(
|
|
...object.methods.slice(0, MERMAID_CLASS_METHOD_LIMIT).map(formatClassMethod).filter((line) => Boolean(line))
|
|
);
|
|
if (object.methods.length > MERMAID_CLASS_METHOD_LIMIT) {
|
|
lines.push("...");
|
|
}
|
|
return lines;
|
|
}
|
|
function formatClassAttribute(attribute) {
|
|
const name = formatMermaidMember(attribute.name);
|
|
if (!name) {
|
|
return null;
|
|
}
|
|
const visibility = formatVisibility(attribute.visibility);
|
|
const type = attribute.type ? `: ${formatMermaidMember(attribute.type)}` : "";
|
|
const required = attribute.required === true ? " required" : "";
|
|
const multiplicity = attribute.multiplicity ? ` ${formatMermaidMember(attribute.multiplicity)}` : "";
|
|
return `${visibility}${name}${type}${multiplicity}${required}`;
|
|
}
|
|
function formatClassMethod(method) {
|
|
const name = formatMermaidMember(method.name);
|
|
if (!name) {
|
|
return null;
|
|
}
|
|
const visibility = formatVisibility(method.visibility);
|
|
const parameters = method.parameters.map((parameter) => {
|
|
const parameterName = formatMermaidMember(parameter.name);
|
|
if (!parameterName) {
|
|
return null;
|
|
}
|
|
const type = parameter.type ? `: ${formatMermaidMember(parameter.type)}` : "";
|
|
return `${parameterName}${type}`;
|
|
}).filter((parameter) => Boolean(parameter)).join(", ");
|
|
const returnType = method.returnType ? `: ${formatMermaidMember(method.returnType)}` : "";
|
|
return `${visibility}${name}(${parameters})${returnType}`;
|
|
}
|
|
function formatVisibility(visibility) {
|
|
switch (visibility) {
|
|
case "public":
|
|
return "+ ";
|
|
case "protected":
|
|
return "# ";
|
|
case "private":
|
|
return "- ";
|
|
case "package":
|
|
return "~ ";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
function escapeMermaidClassText(value) {
|
|
return escapeMermaidLabel(value).replace(/<br\/>/g, " ");
|
|
}
|
|
|
|
// 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 = getNodeLabel(node);
|
|
box.appendChild(heading);
|
|
const description = document.createElement("p");
|
|
description.textContent = getNodeDescription(node);
|
|
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;
|
|
}
|
|
function getNodeLabel(node) {
|
|
if (!node.object) {
|
|
return node.ref ?? node.id;
|
|
}
|
|
return node.object.fileType === "er-entity" ? node.object.logicalName : node.object.name;
|
|
}
|
|
function getNodeDescription(node) {
|
|
if (!node.object) {
|
|
return "No component description available.";
|
|
}
|
|
if (node.object.fileType === "er-entity") {
|
|
return node.object.physicalName;
|
|
}
|
|
if (node.object.fileType === "dfd-object") {
|
|
return node.object.kind;
|
|
}
|
|
return node.object.description ?? "No component description available.";
|
|
}
|
|
|
|
// src/renderers/dfd-mermaid.ts
|
|
function renderDfdMermaidDiagram(diagram, options) {
|
|
const shell = createMermaidShell({
|
|
className: "mdspec-diagram mdspec-diagram--dfd",
|
|
title: options?.hideTitle ? void 0 : `${diagram.diagram.name} (dfd)`,
|
|
forExport: options?.forExport
|
|
});
|
|
if (!options?.hideDetails) {
|
|
shell.root.appendChild(createFlowDetails(diagram.edges));
|
|
}
|
|
const ready = renderMermaidSourceIntoShell(shell, {
|
|
source: buildDfdMermaidSource(diagram),
|
|
renderIdPrefix: "model_weave_dfd",
|
|
fitVerticalAlign: options?.fitVerticalAlign,
|
|
viewportState: options?.viewportState,
|
|
onViewportStateChange: options?.onViewportStateChange,
|
|
showSourcePanel: !options?.forExport,
|
|
sourcePanelContainer: options?.sourcePanelContainer,
|
|
sourcePanelPlacement: options?.sourcePanelPlacement,
|
|
showRenderDebug: !options?.forExport && options?.showMermaidRenderDebug === true
|
|
}).catch(() => {
|
|
shell.root.replaceChildren(
|
|
createMermaidFallbackNotice(
|
|
modelWeaveText(
|
|
"DFD Mermaid rendering failed. Check diagnostics and Mermaid compatibility for this diagram.",
|
|
"DFD Mermaid \u306E\u63CF\u753B\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002Diagnostics \u3068 Mermaid \u4E92\u63DB\u6027\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
)
|
|
)
|
|
);
|
|
});
|
|
setMermaidRenderReadyPromise(shell.root, ready);
|
|
return shell.root;
|
|
}
|
|
function buildDfdMermaidSource(diagram) {
|
|
const palette = getModelWeaveMermaidPalette();
|
|
const lines = [
|
|
"flowchart LR",
|
|
` ${buildModelWeaveMermaidClassDef("dfdExternal", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`,
|
|
` ${buildModelWeaveMermaidClassDef("dfdProcess", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
|
|
` ${buildModelWeaveMermaidClassDef("dfdDatastore", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
|
|
` ${buildModelWeaveMermaidClassDef("dfdOther", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`
|
|
];
|
|
const nodeIds = /* @__PURE__ */ new Map();
|
|
for (const node of diagram.nodes) {
|
|
const object = node.object?.fileType === "dfd-object" ? node.object : void 0;
|
|
const mermaidId = toMermaidNodeId(node.id);
|
|
nodeIds.set(node.id, mermaidId);
|
|
lines.push(` ${mermaidId}${toMermaidNodeDeclaration(node, object)}`);
|
|
}
|
|
for (const edge of diagram.edges) {
|
|
const from = nodeIds.get(edge.source);
|
|
const to = nodeIds.get(edge.target);
|
|
if (!from || !to) {
|
|
continue;
|
|
}
|
|
const label = sanitizeMermaidEdgeLabel(edge.label);
|
|
if (label) {
|
|
lines.push(` ${from} -->|${label}| ${to}`);
|
|
} else {
|
|
lines.push(` ${from} --> ${to}`);
|
|
}
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function createFlowDetails(edges) {
|
|
const section = document.createElement("details");
|
|
section.className = "mdspec-section";
|
|
section.addClass("model-weave-diagram-details");
|
|
section.open = false;
|
|
const summary = document.createElement("summary");
|
|
summary.textContent = `Displayed flows (${edges.length})`;
|
|
summary.addClass("model-weave-diagram-details-summary");
|
|
section.appendChild(summary);
|
|
if (edges.length === 0) {
|
|
const empty = document.createElement("p");
|
|
empty.textContent = modelWeaveText(
|
|
"No flows are currently used for rendering.",
|
|
"\u63CF\u753B\u306B\u4F7F\u308F\u308C\u3066\u3044\u308B flow \u306F\u3042\u308A\u307E\u305B\u3093\u3002"
|
|
);
|
|
empty.addClass("model-weave-diagram-details-empty");
|
|
section.appendChild(empty);
|
|
return section;
|
|
}
|
|
const list = document.createElement("ul");
|
|
list.addClass("model-weave-diagram-details-list");
|
|
for (const edge of edges) {
|
|
const item = document.createElement("li");
|
|
item.addClass("model-weave-diagram-details-item");
|
|
const notes = formatDiagramEdgeNotes(edge.metadata?.notes);
|
|
item.textContent = `${edge.id ?? "-"} / ${edge.source} -> ${edge.target} / ${edge.label ?? "-"}${notes ? ` / ${notes}` : ""}`;
|
|
list.appendChild(item);
|
|
}
|
|
section.appendChild(list);
|
|
return section;
|
|
}
|
|
function toMermaidNodeId(value) {
|
|
const normalized = value.replace(/[^A-Za-z0-9_]/g, "_");
|
|
if (/^[A-Za-z_]/.test(normalized)) {
|
|
return normalized;
|
|
}
|
|
return `N_${normalized}`;
|
|
}
|
|
function toMermaidNodeDeclaration(node, object) {
|
|
const label = escapeMermaidLabel2(node.label ?? object?.name ?? node.ref ?? node.id);
|
|
const kind = object?.kind ?? node.kind;
|
|
switch (kind) {
|
|
case "datastore":
|
|
return `[("${label}")]:::dfdDatastore`;
|
|
case "process":
|
|
return `["${label}"]:::dfdProcess`;
|
|
case "other":
|
|
return `["${label}"]:::dfdOther`;
|
|
case "external":
|
|
default:
|
|
return `["${label}"]:::dfdExternal`;
|
|
}
|
|
}
|
|
function escapeMermaidLabel2(value) {
|
|
return value.replace(/"/g, '\\"').replace(/\r?\n/g, "<br/>");
|
|
}
|
|
function sanitizeMermaidEdgeLabel(value) {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
return trimmed.replace(/\|/g, "/").replace(/[[\]()]/g, " ").replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim();
|
|
}
|
|
function formatDiagramEdgeNotes(notes) {
|
|
if (typeof notes === "string") {
|
|
return notes.trim();
|
|
}
|
|
if (Array.isArray(notes)) {
|
|
return notes.filter((note) => typeof note === "string" && note.trim().length > 0).join(" / ");
|
|
}
|
|
if (notes && typeof notes === "object") {
|
|
try {
|
|
const serialized = JSON.stringify(notes);
|
|
return typeof serialized === "string" && serialized !== "{}" ? serialized : "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// 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 = getNodeLabel2(node);
|
|
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;
|
|
}
|
|
function getNodeLabel2(node) {
|
|
if (!node.object) {
|
|
return node.ref ?? node.id;
|
|
}
|
|
return node.object.fileType === "er-entity" ? node.object.logicalName : node.object.name;
|
|
}
|
|
|
|
// src/renderers/diagram-renderer.ts
|
|
function renderDiagramModel(diagram, options) {
|
|
switch (diagram.diagram.kind) {
|
|
case "class":
|
|
return renderClassDiagramByMode(diagram, options);
|
|
case "er":
|
|
return renderErDiagramByMode(diagram, options);
|
|
case "dfd":
|
|
return renderDfdMermaidDiagram(diagram, options);
|
|
case "flow":
|
|
return renderFlowDiagram(diagram);
|
|
case "component":
|
|
return renderComponentDiagram(diagram);
|
|
default:
|
|
return createReservedKindFallback(diagram.diagram.kind);
|
|
}
|
|
}
|
|
function renderClassDiagramByMode(diagram, options) {
|
|
const mode = options?.renderMode;
|
|
if (mode === "mermaid-detail") {
|
|
return renderClassMermaidDetailDiagram(diagram, options);
|
|
}
|
|
if (mode === "mermaid") {
|
|
return renderClassMermaidDiagram(diagram, options);
|
|
}
|
|
return renderClassDiagram(diagram, options);
|
|
}
|
|
function renderErDiagramByMode(diagram, options) {
|
|
const mode = options?.renderMode;
|
|
if (mode === "mermaid-detail") {
|
|
return renderErMermaidDetailDiagram(diagram, options);
|
|
}
|
|
if (mode === "mermaid") {
|
|
return renderErMermaidDiagram(diagram, options);
|
|
}
|
|
return renderErDiagram(diagram, options);
|
|
}
|
|
function createReservedKindFallback(kind) {
|
|
const root = document.createElement("section");
|
|
root.className = "mdspec-fallback";
|
|
const title = document.createElement("h2");
|
|
title.textContent = modelWeaveText(
|
|
"Diagram preview is not available",
|
|
"Diagram preview \u306F\u5229\u7528\u3067\u304D\u307E\u305B\u3093"
|
|
);
|
|
const message = document.createElement("p");
|
|
message.textContent = modelWeaveText(
|
|
`Reserved diagram kind "${kind}" is not rendered in v1.`,
|
|
`\u4E88\u7D04\u6E08\u307F diagram kind "${kind}" \u306F v1 \u3067\u306F\u63CF\u753B\u3055\u308C\u307E\u305B\u3093\u3002`
|
|
);
|
|
root.append(title, message);
|
|
return root;
|
|
}
|
|
|
|
// src/utils/display-text.ts
|
|
var DISPLAY_TEXT_ESCAPES = {
|
|
"|": "|",
|
|
"[": "[",
|
|
"]": "]",
|
|
'"': '"',
|
|
"\\": "\\"
|
|
};
|
|
function decodeEscapedDisplayText(value) {
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
return value.replace(
|
|
/\\([|[\]"\\])/g,
|
|
(match, escaped) => DISPLAY_TEXT_ESCAPES[escaped] ?? match
|
|
);
|
|
}
|
|
|
|
// src/renderers/app-process-business-flow.ts
|
|
function renderAppProcessBusinessFlow(model, options = {}) {
|
|
const shell = createMermaidShell({
|
|
className: "model-weave-app-process-business-flow",
|
|
title: `${model.title} (app_process / business flow)`,
|
|
forExport: options.forExport
|
|
});
|
|
const source = buildAppProcessBusinessFlowMermaidSource(model);
|
|
const ready = renderMermaidSourceIntoShell(shell, {
|
|
source,
|
|
renderIdPrefix: "model_weave_app_process_flow",
|
|
fitHorizontalAlign: "left",
|
|
fitVerticalAlign: "top",
|
|
minZoom: 0.02,
|
|
minFitScale: 0.03,
|
|
viewportState: options.viewportState,
|
|
onViewportStateChange: options.onViewportStateChange,
|
|
showSourcePanel: !options.forExport,
|
|
sourcePanelContainer: options.sourcePanelContainer ?? shell.root,
|
|
sourcePanelPlacement: options.sourcePanelPlacement,
|
|
showRenderDebug: !options.forExport && options.debug !== false && options.showMermaidRenderDebug === true
|
|
}).catch((error) => {
|
|
shell.root.addClass("model-weave-mermaid-fallback-shell");
|
|
shell.canvas.replaceChildren(
|
|
createMermaidFallbackNotice(
|
|
modelWeaveText(
|
|
"Business Flow Mermaid preview could not be rendered. Use the summary tables below.",
|
|
"Business Flow Mermaid \u30D7\u30EC\u30D3\u30E5\u30FC\u3092\u63CF\u753B\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u4E0B\u306E\u30B5\u30DE\u30EA\u30C6\u30FC\u30D6\u30EB\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
)
|
|
)
|
|
);
|
|
});
|
|
setMermaidRenderReadyPromise(shell.root, ready);
|
|
return shell.root;
|
|
}
|
|
function buildAppProcessBusinessFlowMermaidSource(model) {
|
|
const stepNodeIds = /* @__PURE__ */ new Map();
|
|
const stepNodeIdsByStepId = /* @__PURE__ */ new Map();
|
|
model.steps.forEach((step, index) => {
|
|
const nodeId = `S${index + 1}`;
|
|
stepNodeIds.set(step, nodeId);
|
|
if (step.id) {
|
|
stepNodeIdsByStepId.set(step.id, nodeId);
|
|
}
|
|
});
|
|
const lines = ["flowchart LR"];
|
|
const laneGroups = /* @__PURE__ */ new Map();
|
|
const unlaned = [];
|
|
for (const step of model.steps) {
|
|
const lane = step.lane?.trim();
|
|
if (!lane) {
|
|
unlaned.push(step);
|
|
continue;
|
|
}
|
|
const group = laneGroups.get(lane) ?? [];
|
|
group.push(step);
|
|
laneGroups.set(lane, group);
|
|
}
|
|
let laneIndex = 0;
|
|
for (const [lane, steps] of laneGroups) {
|
|
laneIndex += 1;
|
|
lines.push(` subgraph L${laneIndex}["${escapeMermaidLabel(lane)}"]`);
|
|
for (const step of steps) {
|
|
lines.push(` ${buildStepNodeDeclaration(stepNodeIds.get(step), step)}`);
|
|
}
|
|
lines.push(" end");
|
|
}
|
|
for (const step of unlaned) {
|
|
lines.push(` ${buildStepNodeDeclaration(stepNodeIds.get(step), step)}`);
|
|
}
|
|
const explicitEdges = model.hasExplicitFlows ? buildExplicitFlowEdges(model.flows, stepNodeIdsByStepId) : [];
|
|
const implicitEdges = buildImplicitStepOrderEdges(
|
|
model.steps,
|
|
stepNodeIds,
|
|
getExplicitFlowSourceStepIds(model.flows, stepNodeIdsByStepId)
|
|
);
|
|
const edges = [...implicitEdges, ...explicitEdges];
|
|
for (const edge of edges) {
|
|
const { fromId, toId } = edge;
|
|
if (!fromId || !toId) {
|
|
continue;
|
|
}
|
|
lines.push(
|
|
edge.label ? ` ${fromId} -->|${toMermaidQuotedLabel(edge.label)}| ${toId}` : ` ${fromId} --> ${toId}`
|
|
);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function buildExplicitFlowEdges(flows, stepNodeIdsByStepId) {
|
|
return flows.map((flow) => ({
|
|
fromId: stepNodeIdsByStepId.get(flow.from),
|
|
toId: stepNodeIdsByStepId.get(flow.to),
|
|
label: getFlowLabel(flow)
|
|
})).filter(
|
|
(edge) => Boolean(edge.fromId && edge.toId)
|
|
);
|
|
}
|
|
function buildImplicitStepOrderEdges(steps, stepNodeIds, suppressedSourceStepIds) {
|
|
return steps.slice(0, -1).filter((step) => !suppressedSourceStepIds.has(step.id)).map((step, index) => ({
|
|
fromId: stepNodeIds.get(step),
|
|
toId: stepNodeIds.get(getNextStep(steps, step)),
|
|
label: ""
|
|
})).filter(
|
|
(edge) => Boolean(edge.fromId && edge.toId)
|
|
);
|
|
}
|
|
function getExplicitFlowSourceStepIds(flows, stepNodeIdsByStepId) {
|
|
return new Set(
|
|
flows.filter(
|
|
(flow) => Boolean(
|
|
flow.from && flow.to && stepNodeIdsByStepId.has(flow.from) && stepNodeIdsByStepId.has(flow.to)
|
|
)
|
|
).map((flow) => flow.from)
|
|
);
|
|
}
|
|
function getNextStep(steps, step) {
|
|
const index = steps.indexOf(step);
|
|
return index >= 0 ? steps[index + 1] : void 0;
|
|
}
|
|
function getStepLabel(step) {
|
|
return decodeEscapedDisplayText(step.label?.trim()) || step.id || "(step)";
|
|
}
|
|
function buildStepNodeDeclaration(nodeId, step) {
|
|
const id = nodeId ?? "S";
|
|
const label = toMermaidQuotedLabel(getStepLabel(step));
|
|
switch (getStepShapeKind(step)) {
|
|
case "terminal":
|
|
return `${id}([${label}])`;
|
|
case "decision":
|
|
return `${id}{${label}}`;
|
|
case "input":
|
|
return `${id}[/${label}/]`;
|
|
case "subflow":
|
|
return `${id}[[${label}]]`;
|
|
case "process":
|
|
default:
|
|
return `${id}[${label}]`;
|
|
}
|
|
}
|
|
function getStepShapeKind(step) {
|
|
const kind = step.kind?.trim().toLowerCase();
|
|
switch (kind) {
|
|
case "start":
|
|
case "end":
|
|
return "terminal";
|
|
case "decision":
|
|
return "decision";
|
|
case "input":
|
|
case "screen":
|
|
return "input";
|
|
case "flow":
|
|
case "subflow":
|
|
return "subflow";
|
|
case "process":
|
|
default:
|
|
return "process";
|
|
}
|
|
}
|
|
function getFlowLabel(flow) {
|
|
const label = flow.label?.trim();
|
|
if (label) {
|
|
return decodeEscapedDisplayText(label);
|
|
}
|
|
return decodeEscapedDisplayText(formatConditionLabel(flow.condition) ?? "");
|
|
}
|
|
function formatConditionLabel(condition) {
|
|
const trimmed = condition?.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
const formatted = trimmed.replace(
|
|
/\[\[([^\]]+)\]\](\.\s*[A-Za-z0-9_-]+)?/g,
|
|
(match, inner, suffix) => {
|
|
const display = formatReferenceDisplayLabel(`[[${inner}]]`);
|
|
if (!display) {
|
|
return match;
|
|
}
|
|
return `${display}${suffix ? suffix.replace(/\s+/g, "") : ""}`;
|
|
}
|
|
);
|
|
if (formatted !== trimmed) {
|
|
return formatted;
|
|
}
|
|
return formatReferenceDisplayLabel(trimmed) ?? trimmed;
|
|
}
|
|
function formatReferenceDisplayLabel(reference) {
|
|
const parsed = parseReferenceValue(reference);
|
|
if (!parsed || parsed.kind === "raw") {
|
|
return null;
|
|
}
|
|
const display = parsed.display?.trim();
|
|
if (display) {
|
|
return display;
|
|
}
|
|
const target = parsed.target?.trim();
|
|
if (!target) {
|
|
return null;
|
|
}
|
|
return target.replace(/\\/g, "/").split("/").filter(Boolean).pop()?.replace(/\.md$/i, "") ?? null;
|
|
}
|
|
|
|
// src/renderers/object-context-renderer.ts
|
|
function renderObjectContext(context, options) {
|
|
const root = document.createElement("section");
|
|
root.addClass("model-weave-object-context");
|
|
root.addClass("model-weave-preview-section");
|
|
const titleRow = document.createElement("div");
|
|
titleRow.addClass("model-weave-object-context-title-row");
|
|
const title = document.createElement("h3");
|
|
title.textContent = "Related objects";
|
|
title.addClass("model-weave-object-context-title");
|
|
title.addClass("model-weave-preview-section-title");
|
|
titleRow.appendChild(title);
|
|
const count = document.createElement("span");
|
|
count.textContent = `${context.relatedObjects.length} linked`;
|
|
count.addClass("model-weave-object-context-count");
|
|
titleRow.appendChild(count);
|
|
root.appendChild(titleRow);
|
|
root.appendChild(createMiniGraph(context, options));
|
|
root.appendChild(createRelatedList(context, options));
|
|
return root;
|
|
}
|
|
function createMiniGraph(context, options) {
|
|
const subgraph = buildObjectSubgraphScene(context);
|
|
const graph = renderDiagramModel(subgraph, {
|
|
onOpenObject: options?.onOpenObject,
|
|
hideTitle: true,
|
|
hideDetails: true,
|
|
fitVerticalAlign: options?.fitVerticalAlign ?? "top",
|
|
viewportState: options?.viewportState,
|
|
onViewportStateChange: options?.onViewportStateChange
|
|
});
|
|
graph.addClass("model-weave-object-context-graph");
|
|
return graph;
|
|
}
|
|
function createRelatedList(context, options) {
|
|
const sortedEntries = [...context.relatedObjects].sort(
|
|
(left, right) => compareRelatedEntries(left, right)
|
|
);
|
|
const details = document.createElement("details");
|
|
details.addClass("model-weave-object-context-list");
|
|
details.addClass("model-weave-preview-section");
|
|
const summary = document.createElement("summary");
|
|
summary.textContent = context.object.fileType === "er-entity" ? `Relation details (${sortedEntries.length})` : `Connection details (${sortedEntries.length})`;
|
|
summary.addClass("model-weave-object-context-summary");
|
|
summary.addClass("model-weave-preview-section-title");
|
|
details.appendChild(summary);
|
|
const tableWrap = document.createElement("div");
|
|
tableWrap.addClass("model-weave-object-context-table-wrap");
|
|
tableWrap.addClass("model-weave-table-wrap");
|
|
if (sortedEntries.length === 0) {
|
|
const empty = document.createElement("p");
|
|
empty.textContent = modelWeaveText(
|
|
"No directly related objects found.",
|
|
"\u76F4\u63A5\u95A2\u4FC2\u3059\u308B\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306F\u3042\u308A\u307E\u305B\u3093\u3002"
|
|
);
|
|
empty.addClass("model-weave-object-context-empty");
|
|
details.appendChild(empty);
|
|
return details;
|
|
}
|
|
const table = document.createElement("table");
|
|
table.addClass("model-weave-object-context-table");
|
|
table.addClass("model-weave-data-table");
|
|
const headers = context.object.fileType === "er-entity" ? ["Related", "Direction", "Relation ID", "Source", "Target", "Kind", "Cardinality", "Mappings", "Notes"] : ["Related", "Direction", "Relation ID", "Source", "Target", "Kind", "Label", "Multiplicity", "Notes"];
|
|
const thead = document.createElement("thead");
|
|
const headRow = document.createElement("tr");
|
|
for (const header of headers) {
|
|
const cell = document.createElement("th");
|
|
cell.textContent = header;
|
|
cell.addClass("model-weave-object-context-th");
|
|
headRow.appendChild(cell);
|
|
}
|
|
thead.appendChild(headRow);
|
|
table.appendChild(thead);
|
|
const tbody = document.createElement("tbody");
|
|
for (const entry of sortedEntries) {
|
|
const row = document.createElement("tr");
|
|
const values = context.object.fileType === "er-entity" ? buildErListRow(entry) : buildClassListRow(entry);
|
|
values.forEach((value, index) => {
|
|
const cell = document.createElement("td");
|
|
cell.addClass("model-weave-object-context-td");
|
|
if (index === 0 && options?.onOpenObject) {
|
|
const wrapper = document.createElement("div");
|
|
wrapper.addClass("model-weave-object-context-link-wrap");
|
|
const badge = createDirectionBadge(entry.direction);
|
|
wrapper.appendChild(badge);
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.textContent = value;
|
|
button.addClass("model-weave-object-context-link");
|
|
button.addEventListener("click", () => {
|
|
options.onOpenObject?.(entry.relatedObjectId, { openInNewLeaf: false });
|
|
});
|
|
wrapper.appendChild(button);
|
|
cell.appendChild(wrapper);
|
|
} else if (index === 1) {
|
|
cell.appendChild(createDirectionBadge(entry.direction));
|
|
} else if (index === 5) {
|
|
cell.appendChild(createKindBadge(value));
|
|
} else {
|
|
cell.textContent = value;
|
|
}
|
|
row.appendChild(cell);
|
|
});
|
|
tbody.appendChild(row);
|
|
}
|
|
table.appendChild(tbody);
|
|
tableWrap.appendChild(table);
|
|
details.appendChild(tableWrap);
|
|
return details;
|
|
}
|
|
function buildErListRow(entry) {
|
|
const relation = entry.relation;
|
|
const related = entry.relatedObject;
|
|
const relatedName = related && related.fileType === "er-entity" ? `${related.logicalName} / ${related.physicalName}` : entry.relatedObjectId;
|
|
if (!("domain" in relation) || relation.domain !== "er") {
|
|
const notes = "notes" in relation && typeof relation.notes === "string" && relation.notes.trim() ? relation.notes : "metadata" in relation && typeof relation.metadata?.notes === "string" ? relation.metadata.notes : "-";
|
|
return [
|
|
relatedName,
|
|
formatDirection(entry.direction),
|
|
relation.id || "-",
|
|
relation.source,
|
|
relation.target,
|
|
relation.kind ?? "-",
|
|
"-",
|
|
"-",
|
|
notes
|
|
];
|
|
}
|
|
const mappingSummary = relation.mappings.map((mapping) => `${mapping.localColumn} -> ${mapping.targetColumn}`).join(", ");
|
|
return [
|
|
relatedName,
|
|
formatDirection(entry.direction),
|
|
relation.id || "-",
|
|
relation.sourceEntity,
|
|
relation.targetEntity,
|
|
relation.kind,
|
|
relation.cardinality ?? "-",
|
|
truncateValue(mappingSummary || "-", 72),
|
|
relation.notes || "-"
|
|
];
|
|
}
|
|
function buildClassListRow(entry) {
|
|
const relation = normalizeClassRelation2(entry.relation);
|
|
const relatedName = entry.relatedObject?.fileType === "object" ? entry.relatedObject.name : entry.relatedObjectId;
|
|
const multiplicity = [
|
|
relation.fromMultiplicity ? `from ${relation.fromMultiplicity}` : "",
|
|
relation.toMultiplicity ? `to ${relation.toMultiplicity}` : ""
|
|
].filter(Boolean).join(" / ");
|
|
return [
|
|
relatedName,
|
|
formatDirection(entry.direction),
|
|
relation.id || "-",
|
|
relation.sourceClass,
|
|
relation.targetClass,
|
|
relation.kind,
|
|
relation.label ?? "-",
|
|
multiplicity || "-",
|
|
relation.notes || "-"
|
|
];
|
|
}
|
|
function normalizeClassRelation2(relation) {
|
|
if ("domain" in relation) {
|
|
if (relation.domain === "class") {
|
|
return relation;
|
|
}
|
|
return toClassRelationEdge({
|
|
id: relation.id,
|
|
kind: "association",
|
|
source: relation.source,
|
|
target: relation.target,
|
|
label: relation.label
|
|
});
|
|
}
|
|
return toClassRelationEdge(relation);
|
|
}
|
|
function compareRelatedEntries(left, right) {
|
|
if (left.direction !== right.direction) {
|
|
return left.direction === "outgoing" ? -1 : 1;
|
|
}
|
|
const leftName = getStableRelatedName(left).toLowerCase();
|
|
const rightName = getStableRelatedName(right).toLowerCase();
|
|
if (leftName !== rightName) {
|
|
return leftName.localeCompare(rightName);
|
|
}
|
|
const leftId = getRelationId(left).toLowerCase();
|
|
const rightId = getRelationId(right).toLowerCase();
|
|
return leftId.localeCompare(rightId);
|
|
}
|
|
function getStableRelatedName(entry) {
|
|
if (!entry.relatedObject) {
|
|
return entry.relatedObjectId;
|
|
}
|
|
if (entry.relatedObject.fileType === "er-entity") {
|
|
return `${entry.relatedObject.logicalName}/${entry.relatedObject.physicalName}`;
|
|
}
|
|
return entry.relatedObject.name;
|
|
}
|
|
function getRelationId(entry) {
|
|
const relation = entry.relation;
|
|
if ("domain" in relation) {
|
|
return relation.id || relation.label || relation.kind;
|
|
}
|
|
return relation.id || relation.label || relation.kind;
|
|
}
|
|
function formatDirection(direction) {
|
|
return direction === "outgoing" ? "Outbound" : "Inbound";
|
|
}
|
|
function createDirectionBadge(direction) {
|
|
const badge = document.createElement("span");
|
|
badge.textContent = formatDirection(direction);
|
|
badge.addClass("model-weave-badge");
|
|
badge.addClass(getDirectionBadgeClass(direction));
|
|
return badge;
|
|
}
|
|
function createKindBadge(kind) {
|
|
const badge = document.createElement("span");
|
|
badge.textContent = kind || "-";
|
|
badge.addClass("model-weave-badge");
|
|
badge.addClass(getKindBadgeClass(kind));
|
|
return badge;
|
|
}
|
|
function getDirectionBadgeClass(direction) {
|
|
return direction === "outgoing" ? "model-weave-badge-outgoing" : "model-weave-badge-incoming";
|
|
}
|
|
function getKindBadgeClass(kind) {
|
|
switch (kind) {
|
|
case "inheritance":
|
|
return "model-weave-badge-inheritance";
|
|
case "implementation":
|
|
return "model-weave-badge-implementation";
|
|
case "dependency":
|
|
return "model-weave-badge-dependency";
|
|
case "composition":
|
|
return "model-weave-badge-composition";
|
|
case "aggregation":
|
|
return "model-weave-badge-aggregation";
|
|
case "association":
|
|
return "model-weave-badge-association";
|
|
case "fk":
|
|
return "model-weave-badge-fk";
|
|
default:
|
|
return "model-weave-badge-default";
|
|
}
|
|
}
|
|
function truncateValue(value, maxLength) {
|
|
if (value.length <= maxLength) {
|
|
return value;
|
|
}
|
|
return `${value.slice(0, maxLength - 3)}...`;
|
|
}
|
|
|
|
// src/renderers/source-links-renderer.ts
|
|
var import_fs = require("fs");
|
|
var import_path = __toESM(require("path"));
|
|
var import_obsidian6 = require("obsidian");
|
|
var electron = require("electron");
|
|
function renderSourceLinks(sourceLinks, localSourceRoot) {
|
|
const validSourceLinks = (sourceLinks ?? []).filter(
|
|
(sourceLink) => sourceLink.path.trim()
|
|
);
|
|
if (validSourceLinks.length === 0) {
|
|
return null;
|
|
}
|
|
const section = document.createElement("section");
|
|
section.addClass("model-weave-source-links");
|
|
section.addClass("model-weave-preview-section");
|
|
const title = document.createElement("h3");
|
|
title.textContent = "Source Links";
|
|
title.addClass("model-weave-source-links-title");
|
|
title.addClass("model-weave-preview-section-title");
|
|
section.appendChild(title);
|
|
section.createEl("p", {
|
|
text: modelWeaveText(
|
|
"Open uses your OS/default app and may fail for UNC/WSL paths or unsupported file associations.",
|
|
"Open \u306F OS \u306E\u65E2\u5B9A\u30A2\u30D7\u30EA\u3067\u958B\u304D\u307E\u3059\u3002UNC/WSL \u30D1\u30B9\u3084\u672A\u5BFE\u5FDC\u306E\u95A2\u9023\u4ED8\u3051\u3067\u306F\u5931\u6557\u3059\u308B\u3053\u3068\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
),
|
|
cls: "model-weave-source-links-help"
|
|
});
|
|
const tableWrap = document.createElement("div");
|
|
tableWrap.addClass("model-weave-table-wrap");
|
|
const table = document.createElement("table");
|
|
table.addClass("model-weave-source-links-table");
|
|
table.addClass("model-weave-data-table");
|
|
const thead = table.createEl("thead");
|
|
const headRow = thead.createEl("tr");
|
|
for (const header of ["Path", "Status", modelWeaveText("Resolved Path", "\u89E3\u6C7A\u6E08\u307F\u30D1\u30B9"), "Notes", "Action"]) {
|
|
headRow.createEl("th", {
|
|
text: header,
|
|
cls: "model-weave-source-links-th"
|
|
});
|
|
}
|
|
const tbody = table.createEl("tbody");
|
|
for (const sourceLink of validSourceLinks) {
|
|
const status = resolveSourceLinkStatus(sourceLink, localSourceRoot);
|
|
const row = tbody.createEl("tr");
|
|
row.createEl("td", {
|
|
text: sourceLink.path,
|
|
cls: "model-weave-source-links-td model-weave-source-links-path"
|
|
});
|
|
const statusCell = row.createEl("td", { cls: "model-weave-source-links-td" });
|
|
const badge = statusCell.createEl("span", {
|
|
text: status.label,
|
|
cls: `model-weave-source-links-status ${status.modifierClass}`
|
|
});
|
|
badge.title = status.label;
|
|
row.createEl("td", {
|
|
text: status.resolvedPath,
|
|
cls: "model-weave-source-links-td model-weave-source-links-resolved"
|
|
});
|
|
row.createEl("td", {
|
|
text: sourceLink.notes ?? sourceLink.label ?? "-",
|
|
cls: "model-weave-source-links-td"
|
|
});
|
|
const actionCell = row.createEl("td", { cls: "model-weave-source-links-td" });
|
|
const copyButton = actionCell.createEl("button", {
|
|
text: modelWeaveText("Copy Path", "\u30D1\u30B9\u3092\u30B3\u30D4\u30FC"),
|
|
cls: "model-weave-source-links-open"
|
|
});
|
|
copyButton.type = "button";
|
|
copyButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
void navigator.clipboard?.writeText(status.resolvedPath);
|
|
});
|
|
const button = actionCell.createEl("button", {
|
|
text: modelWeaveText("Open", "\u958B\u304F"),
|
|
cls: "model-weave-source-links-open"
|
|
});
|
|
button.type = "button";
|
|
button.disabled = !status.openable;
|
|
button.title = modelWeaveText("Open with default app", "\u65E2\u5B9A\u30A2\u30D7\u30EA\u3067\u958B\u304F");
|
|
button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
void openResolvedSourcePath(status.resolvedPath);
|
|
});
|
|
if (status.actionNote) {
|
|
actionCell.createEl("span", {
|
|
text: status.actionNote,
|
|
cls: "model-weave-source-links-action-note"
|
|
});
|
|
}
|
|
}
|
|
tableWrap.appendChild(table);
|
|
section.appendChild(tableWrap);
|
|
return section;
|
|
}
|
|
function resolveSourceLinkStatus(sourceLink, localSourceRoot) {
|
|
const resolved = resolveSourceLinkPath(localSourceRoot, sourceLink.path);
|
|
if (resolved.kind === "fileUri") {
|
|
return {
|
|
label: modelWeaveText("unsupported file URI", "file URI \u306F\u672A\u5BFE\u5FDC\u3067\u3059"),
|
|
modifierClass: "model-weave-source-links-status-neutral",
|
|
resolvedPath: resolved.resolvedPath,
|
|
openable: false,
|
|
actionNote: modelWeaveText(
|
|
"Use a filesystem path instead of a file URI",
|
|
"file URI \u3067\u306F\u306A\u304F\u30D5\u30A1\u30A4\u30EB\u30B7\u30B9\u30C6\u30E0\u306E\u30D1\u30B9\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044"
|
|
)
|
|
};
|
|
}
|
|
const { kind, rootPath, resolvedPath } = resolved;
|
|
if (resolved.unsupportedSourceRoot) {
|
|
return {
|
|
label: modelWeaveText("unsupported source root", "source root \u304C\u672A\u5BFE\u5FDC\u3067\u3059"),
|
|
modifierClass: "model-weave-source-links-status-neutral",
|
|
resolvedPath,
|
|
openable: true,
|
|
actionNote: getPathKindNote(kind)
|
|
};
|
|
}
|
|
if (resolved.usedSourceRoot && !isResolvedPathInsideRoot(kind, rootPath, resolvedPath)) {
|
|
return {
|
|
label: modelWeaveText("outside source root", "source root \u306E\u5916\u3067\u3059"),
|
|
modifierClass: "model-weave-source-links-status-neutral",
|
|
resolvedPath,
|
|
openable: true
|
|
};
|
|
}
|
|
const unconfiguredRelative = kind === "relative" && !resolved.usedSourceRoot && !localSourceRoot.trim();
|
|
if (!sourcePathExists(resolvedPath)) {
|
|
return {
|
|
label: unconfiguredRelative ? modelWeaveText("Local source root is not configured", "Local source root \u304C\u672A\u8A2D\u5B9A\u3067\u3059") : modelWeaveText("missing", "\u898B\u3064\u304B\u308A\u307E\u305B\u3093"),
|
|
modifierClass: unconfiguredRelative ? "model-weave-source-links-status-neutral" : "model-weave-source-links-status-missing",
|
|
resolvedPath,
|
|
openable: true,
|
|
actionNote: getPathKindNote(kind)
|
|
};
|
|
}
|
|
const stats = (0, import_fs.statSync)(resolvedPath);
|
|
return {
|
|
label: stats.isFile() ? modelWeaveText("available", "\u5229\u7528\u53EF\u80FD") : modelWeaveText("available directory", "\u5229\u7528\u53EF\u80FD\u306A\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA"),
|
|
modifierClass: "model-weave-source-links-status-available",
|
|
resolvedPath,
|
|
openable: true,
|
|
actionNote: getPathKindNote(kind)
|
|
};
|
|
}
|
|
function classifySourceRootPath(input) {
|
|
const trimmed = input.trim();
|
|
if (/^file:\/\//i.test(trimmed)) {
|
|
return {
|
|
kind: "fileUri",
|
|
normalizedPath: trimmed
|
|
};
|
|
}
|
|
if (/^\/\/[^/]+\/[^/]+/.test(trimmed)) {
|
|
return {
|
|
kind: "slashStyleWindowsUnc",
|
|
normalizedPath: `\\\\${trimmed.slice(2).replace(/\//g, "\\")}`
|
|
};
|
|
}
|
|
if (/^\\\\[^\\]+\\[^\\]+/.test(trimmed)) {
|
|
return {
|
|
kind: "windowsUnc",
|
|
normalizedPath: trimmed
|
|
};
|
|
}
|
|
if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
return {
|
|
kind: "windowsDrive",
|
|
normalizedPath: import_path.default.win32.normalize(trimmed)
|
|
};
|
|
}
|
|
if (trimmed.startsWith("/")) {
|
|
return {
|
|
kind: "posixAbsolute",
|
|
normalizedPath: import_path.default.posix.normalize(trimmed)
|
|
};
|
|
}
|
|
return {
|
|
kind: "relative",
|
|
normalizedPath: trimmed
|
|
};
|
|
}
|
|
function resolveSourceLinkPath(sourceRoot, sourceLinkPath) {
|
|
const linkPath = classifySourceRootPath(sourceLinkPath);
|
|
if (linkPath.kind !== "relative") {
|
|
return {
|
|
kind: linkPath.kind === "slashStyleWindowsUnc" ? "windowsUnc" : linkPath.kind,
|
|
rootPath: "",
|
|
resolvedPath: linkPath.normalizedPath,
|
|
usedSourceRoot: false
|
|
};
|
|
}
|
|
const classified = classifySourceRootPath(sourceRoot);
|
|
if (!sourceRoot.trim() || classified.kind === "relative" || classified.kind === "fileUri") {
|
|
return {
|
|
kind: "relative",
|
|
rootPath: "",
|
|
resolvedPath: sourceLinkPath,
|
|
usedSourceRoot: false,
|
|
unsupportedSourceRoot: Boolean(sourceRoot.trim())
|
|
};
|
|
}
|
|
const cleanedRelativePath = sourceLinkPath.replace(/^[/\\]+/, "");
|
|
const pathApi = classified.kind === "windowsDrive" || classified.kind === "windowsUnc" || classified.kind === "slashStyleWindowsUnc" ? import_path.default.win32 : import_path.default.posix;
|
|
const rootPath = classified.normalizedPath;
|
|
return {
|
|
kind: classified.kind === "slashStyleWindowsUnc" ? "windowsUnc" : classified.kind,
|
|
rootPath,
|
|
resolvedPath: pathApi.normalize(pathApi.join(rootPath, cleanedRelativePath)),
|
|
usedSourceRoot: true
|
|
};
|
|
}
|
|
function isResolvedPathInsideRoot(kind, rootPath, resolvedPath) {
|
|
const pathApi = kind === "windowsDrive" || kind === "windowsUnc" ? import_path.default.win32 : import_path.default.posix;
|
|
const relativePath = pathApi.relative(rootPath, resolvedPath);
|
|
return relativePath === "" || !relativePath.startsWith("..") && !pathApi.isAbsolute(relativePath);
|
|
}
|
|
function sourcePathExists(resolvedPath) {
|
|
try {
|
|
return (0, import_fs.existsSync)(resolvedPath);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
function isUncPathKind(kind) {
|
|
return kind === "windowsUnc" || kind === "slashStyleWindowsUnc";
|
|
}
|
|
function getPathKindNote(kind) {
|
|
return isUncPathKind(kind) ? modelWeaveText(
|
|
"UNC/WSL path. Open may depend on your OS and app support.",
|
|
"UNC/WSL \u30D1\u30B9\u3067\u3059\u3002Open \u306F OS \u3084\u30A2\u30D7\u30EA\u306E\u5BFE\u5FDC\u72B6\u6CC1\u306B\u4F9D\u5B58\u3057\u307E\u3059\u3002"
|
|
) : void 0;
|
|
}
|
|
async function openResolvedSourcePath(resolvedPath) {
|
|
try {
|
|
if (typeof electron.shell?.openPath !== "function") {
|
|
new import_obsidian6.Notice(modelWeaveText(
|
|
"Could not open Source Link: OS open is not available.",
|
|
"Source Link \u3092\u958B\u3051\u307E\u305B\u3093\u3067\u3057\u305F\u3002OS \u306E open \u6A5F\u80FD\u304C\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002"
|
|
));
|
|
return;
|
|
}
|
|
const result = await electron.shell.openPath(resolvedPath);
|
|
if (result) {
|
|
new import_obsidian6.Notice(modelWeaveText(
|
|
`Could not open Source Link: ${result}`,
|
|
`Source Link \u3092\u958B\u3051\u307E\u305B\u3093\u3067\u3057\u305F: ${result}`
|
|
));
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
new import_obsidian6.Notice(modelWeaveText(
|
|
`Could not open Source Link: ${message}`,
|
|
`Source Link \u3092\u958B\u3051\u307E\u305B\u3093\u3067\u3057\u305F: ${message}`
|
|
));
|
|
}
|
|
}
|
|
|
|
// src/renderers/object-renderer.ts
|
|
function renderObjectModel(model, context, localSourceRoot = "") {
|
|
const root = document.createElement("section");
|
|
root.addClass("model-weave-object-focus");
|
|
root.addClass("model-weave-summary-details");
|
|
root.addClass("model-weave-preview-section");
|
|
const title = document.createElement("h2");
|
|
title.textContent = getPrimaryTitle(model);
|
|
title.addClass("model-weave-object-title");
|
|
title.addClass("model-weave-preview-section-title");
|
|
root.appendChild(title);
|
|
const meta = document.createElement("div");
|
|
meta.addClass("model-weave-object-meta");
|
|
meta.addClass("model-weave-detail-card");
|
|
if (model.fileType === "er-entity") {
|
|
appendMeta(meta, "Logical Name", model.logicalName);
|
|
appendMeta(meta, "Physical Name", model.physicalName);
|
|
appendMeta(meta, "Type", "er_entity");
|
|
appendMeta(meta, "Schema Name", model.schemaName ?? "-");
|
|
appendMeta(meta, "DBMS", model.dbms ?? "-");
|
|
appendMeta(meta, "Related Count", String(context?.relatedObjects.length ?? 0));
|
|
} else if (model.fileType === "object") {
|
|
appendMeta(meta, "Name", model.name);
|
|
appendMeta(meta, "Type", "class");
|
|
appendMeta(meta, "Kind", model.kind);
|
|
appendMeta(meta, "Related Count", String(context?.relatedObjects.length ?? 0));
|
|
} else {
|
|
appendMeta(meta, "Name", model.name);
|
|
appendMeta(meta, "Type", "dfd_object");
|
|
appendMeta(meta, "Kind", model.kind);
|
|
}
|
|
root.appendChild(meta);
|
|
const sourceLinks = renderSourceLinks(model.sourceLinks, localSourceRoot);
|
|
if (sourceLinks) {
|
|
root.appendChild(sourceLinks);
|
|
}
|
|
return root;
|
|
}
|
|
function getPrimaryTitle(model) {
|
|
return model.fileType === "er-entity" ? model.logicalName : model.name;
|
|
}
|
|
function appendMeta(container, label, value) {
|
|
const row = document.createElement("div");
|
|
row.addClass("model-weave-detail-card-row");
|
|
const key = document.createElement("div");
|
|
key.textContent = label;
|
|
key.addClass("model-weave-object-meta-key");
|
|
key.addClass("model-weave-detail-card-label");
|
|
const val = document.createElement("div");
|
|
val.textContent = value;
|
|
val.addClass("model-weave-object-meta-val");
|
|
val.addClass("model-weave-detail-card-value");
|
|
row.append(key, val);
|
|
container.appendChild(row);
|
|
}
|
|
|
|
// src/i18n/en.ts
|
|
var EN_MESSAGES = {
|
|
"relationship.title": "Impact / Relationships",
|
|
"relationship.copySummary": "Copy relationship summary",
|
|
"relationship.referencesFromThisObject": "References from this object",
|
|
"relationship.referencedByThisObject": "Referenced by this object",
|
|
"relationship.unresolvedReferences": "Unresolved references",
|
|
"relationship.relatedSourceLinks": "Related source links",
|
|
"relationship.valueUsage": "Value usage",
|
|
"relationship.noOutbound": "No outbound object relationships found.",
|
|
"relationship.noInbound": "No inbound object relationships found.",
|
|
"relationship.noValueUsage": "None",
|
|
"relationship.noUnresolved": "No unresolved outbound references found.",
|
|
"relationship.noRelatedSourceLinks": "No related source links found.",
|
|
"relationship.open": "Open",
|
|
"relationship.sourceLink": "Source Link",
|
|
"relationship.usage.one": "usage",
|
|
"relationship.usage.other": "usages",
|
|
"relationship.note.one": "note",
|
|
"relationship.note.other": "notes"
|
|
};
|
|
|
|
// src/i18n/ja.ts
|
|
var JA_MESSAGES = {
|
|
"relationship.title": "\u5F71\u97FF / \u95A2\u9023",
|
|
"relationship.copySummary": "\u95A2\u9023\u30B5\u30DE\u30EA\u3092\u30B3\u30D4\u30FC",
|
|
"relationship.referencesFromThisObject": "\u3053\u306E\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u304C\u53C2\u7167\u3057\u3066\u3044\u308B\u3082\u306E",
|
|
"relationship.referencedByThisObject": "\u3053\u306E\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3092\u53C2\u7167\u3057\u3066\u3044\u308B\u3082\u306E",
|
|
"relationship.unresolvedReferences": "\u672A\u89E3\u6C7A\u306E\u53C2\u7167",
|
|
"relationship.relatedSourceLinks": "\u95A2\u9023\u30BD\u30FC\u30B9\u30EA\u30F3\u30AF",
|
|
"relationship.valueUsage": "\u5024\u306E\u5229\u7528\u72B6\u6CC1",
|
|
"relationship.noOutbound": "\u53C2\u7167\u5148\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
"relationship.noInbound": "\u3053\u306E\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3092\u53C2\u7167\u3057\u3066\u3044\u308B\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
"relationship.noValueUsage": "\u306A\u3057",
|
|
"relationship.noUnresolved": "\u672A\u89E3\u6C7A\u306E\u53C2\u7167\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
"relationship.noRelatedSourceLinks": "\u95A2\u9023\u30BD\u30FC\u30B9\u30EA\u30F3\u30AF\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
"relationship.open": "\u958B\u304F",
|
|
"relationship.sourceLink": "\u30BD\u30FC\u30B9\u30EA\u30F3\u30AF",
|
|
"relationship.usage.one": "\u4EF6",
|
|
"relationship.usage.other": "\u4EF6",
|
|
"relationship.note.one": "\u4EF6\u306E\u30E1\u30E2",
|
|
"relationship.note.other": "\u4EF6\u306E\u30E1\u30E2"
|
|
};
|
|
|
|
// src/i18n/messages.ts
|
|
var DICTIONARIES = {
|
|
en: EN_MESSAGES,
|
|
ja: JA_MESSAGES
|
|
};
|
|
function createModelWeaveTranslator(language) {
|
|
const dictionary = DICTIONARIES[resolveModelWeaveUiLanguage(language)];
|
|
return (key, params) => {
|
|
const template = dictionary[key] ?? EN_MESSAGES[key] ?? key;
|
|
return interpolateMessage(template, params);
|
|
};
|
|
}
|
|
function resolveModelWeaveUiLanguage(language) {
|
|
return language === "auto" ? getModelWeaveLanguage() : resolveModelWeaveLanguage(language);
|
|
}
|
|
function interpolateMessage(template, params) {
|
|
if (!params) {
|
|
return template;
|
|
}
|
|
return template.replace(/\{([A-Za-z0-9_]+)\}/g, (match, key) => {
|
|
const value = params[key];
|
|
return value === void 0 ? match : String(value);
|
|
});
|
|
}
|
|
|
|
// src/views/usage-view-renderer.ts
|
|
function renderUsageViewSections(container, sections, options) {
|
|
for (const section of sections) {
|
|
renderUsageViewSection(container, section, options);
|
|
}
|
|
}
|
|
function renderUsageDetailSection(container, id, title, emptyText, details, options) {
|
|
const body = createCollapsibleSection(container, id, title, false, options);
|
|
if (details.length === 0) {
|
|
body.createEl("p", { text: emptyText, cls: "model-weave-summary-muted" });
|
|
return;
|
|
}
|
|
const list = body.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const detail of details) {
|
|
const item = list.createEl("li", {
|
|
text: `${detail.label}${detail.meta ? ` (${detail.meta})` : ""}`
|
|
});
|
|
item.title = detail.title ?? detail.notes ?? detail.label;
|
|
}
|
|
}
|
|
function renderGroupedSourceLinkSection(container, id, title, emptyText, sourceLinks, options) {
|
|
const body = createCollapsibleSection(container, id, title, false, options);
|
|
if (sourceLinks.length === 0) {
|
|
body.createEl("p", { text: emptyText, cls: "model-weave-summary-muted" });
|
|
return;
|
|
}
|
|
const list = body.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const sourceLink of sourceLinks) {
|
|
renderGroupedSourceLink(list, sourceLink, options);
|
|
}
|
|
}
|
|
function renderUsageViewSection(container, section, options) {
|
|
const body = createCollapsibleSection(container, section.id, section.title, false, options);
|
|
if (section.items.length === 0) {
|
|
body.createEl("p", { text: section.emptyText, cls: "model-weave-summary-muted" });
|
|
return;
|
|
}
|
|
const list = body.createEl("ul", {
|
|
cls: "model-weave-summary-list model-weave-impact-relationship-list"
|
|
});
|
|
for (const usageItem of section.items) {
|
|
renderUsageItem(list, usageItem, options);
|
|
}
|
|
}
|
|
function renderUsageItem(list, usageItem, options) {
|
|
const item = list.createEl("li", { cls: "model-weave-impact-relationship" });
|
|
const details = item.createEl("details", {
|
|
cls: "model-weave-impact-relationship-item"
|
|
});
|
|
const row = details.createEl("summary", {
|
|
cls: "model-weave-impact-relationship-summary"
|
|
});
|
|
const rowContent = row.createSpan({
|
|
cls: "model-weave-impact-relationship-summary-content"
|
|
});
|
|
rowContent.createSpan({
|
|
cls: "model-weave-impact-relationship-title",
|
|
text: usageItem.summaryText ?? `${usageItem.label} (${usageItem.type ?? "-"}; ${options.formatUsageCount(usageItem.usageCount)})`
|
|
});
|
|
const openPath = usageItem.openTargetPath ?? usageItem.path;
|
|
if (options.onOpenItem && openPath) {
|
|
const openButton = rowContent.createEl("button", {
|
|
text: options.openLabel,
|
|
cls: "model-weave-impact-open-button"
|
|
});
|
|
openButton.type = "button";
|
|
openButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
options.onOpenItem?.(openPath, {
|
|
openInNewLeaf: Boolean(event.ctrlKey || event.metaKey)
|
|
});
|
|
});
|
|
openButton.addEventListener("auxclick", (event) => {
|
|
if (event.button !== 1) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
options.onOpenItem?.(openPath, {
|
|
openInNewLeaf: true
|
|
});
|
|
});
|
|
}
|
|
if (usageItem.path) {
|
|
details.createDiv({
|
|
cls: "model-weave-impact-relationship-path",
|
|
text: usageItem.path
|
|
});
|
|
}
|
|
const detailList = details.createEl("ul", {
|
|
cls: "model-weave-summary-list model-weave-impact-usage-list"
|
|
});
|
|
for (const detail of usageItem.details) {
|
|
const detailItem = detailList.createEl("li", {
|
|
cls: "model-weave-impact-usage-item"
|
|
});
|
|
detailItem.createDiv({
|
|
text: detail.label,
|
|
cls: "model-weave-impact-usage-main"
|
|
});
|
|
if (detail.meta) {
|
|
detailItem.createDiv({
|
|
text: detail.meta,
|
|
cls: "model-weave-impact-usage-meta"
|
|
});
|
|
}
|
|
if (detail.notes) {
|
|
detailItem.createDiv({
|
|
text: detail.notes,
|
|
cls: "model-weave-impact-usage-meta"
|
|
});
|
|
}
|
|
detailItem.title = detail.title ?? detail.notes ?? detail.label;
|
|
}
|
|
if (usageItem.sourceLinks.length > 0) {
|
|
const linkList = details.createEl("ul", {
|
|
cls: "model-weave-summary-list model-weave-impact-source-link-list"
|
|
});
|
|
for (const link of usageItem.sourceLinks) {
|
|
renderGroupedSourceLink(linkList, link, options, options.sourceLinkLabel);
|
|
}
|
|
}
|
|
}
|
|
function renderGroupedSourceLink(list, sourceLink, options, prefix) {
|
|
const label = sourceLink.label ? `${sourceLink.label}: ` : "";
|
|
const noteSuffix = sourceLink.notes.length > 0 ? ` (${options.formatNoteCount(sourceLink.notes.length)})` : "";
|
|
const rowText = prefix ? `${prefix}: ${label}${sourceLink.path}${noteSuffix}` : `[${sourceLink.relationKind}] ${sourceLink.ownerLabel}: ${label}${sourceLink.path}${noteSuffix}`;
|
|
const item = list.createEl("li", {
|
|
cls: "model-weave-impact-source-link-group"
|
|
});
|
|
item.title = sourceLink.notes.length > 0 ? sourceLink.notes.join("\n") : sourceLink.ownerPath ?? sourceLink.path;
|
|
if (sourceLink.notes.length === 0) {
|
|
item.setText(rowText);
|
|
return;
|
|
}
|
|
const details = item.createEl("details", {
|
|
cls: "model-weave-impact-source-link-details"
|
|
});
|
|
details.createEl("summary", { text: rowText });
|
|
const noteList = details.createEl("ul", {
|
|
cls: "model-weave-summary-list model-weave-impact-source-link-note-list"
|
|
});
|
|
for (const note of sourceLink.notes) {
|
|
noteList.createEl("li", { text: note });
|
|
}
|
|
}
|
|
function createCollapsibleSection(container, key, title, defaultOpen, options) {
|
|
const details = container.createEl("details");
|
|
details.addClass("model-weave-preview-section");
|
|
details.open = options.getOpenState ? options.getOpenState(key, defaultOpen) : defaultOpen;
|
|
if (options.setOpenState) {
|
|
details.addEventListener("toggle", () => {
|
|
options.setOpenState?.(key, details.open);
|
|
});
|
|
}
|
|
const summary = details.createEl("summary", { text: title });
|
|
summary.addClass("model-weave-summary-heading");
|
|
summary.addClass("model-weave-preview-section-title");
|
|
return details.createDiv();
|
|
}
|
|
|
|
// src/views/view-icon.ts
|
|
var MODELING_VIEW_ICON = "git-branch";
|
|
|
|
// src/views/modeling-preview-view.ts
|
|
var MODELING_PREVIEW_VIEW_TYPE = "mdspec-preview";
|
|
var VIEWPORT_STATE_CACHE_LIMIT = 50;
|
|
var DEFAULT_VIEWER_PREFERENCES = {
|
|
defaultZoom: "fit",
|
|
fontSize: "normal",
|
|
nodeDensity: "normal",
|
|
localSourceRoot: "",
|
|
uiLanguage: "auto",
|
|
showMermaidRenderDebug: false
|
|
};
|
|
var ModelingPreviewView = class extends import_obsidian7.ItemView {
|
|
constructor(leaf, viewerPreferences = DEFAULT_VIEWER_PREFERENCES) {
|
|
super(leaf);
|
|
this.diagramViewportState = {
|
|
zoom: 1,
|
|
panX: 0,
|
|
panY: 0,
|
|
viewMode: "fit",
|
|
hasAutoFitted: false,
|
|
hasUserInteracted: false
|
|
};
|
|
this.objectGraphViewportState = {
|
|
zoom: 1,
|
|
panX: 0,
|
|
panY: 0,
|
|
viewMode: "fit",
|
|
hasAutoFitted: false,
|
|
hasUserInteracted: false
|
|
};
|
|
this.screenPreviewViewportState = {
|
|
zoom: 1,
|
|
panX: 0,
|
|
panY: 0,
|
|
viewMode: "fit",
|
|
hasAutoFitted: false,
|
|
hasUserInteracted: false
|
|
};
|
|
this.state = {
|
|
mode: "empty",
|
|
message: "\u5BFE\u5FDC\u30D5\u30A1\u30A4\u30EB\u3092\u958B\u304F\u3068\u30D7\u30EC\u30D3\u30E5\u30FC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002",
|
|
warnings: []
|
|
};
|
|
this.diagramFilePath = null;
|
|
this.objectGraphFilePath = null;
|
|
this.screenPreviewFilePath = null;
|
|
this.viewportStateCache = /* @__PURE__ */ new Map();
|
|
this.collapsibleState = /* @__PURE__ */ new Map();
|
|
this.scrollStateByFilePath = /* @__PURE__ */ new Map();
|
|
this.splitRatioByKey = /* @__PURE__ */ new Map();
|
|
this.activeScrollContainer = null;
|
|
this.getCollapsibleOpenState = (key, defaultOpen) => {
|
|
return this.collapsibleState.get(key) ?? defaultOpen;
|
|
};
|
|
this.setCollapsibleOpenState = (key, open) => {
|
|
this.collapsibleState.set(key, open);
|
|
};
|
|
this.viewerPreferences = { ...viewerPreferences };
|
|
this.t = createModelWeaveTranslator(this.viewerPreferences.uiLanguage);
|
|
}
|
|
getViewType() {
|
|
return MODELING_PREVIEW_VIEW_TYPE;
|
|
}
|
|
getDisplayText() {
|
|
return "Modeling preview";
|
|
}
|
|
getIcon() {
|
|
return MODELING_VIEW_ICON;
|
|
}
|
|
onOpen() {
|
|
this.renderCurrentState();
|
|
return Promise.resolve();
|
|
}
|
|
onClose() {
|
|
this.clearView();
|
|
return Promise.resolve();
|
|
}
|
|
applyViewerSettings(viewerPreferences) {
|
|
this.viewerPreferences = { ...viewerPreferences };
|
|
this.t = createModelWeaveTranslator(this.viewerPreferences.uiLanguage);
|
|
}
|
|
refreshForSettingsChange() {
|
|
this.renderCurrentState();
|
|
this.restoreCurrentScrollPosition();
|
|
}
|
|
async exportCurrentDiagramAsPng() {
|
|
const exportRenderable = this.buildCurrentDiagramExportRenderable();
|
|
if (!exportRenderable) {
|
|
return null;
|
|
}
|
|
return exportDiagramRenderableAsPng(this.app, exportRenderable);
|
|
}
|
|
updateContent(state, reason = "rerender") {
|
|
const previousFilePath = this.getCurrentFilePath();
|
|
const nextFilePath = this.getFilePathForState(state);
|
|
if (previousFilePath && nextFilePath && previousFilePath !== nextFilePath) {
|
|
this.resetImpactCollapsibleState();
|
|
}
|
|
this.persistActiveViewportState();
|
|
this.persistCurrentScrollPosition();
|
|
this.prepareViewportState(state, reason);
|
|
this.state = state;
|
|
this.renderCurrentState();
|
|
this.restoreCurrentScrollPosition();
|
|
}
|
|
getCurrentFilePath() {
|
|
return this.getFilePathForState(this.state);
|
|
}
|
|
getFilePathForState(state) {
|
|
switch (state.mode) {
|
|
case "diagram":
|
|
return state.diagram.diagram.path;
|
|
case "object":
|
|
return "filePath" in state.model ? state.model.filePath : state.model.path;
|
|
case "dfd-object":
|
|
return state.model.path;
|
|
case "summary":
|
|
return state.filePath;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
persistActiveViewportState() {
|
|
if (this.diagramFilePath) {
|
|
this.rememberViewportState(this.diagramFilePath, this.diagramViewportState);
|
|
}
|
|
if (this.objectGraphFilePath) {
|
|
this.rememberViewportState(this.objectGraphFilePath, this.objectGraphViewportState);
|
|
}
|
|
if (this.screenPreviewFilePath) {
|
|
this.rememberViewportState(this.screenPreviewFilePath, this.screenPreviewViewportState);
|
|
}
|
|
}
|
|
prepareViewportState(state, reason) {
|
|
if (state.mode === "diagram") {
|
|
const nextFilePath = state.diagram.diagram.path;
|
|
this.prepareFileViewportState(
|
|
this.diagramViewportState,
|
|
this.diagramFilePath,
|
|
nextFilePath,
|
|
reason
|
|
);
|
|
this.diagramFilePath = nextFilePath;
|
|
return;
|
|
}
|
|
if (state.mode === "object" && state.context) {
|
|
const objectPath = "filePath" in state.model ? state.model.filePath : state.model.path;
|
|
this.prepareFileViewportState(
|
|
this.objectGraphViewportState,
|
|
this.objectGraphFilePath,
|
|
objectPath,
|
|
reason
|
|
);
|
|
this.objectGraphFilePath = objectPath;
|
|
return;
|
|
}
|
|
if (state.mode === "dfd-object") {
|
|
this.prepareFileViewportState(
|
|
this.objectGraphViewportState,
|
|
this.objectGraphFilePath,
|
|
state.model.path,
|
|
reason
|
|
);
|
|
this.objectGraphFilePath = state.model.path;
|
|
return;
|
|
}
|
|
if (state.mode === "summary" && ((state.layoutBlocks?.length ?? 0) > 0 || (state.businessFlow?.steps.length ?? 0) > 0)) {
|
|
this.prepareFileViewportState(
|
|
this.screenPreviewViewportState,
|
|
this.screenPreviewFilePath,
|
|
state.filePath,
|
|
reason
|
|
);
|
|
this.screenPreviewFilePath = state.filePath;
|
|
return;
|
|
}
|
|
if (state.mode !== "object") {
|
|
this.objectGraphFilePath = null;
|
|
}
|
|
if (state.mode !== "summary" || (state.layoutBlocks?.length ?? 0) === 0 && (state.businessFlow?.steps.length ?? 0) === 0) {
|
|
this.screenPreviewFilePath = null;
|
|
}
|
|
this.diagramFilePath = null;
|
|
}
|
|
prepareFileViewportState(state, currentFilePath, nextFilePath, reason) {
|
|
if (reason === "manual-fit" || currentFilePath === nextFilePath) {
|
|
return;
|
|
}
|
|
const cached = this.viewportStateCache.get(nextFilePath);
|
|
if (cached) {
|
|
if (cached.viewMode === "fit") {
|
|
resetGraphViewportState(state);
|
|
} else {
|
|
state.zoom = cached.zoom;
|
|
state.panX = cached.panX;
|
|
state.panY = cached.panY;
|
|
state.viewMode = "manual";
|
|
state.hasAutoFitted = true;
|
|
state.hasUserInteracted = true;
|
|
}
|
|
cached.updatedAt = Date.now();
|
|
return;
|
|
}
|
|
resetGraphViewportState(state);
|
|
if (this.viewerPreferences.defaultZoom === "100") {
|
|
state.zoom = 1;
|
|
state.panX = 0;
|
|
state.panY = 0;
|
|
state.viewMode = "manual";
|
|
state.hasAutoFitted = true;
|
|
state.hasUserInteracted = false;
|
|
}
|
|
}
|
|
rememberViewportState(filePath, state) {
|
|
if (!state.hasAutoFitted && !state.hasUserInteracted) {
|
|
return;
|
|
}
|
|
this.viewportStateCache.set(filePath, {
|
|
filePath,
|
|
viewMode: state.viewMode,
|
|
zoom: state.zoom,
|
|
panX: state.panX,
|
|
panY: state.panY,
|
|
updatedAt: Date.now()
|
|
});
|
|
this.pruneViewportStateCache();
|
|
}
|
|
pruneViewportStateCache() {
|
|
if (this.viewportStateCache.size <= VIEWPORT_STATE_CACHE_LIMIT) {
|
|
return;
|
|
}
|
|
const oldestEntries = [...this.viewportStateCache.entries()].sort(
|
|
(left, right) => left[1].updatedAt - right[1].updatedAt
|
|
);
|
|
for (const [filePath] of oldestEntries.slice(
|
|
0,
|
|
this.viewportStateCache.size - VIEWPORT_STATE_CACHE_LIMIT
|
|
)) {
|
|
this.viewportStateCache.delete(filePath);
|
|
}
|
|
}
|
|
getCurrentDiagramFilePath() {
|
|
switch (this.state.mode) {
|
|
case "diagram":
|
|
return this.state.diagram.diagram.path;
|
|
case "object":
|
|
return this.state.context ? "filePath" in this.state.model ? this.state.model.filePath : this.state.model.path : null;
|
|
case "dfd-object":
|
|
return this.state.model.path;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
buildCurrentDiagramExportRenderable() {
|
|
const state = this.state;
|
|
switch (state.mode) {
|
|
case "diagram":
|
|
return {
|
|
filePath: state.diagram.diagram.path,
|
|
renderer: state.rendererSelection?.effectiveMode ?? "custom",
|
|
render: () => renderDiagramModel(state.diagram, {
|
|
hideTitle: true,
|
|
hideDetails: true,
|
|
forExport: true,
|
|
renderMode: state.rendererSelection?.effectiveMode
|
|
})
|
|
};
|
|
case "object": {
|
|
const filePath = this.getCurrentDiagramFilePath();
|
|
if (!filePath) {
|
|
return null;
|
|
}
|
|
if (state.rendererSelection?.actualRenderer === "mermaid") {
|
|
const context2 = state.context ?? {
|
|
object: state.model,
|
|
relatedObjects: [],
|
|
warnings: []
|
|
};
|
|
const subgraph2 = buildObjectSubgraphScene(context2);
|
|
return {
|
|
filePath,
|
|
renderer: state.rendererSelection?.effectiveMode ?? "mermaid",
|
|
render: () => renderDiagramModel(subgraph2, {
|
|
hideTitle: true,
|
|
hideDetails: true,
|
|
forExport: true,
|
|
fitVerticalAlign: "top",
|
|
renderMode: state.rendererSelection?.effectiveMode ?? "mermaid"
|
|
})
|
|
};
|
|
}
|
|
const context = state.context ?? {
|
|
object: state.model,
|
|
relatedObjects: [],
|
|
warnings: []
|
|
};
|
|
const subgraph = buildObjectSubgraphScene(context);
|
|
return {
|
|
filePath,
|
|
renderer: state.rendererSelection?.effectiveMode ?? "custom",
|
|
render: () => renderDiagramModel(subgraph, {
|
|
hideTitle: true,
|
|
hideDetails: true,
|
|
fitVerticalAlign: "top",
|
|
forExport: true
|
|
})
|
|
};
|
|
}
|
|
case "dfd-object":
|
|
return {
|
|
filePath: state.model.path,
|
|
renderer: state.rendererSelection?.effectiveMode ?? "custom",
|
|
render: () => renderDiagramModel(state.diagram, {
|
|
hideTitle: true,
|
|
hideDetails: true,
|
|
forExport: true
|
|
})
|
|
};
|
|
case "summary":
|
|
if ((state.layoutBlocks?.length ?? 0) > 0) {
|
|
return {
|
|
filePath: state.filePath,
|
|
renderer: "custom",
|
|
render: () => createScreenPreviewDiagram(buildScreenPreviewData(state), {
|
|
forExport: true
|
|
})
|
|
};
|
|
}
|
|
if ((state.businessFlow?.steps.length ?? 0) > 0) {
|
|
return {
|
|
filePath: state.filePath,
|
|
renderer: "business-flow",
|
|
render: () => renderAppProcessBusinessFlow(state.businessFlow, {
|
|
forExport: true,
|
|
debug: false
|
|
})
|
|
};
|
|
}
|
|
return null;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
createDiagramViewportStateHandler(filePath) {
|
|
return (viewportState) => {
|
|
if (this.state.mode !== "diagram" || this.diagramFilePath !== filePath || this.state.diagram.diagram.path !== filePath) {
|
|
return;
|
|
}
|
|
this.rememberViewportState(filePath, viewportState);
|
|
};
|
|
}
|
|
createObjectViewportStateHandler(filePath) {
|
|
return (viewportState) => {
|
|
if (this.state.mode !== "object" || this.objectGraphFilePath !== filePath) {
|
|
return;
|
|
}
|
|
const currentPath = "filePath" in this.state.model ? this.state.model.filePath : this.state.model.path;
|
|
if (currentPath !== filePath) {
|
|
return;
|
|
}
|
|
this.rememberViewportState(filePath, viewportState);
|
|
};
|
|
}
|
|
createScreenPreviewViewportStateHandler(filePath) {
|
|
return (viewportState) => {
|
|
if (this.state.mode !== "summary" || this.screenPreviewFilePath !== filePath || this.state.filePath !== filePath) {
|
|
return;
|
|
}
|
|
this.rememberViewportState(filePath, viewportState);
|
|
};
|
|
}
|
|
renderCurrentState() {
|
|
this.clearView();
|
|
switch (this.state.mode) {
|
|
case "object":
|
|
this.renderObjectState(this.state);
|
|
return;
|
|
case "relations":
|
|
this.renderRelationsState(this.state);
|
|
return;
|
|
case "summary":
|
|
this.renderSummaryState(this.state);
|
|
return;
|
|
case "dfd-object":
|
|
this.renderDfdObjectState(this.state);
|
|
return;
|
|
case "diagram":
|
|
this.renderDiagramState(this.state);
|
|
return;
|
|
case "empty":
|
|
default:
|
|
this.renderEmptyState(this.state.message);
|
|
}
|
|
}
|
|
clearView() {
|
|
this.contentEl.empty();
|
|
this.activeScrollContainer = null;
|
|
this.contentEl.classList.remove(
|
|
"model-weave-viewer-root",
|
|
"mw-font-small",
|
|
"mw-font-normal",
|
|
"mw-font-large",
|
|
"mw-density-compact",
|
|
"mw-density-normal",
|
|
"mw-density-relaxed"
|
|
);
|
|
this.contentEl.classList.add("model-weave-viewer-root");
|
|
this.contentEl.classList.add(`mw-font-${this.viewerPreferences.fontSize}`);
|
|
this.contentEl.classList.add(`mw-density-${this.viewerPreferences.nodeDensity}`);
|
|
const fontVars = this.getFontSizeVariables();
|
|
this.contentEl.setCssProps({
|
|
"--model-weave-font-size": fontVars.base,
|
|
"--model-weave-font-size-small": fontVars.small,
|
|
"--model-weave-font-size-large": fontVars.large,
|
|
"--model-weave-font-size-title": fontVars.title,
|
|
"--mw-content-gap": `${this.getDensitySpacing().contentGap}px`
|
|
});
|
|
}
|
|
renderEmptyState(message) {
|
|
const section = document.createElement("section");
|
|
section.addClass("model-weave-viewer-empty");
|
|
const text = document.createElement("p");
|
|
text.textContent = message;
|
|
text.addClass("model-weave-viewer-empty-text");
|
|
section.appendChild(text);
|
|
this.contentEl.appendChild(section);
|
|
}
|
|
renderObjectState(state) {
|
|
const objectPath = "filePath" in state.model ? state.model.filePath : state.model.path;
|
|
const shell = this.createViewerSplitShell(`object:${objectPath}`, 0.62);
|
|
shell.bottomPane.addClass("model-weave-summary-details");
|
|
this.activeScrollContainer = shell.bottomPane;
|
|
renderDiagnostics(
|
|
shell.bottomPane,
|
|
state.warnings,
|
|
state.onOpenDiagnostic ?? void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState
|
|
);
|
|
shell.bottomPane.appendChild(
|
|
renderObjectModel(
|
|
state.model,
|
|
state.context,
|
|
this.viewerPreferences.localSourceRoot
|
|
)
|
|
);
|
|
this.renderImpactSummarySection(
|
|
shell.bottomPane,
|
|
state.impactSummary,
|
|
state.onCopyImpactSummary,
|
|
state.onOpenImpactModel
|
|
);
|
|
if (!state.context) {
|
|
return;
|
|
}
|
|
if (state.rendererSelection?.actualRenderer === "mermaid") {
|
|
const contextRoot2 = renderObjectContext(state.context, {
|
|
onOpenObject: state.onOpenObject ?? void 0,
|
|
viewportState: this.objectGraphViewportState,
|
|
onViewportStateChange: this.createObjectViewportStateHandler(objectPath)
|
|
});
|
|
const relatedList2 = Array.from(contextRoot2.children).find(
|
|
(child) => child instanceof HTMLElement && (child.classList.contains("model-weave-object-context-list") || child.classList.contains("mdspec-related-list"))
|
|
);
|
|
if (relatedList2) {
|
|
relatedList2.remove();
|
|
shell.bottomPane.appendChild(relatedList2);
|
|
}
|
|
const subgraph = buildObjectSubgraphScene(state.context);
|
|
const mermaidRoot = renderDiagramModel(subgraph, {
|
|
hideTitle: true,
|
|
hideDetails: true,
|
|
renderMode: state.rendererSelection?.effectiveMode ?? "mermaid",
|
|
fitVerticalAlign: "top",
|
|
viewportState: this.objectGraphViewportState,
|
|
onViewportStateChange: this.createObjectViewportStateHandler(objectPath),
|
|
sourcePanelContainer: shell.bottomPane,
|
|
sourcePanelPlacement: "prepend",
|
|
showMermaidRenderDebug: this.viewerPreferences.showMermaidRenderDebug
|
|
});
|
|
this.appendRendererSelection(mermaidRoot, state.rendererSelection);
|
|
shell.topPane.appendChild(mermaidRoot);
|
|
return;
|
|
}
|
|
const contextRoot = renderObjectContext(state.context, {
|
|
onOpenObject: state.onOpenObject ?? void 0,
|
|
viewportState: this.objectGraphViewportState,
|
|
onViewportStateChange: this.createObjectViewportStateHandler(objectPath)
|
|
});
|
|
contextRoot.addClass("model-weave-object-context-no-margin");
|
|
const relatedList = Array.from(contextRoot.children).find(
|
|
(child) => child instanceof HTMLElement && (child.classList.contains("model-weave-object-context-list") || child.classList.contains("mdspec-related-list"))
|
|
);
|
|
if (relatedList) {
|
|
relatedList.remove();
|
|
shell.bottomPane.appendChild(relatedList);
|
|
}
|
|
this.appendRendererSelection(contextRoot, state.rendererSelection);
|
|
shell.topPane.appendChild(contextRoot);
|
|
}
|
|
renderRelationsState(state) {
|
|
const model = state.model;
|
|
this.contentEl.createEl("h2", {
|
|
text: model.title ?? model.frontmatter.id?.toString() ?? "Relations"
|
|
});
|
|
if (model.relations.length === 0) {
|
|
this.contentEl.createEl("p", { text: "No relations defined." });
|
|
return;
|
|
}
|
|
const list = this.contentEl.createEl("ul");
|
|
for (const relation of model.relations) {
|
|
const label = relation.label ? ` (${relation.label})` : "";
|
|
list.createEl("li", {
|
|
text: `${relation.source} -[${relation.kind}]-> ${relation.target}${label}`
|
|
});
|
|
}
|
|
}
|
|
renderSummaryState(state) {
|
|
const hasScreenPreview = (state.layoutBlocks?.length ?? 0) > 0;
|
|
const hasBusinessFlow = (state.businessFlow?.steps.length ?? 0) > 0;
|
|
if (hasScreenPreview || hasBusinessFlow) {
|
|
const shell = this.createViewerSplitShell(`summary:${state.filePath}`, 0.48);
|
|
this.activeScrollContainer = shell.bottomPane;
|
|
if (hasScreenPreview) {
|
|
shell.topPane.appendChild(
|
|
createScreenPreviewDiagram(buildScreenPreviewData(state), {
|
|
viewportState: this.screenPreviewViewportState,
|
|
onViewportStateChange: this.createScreenPreviewViewportStateHandler(
|
|
state.filePath
|
|
),
|
|
onNavigateToLocation: state.onNavigateToLocation,
|
|
onOpenLinkedFile: state.onOpenLinkedFile
|
|
})
|
|
);
|
|
} else if (state.businessFlow) {
|
|
if (this.screenPreviewViewportState.viewMode === "fit") {
|
|
resetGraphViewportState(this.screenPreviewViewportState);
|
|
}
|
|
shell.topPane.appendChild(
|
|
renderAppProcessBusinessFlow(state.businessFlow, {
|
|
sourcePanelContainer: shell.bottomPane,
|
|
sourcePanelPlacement: "prepend",
|
|
showMermaidRenderDebug: this.viewerPreferences.showMermaidRenderDebug,
|
|
viewportState: this.screenPreviewViewportState,
|
|
onViewportStateChange: this.createScreenPreviewViewportStateHandler(
|
|
state.filePath
|
|
)
|
|
})
|
|
);
|
|
this.renderSummaryDetails(shell.bottomPane, state, {
|
|
suppressBusinessFlowChart: true
|
|
});
|
|
return;
|
|
}
|
|
this.renderSummaryDetails(shell.bottomPane, state, {
|
|
suppressBusinessFlowChart: hasBusinessFlow
|
|
});
|
|
return;
|
|
}
|
|
const wrapper = this.contentEl.createDiv();
|
|
wrapper.addClass("model-weave-summary-section");
|
|
wrapper.addClass("model-weave-summary-details");
|
|
this.activeScrollContainer = wrapper;
|
|
this.renderSummaryDetails(wrapper, state);
|
|
}
|
|
renderSummaryDetails(container, state, options = {}) {
|
|
container.addClass("model-weave-summary-details");
|
|
if (state.summaryKind === "screen") {
|
|
this.renderScreenSummaryDetails(container, state);
|
|
return;
|
|
}
|
|
container.createEl("h2", { text: state.title });
|
|
if (state.message) {
|
|
container.createEl("p", {
|
|
text: state.message,
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
}
|
|
renderDiagnostics(
|
|
container,
|
|
state.warnings,
|
|
void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState
|
|
);
|
|
if (state.metadata.length > 0) {
|
|
const metadata = container.createDiv({
|
|
cls: "model-weave-preview-section model-weave-summary-metadata"
|
|
});
|
|
metadata.createEl("h3", {
|
|
text: "Overview",
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
this.renderDetailCard(metadata, state.metadata);
|
|
}
|
|
const sourceLinks = renderSourceLinks(
|
|
state.sourceLinks,
|
|
this.viewerPreferences.localSourceRoot
|
|
);
|
|
if (sourceLinks) {
|
|
container.appendChild(sourceLinks);
|
|
}
|
|
this.renderImpactSummarySection(
|
|
container,
|
|
state.impactSummary,
|
|
state.onCopyImpactSummary,
|
|
state.onOpenImpactModel
|
|
);
|
|
if (state.counts.length > 0) {
|
|
const counts = container.createDiv({
|
|
cls: "model-weave-preview-section model-weave-summary-counts"
|
|
});
|
|
counts.createEl("h3", {
|
|
text: "Counts",
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
const list = counts.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const entry of state.counts) {
|
|
list.createEl("li", { text: `${entry.label}: ${entry.value}` });
|
|
}
|
|
}
|
|
if (!options.suppressBusinessFlowChart && state.businessFlow && state.businessFlow.steps.length > 0) {
|
|
const section = container.createDiv({
|
|
cls: "model-weave-preview-section model-weave-app-process-business-flow-section"
|
|
});
|
|
if (this.screenPreviewViewportState.viewMode === "fit") {
|
|
resetGraphViewportState(this.screenPreviewViewportState);
|
|
}
|
|
section.appendChild(
|
|
renderAppProcessBusinessFlow(state.businessFlow, {
|
|
viewportState: this.screenPreviewViewportState,
|
|
onViewportStateChange: this.createScreenPreviewViewportStateHandler(
|
|
state.filePath
|
|
)
|
|
})
|
|
);
|
|
}
|
|
if (state.sections.length > 0) {
|
|
const sections = this.createCollapsibleSection(
|
|
container,
|
|
"detectedSections",
|
|
"Detected sections",
|
|
true
|
|
);
|
|
const list = sections.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const section of state.sections) {
|
|
const item = list.createEl("li", { text: section.label });
|
|
this.bindLocationNavigation(item, state.onNavigateToLocation, section);
|
|
}
|
|
}
|
|
for (const textSection of state.textSections ?? []) {
|
|
if (textSection.lines.length === 0) {
|
|
continue;
|
|
}
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
`text:${textSection.title}`,
|
|
textSection.title,
|
|
true
|
|
);
|
|
const markdown = textSection.lines.join("\n").trim();
|
|
if (!markdown) {
|
|
continue;
|
|
}
|
|
const markdownContainer = section.createDiv({
|
|
cls: "model-weave-summary-markdown"
|
|
});
|
|
void import_obsidian7.MarkdownRenderer.render(
|
|
this.app,
|
|
markdown,
|
|
markdownContainer,
|
|
state.filePath,
|
|
this
|
|
);
|
|
}
|
|
for (const table of state.tables ?? []) {
|
|
this.renderSummaryTable(container, state, table, true);
|
|
}
|
|
if ((state.localProcesses?.length ?? 0) > 0) {
|
|
const localProcesses = this.createCollapsibleSection(
|
|
container,
|
|
"localProcesses",
|
|
"Local Processes",
|
|
true
|
|
);
|
|
const list = localProcesses.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const process of state.localProcesses ?? []) {
|
|
const item = list.createEl("li", { text: process.label });
|
|
this.bindLocationNavigation(item, state.onNavigateToLocation, process);
|
|
}
|
|
}
|
|
if ((state.navigationLists?.length ?? 0) > 0) {
|
|
for (const navigationList of state.navigationLists ?? []) {
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
`navigation:${navigationList.title}`,
|
|
navigationList.title,
|
|
true
|
|
);
|
|
const list = section.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const itemInfo of navigationList.items) {
|
|
const item = list.createEl("li", { text: itemInfo.label });
|
|
this.bindLocationNavigation(item, state.onNavigateToLocation, itemInfo);
|
|
}
|
|
}
|
|
}
|
|
if ((state.relatedReferences?.length ?? 0) > 0) {
|
|
const related = this.createCollapsibleSection(
|
|
container,
|
|
"relatedReferences",
|
|
"Related References",
|
|
true
|
|
);
|
|
const list = related.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const reference of state.relatedReferences ?? []) {
|
|
const label = typeof reference.count === "number" && reference.count > 1 ? `${reference.label} \u2014 ${reference.count} occurrences` : reference.label;
|
|
const item = list.createEl("li", { text: label });
|
|
this.bindLocationNavigation(item, state.onNavigateToLocation, reference);
|
|
}
|
|
}
|
|
}
|
|
renderScreenSummaryDetails(container, state) {
|
|
container.createEl("h2", { text: state.title });
|
|
renderDiagnostics(
|
|
container,
|
|
state.warnings,
|
|
void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState
|
|
);
|
|
if (state.metadata.length > 0) {
|
|
const overview = container.createDiv({
|
|
cls: "model-weave-preview-section model-weave-screen-preview-section-overview"
|
|
});
|
|
overview.createEl("h3", {
|
|
text: "Screen Overview",
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
this.renderDetailCard(overview, state.metadata);
|
|
}
|
|
const sourceLinks = renderSourceLinks(
|
|
state.sourceLinks,
|
|
this.viewerPreferences.localSourceRoot
|
|
);
|
|
if (sourceLinks) {
|
|
container.appendChild(sourceLinks);
|
|
}
|
|
this.renderImpactSummarySection(
|
|
container,
|
|
state.impactSummary,
|
|
state.onCopyImpactSummary,
|
|
state.onOpenImpactModel
|
|
);
|
|
if (state.counts.length > 0) {
|
|
const counts = container.createDiv({
|
|
cls: "model-weave-preview-section model-weave-screen-preview-section-counts"
|
|
});
|
|
counts.createEl("h3", {
|
|
text: "Counts",
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
const list = counts.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const entry of state.counts) {
|
|
list.createEl("li", { text: `${entry.label}: ${entry.value}` });
|
|
}
|
|
}
|
|
const tablesByTitle = new Map((state.tables ?? []).map((table) => [table.title, table]));
|
|
this.renderSummaryTable(container, state, tablesByTitle.get("Structure / Layout"), true);
|
|
this.renderSummaryTable(container, state, tablesByTitle.get("UI Elements / Fields"), true);
|
|
this.renderSummaryTable(container, state, tablesByTitle.get("Behavior / Actions"), true);
|
|
if ((state.localProcesses?.length ?? 0) > 0) {
|
|
this.renderSummaryNavigationList(
|
|
container,
|
|
state,
|
|
"Local processes",
|
|
state.localProcesses ?? [],
|
|
true
|
|
);
|
|
}
|
|
const navigationLists = new Map(
|
|
(state.navigationLists ?? []).map((navigationList) => [
|
|
navigationList.title,
|
|
navigationList.items
|
|
])
|
|
);
|
|
this.renderSummaryNavigationList(
|
|
container,
|
|
state,
|
|
"Invoked processes",
|
|
navigationLists.get("Invoked processes") ?? [],
|
|
true
|
|
);
|
|
this.renderScreenTransitionSummary(container, state);
|
|
this.renderSummaryNavigationList(
|
|
container,
|
|
state,
|
|
"Transitions / Outgoing screens",
|
|
navigationLists.get("Transitions / Outgoing screens") ?? [],
|
|
true
|
|
);
|
|
this.renderSummaryTable(container, state, tablesByTitle.get("Messages"), true);
|
|
if (state.sections.length > 0) {
|
|
const sections = this.createCollapsibleSection(
|
|
container,
|
|
"detectedSections",
|
|
"Detected sections",
|
|
false
|
|
);
|
|
const list = sections.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const section of state.sections) {
|
|
const item = list.createEl("li", { text: section.label });
|
|
this.bindLocationNavigation(item, state.onNavigateToLocation, section);
|
|
}
|
|
}
|
|
}
|
|
renderSummaryTable(container, state, table, defaultOpen) {
|
|
if (!table) {
|
|
return;
|
|
}
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
`summary:${table.title}`,
|
|
table.title,
|
|
defaultOpen
|
|
);
|
|
section.addClass("model-weave-table-wrap");
|
|
const tableEl = section.createEl("table", {
|
|
cls: "model-weave-summary-table model-weave-data-table"
|
|
});
|
|
const thead = tableEl.createEl("thead");
|
|
const headRow = thead.createEl("tr");
|
|
for (const column of table.columns) {
|
|
headRow.createEl("th", {
|
|
text: column,
|
|
cls: "model-weave-summary-th"
|
|
});
|
|
}
|
|
const tbody = tableEl.createEl("tbody");
|
|
if (table.rows.length === 0) {
|
|
const emptyRow = tbody.createEl("tr");
|
|
emptyRow.createEl("td", {
|
|
text: "No rows",
|
|
cls: "model-weave-summary-td model-weave-summary-empty-cell",
|
|
attr: { colspan: `${Math.max(1, table.columns.length)}` }
|
|
});
|
|
return;
|
|
}
|
|
for (const row of table.rows) {
|
|
const tr = tbody.createEl("tr");
|
|
if (row.line !== void 0) {
|
|
tr.addClass("model-weave-clickable");
|
|
}
|
|
this.bindLocationNavigation(tr, state.onNavigateToLocation, row);
|
|
for (const cell of row.cells) {
|
|
tr.createEl("td", {
|
|
text: cell,
|
|
cls: "model-weave-summary-td"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
renderDetailCard(container, entries) {
|
|
const card = container.createDiv({ cls: "model-weave-detail-card" });
|
|
for (const entry of entries) {
|
|
const row = card.createDiv({ cls: "model-weave-detail-card-row" });
|
|
row.createDiv({
|
|
text: entry.label,
|
|
cls: "model-weave-detail-card-label"
|
|
});
|
|
row.createDiv({
|
|
text: entry.value,
|
|
cls: "model-weave-detail-card-value"
|
|
});
|
|
}
|
|
}
|
|
renderSummaryNavigationList(container, state, title, items, defaultOpen) {
|
|
if (items.length === 0) {
|
|
return;
|
|
}
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
`navigation:${title}`,
|
|
title,
|
|
defaultOpen
|
|
);
|
|
const list = section.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const itemInfo of items) {
|
|
const item = list.createEl("li", { text: itemInfo.label });
|
|
this.bindLocationNavigation(item, state.onNavigateToLocation, itemInfo);
|
|
}
|
|
}
|
|
renderScreenTransitionSummary(container, state) {
|
|
const transitions = state.screenPreviewTransitions ?? [];
|
|
if (transitions.length === 0) {
|
|
return;
|
|
}
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
"screenTransitionStatus",
|
|
"Transition target status",
|
|
true
|
|
);
|
|
const list = section.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const transition of transitions) {
|
|
const status = transition.selfTarget ? "self" : transition.unresolved ? "unresolved" : "resolved";
|
|
const item = list.createEl("li", {
|
|
text: `${transition.targetLabel}: ${status} (${transition.actions.length} action${transition.actions.length === 1 ? "" : "s"})`
|
|
});
|
|
item.title = transition.targetTitle ?? transition.targetLabel;
|
|
const firstAction = transition.actions.find(
|
|
(action) => typeof action.line === "number"
|
|
);
|
|
this.bindLocationNavigation(item, state.onNavigateToLocation, firstAction ?? {});
|
|
}
|
|
}
|
|
renderImpactSummarySection(container, summary, onCopyImpactSummary, onOpenImpactModel) {
|
|
if (!summary) {
|
|
return;
|
|
}
|
|
const section = container.createDiv({
|
|
cls: "model-weave-preview-section model-weave-impact-summary"
|
|
});
|
|
const header = section.createDiv({ cls: "model-weave-impact-summary-header" });
|
|
header.createEl("h3", {
|
|
text: this.t("relationship.title"),
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
if (onCopyImpactSummary) {
|
|
const copyButton = header.createEl("button", {
|
|
text: this.t("relationship.copySummary"),
|
|
cls: "mod-cta model-weave-impact-copy-button"
|
|
});
|
|
copyButton.type = "button";
|
|
copyButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
onCopyImpactSummary();
|
|
});
|
|
}
|
|
this.renderDetailCard(section, [
|
|
{
|
|
label: this.t("relationship.referencesFromThisObject"),
|
|
value: String(summary.outboundRelationships.length)
|
|
},
|
|
{
|
|
label: this.t("relationship.referencedByThisObject"),
|
|
value: String(summary.inboundRelationships.length)
|
|
},
|
|
...summary.modelType === "codeset" ? [
|
|
{
|
|
label: this.t("relationship.valueUsage"),
|
|
value: String(summary.valueUsages.length)
|
|
}
|
|
] : [],
|
|
{
|
|
label: this.t("relationship.unresolvedReferences"),
|
|
value: String(summary.unresolvedOutbound.length)
|
|
},
|
|
{
|
|
label: this.t("relationship.relatedSourceLinks"),
|
|
value: String(summary.relatedSourceLinks.length)
|
|
}
|
|
]);
|
|
renderUsageViewSections(
|
|
section,
|
|
this.createImpactUsageSections(summary),
|
|
this.createUsageViewRendererOptions(onOpenImpactModel)
|
|
);
|
|
if (summary.modelType === "codeset") {
|
|
renderUsageViewSections(
|
|
section,
|
|
[this.createImpactValueUsageSection(summary)],
|
|
this.createUsageViewRendererOptions()
|
|
);
|
|
}
|
|
renderUsageDetailSection(
|
|
section,
|
|
"impactUnresolved",
|
|
this.t("relationship.unresolvedReferences"),
|
|
this.t("relationship.noUnresolved"),
|
|
summary.unresolvedOutbound.map(
|
|
(reference) => this.createImpactUnresolvedDetail(reference)
|
|
),
|
|
this.createUsageViewRendererOptions()
|
|
);
|
|
renderGroupedSourceLinkSection(
|
|
section,
|
|
"impactSourceLinks",
|
|
this.t("relationship.relatedSourceLinks"),
|
|
this.t("relationship.noRelatedSourceLinks"),
|
|
summary.relatedSourceLinks.map(
|
|
(sourceLink) => this.createGroupedSourceLink(sourceLink)
|
|
),
|
|
this.createUsageViewRendererOptions()
|
|
);
|
|
}
|
|
createImpactValueUsageSection(summary) {
|
|
return {
|
|
id: "impactValueUsage",
|
|
title: this.t("relationship.valueUsage"),
|
|
emptyText: this.t("relationship.noValueUsage"),
|
|
items: summary.valueUsages.map((valueUsage) => {
|
|
const usageCount = valueUsage.relationships.reduce(
|
|
(total, relationship) => total + relationship.usageCount,
|
|
0
|
|
);
|
|
return {
|
|
label: valueUsage.member,
|
|
usageCount,
|
|
summaryText: `${valueUsage.member} (${this.formatLocalizedCount(usageCount, "relationship.usage.one", "relationship.usage.other")})`,
|
|
details: valueUsage.relationships.flatMap(
|
|
(relationship) => relationship.usages.map(
|
|
(usage) => this.createImpactValueUsageDetail(relationship, usage)
|
|
)
|
|
),
|
|
sourceLinks: []
|
|
};
|
|
})
|
|
};
|
|
}
|
|
createImpactUsageSections(summary) {
|
|
return [
|
|
{
|
|
id: "impactOutbound",
|
|
title: this.t("relationship.referencesFromThisObject"),
|
|
emptyText: this.t("relationship.noOutbound"),
|
|
items: summary.outboundRelationships.map((relationship) => ({
|
|
label: relationship.modelLabel,
|
|
type: relationship.modelType,
|
|
path: relationship.modelPath,
|
|
usageCount: relationship.usageCount,
|
|
openTargetPath: relationship.modelPath,
|
|
details: relationship.usages.map(
|
|
(usage) => this.createImpactRelationshipDetail(usage)
|
|
),
|
|
sourceLinks: relationship.sourceLinks.map(
|
|
(sourceLink) => this.createGroupedSourceLink(sourceLink)
|
|
)
|
|
}))
|
|
},
|
|
{
|
|
id: "impactInbound",
|
|
title: this.t("relationship.referencedByThisObject"),
|
|
emptyText: this.t("relationship.noInbound"),
|
|
items: summary.inboundRelationships.map((relationship) => ({
|
|
label: relationship.modelLabel,
|
|
type: relationship.modelType,
|
|
path: relationship.modelPath,
|
|
usageCount: relationship.usageCount,
|
|
openTargetPath: relationship.modelPath,
|
|
details: relationship.usages.map(
|
|
(usage) => this.createImpactRelationshipDetail(usage)
|
|
),
|
|
sourceLinks: relationship.sourceLinks.map(
|
|
(sourceLink) => this.createGroupedSourceLink(sourceLink)
|
|
)
|
|
}))
|
|
}
|
|
];
|
|
}
|
|
createImpactRelationshipDetail(reference) {
|
|
const location = [reference.section, reference.field].filter(Boolean).join(".");
|
|
return {
|
|
label: `${location ? `${location}: ` : ""}${reference.targetRaw}`,
|
|
meta: [reference.relationKind, reference.sourceContext].filter(Boolean).join("; "),
|
|
notes: reference.notes,
|
|
title: reference.notes ?? reference.targetPath ?? reference.targetRaw
|
|
};
|
|
}
|
|
createImpactValueUsageDetail(relationship, reference) {
|
|
const location = [reference.section, reference.field].filter(Boolean).join(".");
|
|
const meta = [relationship.modelType, location, reference.sourceContext].filter(Boolean).join("; ");
|
|
const label = `${relationship.modelLabel}${meta ? ` (${meta})` : ""}`;
|
|
return {
|
|
label,
|
|
notes: reference.notes,
|
|
title: reference.notes ?? reference.sourcePath
|
|
};
|
|
}
|
|
createImpactUnresolvedDetail(reference) {
|
|
const location = [reference.section, reference.field].filter(Boolean).join(".");
|
|
const meta = [reference.relationKind, location || null].filter(Boolean).join("; ");
|
|
return {
|
|
label: reference.targetRaw,
|
|
meta,
|
|
notes: reference.notes,
|
|
title: reference.notes ?? reference.targetRaw
|
|
};
|
|
}
|
|
createGroupedSourceLink(sourceLink) {
|
|
return {
|
|
relationKind: sourceLink.relationKind,
|
|
ownerLabel: sourceLink.ownerLabel,
|
|
ownerPath: sourceLink.ownerPath,
|
|
path: sourceLink.path,
|
|
notes: sourceLink.notes,
|
|
...sourceLink.label ? { label: sourceLink.label } : {}
|
|
};
|
|
}
|
|
createUsageViewRendererOptions(onOpenImpactModel) {
|
|
return {
|
|
openLabel: this.t("relationship.open"),
|
|
sourceLinkLabel: this.t("relationship.sourceLink"),
|
|
formatUsageCount: (count) => this.formatLocalizedCount(
|
|
count,
|
|
"relationship.usage.one",
|
|
"relationship.usage.other"
|
|
),
|
|
formatNoteCount: (count) => this.formatLocalizedCount(
|
|
count,
|
|
"relationship.note.one",
|
|
"relationship.note.other"
|
|
),
|
|
getOpenState: this.getCollapsibleOpenState,
|
|
setOpenState: this.setCollapsibleOpenState,
|
|
onOpenItem: onOpenImpactModel ?? void 0
|
|
};
|
|
}
|
|
createCollapsibleSection(container, key, title, defaultOpen) {
|
|
const details = container.createEl("details");
|
|
details.addClass("model-weave-preview-section");
|
|
details.open = this.getCollapsibleOpenState(key, defaultOpen);
|
|
details.addEventListener("toggle", () => {
|
|
this.setCollapsibleOpenState(key, details.open);
|
|
});
|
|
const summary = details.createEl("summary", { text: title });
|
|
summary.addClass("model-weave-summary-heading");
|
|
summary.addClass("model-weave-preview-section-title");
|
|
return details.createDiv();
|
|
}
|
|
bindLocationNavigation(element, onNavigate, location) {
|
|
if (!onNavigate || typeof location.line !== "number") {
|
|
return;
|
|
}
|
|
element.tabIndex = 0;
|
|
element.onclick = (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
onNavigate({ line: location.line, ch: location.ch });
|
|
};
|
|
element.onkeydown = (event) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
onNavigate({ line: location.line, ch: location.ch });
|
|
}
|
|
};
|
|
}
|
|
formatLocalizedCount(count, singularKey, pluralKey) {
|
|
const key = count === 1 ? singularKey : pluralKey;
|
|
return `${count} ${this.t(key)}`;
|
|
}
|
|
resetImpactCollapsibleState() {
|
|
for (const key of [...this.collapsibleState.keys()]) {
|
|
if (key.startsWith("impact")) {
|
|
this.collapsibleState.delete(key);
|
|
}
|
|
}
|
|
}
|
|
persistCurrentScrollPosition() {
|
|
const filePath = this.getCurrentFilePath();
|
|
if (!filePath || !this.activeScrollContainer) {
|
|
return;
|
|
}
|
|
this.scrollStateByFilePath.set(filePath, this.activeScrollContainer.scrollTop);
|
|
}
|
|
restoreCurrentScrollPosition() {
|
|
const filePath = this.getCurrentFilePath();
|
|
if (!filePath || !this.activeScrollContainer) {
|
|
return;
|
|
}
|
|
const nextScrollTop = this.scrollStateByFilePath.get(filePath);
|
|
if (typeof nextScrollTop === "number") {
|
|
this.activeScrollContainer.scrollTop = nextScrollTop;
|
|
}
|
|
}
|
|
renderDfdObjectState(state) {
|
|
const shell = this.createViewerSplitShell(`dfd-object:${state.model.path}`, 0.62);
|
|
this.activeScrollContainer = shell.bottomPane;
|
|
renderDiagnostics(
|
|
shell.bottomPane,
|
|
state.warnings,
|
|
state.onOpenDiagnostic ?? void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState
|
|
);
|
|
shell.bottomPane.appendChild(
|
|
renderObjectModel(
|
|
state.model,
|
|
void 0,
|
|
this.viewerPreferences.localSourceRoot
|
|
)
|
|
);
|
|
this.renderImpactSummarySection(
|
|
shell.bottomPane,
|
|
state.impactSummary,
|
|
state.onCopyImpactSummary,
|
|
state.onOpenImpactModel
|
|
);
|
|
const diagramRoot = renderDiagramModel(state.diagram, {
|
|
hideTitle: true,
|
|
hideDetails: false,
|
|
fitVerticalAlign: "top",
|
|
onOpenObject: state.onOpenObject ?? void 0,
|
|
viewportState: this.objectGraphViewportState,
|
|
onViewportStateChange: this.createObjectViewportStateHandler(state.model.path),
|
|
sourcePanelContainer: shell.bottomPane,
|
|
sourcePanelPlacement: "prepend",
|
|
showMermaidRenderDebug: this.viewerPreferences.showMermaidRenderDebug
|
|
});
|
|
this.moveDetailSections(diagramRoot, shell.bottomPane);
|
|
shell.topPane.appendChild(diagramRoot);
|
|
}
|
|
renderDiagramState(state) {
|
|
const filePath = state.diagram.diagram.path;
|
|
const shell = this.createViewerSplitShell(`diagram:${filePath}`, 0.64);
|
|
shell.bottomPane.addClass("model-weave-collection-diagram-lower-pane");
|
|
const lowerSlots = this.createCollectionDiagramLowerPaneSlots(shell.bottomPane);
|
|
this.activeScrollContainer = shell.bottomPane;
|
|
renderDiagnostics(
|
|
lowerSlots.diagnostics,
|
|
state.warnings,
|
|
state.onOpenDiagnostic ?? void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState
|
|
);
|
|
const diagramRoot = renderDiagramModel(state.diagram, {
|
|
onOpenObject: state.onOpenObject ?? void 0,
|
|
renderMode: state.rendererSelection?.effectiveMode,
|
|
viewportState: this.diagramViewportState,
|
|
onViewportStateChange: this.createDiagramViewportStateHandler(filePath),
|
|
sourcePanelContainer: lowerSlots.source,
|
|
showMermaidRenderDebug: this.viewerPreferences.showMermaidRenderDebug
|
|
});
|
|
this.appendRendererSelection(diagramRoot, state.rendererSelection);
|
|
this.moveDetailSections(diagramRoot, lowerSlots.details);
|
|
this.renderImpactSummarySection(
|
|
lowerSlots.impact,
|
|
state.impactSummary,
|
|
state.onCopyImpactSummary,
|
|
state.onOpenImpactModel
|
|
);
|
|
shell.topPane.appendChild(diagramRoot);
|
|
}
|
|
createCollectionDiagramLowerPaneSlots(container) {
|
|
const source = container.createDiv({
|
|
cls: "model-weave-lower-pane-slot model-weave-lower-pane-source-slot"
|
|
});
|
|
const diagnostics = container.createDiv({
|
|
cls: "model-weave-lower-pane-slot model-weave-lower-pane-diagnostics-slot"
|
|
});
|
|
const details = container.createDiv({
|
|
cls: "model-weave-lower-pane-slot model-weave-lower-pane-details-slot"
|
|
});
|
|
const impact = container.createDiv({
|
|
cls: "model-weave-lower-pane-slot model-weave-lower-pane-impact-slot"
|
|
});
|
|
return { source, diagnostics, details, impact };
|
|
}
|
|
moveDetailSections(source, target) {
|
|
let detailWrapper = target.matches(".model-weave-lower-scroll, .model-weave-lower-pane-slot") ? target : target.querySelector(".model-weave-lower-scroll");
|
|
if (!detailWrapper) {
|
|
detailWrapper = target.createDiv({ cls: "model-weave-lower-scroll" });
|
|
}
|
|
const details = Array.from(source.children).filter(
|
|
(child) => child instanceof HTMLElement && child.matches(
|
|
"details, .mdspec-related-list, .model-weave-object-context-list"
|
|
)
|
|
).filter((child) => child instanceof HTMLElement);
|
|
for (const detail of details) {
|
|
detail.remove();
|
|
detail.addClass("model-weave-detail-panel");
|
|
detailWrapper.appendChild(detail);
|
|
}
|
|
}
|
|
appendRendererSelection(container, selection) {
|
|
if (!selection || !selection.onSelectMode || (selection.supportedModes?.length ?? 0) < 2) {
|
|
return;
|
|
}
|
|
const toolbar = container.querySelector(".mdspec-zoom-toolbar");
|
|
if (!toolbar) {
|
|
return;
|
|
}
|
|
toolbar.addClass("model-weave-render-mode-toolbar-host");
|
|
toolbar.querySelector(".mdspec-renderer-select-group")?.remove();
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = "mdspec-renderer-select-group model-weave-render-mode-row";
|
|
const title = document.createElement("span");
|
|
title.addClass("model-weave-render-mode-label");
|
|
title.textContent = "Renderer";
|
|
const meta = document.createElement("span");
|
|
meta.textContent = `selected ${selection.selectedMode} / effective ${selection.effectiveMode} / source ${selection.source}`;
|
|
if (selection.fallbackReason) {
|
|
meta.textContent += ` / ${selection.fallbackReason}`;
|
|
}
|
|
title.title = meta.textContent;
|
|
wrapper.appendChild(title);
|
|
const select = document.createElement("select");
|
|
select.addClass("model-weave-render-mode-select");
|
|
select.title = meta.textContent;
|
|
for (const mode of selection.supportedModes) {
|
|
const option = document.createElement("option");
|
|
option.value = mode;
|
|
option.textContent = this.formatRenderModeLabel(mode);
|
|
option.selected = mode === selection.visibleSelectedMode;
|
|
select.appendChild(option);
|
|
}
|
|
select.addEventListener("change", () => {
|
|
const selectedMode = selection.supportedModes.find((mode) => mode === select.value);
|
|
if (selectedMode) {
|
|
selection.onSelectMode?.(selectedMode);
|
|
}
|
|
});
|
|
wrapper.appendChild(select);
|
|
toolbar.appendChild(wrapper);
|
|
}
|
|
formatRenderModeLabel(mode) {
|
|
return mode.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
}
|
|
createViewerSplitShell(key, defaultTopRatio) {
|
|
const density = this.getDensitySpacing();
|
|
const root = this.contentEl.createDiv();
|
|
root.addClass("model-weave-viewer-split-shell");
|
|
const topPane = root.createDiv();
|
|
topPane.addClass("model-weave-viewer-upper-pane");
|
|
topPane.setCssProps({
|
|
"--mw-pane-padding": `${density.topPanePadding}px`,
|
|
"--mw-pane-gap": `${density.topPaneGap}px`
|
|
});
|
|
const handle = root.createDiv();
|
|
handle.addClass("model-weave-viewer-resize-handle");
|
|
const grip = handle.createDiv();
|
|
grip.addClass("model-weave-viewer-resize-grip");
|
|
const bottomPane = root.createDiv();
|
|
bottomPane.addClass("model-weave-viewer-lower-pane");
|
|
bottomPane.addClass("model-weave-viewer-lower-scroll");
|
|
bottomPane.setCssProps({
|
|
"--mw-pane-padding": `${density.bottomPanePadding}px ${density.bottomPanePadding + 2}px ${density.bottomPanePadding + 4}px`,
|
|
"--mw-pane-gap": `${density.bottomPaneGap}px`
|
|
});
|
|
const minTop = 180;
|
|
const minBottom = 180;
|
|
const clampRatio = (ratio) => Math.min(0.8, Math.max(0.3, ratio));
|
|
const applyRatio = (ratio) => {
|
|
const bounded = clampRatio(ratio);
|
|
const rootHeight = root.getBoundingClientRect().height;
|
|
const available = rootHeight > 0 ? Math.max(rootHeight - 10, minTop + minBottom) : 0;
|
|
if (available <= 0) {
|
|
topPane.setCssProps({ "--mw-pane-flex": `${bounded} 1 0` });
|
|
bottomPane.setCssProps({ "--mw-pane-flex": `${1 - bounded} 1 0` });
|
|
this.splitRatioByKey.set(key, bounded);
|
|
return;
|
|
}
|
|
const topPixels = Math.max(
|
|
minTop,
|
|
Math.min(available - minBottom, Math.round(available * bounded))
|
|
);
|
|
const bottomPixels = Math.max(minBottom, available - topPixels);
|
|
topPane.setCssProps({ "--mw-pane-flex": `0 0 ${topPixels}px` });
|
|
bottomPane.setCssProps({ "--mw-pane-flex": `0 0 ${bottomPixels}px` });
|
|
this.splitRatioByKey.set(key, topPixels / available);
|
|
};
|
|
const initialRatio = clampRatio(
|
|
this.splitRatioByKey.get(key) ?? defaultTopRatio
|
|
);
|
|
applyRatio(initialRatio);
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
applyRatio(this.splitRatioByKey.get(key) ?? initialRatio);
|
|
});
|
|
resizeObserver.observe(root);
|
|
handle.addEventListener("pointerdown", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const pointerId = event.pointerId;
|
|
handle.setPointerCapture(pointerId);
|
|
const rootRect = root.getBoundingClientRect();
|
|
const available = Math.max(rootRect.height - 10, minTop + minBottom);
|
|
const onMove = (moveEvent) => {
|
|
const offset = moveEvent.clientY - rootRect.top;
|
|
const topPixels = Math.max(
|
|
minTop,
|
|
Math.min(available - minBottom, offset)
|
|
);
|
|
applyRatio(topPixels / available);
|
|
};
|
|
const onUp = () => {
|
|
handle.releasePointerCapture(pointerId);
|
|
handle.removeEventListener("pointermove", onMove);
|
|
handle.removeEventListener("pointerup", onUp);
|
|
handle.removeEventListener("pointercancel", onUp);
|
|
};
|
|
handle.addEventListener("pointermove", onMove);
|
|
handle.addEventListener("pointerup", onUp);
|
|
handle.addEventListener("pointercancel", onUp);
|
|
});
|
|
return { root, topPane, bottomPane };
|
|
}
|
|
getFontSizeVariables() {
|
|
switch (this.viewerPreferences.fontSize) {
|
|
case "small":
|
|
return {
|
|
base: "12px",
|
|
small: "11px",
|
|
large: "13px",
|
|
title: "15px"
|
|
};
|
|
case "large":
|
|
return {
|
|
base: "17px",
|
|
small: "15px",
|
|
large: "19px",
|
|
title: "20px"
|
|
};
|
|
default:
|
|
return {
|
|
base: "14px",
|
|
small: "12px",
|
|
large: "16px",
|
|
title: "17px"
|
|
};
|
|
}
|
|
}
|
|
getDensitySpacing() {
|
|
switch (this.viewerPreferences.nodeDensity) {
|
|
case "compact":
|
|
return {
|
|
contentGap: 8,
|
|
topPanePadding: 8,
|
|
topPaneGap: 8,
|
|
bottomPanePadding: 8,
|
|
bottomPaneGap: 10
|
|
};
|
|
case "relaxed":
|
|
return {
|
|
contentGap: 12,
|
|
topPanePadding: 12,
|
|
topPaneGap: 12,
|
|
bottomPanePadding: 12,
|
|
bottomPaneGap: 14
|
|
};
|
|
default:
|
|
return {
|
|
contentGap: 10,
|
|
topPanePadding: 10,
|
|
topPaneGap: 10,
|
|
bottomPanePadding: 10,
|
|
bottomPaneGap: 12
|
|
};
|
|
}
|
|
}
|
|
};
|
|
var SCREEN_CANVAS_PADDING = 48;
|
|
var SCREEN_MIN_ZOOM = 0.45;
|
|
var SCREEN_MAX_ZOOM = 2.4;
|
|
var SCREEN_INITIAL_ZOOM = 1;
|
|
var SCREEN_BOX_WIDTH = 420;
|
|
var SCREEN_BOX_HEADER_HEIGHT = 42;
|
|
var SCREEN_SECTION_HEADER_HEIGHT = 24;
|
|
var SCREEN_SECTION_PADDING = 10;
|
|
var SCREEN_SECTION_GAP = 8;
|
|
var SCREEN_FIELD_ROW_HEIGHT = 22;
|
|
var SCREEN_MAX_TITLE_CHARS = 34;
|
|
var SCREEN_MAX_SECTION_CHARS = 36;
|
|
var SCREEN_MAX_FIELD_CHARS = 40;
|
|
var SCREEN_TRANSITION_LANE_WIDTH = 168;
|
|
var SCREEN_TARGET_BOX_WIDTH = 240;
|
|
var SCREEN_TARGET_BOX_MIN_HEIGHT = 96;
|
|
var SCREEN_TARGET_BOX_HEADER_VERTICAL_PADDING = 16;
|
|
var SCREEN_TARGET_BOX_BODY_VERTICAL_PADDING = 20;
|
|
var SCREEN_TARGET_KIND_LINE_HEIGHT = 16;
|
|
var SCREEN_TARGET_TITLE_LINE_HEIGHT = 22;
|
|
var SCREEN_TARGET_ROW_LINE_HEIGHT = 16;
|
|
var SCREEN_TARGET_ROW_GAP = 4;
|
|
var SCREEN_TARGET_BOX_GAP = 24;
|
|
var SCREEN_LABEL_PILL_WIDTH = 132;
|
|
var SCREEN_LABEL_PILL_HEIGHT = 24;
|
|
var SCREEN_LABEL_PILL_GAP = 8;
|
|
var SCREEN_ARROW_COLOR = "#64748b";
|
|
function buildScreenPreviewData(state) {
|
|
return {
|
|
title: state.title,
|
|
sourcePath: state.filePath,
|
|
blocks: state.layoutBlocks ?? [],
|
|
transitions: state.screenPreviewTransitions ?? []
|
|
};
|
|
}
|
|
function createScreenPreviewDiagram(data, options) {
|
|
const root = document.createElement("section");
|
|
root.className = "mdspec-diagram mdspec-diagram--screen";
|
|
root.addClass("model-weave-screen-preview");
|
|
root.addClass("model-weave-screen-chart");
|
|
const scene = buildScreenPreviewScene(data);
|
|
const canvas = document.createElement("div");
|
|
canvas.className = "mdspec-screen-canvas";
|
|
canvas.addClass("model-weave-screen-preview-layout-block");
|
|
if (!options?.forExport) {
|
|
canvas.addClass("model-weave-screen-preview-layout-block-interactive");
|
|
}
|
|
const toolbar = options?.forExport ? null : createZoomToolbar("Wheel: zoom / Drag background: pan");
|
|
if (toolbar) {
|
|
root.appendChild(toolbar.root);
|
|
}
|
|
const viewport = document.createElement("div");
|
|
viewport.className = "mdspec-screen-viewport";
|
|
viewport.addClass("model-weave-screen-preview-viewport");
|
|
const surface = document.createElement("div");
|
|
surface.className = "mdspec-screen-surface";
|
|
surface.dataset.modelWeaveExportSurface = "true";
|
|
surface.dataset.modelWeaveRenderer = "custom";
|
|
surface.dataset.modelWeaveSceneWidth = `${scene.width}`;
|
|
surface.dataset.modelWeaveSceneHeight = `${scene.height}`;
|
|
surface.addClass("model-weave-screen-preview-surface");
|
|
surface.setCssProps({
|
|
"--mw-scene-width": `${scene.width}px`,
|
|
"--mw-scene-height": `${scene.height}px`
|
|
});
|
|
surface.appendChild(createScreenPreviewTransitionSvg(scene));
|
|
surface.appendChild(
|
|
createScreenPreviewMainBox(data, scene.mainBoxHeight, scene.mainBoxTop, options)
|
|
);
|
|
for (const target of scene.targets) {
|
|
surface.appendChild(createScreenPreviewTargetBox(target, options));
|
|
}
|
|
for (const target of scene.targets) {
|
|
for (const pill of target.labelPills) {
|
|
surface.appendChild(createScreenPreviewActionPill(pill, options?.onNavigateToLocation));
|
|
}
|
|
}
|
|
viewport.appendChild(surface);
|
|
canvas.appendChild(viewport);
|
|
root.appendChild(canvas);
|
|
if (toolbar) {
|
|
attachGraphViewportInteractions(canvas, surface, toolbar, scene, {
|
|
minZoom: SCREEN_MIN_ZOOM,
|
|
maxZoom: SCREEN_MAX_ZOOM,
|
|
initialZoom: SCREEN_INITIAL_ZOOM,
|
|
fitVerticalAlign: "top",
|
|
fitContentBounds: {
|
|
top: scene.contentTop,
|
|
bottom: scene.contentBottom
|
|
},
|
|
nodeSelector: ".model-weave-screen-card, .model-weave-screen-transition-label, .model-weave-screen-preview-card, .model-weave-screen-preview-target-box, .model-weave-screen-preview-edge-label",
|
|
viewportState: options?.viewportState,
|
|
onViewportStateChange: options?.onViewportStateChange
|
|
});
|
|
}
|
|
return root;
|
|
}
|
|
function buildScreenPreviewScene(data) {
|
|
const blocks = data.blocks.length > 0 ? data.blocks : [{ label: "\u672A\u5206\u985E [unassigned]", items: [] }];
|
|
const mainBoxHeight = SCREEN_BOX_HEADER_HEIGHT + blocks.reduce((sum, block) => {
|
|
return sum + SCREEN_SECTION_HEADER_HEIGHT + SCREEN_SECTION_PADDING * 2 + block.items.length * SCREEN_FIELD_ROW_HEIGHT;
|
|
}, 0) + Math.max(0, blocks.length - 1) * SCREEN_SECTION_GAP;
|
|
const targetGroups = data.transitions;
|
|
const targetHeights = targetGroups.map((target) => {
|
|
const targetBoxHeight = measureScreenPreviewTargetBoxHeight(target);
|
|
const labelsHeight = target.actions.length * SCREEN_LABEL_PILL_HEIGHT + Math.max(0, target.actions.length - 1) * SCREEN_LABEL_PILL_GAP;
|
|
return Math.max(
|
|
targetBoxHeight,
|
|
labelsHeight + SCREEN_SECTION_PADDING * 2
|
|
);
|
|
});
|
|
const targetStackHeight = targetHeights.reduce((sum, currentHeight) => sum + currentHeight, 0) + Math.max(0, targetHeights.length - 1) * SCREEN_TARGET_BOX_GAP;
|
|
const contentHeight = Math.max(mainBoxHeight, targetStackHeight);
|
|
const mainBoxTop = SCREEN_CANVAS_PADDING + (contentHeight - mainBoxHeight) / 2;
|
|
const labelStartX = SCREEN_CANVAS_PADDING + SCREEN_BOX_WIDTH + 28;
|
|
const targetX = labelStartX + SCREEN_TRANSITION_LANE_WIDTH;
|
|
const width = SCREEN_CANVAS_PADDING * 2 + SCREEN_BOX_WIDTH + (targetGroups.length > 0 ? 28 + SCREEN_TRANSITION_LANE_WIDTH + SCREEN_TARGET_BOX_WIDTH : 0);
|
|
const height = SCREEN_CANVAS_PADDING * 2 + contentHeight;
|
|
const targets = [];
|
|
let nextTargetY = SCREEN_CANVAS_PADDING + (contentHeight - targetStackHeight) / 2;
|
|
targetGroups.forEach((target, index) => {
|
|
const groupHeight = targetHeights[index] ?? SCREEN_TARGET_BOX_MIN_HEIGHT;
|
|
const targetBoxHeight = measureScreenPreviewTargetBoxHeight(target);
|
|
const targetBoxY = nextTargetY + (groupHeight - targetBoxHeight) / 2;
|
|
const labelsHeight = target.actions.length * SCREEN_LABEL_PILL_HEIGHT + Math.max(0, target.actions.length - 1) * SCREEN_LABEL_PILL_GAP;
|
|
const labelStartY = nextTargetY + (groupHeight - labelsHeight) / 2;
|
|
const labelPills = target.actions.map((action, actionIndex) => ({
|
|
action,
|
|
x: labelStartX,
|
|
y: labelStartY + actionIndex * (SCREEN_LABEL_PILL_HEIGHT + SCREEN_LABEL_PILL_GAP),
|
|
width: SCREEN_LABEL_PILL_WIDTH,
|
|
height: SCREEN_LABEL_PILL_HEIGHT
|
|
}));
|
|
targets.push({
|
|
target,
|
|
x: targetX,
|
|
y: targetBoxY,
|
|
width: SCREEN_TARGET_BOX_WIDTH,
|
|
height: targetBoxHeight,
|
|
centerY: targetBoxY + targetBoxHeight / 2,
|
|
labelPills
|
|
});
|
|
nextTargetY += groupHeight + SCREEN_TARGET_BOX_GAP;
|
|
});
|
|
const contentTop = Math.min(
|
|
mainBoxTop,
|
|
...targets.map((target) => target.y),
|
|
...targets.flatMap((target) => target.labelPills.map((pill) => pill.y))
|
|
);
|
|
const contentBottom = Math.max(
|
|
mainBoxTop + mainBoxHeight,
|
|
...targets.map((target) => target.y + target.height),
|
|
...targets.flatMap(
|
|
(target) => target.labelPills.map((pill) => pill.y + pill.height)
|
|
)
|
|
);
|
|
return {
|
|
width,
|
|
height,
|
|
contentTop,
|
|
contentBottom,
|
|
mainBoxHeight,
|
|
mainBoxTop,
|
|
targets
|
|
};
|
|
}
|
|
function measureScreenPreviewTargetBoxHeight(target) {
|
|
const availableTextUnits = 24;
|
|
const titleLines = estimateScreenPreviewLineCount(
|
|
truncateScreenPreviewText(target.targetLabel, SCREEN_MAX_SECTION_CHARS),
|
|
availableTextUnits
|
|
);
|
|
const bodyRows = [
|
|
target.selfTarget ? "Self transition" : target.unresolved ? "Transition target not resolved" : "Open target screen",
|
|
target.actions.length > 1 ? `${target.actions.length} actions` : ""
|
|
].filter((row) => row.length > 0);
|
|
const bodyLines = bodyRows.reduce(
|
|
(sum, row) => sum + estimateScreenPreviewLineCount(row, availableTextUnits),
|
|
0
|
|
);
|
|
const bodyGap = Math.max(0, bodyRows.length - 1) * SCREEN_TARGET_ROW_GAP;
|
|
return Math.max(
|
|
SCREEN_TARGET_BOX_MIN_HEIGHT,
|
|
SCREEN_TARGET_BOX_HEADER_VERTICAL_PADDING + SCREEN_TARGET_KIND_LINE_HEIGHT + titleLines * SCREEN_TARGET_TITLE_LINE_HEIGHT + SCREEN_TARGET_BOX_BODY_VERTICAL_PADDING + bodyLines * SCREEN_TARGET_ROW_LINE_HEIGHT + bodyGap
|
|
);
|
|
}
|
|
function estimateScreenPreviewLineCount(text, availableUnitsPerLine) {
|
|
const trimmed = text.trim();
|
|
if (!trimmed) {
|
|
return 1;
|
|
}
|
|
return Math.max(1, Math.ceil(measureScreenPreviewTextUnits(trimmed) / availableUnitsPerLine));
|
|
}
|
|
function measureScreenPreviewTextUnits(text) {
|
|
let units = 0;
|
|
for (const char of text) {
|
|
units += /[\u1100-\u11ff\u2e80-\u9fff\uf900-\ufaff\uff00-\uffef]/u.test(char) ? 1.6 : 1;
|
|
}
|
|
return units;
|
|
}
|
|
function createScreenPreviewMainBox(data, height, top, options) {
|
|
const box = document.createElement("div");
|
|
box.className = "mdspec-screen-preview-box";
|
|
box.addClass("model-weave-screen-preview-card");
|
|
box.addClass("model-weave-screen-card");
|
|
box.setCssProps({
|
|
"--mw-node-x": `${SCREEN_CANVAS_PADDING}px`,
|
|
"--mw-node-y": `${top}px`,
|
|
"--mw-node-width": `${SCREEN_BOX_WIDTH}px`,
|
|
"--mw-node-height": `${height}px`
|
|
});
|
|
const header = document.createElement("header");
|
|
header.addClass("model-weave-screen-preview-header");
|
|
header.addClass("model-weave-screen-card-header");
|
|
const kind = document.createElement("div");
|
|
kind.addClass("model-weave-screen-preview-muted");
|
|
kind.textContent = "Screen";
|
|
const title = document.createElement("div");
|
|
title.addClass("model-weave-screen-preview-title");
|
|
title.addClass("model-weave-screen-card-title");
|
|
title.textContent = truncateScreenPreviewText(data.title, SCREEN_MAX_TITLE_CHARS);
|
|
header.append(kind, title);
|
|
box.appendChild(header);
|
|
const body = document.createElement("div");
|
|
body.addClass("model-weave-screen-preview-sections");
|
|
body.addClass("model-weave-screen-card-body");
|
|
const blocks = data.blocks.length > 0 ? data.blocks : [{ label: "\u672A\u5206\u985E [unassigned]", items: [] }];
|
|
blocks.forEach((block, index) => {
|
|
const section = document.createElement("section");
|
|
section.addClass("model-weave-screen-preview-section");
|
|
section.addClass("model-weave-screen-card-section");
|
|
if (index > 0) {
|
|
section.addClass("model-weave-screen-preview-section-bordered");
|
|
}
|
|
const sectionHeading = document.createElement("div");
|
|
sectionHeading.addClass("model-weave-screen-preview-section-title");
|
|
sectionHeading.textContent = truncateScreenPreviewText(block.label, SCREEN_MAX_SECTION_CHARS);
|
|
section.appendChild(sectionHeading);
|
|
if (block.items.length === 0) {
|
|
const empty = document.createElement("div");
|
|
empty.addClass("model-weave-screen-preview-empty");
|
|
empty.textContent = "None";
|
|
section.appendChild(empty);
|
|
} else {
|
|
const list = document.createElement("ul");
|
|
list.addClass("model-weave-screen-preview-list");
|
|
for (const item of block.items) {
|
|
const entry = document.createElement("li");
|
|
entry.textContent = truncateScreenPreviewText(item.label, SCREEN_MAX_FIELD_CHARS);
|
|
list.appendChild(entry);
|
|
}
|
|
section.appendChild(list);
|
|
}
|
|
body.appendChild(section);
|
|
});
|
|
box.appendChild(body);
|
|
if (data.sourcePath && options?.onOpenLinkedFile) {
|
|
box.tabIndex = 0;
|
|
box.setAttribute("role", "button");
|
|
box.addClass("model-weave-screen-preview-clickable");
|
|
box.title = `Open ${data.title}
|
|
${data.sourcePath}`;
|
|
const openSource = (openInNewLeaf) => {
|
|
options.onOpenLinkedFile?.(data.sourcePath, { openInNewLeaf });
|
|
};
|
|
box.addEventListener("click", (event) => {
|
|
if (event.defaultPrevented) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
openSource(Boolean(event.ctrlKey || event.metaKey));
|
|
});
|
|
box.addEventListener("auxclick", (event) => {
|
|
if (event.button !== 1) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
openSource(true);
|
|
});
|
|
box.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
openSource(Boolean(event.ctrlKey || event.metaKey));
|
|
}
|
|
});
|
|
box.addEventListener("pointerdown", (event) => {
|
|
event.stopPropagation();
|
|
});
|
|
}
|
|
return box;
|
|
}
|
|
function createScreenPreviewTransitionSvg(scene) {
|
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
svg.setAttribute("width", `${scene.width}`);
|
|
svg.setAttribute("height", `${scene.height}`);
|
|
svg.setAttribute("viewBox", `0 0 ${scene.width} ${scene.height}`);
|
|
svg.addClass("model-weave-screen-preview-overlay");
|
|
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
|
|
marker.setAttribute("id", "mdspec-screen-preview-arrow");
|
|
marker.setAttribute("markerWidth", "10");
|
|
marker.setAttribute("markerHeight", "10");
|
|
marker.setAttribute("refX", "8");
|
|
marker.setAttribute("refY", "5");
|
|
marker.setAttribute("orient", "auto");
|
|
marker.setAttribute("markerUnits", "userSpaceOnUse");
|
|
const markerPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
markerPath.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
|
|
markerPath.setAttribute("fill", SCREEN_ARROW_COLOR);
|
|
marker.appendChild(markerPath);
|
|
defs.appendChild(marker);
|
|
svg.appendChild(defs);
|
|
const sourceX = SCREEN_CANVAS_PADDING + SCREEN_BOX_WIDTH;
|
|
const sourceY = scene.mainBoxTop + scene.mainBoxHeight / 2;
|
|
for (const target of scene.targets) {
|
|
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
line.setAttribute("x1", `${sourceX}`);
|
|
line.setAttribute("y1", `${sourceY}`);
|
|
line.setAttribute("x2", `${target.x}`);
|
|
line.setAttribute("y2", `${target.centerY}`);
|
|
line.setAttribute("stroke", SCREEN_ARROW_COLOR);
|
|
line.setAttribute("stroke-width", "2");
|
|
line.setAttribute("stroke-linecap", "round");
|
|
line.setAttribute("marker-end", "url(#mdspec-screen-preview-arrow)");
|
|
svg.appendChild(line);
|
|
}
|
|
return svg;
|
|
}
|
|
function createScreenPreviewTargetBox(target, options) {
|
|
const box = document.createElement("div");
|
|
box.className = "mdspec-screen-preview-target-box";
|
|
box.addClass("model-weave-screen-preview-target-box");
|
|
box.addClass("model-weave-screen-card");
|
|
if (target.target.unresolved) {
|
|
box.addClass("model-weave-screen-preview-target-box-unresolved");
|
|
}
|
|
box.setCssProps({
|
|
"--mw-node-x": `${target.x}px`,
|
|
"--mw-node-y": `${target.y}px`,
|
|
"--mw-node-width": `${target.width}px`,
|
|
"--mw-node-height": `${target.height}px`
|
|
});
|
|
const header = document.createElement("header");
|
|
header.addClass("model-weave-screen-preview-target-header");
|
|
header.addClass("model-weave-screen-card-header");
|
|
if (target.target.unresolved) {
|
|
header.addClass("model-weave-screen-preview-target-header-unresolved");
|
|
}
|
|
const kind = document.createElement("div");
|
|
kind.addClass("model-weave-screen-preview-target-kind");
|
|
kind.textContent = target.target.unresolved ? "unresolved screen" : "screen";
|
|
const title = document.createElement("div");
|
|
title.addClass("model-weave-screen-preview-target-title");
|
|
title.addClass("model-weave-screen-card-title");
|
|
title.textContent = truncateScreenPreviewText(target.target.targetLabel, SCREEN_MAX_SECTION_CHARS);
|
|
if (target.target.targetTitle) {
|
|
title.title = target.target.targetTitle;
|
|
}
|
|
header.append(kind, title);
|
|
box.appendChild(header);
|
|
const body = document.createElement("div");
|
|
body.addClass("model-weave-screen-preview-target-body");
|
|
body.addClass("model-weave-screen-card-body");
|
|
if (target.target.selfTarget) {
|
|
body.createEl("div", {
|
|
text: "Self transition",
|
|
cls: "model-weave-screen-preview-row"
|
|
});
|
|
} else if (target.target.unresolved) {
|
|
body.createEl("div", {
|
|
text: "Transition target not resolved",
|
|
cls: "model-weave-screen-preview-row"
|
|
});
|
|
} else {
|
|
body.createEl("div", {
|
|
text: "Open target screen",
|
|
cls: "model-weave-screen-preview-row"
|
|
});
|
|
}
|
|
if (target.target.actions.length > 1) {
|
|
body.createEl("div", {
|
|
text: `${target.target.actions.length} actions`,
|
|
cls: "model-weave-screen-preview-row"
|
|
});
|
|
}
|
|
box.appendChild(body);
|
|
if (target.target.targetPath && options?.onOpenLinkedFile) {
|
|
box.tabIndex = 0;
|
|
box.setAttribute("role", "button");
|
|
box.addClass("model-weave-screen-preview-clickable");
|
|
box.title = target.target.targetTitle || target.target.targetLabel;
|
|
const openTarget = (openInNewLeaf) => {
|
|
options.onOpenLinkedFile?.(target.target.targetPath, { openInNewLeaf });
|
|
};
|
|
box.onclick = (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
openTarget(Boolean(event.metaKey || event.ctrlKey));
|
|
};
|
|
box.onauxclick = (event) => {
|
|
if (event.button !== 1) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
openTarget(true);
|
|
};
|
|
box.onkeydown = (event) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
openTarget(Boolean(event.ctrlKey || event.metaKey));
|
|
}
|
|
};
|
|
box.addEventListener("pointerdown", (event) => {
|
|
event.stopPropagation();
|
|
});
|
|
}
|
|
return box;
|
|
}
|
|
function createScreenPreviewActionPill(pill, _onNavigateToLocation) {
|
|
const element = document.createElement("span");
|
|
element.className = "model-weave-screen-preview-edge-label";
|
|
element.addClass("model-weave-screen-transition-label");
|
|
element.setCssProps({
|
|
"--mw-node-x": `${pill.x}px`,
|
|
"--mw-node-y": `${pill.y}px`,
|
|
"--mw-node-width": `${pill.width}px`,
|
|
"--mw-node-height": `${pill.height}px`
|
|
});
|
|
element.textContent = truncateScreenPreviewText(pill.action.label, 18);
|
|
if (pill.action.title) {
|
|
element.title = pill.action.title;
|
|
}
|
|
return element;
|
|
}
|
|
function truncateScreenPreviewText(value, maxChars) {
|
|
const normalized = value.trim();
|
|
if (normalized.length <= maxChars) {
|
|
return normalized;
|
|
}
|
|
return `${normalized.slice(0, Math.max(0, maxChars - 1))}\u2026`;
|
|
}
|
|
function renderDiagnostics(container, diagnostics, onOpenDiagnostic, getOpenState, setOpenState) {
|
|
const notes = diagnostics.filter((diagnostic) => diagnostic.severity === "info");
|
|
const warnings = diagnostics.filter((diagnostic) => diagnostic.severity === "warning");
|
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
if (notes.length === 0 && warnings.length === 0 && errors.length === 0) {
|
|
return;
|
|
}
|
|
if (notes.length > 0) {
|
|
renderDiagnosticSection(
|
|
container,
|
|
"Notes",
|
|
notes,
|
|
onOpenDiagnostic,
|
|
"model-weave-diagnostics-summary-note",
|
|
getOpenState,
|
|
setOpenState
|
|
);
|
|
}
|
|
if (warnings.length > 0) {
|
|
renderDiagnosticSection(
|
|
container,
|
|
"Warnings",
|
|
warnings,
|
|
onOpenDiagnostic,
|
|
"model-weave-diagnostics-summary-warning",
|
|
getOpenState,
|
|
setOpenState
|
|
);
|
|
}
|
|
if (errors.length > 0) {
|
|
renderDiagnosticSection(
|
|
container,
|
|
"Errors",
|
|
errors,
|
|
onOpenDiagnostic,
|
|
"model-weave-diagnostics-summary-error",
|
|
getOpenState,
|
|
setOpenState
|
|
);
|
|
}
|
|
}
|
|
function renderDiagnosticSection(container, title, diagnostics, onOpenDiagnostic, summaryModifierClass, getOpenState, setOpenState) {
|
|
const details = container.createEl("details");
|
|
details.className = "mdspec-diagnostic-section";
|
|
details.addClass("model-weave-preview-section");
|
|
const key = title.toLowerCase();
|
|
details.open = getOpenState ? getOpenState(key, title !== "Notes") : title !== "Notes";
|
|
if (setOpenState) {
|
|
details.addEventListener("toggle", () => {
|
|
setOpenState(key, details.open);
|
|
});
|
|
}
|
|
details.addClass("model-weave-diagnostics-details");
|
|
const summary = details.createEl("summary", {
|
|
text: `${title} (${diagnostics.length})`
|
|
});
|
|
summary.addClass("model-weave-diagnostics-summary");
|
|
summary.addClass("model-weave-preview-section-title");
|
|
summary.addClass(summaryModifierClass);
|
|
const list = details.createEl("ul", { cls: "model-weave-diagnostics-list" });
|
|
for (const diagnostic of diagnostics) {
|
|
const item = list.createEl("li", { cls: "model-weave-diagnostics-item" });
|
|
item.textContent = localizeDiagnosticMessage(diagnostic.message);
|
|
if (onOpenDiagnostic) {
|
|
item.addClass("model-weave-diagnostics-item-clickable");
|
|
item.addClass("model-weave-clickable");
|
|
item.title = "Open this diagnostic in the editor";
|
|
item.tabIndex = 0;
|
|
item.onclick = () => {
|
|
const selection = window.getSelection();
|
|
if (selection && !selection.isCollapsed) {
|
|
return;
|
|
}
|
|
onOpenDiagnostic(diagnostic);
|
|
};
|
|
item.onkeydown = (event) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
onOpenDiagnostic(diagnostic);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// src/main.ts
|
|
var LEGACY_PREVIEW_VIEW_TYPES = [
|
|
"mdspec-object-preview",
|
|
"mdspec-relations-preview",
|
|
"mdspec-diagram-preview"
|
|
];
|
|
var UNSUPPORTED_MESSAGE = modelWeaveText(
|
|
"This file format is not supported. Supported formats: class / class_diagram / er_entity / er_diagram / dfd_object / dfd_diagram / data_object / app_process / screen / rule / codeset / message / mapping",
|
|
"\u3053\u306E\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F\u306F\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u5BFE\u5FDC\u5F62\u5F0F: class / class_diagram / er_entity / er_diagram / dfd_object / dfd_diagram / data_object / app_process / screen / rule / codeset / message / mapping"
|
|
);
|
|
var DEPRECATED_ER_RELATION_MESSAGE = modelWeaveText(
|
|
"This file format is not supported. Use er_entity with ## Relations instead of the legacy er_relation format.",
|
|
"\u3053\u306E\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F\u306F\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u65E7 er_relation \u5F62\u5F0F\u3067\u306F\u306A\u304F\u3001er_entity \u306E ## Relations \u3092\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
);
|
|
var DEPRECATED_DIAGRAM_MESSAGE = modelWeaveText(
|
|
"This file format is not supported. Migrate legacy diagram_v1 files to class_diagram or er_diagram.",
|
|
"\u3053\u306E\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F\u306F\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u65E7 diagram_v1 \u30D5\u30A1\u30A4\u30EB\u306F class_diagram \u307E\u305F\u306F er_diagram \u306B\u79FB\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
);
|
|
var MARKDOWN_ONLY_NOTICE2 = modelWeaveText(
|
|
"Template insertion is available only for Markdown files.",
|
|
"\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u633F\u5165\u306F Markdown \u30D5\u30A1\u30A4\u30EB\u3067\u306E\u307F\u5229\u7528\u3067\u304D\u307E\u3059\u3002"
|
|
);
|
|
var NON_EMPTY_FILE_NOTICE = modelWeaveText(
|
|
"Current file is not empty. Template insertion is available only for empty files.",
|
|
"\u73FE\u5728\u306E\u30D5\u30A1\u30A4\u30EB\u306F\u7A7A\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u633F\u5165\u306F\u7A7A\u306E\u30D5\u30A1\u30A4\u30EB\u3067\u306E\u307F\u5229\u7528\u3067\u304D\u307E\u3059\u3002"
|
|
);
|
|
var ER_RELATION_TYPE_NOTICE = modelWeaveText(
|
|
"ER relation block insertion is available only for er_entity files.",
|
|
"ER relation block \u306E\u633F\u5165\u306F er_entity \u30D5\u30A1\u30A4\u30EB\u3067\u306E\u307F\u5229\u7528\u3067\u304D\u307E\u3059\u3002"
|
|
);
|
|
var MODEL_WEAVE_DEFAULT_ZOOM_OPTIONS = [
|
|
"fit",
|
|
"100"
|
|
];
|
|
var MODEL_WEAVE_FONT_SIZE_OPTIONS = [
|
|
"small",
|
|
"normal",
|
|
"large"
|
|
];
|
|
var MODEL_WEAVE_NODE_DENSITY_OPTIONS = [
|
|
"compact",
|
|
"normal",
|
|
"relaxed"
|
|
];
|
|
var MODEL_WEAVE_UI_LANGUAGE_OPTIONS = [
|
|
"auto",
|
|
"en",
|
|
"ja"
|
|
];
|
|
var CLASS_RENDER_MODE_OPTIONS = [
|
|
"custom",
|
|
"mermaid",
|
|
"mermaid-detail"
|
|
];
|
|
var ER_RENDER_MODE_OPTIONS = [
|
|
"custom",
|
|
"mermaid",
|
|
"mermaid-detail"
|
|
];
|
|
var DFD_RENDER_MODE_OPTIONS = [
|
|
"mermaid"
|
|
];
|
|
var PROCESS_RENDER_MODE_OPTIONS = [
|
|
"custom"
|
|
];
|
|
var SCREEN_RENDER_MODE_OPTIONS = [
|
|
"custom"
|
|
];
|
|
function isClassRenderModeOption(value) {
|
|
return CLASS_RENDER_MODE_OPTIONS.some((candidate) => candidate === value);
|
|
}
|
|
function isErRenderModeOption(value) {
|
|
return ER_RENDER_MODE_OPTIONS.some((candidate) => candidate === value);
|
|
}
|
|
function isDfdRenderModeOption(value) {
|
|
return DFD_RENDER_MODE_OPTIONS.some((candidate) => candidate === value);
|
|
}
|
|
function isProcessRenderModeOption(value) {
|
|
return PROCESS_RENDER_MODE_OPTIONS.some((candidate) => candidate === value);
|
|
}
|
|
function isScreenRenderModeOption(value) {
|
|
return SCREEN_RENDER_MODE_OPTIONS.some((candidate) => candidate === value);
|
|
}
|
|
function isDefaultZoomOption(value) {
|
|
return MODEL_WEAVE_DEFAULT_ZOOM_OPTIONS.some((candidate) => candidate === value);
|
|
}
|
|
function isFontSizeOption(value) {
|
|
return MODEL_WEAVE_FONT_SIZE_OPTIONS.some((candidate) => candidate === value);
|
|
}
|
|
function isNodeDensityOption(value) {
|
|
return MODEL_WEAVE_NODE_DENSITY_OPTIONS.some((candidate) => candidate === value);
|
|
}
|
|
function isUiLanguageOption(value) {
|
|
return MODEL_WEAVE_UI_LANGUAGE_OPTIONS.some((candidate) => candidate === value);
|
|
}
|
|
var ModelWeavePlugin = class extends import_obsidian8.Plugin {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.index = null;
|
|
this.previewLeaf = null;
|
|
this.rendererOverridesByFilePath = /* @__PURE__ */ new Map();
|
|
this.rendererOverrideFilePath = null;
|
|
this.settings = DEFAULT_MODEL_WEAVE_SETTINGS;
|
|
}
|
|
async onload() {
|
|
this.settings = normalizeModelWeaveSettings(await this.loadData());
|
|
this.registerView(
|
|
MODELING_PREVIEW_VIEW_TYPE,
|
|
(leaf) => new ModelingPreviewView(leaf, this.getViewerPreferences())
|
|
);
|
|
this.addSettingTab(new ModelWeaveSettingTab(this.app, this));
|
|
this.addCommand({
|
|
id: "rebuild-modeling-index",
|
|
name: "Rebuild modeling index",
|
|
callback: async () => {
|
|
await this.rebuildIndex({ parseMode: "full" });
|
|
await this.syncPreviewToActiveFile(false, "rerender");
|
|
new import_obsidian8.Notice("Modeling index rebuilt");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "open-modeling-preview",
|
|
name: "Open modeling preview for active file",
|
|
callback: async () => {
|
|
await this.openPreviewForActiveFile();
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-class-template",
|
|
name: "Insert class template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("class");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-class-diagram-template",
|
|
name: "Insert class diagram template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("classDiagram");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-er-entity-template",
|
|
name: "Insert entity template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("erEntity");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-er-diagram-template",
|
|
name: "Insert entity diagram template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("erDiagram");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-dfd-object-template",
|
|
name: "Insert data flow object template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("dfdObject");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-dfd-diagram-template",
|
|
name: "Insert data flow diagram template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("dfdDiagram");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-data-object-template",
|
|
name: "Insert data object template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("dataObject");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-data-object-file-layout-template",
|
|
name: "Insert data object file layout template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("dataObjectFileLayout");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-app-process-template",
|
|
name: "Insert app process template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("appProcess");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-screen-template",
|
|
name: "Insert screen template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("screen");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-codeset-template",
|
|
name: "Insert codeset template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("codeSet");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-message-template",
|
|
name: "Insert message template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("message");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-rule-template",
|
|
name: "Insert rule template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("rule");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-mapping-template",
|
|
name: "Insert mapping template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("mapping");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-er-relation-block",
|
|
name: "Insert entity relation block",
|
|
callback: async () => {
|
|
await this.insertErRelationBlock();
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "complete-current-field",
|
|
name: "Complete current field",
|
|
callback: async () => {
|
|
await this.ensureMemberLookupIndex();
|
|
openModelWeaveCompletion(this.app, () => this.index);
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "export-current-diagram-as-png",
|
|
name: "Export current diagram as PNG",
|
|
callback: async () => {
|
|
await this.exportCurrentDiagramAsPng();
|
|
}
|
|
});
|
|
this.registerEvent(
|
|
this.app.workspace.on("file-open", async () => {
|
|
await this.syncPreviewToActiveFile(false, "external-file-open");
|
|
})
|
|
);
|
|
this.registerEvent(
|
|
this.app.workspace.on("active-leaf-change", async (leaf) => {
|
|
if (leaf && this.isPreviewLeaf(leaf)) {
|
|
return;
|
|
}
|
|
await this.syncPreviewToActiveFile(false, "external-file-open");
|
|
})
|
|
);
|
|
this.registerEvent(
|
|
this.app.vault.on("modify", async () => {
|
|
await this.rebuildIndex();
|
|
await this.syncPreviewToActiveFile(false, "rerender");
|
|
})
|
|
);
|
|
this.registerEvent(
|
|
this.app.vault.on("create", async () => {
|
|
await this.rebuildIndex();
|
|
await this.syncPreviewToActiveFile(false, "rerender");
|
|
})
|
|
);
|
|
this.registerEvent(
|
|
this.app.vault.on("delete", async () => {
|
|
await this.rebuildIndex();
|
|
await this.syncPreviewToActiveFile(false, "rerender");
|
|
})
|
|
);
|
|
this.registerEvent(
|
|
this.app.vault.on("rename", async () => {
|
|
await this.rebuildIndex();
|
|
await this.syncPreviewToActiveFile(false, "rerender");
|
|
})
|
|
);
|
|
await this.rebuildIndex();
|
|
this.app.workspace.onLayoutReady(() => {
|
|
void this.normalizePreviewLeaves().then(
|
|
() => this.syncPreviewToActiveFile(true, "initial-open")
|
|
);
|
|
});
|
|
}
|
|
onunload() {
|
|
if (this.previewLeaf) {
|
|
this.previewLeaf.detach();
|
|
this.previewLeaf = null;
|
|
}
|
|
}
|
|
async rebuildIndex(options = {}) {
|
|
const parseMode = options.parseMode ?? "shallow";
|
|
const markdownFiles = this.app.vault.getMarkdownFiles();
|
|
const files = parseMode === "full" ? await Promise.all(
|
|
markdownFiles.map(async (file) => ({
|
|
path: file.path,
|
|
content: await this.app.vault.cachedRead(file)
|
|
}))
|
|
) : markdownFiles.map((file) => ({
|
|
path: file.path,
|
|
frontmatter: this.getCachedFrontmatter(file)
|
|
}));
|
|
this.index = buildVaultIndex(files, {
|
|
parseMode,
|
|
resolveRelations: parseMode === "full",
|
|
indexMembers: parseMode === "full",
|
|
validate: parseMode === "full"
|
|
});
|
|
}
|
|
getCachedFrontmatter(file) {
|
|
const frontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter;
|
|
return frontmatter ? { ...frontmatter } : void 0;
|
|
}
|
|
async ensureFullModelForFile(file) {
|
|
if (!this.index) {
|
|
return null;
|
|
}
|
|
if (this.index.state.fullParsedFilePaths[file.path]) {
|
|
return this.index.modelsByFilePath[file.path] ?? null;
|
|
}
|
|
const content = await this.app.vault.cachedRead(file);
|
|
replaceVaultIndexFile(this.index, { path: file.path, content }, "full");
|
|
return this.index.modelsByFilePath[file.path] ?? null;
|
|
}
|
|
async ensureFullParsedFiles(shouldParse) {
|
|
if (!this.index) {
|
|
return;
|
|
}
|
|
const candidates = Object.values(this.index.modelsByFilePath).filter(shouldParse).map((model) => model.path);
|
|
for (const filePath of candidates) {
|
|
if (this.index.state.fullParsedFilePaths[filePath]) {
|
|
continue;
|
|
}
|
|
const file = this.app.vault.getAbstractFileByPath(filePath);
|
|
if (file instanceof import_obsidian8.TFile) {
|
|
await this.ensureFullModelForFile(file);
|
|
}
|
|
}
|
|
}
|
|
async ensureRelationLookupIndex() {
|
|
if (!this.index || this.index.state.relationLookupsBuilt) {
|
|
return;
|
|
}
|
|
await this.ensureFullParsedFiles((model) => model.fileType === "relations");
|
|
if (this.index) {
|
|
ensureRelationLookups(this.index);
|
|
}
|
|
}
|
|
async ensureMemberLookupIndex() {
|
|
if (!this.index || this.index.state.memberLookupsBuilt) {
|
|
return;
|
|
}
|
|
await this.ensureFullParsedFiles(
|
|
(model) => [
|
|
"object",
|
|
"data-object",
|
|
"app-process",
|
|
"screen",
|
|
"codeset",
|
|
"message",
|
|
"rule",
|
|
"er-entity"
|
|
].includes(model.fileType)
|
|
);
|
|
if (this.index) {
|
|
ensureMemberLookups(this.index);
|
|
}
|
|
}
|
|
async ensureImpactIndexReady() {
|
|
if (!this.index || !this.settings.enableRelationshipView) {
|
|
return;
|
|
}
|
|
await this.ensureFullParsedFiles(
|
|
(model) => [
|
|
"object",
|
|
"er-entity",
|
|
"diagram",
|
|
"dfd-object",
|
|
"dfd-diagram",
|
|
"data-object",
|
|
"app-process",
|
|
"screen",
|
|
"rule",
|
|
"codeset",
|
|
"message",
|
|
"mapping"
|
|
].includes(model.fileType)
|
|
);
|
|
if (this.index) {
|
|
ensureMemberLookups(this.index);
|
|
}
|
|
}
|
|
buildImpactPreviewProps(model) {
|
|
if (!this.settings.enableRelationshipView || !this.index || model.fileType === "markdown" || model.fileType === "relations") {
|
|
return {};
|
|
}
|
|
const impactSummary = buildImpactSummary(model, this.index);
|
|
return {
|
|
impactSummary,
|
|
onCopyImpactSummary: () => {
|
|
void this.copyImpactSummary(impactSummary);
|
|
},
|
|
onOpenImpactModel: (filePath, navigation) => {
|
|
void this.openReferencedFile(filePath, Boolean(navigation?.openInNewLeaf));
|
|
}
|
|
};
|
|
}
|
|
async copyImpactSummary(summary) {
|
|
try {
|
|
await navigator.clipboard.writeText(formatImpactSummaryAsMarkdown(summary));
|
|
new import_obsidian8.Notice("Relationship summary copied");
|
|
} catch {
|
|
new import_obsidian8.Notice("Failed to copy relationship summary");
|
|
}
|
|
}
|
|
getSettings() {
|
|
return this.settings;
|
|
}
|
|
getViewerPreferences() {
|
|
return {
|
|
defaultZoom: this.settings.defaultZoom,
|
|
fontSize: this.settings.fontSize,
|
|
nodeDensity: this.settings.nodeDensity,
|
|
localSourceRoot: this.settings.localSourceRoot,
|
|
uiLanguage: this.settings.uiLanguage,
|
|
showMermaidRenderDebug: this.settings.showMermaidRenderDebug
|
|
};
|
|
}
|
|
async updateSettings(partial, options) {
|
|
this.settings = normalizeModelWeaveSettings({
|
|
...this.settings,
|
|
...partial
|
|
});
|
|
await this.saveData(this.settings);
|
|
if (options?.refreshViews === false) {
|
|
return;
|
|
}
|
|
await this.refreshOpenModelWeaveViews();
|
|
}
|
|
async refreshOpenModelWeaveViews() {
|
|
const leaves = this.app.workspace.getLeavesOfType(MODELING_PREVIEW_VIEW_TYPE);
|
|
for (const leaf of leaves) {
|
|
await leaf.loadIfDeferred();
|
|
const view = leaf.view;
|
|
if (!(view instanceof ModelingPreviewView)) {
|
|
continue;
|
|
}
|
|
view.applyViewerSettings(this.getViewerPreferences());
|
|
const currentFilePath = view.getCurrentFilePath();
|
|
if (!currentFilePath) {
|
|
view.refreshForSettingsChange();
|
|
continue;
|
|
}
|
|
const target = this.app.vault.getAbstractFileByPath(currentFilePath);
|
|
if (target instanceof import_obsidian8.TFile) {
|
|
await this.showPreviewForFile(target, leaf, false, "rerender");
|
|
} else {
|
|
view.refreshForSettingsChange();
|
|
}
|
|
}
|
|
}
|
|
async openPreviewForActiveFile() {
|
|
if (!this.index) {
|
|
await this.rebuildIndex();
|
|
}
|
|
const file = this.app.workspace.getActiveFile();
|
|
if (!file) {
|
|
new import_obsidian8.Notice("No active Markdown file.");
|
|
return;
|
|
}
|
|
await this.showPreviewForFile(file, void 0, true, "external-file-open");
|
|
}
|
|
async exportCurrentDiagramAsPng() {
|
|
const view = await this.findExportableModelWeaveView();
|
|
if (!view) {
|
|
new import_obsidian8.Notice(modelWeaveText(
|
|
"No exportable diagram is currently displayed.",
|
|
"\u73FE\u5728\u3001\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8\u3067\u304D\u308B diagram \u306F\u8868\u793A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002"
|
|
));
|
|
return;
|
|
}
|
|
try {
|
|
const exportPath = await view.exportCurrentDiagramAsPng();
|
|
if (!exportPath) {
|
|
new import_obsidian8.Notice("The current view is not ready for export.");
|
|
return;
|
|
}
|
|
new import_obsidian8.Notice(`Diagram exported: ${exportPath}`);
|
|
} catch (error) {
|
|
if (error instanceof DiagramExportError) {
|
|
if (error.code === "bounds-invalid") {
|
|
new import_obsidian8.Notice("The current diagram has no measurable export bounds.");
|
|
return;
|
|
}
|
|
new import_obsidian8.Notice("Failed to export the current diagram as PNG.");
|
|
return;
|
|
}
|
|
new import_obsidian8.Notice("Failed to export the current diagram as PNG.");
|
|
}
|
|
}
|
|
async insertTemplateIntoActiveFile(templateKey) {
|
|
const target = await this.getActiveMarkdownTarget();
|
|
if (!target) {
|
|
new import_obsidian8.Notice(MARKDOWN_ONLY_NOTICE2);
|
|
return;
|
|
}
|
|
const currentContent = target.getContent();
|
|
if (currentContent.trim().length > 0) {
|
|
new import_obsidian8.Notice(NON_EMPTY_FILE_NOTICE);
|
|
return;
|
|
}
|
|
await target.setContent(MODEL_WEAVE_TEMPLATES[templateKey]);
|
|
}
|
|
async insertErRelationBlock() {
|
|
const target = await this.getActiveMarkdownTarget();
|
|
if (!target) {
|
|
new import_obsidian8.Notice(MARKDOWN_ONLY_NOTICE2);
|
|
return;
|
|
}
|
|
if (this.getActiveFileType(target.file) !== "er_entity") {
|
|
new import_obsidian8.Notice(ER_RELATION_TYPE_NOTICE);
|
|
return;
|
|
}
|
|
const lineEnding = this.detectLineEnding(target.getContent());
|
|
const block = MODEL_WEAVE_RELATION_TEMPLATES.erRelationBlock.join(lineEnding);
|
|
const nextContent = this.appendErRelationBlock(target.getContent(), block, lineEnding);
|
|
await target.setContent(nextContent);
|
|
}
|
|
async getActiveMarkdownTarget() {
|
|
const file = this.app.workspace.getActiveFile();
|
|
if (!file || file.extension !== "md") {
|
|
return null;
|
|
}
|
|
const activeView = this.app.workspace.getActiveViewOfType(import_obsidian8.MarkdownView);
|
|
if (activeView?.file?.path === file.path) {
|
|
return {
|
|
file,
|
|
getContent: () => activeView.editor.getValue(),
|
|
setContent: async (content) => {
|
|
activeView.editor.setValue(content);
|
|
await this.app.vault.modify(file, content);
|
|
}
|
|
};
|
|
}
|
|
const cachedContent = await this.app.vault.cachedRead(file);
|
|
return {
|
|
file,
|
|
getContent: () => cachedContent,
|
|
setContent: async (content) => {
|
|
await this.app.vault.modify(file, content);
|
|
}
|
|
};
|
|
}
|
|
getActiveFileType(file) {
|
|
const frontmatterType = this.app.metadataCache.getFileCache(file)?.frontmatter?.type;
|
|
if (typeof frontmatterType === "string" && frontmatterType.trim()) {
|
|
return frontmatterType.trim();
|
|
}
|
|
return void 0;
|
|
}
|
|
detectLineEnding(content) {
|
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
}
|
|
appendErRelationBlock(content, block, lineEnding) {
|
|
const section = this.findSection(content, "Relations");
|
|
if (section) {
|
|
const after = content.slice(section.end);
|
|
const sectionText = content.slice(section.start, section.end).replace(/\s*$/u, "");
|
|
const updatedSection = `${sectionText}${lineEnding}${lineEnding}${block}${lineEnding}`;
|
|
return `${content.slice(0, section.start)}${updatedSection}${after.replace(/^\s*/u, "")}`;
|
|
}
|
|
const relationsSection = `## Relations${lineEnding}${lineEnding}${block}${lineEnding}`;
|
|
return this.insertSectionBeforeNotesOrEnd(content, relationsSection, lineEnding);
|
|
}
|
|
insertSectionBeforeNotesOrEnd(content, sectionContent, lineEnding) {
|
|
const notesSection = this.findSection(content, "Notes");
|
|
const trimmedSection = sectionContent.replace(/\s*$/u, "");
|
|
if (notesSection) {
|
|
const before = content.slice(0, notesSection.start).replace(/\s*$/u, "");
|
|
const after = content.slice(notesSection.start).replace(/^\s*/u, "");
|
|
return `${before}${lineEnding}${lineEnding}${trimmedSection}${lineEnding}${lineEnding}${after}`;
|
|
}
|
|
const trimmedContent = content.replace(/\s*$/u, "");
|
|
if (!trimmedContent) {
|
|
return `${trimmedSection}${lineEnding}`;
|
|
}
|
|
return `${trimmedContent}${lineEnding}${lineEnding}${trimmedSection}${lineEnding}`;
|
|
}
|
|
findSection(content, sectionName) {
|
|
const headingRegex = new RegExp(`^##\\s+${sectionName}\\s*$`, "m");
|
|
const headingMatch = headingRegex.exec(content);
|
|
if (!headingMatch || headingMatch.index === void 0) {
|
|
return null;
|
|
}
|
|
const start = headingMatch.index;
|
|
const searchStart = start + headingMatch[0].length;
|
|
const remainder = content.slice(searchStart);
|
|
const nextHeadingMatch = /^##\s+/m.exec(remainder);
|
|
const end = nextHeadingMatch && nextHeadingMatch.index !== void 0 ? searchStart + nextHeadingMatch.index : content.length;
|
|
return { start, end };
|
|
}
|
|
async syncPreviewToActiveFile(openIfSupported = false, reason = "rerender") {
|
|
const file = this.app.workspace.getActiveFile();
|
|
const previewLeaf = this.getManagedPreviewLeaf();
|
|
if (!file) {
|
|
if (previewLeaf) {
|
|
await this.updateEmptyState(previewLeaf, [], void 0, reason);
|
|
}
|
|
return;
|
|
}
|
|
if (!this.index) {
|
|
await this.rebuildIndex();
|
|
}
|
|
if (this.rendererOverrideFilePath !== null && this.rendererOverrideFilePath !== file.path) {
|
|
this.rendererOverridesByFilePath.clear();
|
|
this.rendererOverrideFilePath = null;
|
|
}
|
|
const model = this.index?.modelsByFilePath[file.path];
|
|
const fileType = model ? detectFileType(model.frontmatter) : "markdown";
|
|
const isSupported = fileType === "object" || fileType === "er-entity" || fileType === "diagram" || fileType === "dfd-object" || fileType === "dfd-diagram" || fileType === "data-object" || fileType === "app-process" || fileType === "screen" || fileType === "rule" || fileType === "codeset" || fileType === "message" || fileType === "mapping";
|
|
if (!previewLeaf && !openIfSupported) {
|
|
return;
|
|
}
|
|
if (previewLeaf && reason === "external-file-open") {
|
|
await previewLeaf.loadIfDeferred();
|
|
const currentView = previewLeaf.view;
|
|
if (currentView instanceof ModelingPreviewView && currentView.getCurrentFilePath() === file.path) {
|
|
return;
|
|
}
|
|
}
|
|
if (!isSupported) {
|
|
if (previewLeaf) {
|
|
await this.updateEmptyState(
|
|
previewLeaf,
|
|
[],
|
|
await this.getEmptyStateMessage(file),
|
|
reason
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
await this.showPreviewForFile(
|
|
file,
|
|
previewLeaf ?? void 0,
|
|
openIfSupported,
|
|
reason
|
|
);
|
|
}
|
|
async showPreviewForFile(file, preferredLeaf, activate = true, reason = "rerender") {
|
|
if (!this.index) {
|
|
await this.rebuildIndex();
|
|
}
|
|
if (!this.index) {
|
|
return;
|
|
}
|
|
const model = await this.ensureFullModelForFile(file);
|
|
const leaf = await this.ensurePreviewLeaf(preferredLeaf, activate);
|
|
await leaf.loadIfDeferred();
|
|
const view = leaf.view;
|
|
if (!(view instanceof ModelingPreviewView)) {
|
|
return;
|
|
}
|
|
view.applyViewerSettings(this.getViewerPreferences());
|
|
if (!model) {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: await this.getEmptyStateMessage(file),
|
|
warnings: []
|
|
}, reason);
|
|
return;
|
|
}
|
|
const fileType = detectFileType(model.frontmatter);
|
|
const renderMode = this.resolveFileRenderMode(
|
|
file.path,
|
|
fileType,
|
|
model.frontmatter,
|
|
"kind" in model && typeof model.kind === "string" ? model.kind : null
|
|
);
|
|
const renderModeWarnings = renderMode.diagnostics;
|
|
const rendererSelection = this.buildRendererSelectionState(
|
|
file.path,
|
|
renderMode,
|
|
fileType,
|
|
"kind" in model && typeof model.kind === "string" ? model.kind : null
|
|
);
|
|
await this.ensureImpactIndexReady();
|
|
const impactPreviewProps = this.buildImpactPreviewProps(
|
|
this.index.modelsByFilePath[file.path] ?? model
|
|
);
|
|
switch (fileType) {
|
|
case "object":
|
|
case "er-entity": {
|
|
const objectModel = model.fileType === "object" || model.fileType === "er-entity" ? model : null;
|
|
if (objectModel?.fileType === "object") {
|
|
await this.ensureFullParsedFiles((candidate) => candidate.fileType === "object");
|
|
await this.ensureRelationLookupIndex();
|
|
} else if (objectModel?.fileType === "er-entity") {
|
|
await this.ensureFullParsedFiles((candidate) => candidate.fileType === "er-entity");
|
|
}
|
|
const context = objectModel && this.index ? resolveObjectContext(objectModel, this.index) : null;
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings,
|
|
...context?.warnings ?? []
|
|
];
|
|
if (renderMode.actualRenderer === "mermaid" && context && !context.relatedObjects.some((entry) => entry.direction === "outgoing")) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: "Mermaid overview: no outbound relations to display.",
|
|
severity: "info",
|
|
filePath: file.path,
|
|
section: "Relations"
|
|
});
|
|
}
|
|
if (objectModel) {
|
|
const diagnostics = buildCurrentObjectDiagnostics(
|
|
objectModel,
|
|
this.index,
|
|
context,
|
|
warnings
|
|
);
|
|
view.updateContent({
|
|
mode: "object",
|
|
model: objectModel,
|
|
context,
|
|
...impactPreviewProps,
|
|
warnings: diagnostics,
|
|
rendererSelection,
|
|
onOpenDiagnostic: (diagnostic) => {
|
|
void this.openDiagnosticLocation(file.path, diagnostic);
|
|
},
|
|
onOpenObject: (objectId, navigation) => {
|
|
void this.openObjectNote(objectId, file.path, navigation);
|
|
}
|
|
}, reason);
|
|
} else {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
}, reason);
|
|
}
|
|
return;
|
|
}
|
|
case "dfd-object": {
|
|
const dfdObject = model.fileType === "dfd-object" ? model : null;
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings
|
|
];
|
|
if (dfdObject) {
|
|
const diagnostics = buildCurrentObjectDiagnostics(
|
|
dfdObject,
|
|
this.index,
|
|
null,
|
|
warnings
|
|
);
|
|
const diagram = buildDfdObjectScene(dfdObject);
|
|
view.updateContent({
|
|
mode: "dfd-object",
|
|
model: dfdObject,
|
|
diagram,
|
|
...impactPreviewProps,
|
|
warnings: [...diagnostics, ...diagram.warnings],
|
|
rendererSelection,
|
|
onOpenDiagnostic: (diagnostic) => {
|
|
void this.openDiagnosticLocation(file.path, diagnostic);
|
|
},
|
|
onOpenObject: (objectId, navigation) => {
|
|
void this.openObjectNote(objectId, file.path, navigation);
|
|
}
|
|
}, reason);
|
|
} else {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
}, reason);
|
|
}
|
|
return;
|
|
}
|
|
case "diagram": {
|
|
if (model.fileType === "diagram") {
|
|
if (model.kind === "er") {
|
|
await this.ensureFullParsedFiles((candidate) => candidate.fileType === "er-entity");
|
|
} else {
|
|
await this.ensureFullParsedFiles((candidate) => candidate.fileType === "object");
|
|
await this.ensureRelationLookupIndex();
|
|
}
|
|
}
|
|
const resolved = model.fileType === "diagram" && this.index ? resolveDiagramRelations(model, this.index) : null;
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings,
|
|
...resolved?.warnings ?? []
|
|
];
|
|
const diagnostics = resolved ? buildCurrentDiagramDiagnostics(resolved, warnings) : warnings;
|
|
view.updateContent(
|
|
resolved ? {
|
|
mode: "diagram",
|
|
diagram: resolved,
|
|
...impactPreviewProps,
|
|
warnings: diagnostics,
|
|
rendererSelection,
|
|
onOpenDiagnostic: (diagnostic) => {
|
|
void this.openDiagnosticLocation(file.path, diagnostic);
|
|
},
|
|
onOpenObject: (objectId, navigation) => {
|
|
void this.openObjectNote(objectId, file.path, navigation);
|
|
}
|
|
} : {
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
},
|
|
reason
|
|
);
|
|
return;
|
|
}
|
|
case "dfd-diagram": {
|
|
if (model.fileType === "dfd-diagram") {
|
|
await this.ensureFullParsedFiles((candidate) => candidate.fileType === "dfd-object");
|
|
}
|
|
const resolved = model.fileType === "dfd-diagram" && this.index ? resolveDiagramRelations(model, this.index) : null;
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings,
|
|
...resolved?.warnings ?? []
|
|
];
|
|
const diagnostics = resolved ? buildCurrentDiagramDiagnostics(resolved, warnings) : warnings;
|
|
view.updateContent(
|
|
resolved ? {
|
|
mode: "diagram",
|
|
diagram: resolved,
|
|
...impactPreviewProps,
|
|
warnings: diagnostics,
|
|
rendererSelection,
|
|
onOpenDiagnostic: (diagnostic) => {
|
|
void this.openDiagnosticLocation(file.path, diagnostic);
|
|
},
|
|
onOpenObject: (objectId, navigation) => {
|
|
void this.openObjectNote(objectId, file.path, navigation);
|
|
}
|
|
} : {
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
},
|
|
reason
|
|
);
|
|
return;
|
|
}
|
|
case "data-object": {
|
|
await this.ensureMemberLookupIndex();
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings
|
|
];
|
|
if (model.fileType === "data-object") {
|
|
const diagnostics = buildCurrentObjectDiagnostics(
|
|
model,
|
|
this.index,
|
|
null,
|
|
warnings
|
|
);
|
|
view.updateContent({
|
|
mode: "summary",
|
|
rendererSelection,
|
|
...impactPreviewProps,
|
|
filePath: model.path,
|
|
title: model.name || model.id || this.getPathBasename(model.path),
|
|
sourceLinks: model.sourceLinks,
|
|
metadata: [
|
|
{ label: "type", value: "data_object" },
|
|
{ label: "id", value: model.id || "(missing)" },
|
|
{ label: "name", value: model.name || "(missing)" },
|
|
...model.kind ? [{ label: "kind", value: model.kind }] : [],
|
|
...model.dataFormat ? [{ label: "data_format", value: model.dataFormat }] : [],
|
|
{ label: "path", value: model.path }
|
|
],
|
|
sections: this.describeDataObjectSections(model, file.path),
|
|
counts: [
|
|
{ label: "Format entries", value: model.formatEntries.length },
|
|
{ label: "Records", value: model.records.length },
|
|
{ label: "Fields", value: model.fields.length }
|
|
],
|
|
tables: this.buildDataObjectSummaryTables(model, file.path),
|
|
warnings: diagnostics,
|
|
onNavigateToLocation: (location) => {
|
|
void this.openFileLocation(file.path, location.line, location.ch ?? 0);
|
|
}
|
|
}, reason);
|
|
} else {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
}, reason);
|
|
}
|
|
return;
|
|
}
|
|
case "app-process": {
|
|
await this.ensureMemberLookupIndex();
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings
|
|
];
|
|
if (model.fileType === "app-process") {
|
|
const diagnostics = buildCurrentObjectDiagnostics(
|
|
model,
|
|
this.index,
|
|
null,
|
|
[
|
|
...warnings,
|
|
...this.buildAppProcessBusinessFlowWarnings(model)
|
|
]
|
|
);
|
|
view.updateContent({
|
|
mode: "summary",
|
|
rendererSelection,
|
|
...impactPreviewProps,
|
|
filePath: model.path,
|
|
title: model.name || model.id || this.getPathBasename(model.path),
|
|
sourceLinks: model.sourceLinks,
|
|
metadata: [
|
|
{ label: "type", value: "app_process" },
|
|
{ label: "id", value: model.id || "(missing)" },
|
|
{ label: "name", value: model.name || "(missing)" },
|
|
...model.kind ? [{ label: "kind", value: model.kind }] : [],
|
|
{ label: "path", value: model.path }
|
|
],
|
|
sections: this.describeAppProcessSections(model, file.path),
|
|
counts: [
|
|
{ label: "Triggers", value: model.triggers.length },
|
|
{ label: "Inputs", value: model.inputs.length },
|
|
{ label: "Outputs", value: model.outputs.length },
|
|
{ label: "Transitions", value: model.transitions.length },
|
|
...model.steps?.length ? [{ label: "Steps", value: model.steps.length }] : [],
|
|
...model.hasExplicitFlows ? [{ label: "Flows", value: model.flows?.length ?? 0 }] : []
|
|
],
|
|
textSections: this.buildAppProcessTextSections(model),
|
|
tables: this.buildAppProcessSummaryTables(model, file.path),
|
|
businessFlow: (model.steps?.length ?? 0) > 0 ? {
|
|
title: model.name || model.id,
|
|
steps: model.steps ?? [],
|
|
flows: model.flows ?? [],
|
|
hasExplicitFlows: Boolean(model.hasExplicitFlows)
|
|
} : void 0,
|
|
warnings: diagnostics,
|
|
onNavigateToLocation: (location) => {
|
|
void this.openFileLocation(file.path, location.line, location.ch ?? 0);
|
|
}
|
|
}, reason);
|
|
} else {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
}, reason);
|
|
}
|
|
return;
|
|
}
|
|
case "screen": {
|
|
await this.ensureMemberLookupIndex();
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings
|
|
];
|
|
if (model.fileType === "screen") {
|
|
const diagnostics = buildCurrentObjectDiagnostics(
|
|
model,
|
|
this.index,
|
|
null,
|
|
warnings
|
|
);
|
|
const localProcesses = model.localProcesses.length > 0 ? model.localProcesses.map((process) => ({
|
|
label: process.heading,
|
|
line: process.line,
|
|
ch: 0
|
|
})) : this.collectScreenLocalProcesses(file.path);
|
|
const invokedProcesses = this.collectScreenInvokedProcesses(model);
|
|
const outgoingScreens = this.collectScreenOutgoingScreens(model);
|
|
const screenPreviewTransitions = this.buildScreenPreviewTransitions(model);
|
|
view.updateContent({
|
|
mode: "summary",
|
|
summaryKind: "screen",
|
|
rendererSelection,
|
|
...impactPreviewProps,
|
|
filePath: model.path,
|
|
title: model.name || model.id || this.getPathBasename(model.path),
|
|
sourceLinks: model.sourceLinks,
|
|
metadata: [
|
|
{ label: "type", value: "screen" },
|
|
{ label: "id", value: model.id || "(missing)" },
|
|
{ label: "name", value: model.name || "(missing)" },
|
|
...model.screenType ? [{ label: "screen_type", value: model.screenType }] : [],
|
|
{ label: "path", value: model.path }
|
|
],
|
|
sections: this.describeScreenSections(model, file.path),
|
|
counts: [
|
|
{ label: "Layouts", value: model.layouts.length },
|
|
{ label: "Fields", value: model.fields.length },
|
|
{ label: "Actions", value: model.actions.length },
|
|
{ label: "Messages", value: model.messages.length },
|
|
{
|
|
label: "Local processes",
|
|
value: localProcesses.length
|
|
},
|
|
{ label: "Invoked processes", value: invokedProcesses.length },
|
|
{ label: "Outgoing screens", value: outgoingScreens.length }
|
|
],
|
|
tables: this.buildScreenSummaryTables(model, file.path),
|
|
layoutBlocks: this.buildScreenLayoutBlocks(model),
|
|
screenPreviewTransitions,
|
|
localProcesses,
|
|
navigationLists: [
|
|
{ title: "Invoked processes", items: invokedProcesses },
|
|
{ title: "Transitions / Outgoing screens", items: outgoingScreens }
|
|
],
|
|
warnings: diagnostics,
|
|
onNavigateToLocation: (location) => {
|
|
void this.openFileLocation(file.path, location.line, location.ch ?? 0);
|
|
},
|
|
onOpenLinkedFile: (targetPath, navigation) => {
|
|
void this.openReferencedFile(
|
|
targetPath,
|
|
navigation?.openInNewLeaf ?? false
|
|
);
|
|
}
|
|
}, reason);
|
|
} else {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
}, reason);
|
|
}
|
|
return;
|
|
}
|
|
case "codeset": {
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings
|
|
];
|
|
if (model.fileType === "codeset") {
|
|
const diagnostics = buildCurrentObjectDiagnostics(
|
|
model,
|
|
this.index,
|
|
null,
|
|
warnings
|
|
);
|
|
view.updateContent({
|
|
mode: "summary",
|
|
rendererSelection,
|
|
...impactPreviewProps,
|
|
filePath: model.path,
|
|
title: model.name || model.id || this.getPathBasename(model.path),
|
|
sourceLinks: model.sourceLinks,
|
|
metadata: [
|
|
{ label: "type", value: "codeset" },
|
|
{ label: "id", value: model.id || "(missing)" },
|
|
{ label: "name", value: model.name || "(missing)" },
|
|
...model.kind ? [{ label: "kind", value: model.kind }] : [],
|
|
{ label: "path", value: model.path }
|
|
],
|
|
sections: this.describeCodeSetSections(model, file.path),
|
|
counts: [{ label: "Values", value: model.values.length }],
|
|
textSections: [
|
|
...model.summary?.trim() ? [{ title: "Summary", lines: [model.summary.trim()] }] : [],
|
|
...(model.notes ?? []).length > 0 ? [{ title: "Notes", lines: model.notes ?? [] }] : []
|
|
],
|
|
tables: this.buildCodeSetSummaryTables(file.path),
|
|
warnings: diagnostics,
|
|
onNavigateToLocation: (location) => {
|
|
void this.openFileLocation(file.path, location.line, location.ch ?? 0);
|
|
}
|
|
}, reason);
|
|
} else {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
}, reason);
|
|
}
|
|
return;
|
|
}
|
|
case "message": {
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings
|
|
];
|
|
if (model.fileType === "message") {
|
|
const diagnostics = buildCurrentObjectDiagnostics(
|
|
model,
|
|
this.index,
|
|
null,
|
|
warnings
|
|
);
|
|
view.updateContent({
|
|
mode: "summary",
|
|
rendererSelection,
|
|
...impactPreviewProps,
|
|
filePath: model.path,
|
|
title: model.name || model.id || this.getPathBasename(model.path),
|
|
sourceLinks: model.sourceLinks,
|
|
metadata: [
|
|
{ label: "type", value: "message" },
|
|
{ label: "id", value: model.id || "(missing)" },
|
|
{ label: "name", value: model.name || "(missing)" },
|
|
...model.kind ? [{ label: "kind", value: model.kind }] : [],
|
|
{ label: "path", value: model.path }
|
|
],
|
|
sections: this.describeMessageSections(model, file.path),
|
|
counts: [{ label: "Messages", value: model.messages.length }],
|
|
textSections: [
|
|
...model.summary?.trim() ? [{ title: "Summary", lines: [model.summary.trim()] }] : [],
|
|
...(model.notes ?? []).length > 0 ? [{ title: "Notes", lines: model.notes ?? [] }] : []
|
|
],
|
|
tables: this.buildMessageSummaryTables(file.path),
|
|
warnings: diagnostics,
|
|
onNavigateToLocation: (location) => {
|
|
void this.openFileLocation(file.path, location.line, location.ch ?? 0);
|
|
}
|
|
}, reason);
|
|
} else {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
}, reason);
|
|
}
|
|
return;
|
|
}
|
|
case "rule": {
|
|
await this.ensureMemberLookupIndex();
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings
|
|
];
|
|
if (model.fileType === "rule") {
|
|
const diagnostics = buildCurrentObjectDiagnostics(
|
|
model,
|
|
this.index,
|
|
null,
|
|
warnings
|
|
);
|
|
view.updateContent({
|
|
mode: "summary",
|
|
rendererSelection,
|
|
...impactPreviewProps,
|
|
filePath: model.path,
|
|
title: model.name || model.id || this.getPathBasename(model.path),
|
|
sourceLinks: model.sourceLinks,
|
|
metadata: [
|
|
{ label: "type", value: "rule" },
|
|
{ label: "id", value: model.id || "(missing)" },
|
|
{ label: "name", value: model.name || "(missing)" },
|
|
...model.kind ? [{ label: "kind", value: model.kind }] : [],
|
|
{ label: "path", value: model.path }
|
|
],
|
|
sections: this.describeRuleSections(model, file.path),
|
|
counts: [
|
|
{ label: "Inputs", value: model.inputs.length },
|
|
{ label: "References", value: model.references.length },
|
|
{ label: "Messages", value: model.messages.length }
|
|
],
|
|
tables: this.buildRuleSummaryTables(model, file.path),
|
|
warnings: diagnostics,
|
|
onNavigateToLocation: (location) => {
|
|
void this.openFileLocation(file.path, location.line, location.ch ?? 0);
|
|
}
|
|
}, reason);
|
|
} else {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
}, reason);
|
|
}
|
|
return;
|
|
}
|
|
case "mapping": {
|
|
await this.ensureMemberLookupIndex();
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings
|
|
];
|
|
if (model.fileType === "mapping") {
|
|
const diagnostics = buildCurrentObjectDiagnostics(
|
|
model,
|
|
this.index,
|
|
null,
|
|
warnings
|
|
);
|
|
view.updateContent({
|
|
mode: "summary",
|
|
rendererSelection,
|
|
...impactPreviewProps,
|
|
filePath: model.path,
|
|
title: model.name || model.id || this.getPathBasename(model.path),
|
|
sourceLinks: model.sourceLinks,
|
|
metadata: [
|
|
{ label: "type", value: "mapping" },
|
|
{ label: "id", value: model.id || "(missing)" },
|
|
{ label: "name", value: model.name || "(missing)" },
|
|
...model.kind ? [{ label: "kind", value: model.kind }] : [],
|
|
...model.source ? [{ label: "source", value: this.formatReferenceDisplay(model.source) }] : [],
|
|
...model.target ? [{ label: "target", value: this.formatReferenceDisplay(model.target) }] : [],
|
|
{ label: "path", value: model.path }
|
|
],
|
|
sections: this.describeMappingSections(model, file.path),
|
|
counts: [
|
|
{ label: "Scope", value: model.scope.length },
|
|
{ label: "Mappings", value: model.mappings.length }
|
|
],
|
|
tables: this.buildMappingSummaryTables(file.path),
|
|
warnings: diagnostics,
|
|
onNavigateToLocation: (location) => {
|
|
void this.openFileLocation(file.path, location.line, location.ch ?? 0);
|
|
}
|
|
}, reason);
|
|
} else {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
}, reason);
|
|
}
|
|
return;
|
|
}
|
|
case "markdown":
|
|
default:
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: await this.getEmptyStateMessage(file),
|
|
warnings: this.index.warningsByFilePath[file.path] ?? []
|
|
}, reason);
|
|
}
|
|
}
|
|
async updateEmptyState(leaf, warnings = [], message = UNSUPPORTED_MESSAGE, reason = "rerender") {
|
|
await leaf.loadIfDeferred();
|
|
if (leaf.view instanceof ModelingPreviewView) {
|
|
leaf.view.updateContent({
|
|
mode: "empty",
|
|
message,
|
|
warnings
|
|
}, reason);
|
|
}
|
|
}
|
|
async getEmptyStateMessage(file) {
|
|
const frontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter;
|
|
if (frontmatter?.type === "er_relation") {
|
|
return DEPRECATED_ER_RELATION_MESSAGE;
|
|
}
|
|
if (frontmatter?.type === "diagram" || frontmatter?.schema === "diagram_v1" || typeof frontmatter?.diagram_kind === "string") {
|
|
return DEPRECATED_DIAGRAM_MESSAGE;
|
|
}
|
|
const content = await this.app.vault.cachedRead(file);
|
|
if (/^\s*---[\s\S]*?\btype\s*:\s*er_relation\b[\s\S]*?---/m.test(content)) {
|
|
return DEPRECATED_ER_RELATION_MESSAGE;
|
|
}
|
|
if (/^\s*---[\s\S]*?\btype\s*:\s*diagram\b[\s\S]*?---/m.test(content) || /^\s*---[\s\S]*?\bschema\s*:\s*diagram_v1\b[\s\S]*?---/m.test(content) || /^\s*---[\s\S]*?\bdiagram_kind\s*:\s*[A-Za-z0-9_-]+\b[\s\S]*?---/m.test(content)) {
|
|
return DEPRECATED_DIAGRAM_MESSAGE;
|
|
}
|
|
return UNSUPPORTED_MESSAGE;
|
|
}
|
|
describeDataObjectSections(model, filePath) {
|
|
const lines = this.getFileLines(filePath);
|
|
const sections = [];
|
|
const orderedKeys = ["Summary", "Format", "Records", "Fields", "Notes"];
|
|
for (const key of orderedKeys) {
|
|
if (!(key in model.sections)) {
|
|
continue;
|
|
}
|
|
const line = this.findHeadingLine(lines, key);
|
|
if (key === "Format") {
|
|
sections.push({ label: `Format: ${model.formatEntries.length} rows`, line, ch: 0 });
|
|
} else if (key === "Records") {
|
|
sections.push({ label: `Records: ${model.records.length} rows`, line, ch: 0 });
|
|
} else if (key === "Fields") {
|
|
sections.push({ label: `Fields: ${model.fields.length} rows`, line, ch: 0 });
|
|
} else {
|
|
sections.push({ label: key, line, ch: 0 });
|
|
}
|
|
}
|
|
return sections;
|
|
}
|
|
buildDataObjectSummaryTables(model, filePath) {
|
|
const formatRows = this.readTableRows(filePath, "Format");
|
|
const recordRows = this.readTableRows(filePath, "Records");
|
|
const fieldRows = this.readTableRows(filePath, "Fields");
|
|
const tables = [];
|
|
if (model.formatEntries.length > 0) {
|
|
tables.push({
|
|
title: "Format summary",
|
|
columns: ["key", "value", "notes"],
|
|
rows: formatRows.map((row) => ({
|
|
cells: [row.record.key ?? "", row.record.value ?? "", row.record.notes ?? ""],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
}
|
|
if (model.records.length > 0) {
|
|
tables.push({
|
|
title: "Records summary",
|
|
columns: ["record_type", "name", "occurrence", "notes"],
|
|
rows: recordRows.map((row) => ({
|
|
cells: [
|
|
row.record.record_type ?? "",
|
|
row.record.name ?? "",
|
|
row.record.occurrence ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
}
|
|
if (model.fieldMode === "file_layout") {
|
|
tables.push({
|
|
title: "Fields summary",
|
|
columns: ["record_type", "no", "name", "label", "type", "length", "position", "field_format", "ref", "notes"],
|
|
rows: fieldRows.map((row) => ({
|
|
cells: [
|
|
row.record.record_type ?? "",
|
|
row.record.no ?? "",
|
|
row.record.name ?? "",
|
|
row.record.label ?? "",
|
|
row.record.type ?? "",
|
|
row.record.length ?? "",
|
|
row.record.position ?? "",
|
|
row.record.field_format ?? "",
|
|
this.formatReferenceDisplay(row.record.ref),
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
} else {
|
|
tables.push({
|
|
title: "Fields summary",
|
|
columns: ["name", "label", "type", "length", "required", "ref", "notes"],
|
|
rows: fieldRows.map((row) => ({
|
|
cells: [
|
|
row.record.name ?? "",
|
|
row.record.label ?? "",
|
|
row.record.type ?? "",
|
|
row.record.length ?? "",
|
|
row.record.required ?? "",
|
|
this.formatReferenceDisplay(row.record.ref),
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
}
|
|
return tables;
|
|
}
|
|
getPathBasename(path2) {
|
|
const slashNormalized = path2.replace(/\\/g, "/");
|
|
const lastSegment = slashNormalized.split("/").pop() ?? slashNormalized;
|
|
return lastSegment.replace(/\.md$/i, "");
|
|
}
|
|
describeScreenSections(model, filePath) {
|
|
const lines = this.getFileLines(filePath);
|
|
const sections = [];
|
|
const orderedKeys = [
|
|
"Summary",
|
|
"Layout",
|
|
"Fields",
|
|
"Actions",
|
|
"Messages",
|
|
"Notes",
|
|
"Local Processes",
|
|
"Transitions"
|
|
];
|
|
for (const key of orderedKeys) {
|
|
if (!(key in model.sections)) {
|
|
continue;
|
|
}
|
|
const line = this.findHeadingLine(lines, key);
|
|
if (key === "Layout") {
|
|
sections.push({ label: `Layout: ${model.layouts.length} rows`, line, ch: 0 });
|
|
} else if (key === "Fields") {
|
|
sections.push({ label: `Fields: ${model.fields.length} rows`, line, ch: 0 });
|
|
} else if (key === "Actions") {
|
|
sections.push({ label: `Actions: ${model.actions.length} rows`, line, ch: 0 });
|
|
} else if (key === "Messages") {
|
|
sections.push({ label: `Messages: ${model.messages.length} rows`, line, ch: 0 });
|
|
} else if (key === "Local Processes") {
|
|
sections.push({
|
|
label: model.localProcesses.length > 0 ? `Local Processes: ${model.localProcesses.length} headings` : "Local Processes",
|
|
line,
|
|
ch: 0
|
|
});
|
|
} else if (key === "Transitions") {
|
|
sections.push({
|
|
label: model.legacyTransitions.length > 0 ? `Transitions (legacy): ${model.legacyTransitions.length} rows` : "Transitions (legacy)",
|
|
line,
|
|
ch: 0
|
|
});
|
|
} else {
|
|
sections.push({ label: key, line, ch: 0 });
|
|
}
|
|
}
|
|
return sections;
|
|
}
|
|
describeAppProcessSections(model, filePath) {
|
|
const lines = this.getFileLines(filePath);
|
|
const sections = [];
|
|
const orderedKeys = [
|
|
"Summary",
|
|
"Triggers",
|
|
"Inputs",
|
|
"Steps",
|
|
"Flows",
|
|
"Outputs",
|
|
"Transitions",
|
|
"Errors",
|
|
"Notes"
|
|
];
|
|
for (const key of orderedKeys) {
|
|
if (!(key in model.sections)) {
|
|
continue;
|
|
}
|
|
const line = this.findHeadingLine(lines, key);
|
|
if (key === "Inputs") {
|
|
sections.push({ label: `Inputs: ${model.inputs.length} rows`, line, ch: 0 });
|
|
} else if (key === "Outputs") {
|
|
sections.push({ label: `Outputs: ${model.outputs.length} rows`, line, ch: 0 });
|
|
} else if (key === "Triggers") {
|
|
sections.push({ label: `Triggers: ${model.triggers.length} rows`, line, ch: 0 });
|
|
} else if (key === "Transitions") {
|
|
sections.push({ label: `Transitions: ${model.transitions.length} rows`, line, ch: 0 });
|
|
} else if (key === "Steps") {
|
|
sections.push({
|
|
label: (model.steps?.length ?? 0) > 0 ? `Steps: ${model.steps?.length ?? 0} rows` : "Steps: prose",
|
|
line,
|
|
ch: 0
|
|
});
|
|
} else if (key === "Flows") {
|
|
sections.push({ label: `Flows: ${model.flows?.length ?? 0} rows`, line, ch: 0 });
|
|
} else {
|
|
sections.push({ label: key, line, ch: 0 });
|
|
}
|
|
}
|
|
return sections;
|
|
}
|
|
buildAppProcessTextSections(model) {
|
|
const sections = [];
|
|
if ((model.steps?.length ?? 0) === 0 && !this.sectionContainsMarkdownTable(model.sections.Steps)) {
|
|
const stepsLines = this.getReadableSectionLines(model.sections.Steps);
|
|
if (stepsLines.length > 0) {
|
|
sections.push({ title: "Steps", lines: stepsLines });
|
|
}
|
|
}
|
|
return sections;
|
|
}
|
|
getReadableSectionLines(lines) {
|
|
return (lines ?? []).map((line) => line.replace(/\s+$/u, ""));
|
|
}
|
|
sectionContainsMarkdownTable(lines) {
|
|
const tableLines = (lines ?? []).map((line) => line.trim()).filter((line) => line.startsWith("|"));
|
|
if (tableLines.length < 2) {
|
|
return false;
|
|
}
|
|
return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(tableLines[1]);
|
|
}
|
|
describeCodeSetSections(model, filePath) {
|
|
const lines = this.getFileLines(filePath);
|
|
const orderedKeys = ["Summary", "Values", "Notes"];
|
|
const sections = [];
|
|
for (const key of orderedKeys) {
|
|
if (!(key in model.sections)) {
|
|
continue;
|
|
}
|
|
const line = this.findHeadingLine(lines, key);
|
|
if (key === "Values") {
|
|
sections.push({ label: `Values: ${model.values.length} rows`, line, ch: 0 });
|
|
} else {
|
|
sections.push({ label: key, line, ch: 0 });
|
|
}
|
|
}
|
|
return sections;
|
|
}
|
|
describeMessageSections(model, filePath) {
|
|
const lines = this.getFileLines(filePath);
|
|
const orderedKeys = ["Summary", "Messages", "Notes"];
|
|
const sections = [];
|
|
for (const key of orderedKeys) {
|
|
if (!(key in model.sections)) {
|
|
continue;
|
|
}
|
|
const line = this.findHeadingLine(lines, key);
|
|
if (key === "Messages") {
|
|
sections.push({ label: `Messages: ${model.messages.length} rows`, line, ch: 0 });
|
|
} else {
|
|
sections.push({ label: key, line, ch: 0 });
|
|
}
|
|
}
|
|
return sections;
|
|
}
|
|
describeRuleSections(model, filePath) {
|
|
const lines = this.getFileLines(filePath);
|
|
const orderedKeys = ["Summary", "Inputs", "References", "Conditions", "Messages", "Notes"];
|
|
const sections = [];
|
|
for (const key of orderedKeys) {
|
|
if (!(key in model.sections)) {
|
|
continue;
|
|
}
|
|
const line = this.findHeadingLine(lines, key);
|
|
if (key === "Inputs") {
|
|
sections.push({ label: `Inputs: ${model.inputs.length} rows`, line, ch: 0 });
|
|
} else if (key === "References") {
|
|
sections.push({ label: `References: ${model.references.length} rows`, line, ch: 0 });
|
|
} else if (key === "Messages") {
|
|
sections.push({ label: `Messages: ${model.messages.length} rows`, line, ch: 0 });
|
|
} else {
|
|
sections.push({ label: key, line, ch: 0 });
|
|
}
|
|
}
|
|
return sections;
|
|
}
|
|
describeMappingSections(model, filePath) {
|
|
const lines = this.getFileLines(filePath);
|
|
const orderedKeys = ["Summary", "Scope", "Mappings", "Rules", "Notes"];
|
|
const sections = [];
|
|
for (const key of orderedKeys) {
|
|
if (!(key in model.sections)) {
|
|
continue;
|
|
}
|
|
const line = this.findHeadingLine(lines, key);
|
|
if (key === "Scope") {
|
|
sections.push({ label: `Scope: ${model.scope.length} rows`, line, ch: 0 });
|
|
} else if (key === "Mappings") {
|
|
sections.push({ label: `Mappings: ${model.mappings.length} rows`, line, ch: 0 });
|
|
} else {
|
|
sections.push({ label: key, line, ch: 0 });
|
|
}
|
|
}
|
|
return sections;
|
|
}
|
|
buildScreenSummaryTables(model, filePath) {
|
|
const layoutRows = this.readTableRows(filePath, "Layout");
|
|
const fieldsRows = this.readTableRows(filePath, "Fields");
|
|
const actionsRows = this.readTableRows(filePath, "Actions");
|
|
const messagesRows = this.readTableRows(filePath, "Messages");
|
|
const tables = [
|
|
{
|
|
title: "Structure / Layout",
|
|
columns: ["id", "label", "kind", "purpose", "notes"],
|
|
rows: layoutRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
row.record.label ?? "",
|
|
row.record.kind ?? "",
|
|
row.record.purpose ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
},
|
|
{
|
|
title: "UI Elements / Fields",
|
|
columns: ["id", "label", "kind", "layout", "ref", "notes"],
|
|
rows: fieldsRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
row.record.label ?? "",
|
|
row.record.kind ?? "",
|
|
row.record.layout ?? "",
|
|
this.formatReferenceDisplay(row.record.ref),
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
},
|
|
{
|
|
title: "Behavior / Actions",
|
|
columns: [
|
|
"id",
|
|
"label",
|
|
"target",
|
|
"event",
|
|
"invoke",
|
|
"transition",
|
|
"transition_status",
|
|
"notes"
|
|
],
|
|
rows: actionsRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
row.record.label ?? "",
|
|
row.record.target ?? "",
|
|
row.record.event ?? "",
|
|
this.formatReferenceDisplay(row.record.invoke),
|
|
this.formatReferenceDisplay(row.record.transition),
|
|
this.getScreenTransitionStatus(row.record.transition),
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
}
|
|
];
|
|
if (model.messages.length > 0) {
|
|
tables.push({
|
|
title: "Messages",
|
|
columns: ["id", "text", "severity", "timing", "notes"],
|
|
rows: messagesRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
this.formatReferenceDisplay(row.record.text),
|
|
row.record.severity ?? "",
|
|
row.record.timing ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
}
|
|
return tables;
|
|
}
|
|
collectScreenInvokedProcesses(model) {
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const items = [];
|
|
for (const action of model.actions) {
|
|
const invoke = action.invoke?.trim();
|
|
if (!invoke) {
|
|
continue;
|
|
}
|
|
const display = this.formatReferenceDisplay(invoke);
|
|
const key = `${action.label?.trim() ?? ""}|${display}`;
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
items.push({
|
|
label: `${action.label?.trim() || "(action)"} -> ${display}`,
|
|
line: action.rowLine,
|
|
ch: 0
|
|
});
|
|
}
|
|
return items;
|
|
}
|
|
collectScreenOutgoingScreens(model) {
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const items = [];
|
|
for (const action of model.actions) {
|
|
const transition = action.transition?.trim();
|
|
if (!transition) {
|
|
continue;
|
|
}
|
|
const display = this.formatReferenceDisplay(transition);
|
|
const key = `${action.label?.trim() ?? ""}|${display}`;
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
const status = this.getScreenTransitionStatus(transition);
|
|
items.push({
|
|
label: `${action.label?.trim() || "(action)"} -> ${display} [${status}]`,
|
|
line: action.rowLine,
|
|
ch: 0
|
|
});
|
|
}
|
|
return items;
|
|
}
|
|
getScreenTransitionStatus(transition) {
|
|
const target = transition?.trim();
|
|
if (!target) {
|
|
return "";
|
|
}
|
|
const resolved = this.index ? resolveReferenceIdentity(target, this.index) : null;
|
|
if (!resolved?.resolvedModel) {
|
|
return "unresolved";
|
|
}
|
|
return resolved.resolvedModel.fileType === "screen" ? "resolved" : "unresolved";
|
|
}
|
|
resolveFileRenderMode(filePath, fileType, frontmatter, modelKind = null) {
|
|
return resolveRenderMode({
|
|
filePath,
|
|
formatType: fileType,
|
|
modelKind: modelKind ?? (typeof frontmatter.kind === "string" ? frontmatter.kind : null),
|
|
toolbarOverride: this.rendererOverrideFilePath === filePath ? this.rendererOverridesByFilePath.get(filePath) ?? null : null,
|
|
frontmatterRenderMode: frontmatter.render_mode,
|
|
settingsDefaultRenderMode: this.getDefaultRenderModeForFormat(
|
|
fileType,
|
|
modelKind ?? (typeof frontmatter.kind === "string" ? frontmatter.kind : null)
|
|
)
|
|
});
|
|
}
|
|
getDefaultRenderModeForFormat(fileType, modelKind) {
|
|
if (fileType === "diagram") {
|
|
if (modelKind === "class") {
|
|
return this.settings.defaultClassRenderMode;
|
|
}
|
|
if (modelKind === "er") {
|
|
return this.settings.defaultErRenderMode;
|
|
}
|
|
return "custom";
|
|
}
|
|
switch (fileType) {
|
|
case "object":
|
|
return this.settings.defaultClassRenderMode;
|
|
case "er-entity":
|
|
return this.settings.defaultErRenderMode;
|
|
case "dfd-diagram":
|
|
return this.settings.defaultDfdRenderMode;
|
|
case "dfd-object":
|
|
return "custom";
|
|
case "app-process":
|
|
return this.settings.defaultProcessRenderMode;
|
|
case "screen":
|
|
return this.settings.defaultScreenRenderMode;
|
|
default:
|
|
return "custom";
|
|
}
|
|
}
|
|
buildRendererSelectionState(filePath, resolved, fileType, modelKind) {
|
|
const supportedModes = getSupportedRenderModes(fileType, modelKind);
|
|
const visibleSelectedMode = supportedModes.includes(resolved.selectedMode) ? resolved.selectedMode : supportedModes[0] ?? "custom";
|
|
return {
|
|
selectedMode: resolved.selectedMode,
|
|
visibleSelectedMode,
|
|
supportedModes,
|
|
effectiveMode: resolved.effectiveMode,
|
|
actualRenderer: resolved.actualRenderer,
|
|
source: resolved.source,
|
|
fallbackReason: resolved.fallbackReason,
|
|
onSelectMode: (mode) => {
|
|
this.rendererOverridesByFilePath.clear();
|
|
this.rendererOverridesByFilePath.set(filePath, mode);
|
|
this.rendererOverrideFilePath = filePath;
|
|
void this.syncPreviewToActiveFile(false, "rerender");
|
|
}
|
|
};
|
|
}
|
|
buildScreenPreviewTransitions(model) {
|
|
const groups = /* @__PURE__ */ new Map();
|
|
for (const action of model.actions) {
|
|
const transition = action.transition?.trim();
|
|
if (!transition) {
|
|
continue;
|
|
}
|
|
const labelInfo = this.buildScreenActionPreviewLabel(action);
|
|
const resolved = this.index ? resolveReferenceIdentity(transition, this.index) : { resolvedModel: null };
|
|
const resolvedModel = resolved.resolvedModel?.fileType === "screen" ? resolved.resolvedModel : null;
|
|
const targetPath = resolvedModel?.path;
|
|
const targetLabel = resolvedModel?.name?.trim() || resolvedModel?.id?.trim() || this.formatReferenceDisplay(transition) || transition;
|
|
const targetTitle = targetPath ? `${targetLabel}
|
|
${targetPath}` : `${targetLabel}
|
|
${transition}`;
|
|
const key = targetPath ? `path:${targetPath}` : `raw:${transition}`;
|
|
const group = groups.get(key) ?? {
|
|
key,
|
|
targetLabel,
|
|
targetTitle,
|
|
targetPath,
|
|
unresolved: !targetPath,
|
|
selfTarget: targetPath === model.path,
|
|
actions: []
|
|
};
|
|
group.actions.push({
|
|
label: labelInfo.shortLabel,
|
|
fullLabel: labelInfo.fullLabel,
|
|
title: [
|
|
labelInfo.fullLabel,
|
|
action.id?.trim() ? `id: ${action.id.trim()}` : "",
|
|
action.target?.trim() ? `target: ${action.target.trim()}` : "",
|
|
action.event?.trim() ? `event: ${action.event.trim()}` : "",
|
|
`transition: ${targetLabel}`
|
|
].filter(Boolean).join("\n"),
|
|
line: action.rowLine,
|
|
ch: 0
|
|
});
|
|
groups.set(key, group);
|
|
}
|
|
return [...groups.values()];
|
|
}
|
|
buildScreenActionPreviewLabel(action) {
|
|
const label = action.label?.trim();
|
|
if (label) {
|
|
return { shortLabel: label, fullLabel: label };
|
|
}
|
|
const id = action.id?.trim();
|
|
if (id) {
|
|
return { shortLabel: id, fullLabel: id };
|
|
}
|
|
const target = action.target?.trim();
|
|
const event = action.event?.trim();
|
|
if (target && event) {
|
|
const fullLabel = `${target}.${event}`;
|
|
return { shortLabel: fullLabel, fullLabel };
|
|
}
|
|
if (event) {
|
|
return { shortLabel: event, fullLabel: event };
|
|
}
|
|
return { shortLabel: "(action)", fullLabel: "(action)" };
|
|
}
|
|
buildScreenLayoutBlocks(model) {
|
|
const layoutMap = new Map(
|
|
model.layouts.map((layout) => [layout.id.trim(), layout]).filter(([layoutId]) => Boolean(layoutId))
|
|
);
|
|
const fieldsByLayout = /* @__PURE__ */ new Map();
|
|
const ungrouped = [];
|
|
for (const field of model.fields) {
|
|
const item = {
|
|
label: field.label?.trim() || field.id,
|
|
line: field.rowLine,
|
|
ch: 0
|
|
};
|
|
const layoutId = field.layout?.trim();
|
|
if (!layoutId || !layoutMap.has(layoutId)) {
|
|
ungrouped.push(item);
|
|
continue;
|
|
}
|
|
const group = fieldsByLayout.get(layoutId) ?? [];
|
|
group.push(item);
|
|
fieldsByLayout.set(layoutId, group);
|
|
}
|
|
const blocks = model.layouts.map((layout) => ({
|
|
label: layout.label?.trim() ? `${layout.label.trim()} [${layout.id}]` : `[${layout.id}]`,
|
|
subtitle: [layout.kind?.trim(), layout.purpose?.trim()].filter(Boolean).join(" / ") || void 0,
|
|
line: layout.rowLine,
|
|
ch: 0,
|
|
items: fieldsByLayout.get(layout.id.trim()) ?? []
|
|
}));
|
|
if (ungrouped.length > 0) {
|
|
blocks.push({
|
|
label: "\u672A\u5206\u985E [unassigned]",
|
|
subtitle: "layout \u672A\u6307\u5B9A\u307E\u305F\u306F\u672A\u5B9A\u7FA9",
|
|
line: void 0,
|
|
ch: 0,
|
|
items: ungrouped
|
|
});
|
|
}
|
|
return blocks;
|
|
}
|
|
buildAppProcessSummaryTables(model, filePath) {
|
|
const inputRows = this.readTableRows(filePath, "Inputs");
|
|
const outputRows = this.readTableRows(filePath, "Outputs");
|
|
const triggerRows = this.readTableRows(filePath, "Triggers");
|
|
const transitionRows = this.readTableRows(filePath, "Transitions");
|
|
const stepRows = this.readTableRows(filePath, "Steps");
|
|
const flowRows = this.readTableRows(filePath, "Flows");
|
|
const tables = [];
|
|
if (model.triggers.length > 0) {
|
|
tables.push({
|
|
title: "Triggers Summary",
|
|
columns: ["id", "kind", "source", "event", "notes"],
|
|
rows: triggerRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
row.record.kind ?? "",
|
|
this.formatReferenceDisplay(row.record.source),
|
|
row.record.event ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
}
|
|
if ((model.steps?.length ?? 0) > 0) {
|
|
tables.push({
|
|
title: "Steps Summary",
|
|
columns: ["id", "lane", "label", "kind", "input", "output", "rule", "invoke", "screen", "notes"],
|
|
rows: stepRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
row.record.lane ?? "",
|
|
row.record.label ?? "",
|
|
row.record.kind ?? "",
|
|
this.formatReferenceDisplay(row.record.input),
|
|
this.formatReferenceDisplay(row.record.output),
|
|
this.formatReferenceDisplay(row.record.rule),
|
|
this.formatReferenceDisplay(row.record.invoke),
|
|
this.formatReferenceDisplay(row.record.screen),
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
}
|
|
tables.push({
|
|
title: "Inputs Summary",
|
|
columns: ["id", "data", "source", "required", "notes"],
|
|
rows: inputRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
this.formatReferenceDisplay(row.record.data),
|
|
this.formatReferenceDisplay(row.record.source),
|
|
row.record.required ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
tables.push({
|
|
title: "Outputs Summary",
|
|
columns: ["id", "data", "target", "notes"],
|
|
rows: outputRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
this.formatReferenceDisplay(row.record.data),
|
|
this.formatReferenceDisplay(row.record.target),
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
if (model.transitions.length > 0) {
|
|
tables.push({
|
|
title: "Transitions Summary",
|
|
columns: ["id", "event", "to", "condition", "notes"],
|
|
rows: transitionRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
row.record.event ?? "",
|
|
this.formatReferenceDisplay(row.record.to),
|
|
row.record.condition ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
}
|
|
if (model.hasExplicitFlows) {
|
|
tables.push({
|
|
title: "Flows Summary",
|
|
columns: ["from", "to", "condition", "label", "notes"],
|
|
rows: flowRows.map((row) => ({
|
|
cells: [
|
|
row.record.from ?? "",
|
|
row.record.to ?? "",
|
|
row.record.condition ?? "",
|
|
row.record.label ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
}
|
|
return tables;
|
|
}
|
|
buildAppProcessBusinessFlowWarnings(model) {
|
|
const steps = model.steps ?? [];
|
|
if (steps.length === 0 || !model.hasExplicitFlows) {
|
|
return [];
|
|
}
|
|
const stepIds = new Set(steps.map((step) => step.id).filter(Boolean));
|
|
const warnings = [];
|
|
for (const flow of model.flows ?? []) {
|
|
if (!flow.from || !stepIds.has(flow.from)) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: flow.from ? modelWeaveText(
|
|
`app_process Flow.from references missing step "${flow.from}"`,
|
|
`app_process Flow.from \u304C\u5B58\u5728\u3057\u306A\u3044 step "${flow.from}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`
|
|
) : modelWeaveText(
|
|
"app_process Flow.from is missing a step id",
|
|
"app_process Flow.from \u306E step id \u304C\u3042\u308A\u307E\u305B\u3093\u3002"
|
|
),
|
|
severity: "warning",
|
|
path: model.path,
|
|
field: "Flows.from"
|
|
});
|
|
}
|
|
if (!flow.to || !stepIds.has(flow.to)) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: flow.to ? modelWeaveText(
|
|
`app_process Flow.to references missing step "${flow.to}"`,
|
|
`app_process Flow.to \u304C\u5B58\u5728\u3057\u306A\u3044 step "${flow.to}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`
|
|
) : modelWeaveText(
|
|
"app_process Flow.to is missing a step id",
|
|
"app_process Flow.to \u306E step id \u304C\u3042\u308A\u307E\u305B\u3093\u3002"
|
|
),
|
|
severity: "warning",
|
|
path: model.path,
|
|
field: "Flows.to"
|
|
});
|
|
}
|
|
}
|
|
return warnings;
|
|
}
|
|
buildCodeSetSummaryTables(filePath) {
|
|
const valueRows = this.readTableRows(filePath, "Values");
|
|
return [
|
|
{
|
|
title: "Values Summary",
|
|
columns: ["code", "label", "sort_order", "active", "notes"],
|
|
rows: valueRows.map((row) => ({
|
|
cells: [
|
|
row.record.code ?? "",
|
|
row.record.label ?? "",
|
|
row.record.sort_order ?? "",
|
|
row.record.active ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
}
|
|
];
|
|
}
|
|
buildMessageSummaryTables(filePath) {
|
|
const messageRows = this.readTableRows(filePath, "Messages");
|
|
return [
|
|
{
|
|
title: "Messages Summary",
|
|
columns: ["message_id", "text", "severity", "timing", "audience", "active", "notes"],
|
|
rows: messageRows.map((row) => ({
|
|
cells: [
|
|
row.record.message_id ?? "",
|
|
row.record.text ?? "",
|
|
row.record.severity ?? "",
|
|
row.record.timing ?? "",
|
|
row.record.audience ?? "",
|
|
row.record.active ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
}
|
|
];
|
|
}
|
|
buildRuleSummaryTables(model, filePath) {
|
|
const inputRows = this.readTableRows(filePath, "Inputs");
|
|
const referenceRows = this.readTableRows(filePath, "References");
|
|
const messageRows = this.readTableRows(filePath, "Messages");
|
|
const tables = [
|
|
{
|
|
title: "Inputs Summary",
|
|
columns: ["id", "data", "source", "required", "notes"],
|
|
rows: inputRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
this.formatReferenceDisplay(row.record.data),
|
|
this.formatReferenceDisplay(row.record.source),
|
|
row.record.required ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
},
|
|
{
|
|
title: "References Summary",
|
|
columns: ["ref", "usage", "notes"],
|
|
rows: referenceRows.map((row) => ({
|
|
cells: [
|
|
this.formatReferenceDisplay(row.record.ref),
|
|
row.record.usage ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
}
|
|
];
|
|
if (model.messages.length > 0) {
|
|
tables.push({
|
|
title: "Messages Summary",
|
|
columns: ["severity", "message", "condition", "notes"],
|
|
rows: messageRows.map((row) => ({
|
|
cells: [
|
|
row.record.severity ?? "",
|
|
this.formatReferenceDisplay(row.record.message),
|
|
row.record.condition ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
}
|
|
return tables;
|
|
}
|
|
buildMappingSummaryTables(filePath) {
|
|
const scopeRows = this.readTableRows(filePath, "Scope");
|
|
const mappingRows = this.readTableRows(filePath, "Mappings");
|
|
return [
|
|
{
|
|
title: "Scope Summary",
|
|
columns: ["role", "ref", "notes"],
|
|
rows: scopeRows.map((row) => ({
|
|
cells: [
|
|
row.record.role ?? "",
|
|
this.formatReferenceDisplay(row.record.ref),
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
},
|
|
{
|
|
title: "Mappings Summary",
|
|
columns: ["target_ref", "source_ref", "transform", "rule", "required", "notes"],
|
|
rows: mappingRows.map((row) => ({
|
|
cells: [
|
|
this.formatReferenceDisplay(row.record.target_ref),
|
|
this.formatReferenceDisplay(row.record.source_ref),
|
|
row.record.transform ?? "",
|
|
this.formatReferenceDisplay(row.record.rule),
|
|
row.record.required ?? "",
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
}
|
|
];
|
|
}
|
|
collectScreenLocalProcesses(filePath) {
|
|
const lines = this.getFileLines(filePath);
|
|
const sectionLine = this.findHeadingLine(lines, "Local Processes");
|
|
if (sectionLine === void 0) {
|
|
return [];
|
|
}
|
|
const results = [];
|
|
for (let index = sectionLine + 1; index < lines.length; index += 1) {
|
|
const line = lines[index] ?? "";
|
|
const trimmed = line.trim();
|
|
if (/^##\s+/.test(trimmed)) {
|
|
break;
|
|
}
|
|
const match = trimmed.match(/^###\s+(.+)$/);
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
results.push({
|
|
label: match[1].trim(),
|
|
line: index,
|
|
ch: Math.max(0, line.indexOf("###"))
|
|
});
|
|
}
|
|
return results;
|
|
}
|
|
formatReferenceDisplay(value) {
|
|
const trimmed = value?.trim();
|
|
if (!trimmed) {
|
|
return "";
|
|
}
|
|
const qualified = parseQualifiedRef(trimmed);
|
|
if (qualified?.hasMemberRef) {
|
|
const baseLabel = this.formatBaseReferenceDisplay(qualified.baseRefRaw);
|
|
return qualified.memberRef ? `${baseLabel}.${qualified.memberRef}` : baseLabel;
|
|
}
|
|
return this.formatBaseReferenceDisplay(trimmed);
|
|
}
|
|
formatBaseReferenceDisplay(value) {
|
|
const parsed = parseReferenceValue(value);
|
|
if (!parsed) {
|
|
return value;
|
|
}
|
|
if (parsed.display?.trim()) {
|
|
return parsed.display.trim();
|
|
}
|
|
if (parsed.target?.trim()) {
|
|
return this.getPathBasename(parsed.target.trim());
|
|
}
|
|
return parsed.raw || value;
|
|
}
|
|
getFileLines(filePath) {
|
|
const content = this.index?.sourceFilesByPath[filePath]?.content ?? "";
|
|
return content.split(/\r?\n/);
|
|
}
|
|
findHeadingLine(lines, sectionName) {
|
|
const heading = `## ${sectionName}`;
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|
if ((lines[index] ?? "").trim() === heading) {
|
|
return index;
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
readTableRows(filePath, sectionName, filterColumns) {
|
|
const lines = this.getFileLines(filePath);
|
|
const sectionLine = this.findHeadingLine(lines, sectionName);
|
|
if (sectionLine === void 0) {
|
|
return [];
|
|
}
|
|
let header = null;
|
|
const rows = [];
|
|
for (let index = sectionLine + 1; index < lines.length; index += 1) {
|
|
const line = lines[index] ?? "";
|
|
const trimmed = line.trim();
|
|
if (/^##\s+/.test(trimmed)) {
|
|
break;
|
|
}
|
|
if (!trimmed.startsWith("|")) {
|
|
continue;
|
|
}
|
|
if (this.isMarkdownSeparatorLine(line)) {
|
|
continue;
|
|
}
|
|
const values = splitMarkdownTableRow(line);
|
|
if (!values) {
|
|
continue;
|
|
}
|
|
if (!header) {
|
|
header = values;
|
|
continue;
|
|
}
|
|
const record = {};
|
|
for (let columnIndex = 0; columnIndex < header.length; columnIndex += 1) {
|
|
record[header[columnIndex]] = values[columnIndex] ?? "";
|
|
}
|
|
if (Object.values(record).every((value) => !value.trim())) {
|
|
continue;
|
|
}
|
|
if (filterColumns) {
|
|
const filtered = {};
|
|
for (const key of filterColumns) {
|
|
filtered[key] = record[key] ?? "";
|
|
}
|
|
rows.push({
|
|
record: filtered,
|
|
line: index,
|
|
ch: getMarkdownTableCellRanges(line)?.[0]?.contentStart ?? 0
|
|
});
|
|
} else {
|
|
rows.push({
|
|
record,
|
|
line: index,
|
|
ch: getMarkdownTableCellRanges(line)?.[0]?.contentStart ?? 0
|
|
});
|
|
}
|
|
}
|
|
return rows;
|
|
}
|
|
isMarkdownSeparatorLine(line) {
|
|
const cells = splitMarkdownTableRow(line);
|
|
return Boolean(cells && cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell)));
|
|
}
|
|
async openObjectNote(objectId, sourcePath, navigation) {
|
|
if (!this.index) {
|
|
await this.rebuildIndex();
|
|
}
|
|
if (!this.index) {
|
|
new import_obsidian8.Notice(modelWeaveText(
|
|
"Model index is not available",
|
|
"Model index \u304C\u5229\u7528\u3067\u304D\u307E\u305B\u3093\u3002\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u3092\u518D\u69CB\u7BC9\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
));
|
|
return;
|
|
}
|
|
const result = await openModelObjectNote(this.app, this.index, objectId, {
|
|
sourcePath,
|
|
openInNewLeaf: navigation?.openInNewLeaf ?? false
|
|
});
|
|
if (!result.ok) {
|
|
new import_obsidian8.Notice(result.reason ?? `Could not open object "${objectId}"`);
|
|
return;
|
|
}
|
|
await this.syncPreviewToActiveFile(false, "viewer-node-navigation");
|
|
}
|
|
async openDiagnosticLocation(filePath, diagnostic) {
|
|
const targetPath = diagnostic.filePath ?? diagnostic.path ?? filePath;
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(targetPath);
|
|
if (!(abstractFile instanceof import_obsidian8.TFile)) {
|
|
return;
|
|
}
|
|
const activeMarkdownView = this.app.workspace.getActiveViewOfType(import_obsidian8.MarkdownView);
|
|
let targetLeaf = activeMarkdownView?.file?.path === targetPath ? activeMarkdownView.leaf : this.findMarkdownLeafForPath(targetPath);
|
|
if (!targetLeaf) {
|
|
targetLeaf = this.app.workspace.getMostRecentLeaf();
|
|
if (targetLeaf && this.isPreviewLeaf(targetLeaf)) {
|
|
targetLeaf = this.app.workspace.getLeaf(true);
|
|
}
|
|
}
|
|
if (!targetLeaf) {
|
|
return;
|
|
}
|
|
if (targetLeaf.view.file?.path !== targetPath) {
|
|
await targetLeaf.openFile(abstractFile);
|
|
}
|
|
this.app.workspace.setActiveLeaf(targetLeaf, { focus: true });
|
|
const markdownView = targetLeaf.view instanceof import_obsidian8.MarkdownView ? targetLeaf.view : this.app.workspace.getActiveViewOfType(import_obsidian8.MarkdownView);
|
|
const editor = markdownView?.editor;
|
|
if (!editor) {
|
|
return;
|
|
}
|
|
const content = editor.getValue();
|
|
const targetLine = resolveDiagnosticLine(content, diagnostic);
|
|
await this.openFileLocation(targetPath, targetLine, 0, targetLeaf);
|
|
}
|
|
async openFileLocation(filePath, line, ch = 0, preferredLeaf) {
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(filePath);
|
|
if (!(abstractFile instanceof import_obsidian8.TFile)) {
|
|
return;
|
|
}
|
|
const activeMarkdownView = this.app.workspace.getActiveViewOfType(import_obsidian8.MarkdownView);
|
|
let targetLeaf = preferredLeaf ?? (activeMarkdownView?.file?.path === filePath ? activeMarkdownView.leaf : this.findMarkdownLeafForPath(filePath));
|
|
if (!targetLeaf) {
|
|
targetLeaf = this.app.workspace.getMostRecentLeaf();
|
|
if (targetLeaf && this.isPreviewLeaf(targetLeaf)) {
|
|
targetLeaf = this.app.workspace.getLeaf(true);
|
|
}
|
|
}
|
|
if (!targetLeaf) {
|
|
return;
|
|
}
|
|
if (targetLeaf.view.file?.path !== filePath) {
|
|
await targetLeaf.openFile(abstractFile);
|
|
}
|
|
this.app.workspace.setActiveLeaf(targetLeaf, { focus: true });
|
|
const markdownView = targetLeaf.view instanceof import_obsidian8.MarkdownView ? targetLeaf.view : this.app.workspace.getActiveViewOfType(import_obsidian8.MarkdownView);
|
|
const editor = markdownView?.editor;
|
|
if (!editor) {
|
|
return;
|
|
}
|
|
editor.setCursor({ line, ch });
|
|
editor.scrollIntoView(
|
|
{
|
|
from: { line, ch },
|
|
to: { line, ch }
|
|
},
|
|
true
|
|
);
|
|
editor.focus?.();
|
|
editor.cm?.focus?.();
|
|
}
|
|
async openReferencedFile(filePath, openInNewLeaf = false) {
|
|
const preferredLeaf = openInNewLeaf ? this.app.workspace.getLeaf(true) : void 0;
|
|
await this.openFileLocation(filePath, 0, 0, preferredLeaf);
|
|
}
|
|
findMarkdownLeafForPath(filePath) {
|
|
for (const leaf of this.app.workspace.getLeavesOfType("markdown")) {
|
|
const viewFile = leaf.view.file ?? null;
|
|
if (viewFile?.path === filePath) {
|
|
return leaf;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
async ensurePreviewLeaf(preferredLeaf, activate = true) {
|
|
const leaf = preferredLeaf ?? await this.findOrCreatePreviewLeaf();
|
|
await leaf.setViewState({
|
|
type: MODELING_PREVIEW_VIEW_TYPE,
|
|
active: activate
|
|
});
|
|
this.previewLeaf = leaf;
|
|
return leaf;
|
|
}
|
|
async findOrCreatePreviewLeaf() {
|
|
const existing = this.getManagedPreviewLeaf();
|
|
if (existing) {
|
|
await this.closeDuplicatePreviewLeaves(existing);
|
|
return existing;
|
|
}
|
|
const leaf = this.app.workspace.getRightLeaf(false) ?? this.app.workspace.getLeaf(true);
|
|
this.previewLeaf = leaf;
|
|
return leaf;
|
|
}
|
|
getManagedPreviewLeaf() {
|
|
if (this.previewLeaf && this.isPreviewLeaf(this.previewLeaf)) {
|
|
return this.previewLeaf;
|
|
}
|
|
const leaves = this.getAllPreviewLeaves();
|
|
if (leaves.length === 0) {
|
|
this.previewLeaf = null;
|
|
return null;
|
|
}
|
|
this.previewLeaf = leaves[0];
|
|
return this.previewLeaf;
|
|
}
|
|
async findExportableModelWeaveView() {
|
|
const candidateLeaves = [];
|
|
const mostRecentLeaf = this.app.workspace.getMostRecentLeaf();
|
|
if (mostRecentLeaf) {
|
|
candidateLeaves.push(mostRecentLeaf);
|
|
}
|
|
if (this.previewLeaf) {
|
|
candidateLeaves.push(this.previewLeaf);
|
|
}
|
|
candidateLeaves.push(...this.getAllPreviewLeaves());
|
|
const orderedLeaves = Array.from(new Set(candidateLeaves));
|
|
const loadedViews = [];
|
|
for (const leaf of orderedLeaves) {
|
|
if (!this.isPreviewLeaf(leaf)) {
|
|
continue;
|
|
}
|
|
await leaf.loadIfDeferred();
|
|
const view = leaf.view;
|
|
if (view instanceof ModelingPreviewView) {
|
|
loadedViews.push(view);
|
|
if (this.isExportablePreviewView(view)) {
|
|
this.previewLeaf = leaf;
|
|
return view;
|
|
}
|
|
}
|
|
}
|
|
if (loadedViews.length > 0) {
|
|
return loadedViews[0];
|
|
}
|
|
return null;
|
|
}
|
|
isExportablePreviewView(view) {
|
|
const container = view.contentEl;
|
|
if (!container?.isConnected) {
|
|
return false;
|
|
}
|
|
if (container.getClientRects().length > 0) {
|
|
return true;
|
|
}
|
|
return container.clientWidth > 0 || container.clientHeight > 0;
|
|
}
|
|
getAllPreviewLeaves() {
|
|
const leaves = [
|
|
...this.app.workspace.getLeavesOfType(MODELING_PREVIEW_VIEW_TYPE),
|
|
...LEGACY_PREVIEW_VIEW_TYPES.flatMap(
|
|
(viewType) => this.app.workspace.getLeavesOfType(viewType)
|
|
)
|
|
];
|
|
return Array.from(new Set(leaves));
|
|
}
|
|
async closeDuplicatePreviewLeaves(keepLeaf) {
|
|
const duplicates = this.getAllPreviewLeaves().filter((leaf) => leaf !== keepLeaf);
|
|
for (const leaf of duplicates) {
|
|
await leaf.loadIfDeferred();
|
|
leaf.detach();
|
|
}
|
|
}
|
|
isPreviewLeaf(leaf) {
|
|
const viewType = leaf.view.getViewType();
|
|
return viewType === MODELING_PREVIEW_VIEW_TYPE || LEGACY_PREVIEW_VIEW_TYPES.includes(
|
|
viewType
|
|
);
|
|
}
|
|
async normalizePreviewLeaves() {
|
|
const leaves = this.getAllPreviewLeaves();
|
|
if (leaves.length === 0) {
|
|
return;
|
|
}
|
|
const keepLeaf = leaves[0];
|
|
await keepLeaf.loadIfDeferred();
|
|
await keepLeaf.setViewState({
|
|
type: MODELING_PREVIEW_VIEW_TYPE,
|
|
active: false
|
|
});
|
|
this.previewLeaf = keepLeaf;
|
|
await this.closeDuplicatePreviewLeaves(keepLeaf);
|
|
}
|
|
};
|
|
function resolveDiagnosticLine(content, diagnostic) {
|
|
if (typeof diagnostic.line === "number" && diagnostic.line >= 0) {
|
|
return diagnostic.line;
|
|
}
|
|
if (typeof diagnostic.fromLine === "number" && diagnostic.fromLine >= 0) {
|
|
return diagnostic.fromLine;
|
|
}
|
|
if (typeof diagnostic.toLine === "number" && diagnostic.toLine >= 0) {
|
|
return diagnostic.toLine;
|
|
}
|
|
const lines = content.split(/\r?\n/);
|
|
const frontmatterField = typeof diagnostic.field === "string" ? diagnostic.field : "";
|
|
const section = resolveDiagnosticSection(diagnostic);
|
|
if (frontmatterField && isFrontmatterField(frontmatterField)) {
|
|
const frontmatterLine = findFrontmatterFieldLine(lines, frontmatterField);
|
|
if (frontmatterLine >= 0) {
|
|
return frontmatterLine;
|
|
}
|
|
}
|
|
const relatedId = typeof diagnostic.context?.relatedId === "string" ? diagnostic.context.relatedId : null;
|
|
if (section === "Relations" && relatedId) {
|
|
const relationBlockLine = findLineIndex(lines, (line) => line.trim() === `### ${relatedId}`);
|
|
if (relationBlockLine >= 0) {
|
|
return relationBlockLine;
|
|
}
|
|
const relationRowLine = findLineIndex(lines, (line) => line.includes(`| ${relatedId} |`));
|
|
if (relationRowLine >= 0) {
|
|
return relationRowLine;
|
|
}
|
|
}
|
|
if (section) {
|
|
const sectionLine = findLineIndex(lines, (line) => line.trim() === `## ${section}`);
|
|
if (sectionLine >= 0) {
|
|
return sectionLine;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
function resolveDiagnosticSection(diagnostic) {
|
|
if (typeof diagnostic.section === "string" && diagnostic.section.trim()) {
|
|
return diagnostic.section.trim();
|
|
}
|
|
const contextSection = typeof diagnostic.context?.section === "string" ? diagnostic.context.section : null;
|
|
if (contextSection) {
|
|
return contextSection;
|
|
}
|
|
const field = typeof diagnostic.field === "string" ? diagnostic.field : "";
|
|
if (field.startsWith("Relations:")) {
|
|
return "Relations";
|
|
}
|
|
const fieldToSection = {
|
|
objectRefs: "Objects",
|
|
relations: "Relations",
|
|
relatedObjects: "Relations",
|
|
Attributes: "Attributes",
|
|
Methods: "Methods",
|
|
Relations: "Relations",
|
|
Objects: "Objects",
|
|
Columns: "Columns",
|
|
Indexes: "Indexes",
|
|
Notes: "Notes",
|
|
Summary: "Summary",
|
|
Overview: "Overview"
|
|
};
|
|
return fieldToSection[field] ?? null;
|
|
}
|
|
function isFrontmatterField(field) {
|
|
return [
|
|
"type",
|
|
"id",
|
|
"name",
|
|
"kind",
|
|
"logical_name",
|
|
"physical_name",
|
|
"schema_name",
|
|
"dbms",
|
|
"package",
|
|
"stereotype"
|
|
].includes(field);
|
|
}
|
|
function findFrontmatterFieldLine(lines, field) {
|
|
if ((lines[0] ?? "").trim() !== "---") {
|
|
return -1;
|
|
}
|
|
for (let index = 1; index < lines.length; index += 1) {
|
|
const trimmed = lines[index].trim();
|
|
if (trimmed === "---") {
|
|
break;
|
|
}
|
|
if (trimmed.startsWith(`${field}:`)) {
|
|
return index;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
function findLineIndex(lines, predicate) {
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|
if (predicate(lines[index] ?? "")) {
|
|
return index;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
var ModelWeaveSettingTab = class extends import_obsidian8.PluginSettingTab {
|
|
constructor(app, plugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
display() {
|
|
const { containerEl } = this;
|
|
const settings = this.plugin.getSettings();
|
|
containerEl.empty();
|
|
new import_obsidian8.Setting(containerEl).setName("Viewer").setHeading();
|
|
new import_obsidian8.Setting(containerEl).setName("Default Class render mode").setDesc(
|
|
"Used for class and class_diagram files when frontmatter.render_mode is not set."
|
|
).addDropdown((dropdown) => {
|
|
dropdown.addOption("custom", "Custom").addOption("mermaid", "Mermaid").addOption("mermaid-detail", "Mermaid Detail").setValue(settings.defaultClassRenderMode).onChange(async (value) => {
|
|
if (!isClassRenderModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultClassRenderMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("Default ER render mode").setDesc(
|
|
"Used for er_entity and er_diagram files when frontmatter.render_mode is not set."
|
|
).addDropdown((dropdown) => {
|
|
dropdown.addOption("custom", "Custom").addOption("mermaid", "Mermaid").addOption("mermaid-detail", "Mermaid Detail").setValue(settings.defaultErRenderMode).onChange(async (value) => {
|
|
if (!isErRenderModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultErRenderMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("Default DFD render mode").setDesc(
|
|
"Used for dfd_diagram files when frontmatter.render_mode is not set."
|
|
).addDropdown((dropdown) => {
|
|
dropdown.addOption("mermaid", "Mermaid").setValue(settings.defaultDfdRenderMode).onChange(async (value) => {
|
|
if (!isDfdRenderModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultDfdRenderMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("Default Process render mode").setDesc(
|
|
"Used for app_process files when frontmatter.render_mode is not set."
|
|
).addDropdown((dropdown) => {
|
|
dropdown.addOption("custom", "Custom").setValue(settings.defaultProcessRenderMode).onChange(async (value) => {
|
|
if (!isProcessRenderModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultProcessRenderMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("Default Screen render mode").setDesc(
|
|
"Used for screen files when frontmatter.render_mode is not set."
|
|
).addDropdown((dropdown) => {
|
|
dropdown.addOption("custom", "Custom").setValue(settings.defaultScreenRenderMode).onChange(async (value) => {
|
|
if (!isScreenRenderModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultScreenRenderMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("Default zoom").setDesc(
|
|
"Initial diagram zoom when no saved viewport state exists. Fit uses fit-to-view; 100% opens at actual scale."
|
|
).addDropdown((dropdown) => {
|
|
dropdown.addOption("fit", "Fit").addOption("100", "100%").setValue(settings.defaultZoom).onChange(async (value) => {
|
|
if (!isDefaultZoomOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultZoom: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("Font size").setDesc("Adjusts the base preview text size across viewers.").addDropdown((dropdown) => {
|
|
dropdown.addOption("small", "Small").addOption("normal", "Normal").addOption("large", "Large").setValue(settings.fontSize).onChange(async (value) => {
|
|
if (!isFontSizeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
fontSize: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("Node density").setDesc(
|
|
"Controls diagram compactness where supported. Compact reduces padding and gaps; relaxed gives more breathing room."
|
|
).addDropdown((dropdown) => {
|
|
dropdown.addOption("compact", "Compact").addOption("normal", "Normal").addOption("relaxed", "Relaxed").setValue(settings.nodeDensity).onChange(async (value) => {
|
|
if (!isNodeDensityOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
nodeDensity: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("Relationship View").setDesc(
|
|
"Show object-level inbound/outbound relationships in previews. Disable this for large vaults or reverse engineering workflows when preview speed matters more."
|
|
).addToggle((toggle) => {
|
|
toggle.setValue(settings.enableRelationshipView).onChange(async (value) => {
|
|
await this.plugin.updateSettings({
|
|
enableRelationshipView: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("Show Mermaid Render Debug").setDesc(
|
|
"Show collapsed Mermaid rendering diagnostics under Mermaid diagrams. Mermaid Source remains available regardless of this setting."
|
|
).addToggle((toggle) => {
|
|
toggle.setValue(settings.showMermaidRenderDebug).onChange(async (value) => {
|
|
await this.plugin.updateSettings({
|
|
showMermaidRenderDebug: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("UI language").setDesc("Language for Model Weave viewer captions. Auto currently falls back to English.").addDropdown((dropdown) => {
|
|
dropdown.addOption("auto", "Auto").addOption("en", "English").addOption("ja", "\u65E5\u672C\u8A9E").setValue(settings.uiLanguage).onChange(async (value) => {
|
|
if (!isUiLanguageOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
uiLanguage: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("Local source root").setDesc("Base directory used to resolve relative Source Links outside the Obsidian vault.").addText((text) => {
|
|
text.setPlaceholder("/path/to/source/checkout").setValue(settings.localSourceRoot).onChange(async (value) => {
|
|
await this.plugin.updateSettings({
|
|
localSourceRoot: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName("Refresh open views").setDesc("Re-render open previews using the current settings.").addButton((button) => {
|
|
button.setButtonText("Refresh").onClick(async () => {
|
|
await this.plugin.refreshOpenModelWeaveViews();
|
|
new import_obsidian8.Notice("Refreshed open views");
|
|
});
|
|
});
|
|
}
|
|
};
|