mirror of
https://github.com/nejimakibird/model-weave.git
synced 2026-07-22 06:51:24 +00:00
27549 lines
965 KiB
JavaScript
27549 lines
965 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",
|
|
domainSources: [],
|
|
domains: [],
|
|
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/domain-relationships.ts
|
|
var CONFLICT_FIELDS = ["name", "kind", "parent"];
|
|
function buildDomainRelationshipSummaries(model, index) {
|
|
const currentDomainsById = new Map(model.domains.map((domain) => [domain.id, domain]));
|
|
const childrenByParent = /* @__PURE__ */ new Map();
|
|
for (const domain of model.domains) {
|
|
if (!domain.parent || !currentDomainsById.has(domain.parent)) {
|
|
continue;
|
|
}
|
|
if (!childrenByParent.has(domain.parent)) {
|
|
childrenByParent.set(domain.parent, []);
|
|
}
|
|
childrenByParent.get(domain.parent).push(domain.id);
|
|
}
|
|
const standaloneDefinitions = collectStandaloneDomainDefinitions(index);
|
|
const dfdLocalReferences = collectDfdLocalDomainReferences(index);
|
|
const dfdObjectReferences = collectDfdObjectDomainReferences(index);
|
|
return model.domains.map((domain) => ({
|
|
domain,
|
|
parentId: domain.parent,
|
|
childIds: [...childrenByParent.get(domain.id) ?? []].sort(compareText),
|
|
definedIn: [...standaloneDefinitions.get(domain.id) ?? []].sort(compareByPath),
|
|
conflicts: findConflictingFields(standaloneDefinitions.get(domain.id) ?? []),
|
|
dfdLocalDomainReferences: [...dfdLocalReferences.get(domain.id) ?? []].sort(compareByPath),
|
|
dfdObjectReferences: [...dfdObjectReferences.get(domain.id) ?? []].sort(
|
|
compareDfdObjectReference
|
|
)
|
|
}));
|
|
}
|
|
function collectStandaloneDomainDefinitions(index) {
|
|
const definitions = /* @__PURE__ */ new Map();
|
|
for (const model of Object.values(index.modelsByFilePath)) {
|
|
if (model.fileType !== "domains") {
|
|
continue;
|
|
}
|
|
for (const domain of model.domains) {
|
|
pushMappedValue(definitions, domain.id, {
|
|
path: model.path,
|
|
domain
|
|
});
|
|
}
|
|
}
|
|
return definitions;
|
|
}
|
|
function collectDfdLocalDomainReferences(index) {
|
|
const references = /* @__PURE__ */ new Map();
|
|
for (const diagram of getDfdDiagrams(index)) {
|
|
for (const domain of diagram.domains ?? []) {
|
|
pushMappedValue(references, domain.id, {
|
|
path: diagram.path
|
|
});
|
|
}
|
|
}
|
|
return references;
|
|
}
|
|
function collectDfdObjectDomainReferences(index) {
|
|
const references = /* @__PURE__ */ new Map();
|
|
for (const diagram of getDfdDiagrams(index)) {
|
|
for (const entry of diagram.objectEntries) {
|
|
const domain = entry.domain?.trim();
|
|
if (!domain) {
|
|
continue;
|
|
}
|
|
pushMappedValue(references, domain, {
|
|
path: diagram.path,
|
|
objectId: entry.id?.trim() || entry.ref?.trim() || String(entry.rowIndex + 1),
|
|
label: entry.label?.trim() || void 0
|
|
});
|
|
}
|
|
}
|
|
return references;
|
|
}
|
|
function getDfdDiagrams(index) {
|
|
return Object.values(index.modelsByFilePath).filter(
|
|
(model) => model.fileType === "dfd-diagram"
|
|
);
|
|
}
|
|
function findConflictingFields(definitions) {
|
|
if (definitions.length < 2) {
|
|
return [];
|
|
}
|
|
return CONFLICT_FIELDS.filter((field) => {
|
|
const values = new Set(
|
|
definitions.map((definition) => definition.domain[field]?.trim() ?? "")
|
|
);
|
|
return values.size > 1;
|
|
});
|
|
}
|
|
function pushMappedValue(map, key, value) {
|
|
if (!map.has(key)) {
|
|
map.set(key, []);
|
|
}
|
|
map.get(key).push(value);
|
|
}
|
|
function compareByPath(left, right) {
|
|
return compareText(left.path, right.path);
|
|
}
|
|
function compareDfdObjectReference(left, right) {
|
|
return compareText(left.path, right.path) || compareText(left.objectId, right.objectId);
|
|
}
|
|
function compareText(left, right) {
|
|
return left.localeCompare(right);
|
|
}
|
|
|
|
// src/core/domain-diagnostics.ts
|
|
function formatDomainIdRequiredMessage() {
|
|
return "Domain id is required.";
|
|
}
|
|
function formatDuplicateDomainIdMessage(id) {
|
|
return `duplicate Domain id "${id}"`;
|
|
}
|
|
function formatDomainParentUnknownMessage(parent) {
|
|
return `Domain parent "${parent}" is not defined.`;
|
|
}
|
|
function formatDomainSelfParentMessage(id) {
|
|
return `Domain "${id}" cannot use itself as parent.`;
|
|
}
|
|
function formatDomainParentCycleMessage(chain) {
|
|
return `Domain parent cycle detected: ${chain.join(" -> ")}`;
|
|
}
|
|
function formatDfdLocalDomainMissingSharedMessage(id) {
|
|
return `DFD-local Domain "${id}" is not defined in shared Domains.`;
|
|
}
|
|
function formatDfdLocalDomainFieldMismatchMessage(id, field, localValue, sharedValue) {
|
|
return `DFD-local Domain "${id}" has ${field} "${localValue}", but shared Domains define ${field} "${sharedValue}".`;
|
|
}
|
|
function formatDfdLocalDomainOverridesSourceMessage(id, field, localValue, sourceValue) {
|
|
return `DFD-local Domain "${id}" overrides Domain Source ${field} "${sourceValue}" with "${localValue}".`;
|
|
}
|
|
function formatDfdObjectUnknownLocalDomainMessage(objectId, domainId) {
|
|
return `DFD object "${objectId}" references unknown local Domain "${domainId}".`;
|
|
}
|
|
function formatDfdObjectUnknownDomainMessage(objectId, domainId) {
|
|
return `DFD object "${objectId}" references unknown Domain "${domainId}".`;
|
|
}
|
|
function formatDfdObjectDomainWithoutLocalDomainsMessage(objectId, domainId) {
|
|
return `DFD object "${objectId}" references Domain "${domainId}", but this DFD has no local Domains.`;
|
|
}
|
|
function formatStandaloneDomainDuplicateMessage(id) {
|
|
return `Domain "${id}" is defined in multiple Domains files.`;
|
|
}
|
|
function formatStandaloneDomainFieldConflictMessage(id, field) {
|
|
return `Domain "${id}" has conflicting ${field} values across Domains files.`;
|
|
}
|
|
function formatDomainDiagramMissingRefMessage() {
|
|
return "Domain Source ref is required.";
|
|
}
|
|
function formatDomainDiagramUnresolvedSourceMessage(ref) {
|
|
return `Domain Source ref "${ref}" could not be resolved. Check the ID or file name.`;
|
|
}
|
|
function formatDomainDiagramInvalidSourceTypeMessage(ref, fileType) {
|
|
return `Domain Source ref "${ref}" resolves to type "${fileType}", but expected type "domains".`;
|
|
}
|
|
function formatDomainDiagramNoValidSourcesMessage() {
|
|
return "Domain Diagram has no valid Domain Sources.";
|
|
}
|
|
function formatDomainDiagramEmptySourceMessage(ref) {
|
|
return `Domain Source ref "${ref}" has no Domain rows.`;
|
|
}
|
|
function formatDomainDiagramDuplicateDomainMessage(id, earlierSource, laterSource) {
|
|
return `Domain "${id}" is defined by multiple Domain Diagram sources: "${earlierSource}" and "${laterSource}".`;
|
|
}
|
|
function formatDomainDiagramFieldConflictMessage(id, field, earlierSource, laterSource) {
|
|
return `Domain "${id}" has conflicting ${field} values between Domain Diagram sources "${earlierSource}" and "${laterSource}".`;
|
|
}
|
|
function formatAppProcessUnknownDomainMessage(stepId, domainId) {
|
|
return `app_process Step "${stepId}" references unknown Domain "${domainId}".`;
|
|
}
|
|
function formatAppProcessUnknownLocalDomainMessage(stepId, domainId) {
|
|
return `app_process Step "${stepId}" references unknown local Domain "${domainId}".`;
|
|
}
|
|
function formatAppProcessLocalDomainFieldOverrideMessage(id, field) {
|
|
return `app_process local Domain "${id}" overrides external Domain ${field}.`;
|
|
}
|
|
|
|
// 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 extractModelReferenceCandidates(value) {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return [];
|
|
}
|
|
const qualified = parseQualifiedRef(trimmed);
|
|
if (qualified?.hasMemberRef && qualified.memberRef && isLikelySingleModelReference(qualified.baseRefRaw)) {
|
|
return [trimmed];
|
|
}
|
|
const wikilinks = extractWikilinkReferences(trimmed);
|
|
if (wikilinks.length > 0) {
|
|
return wikilinks;
|
|
}
|
|
const parsed = parseReferenceValue(trimmed);
|
|
if (parsed?.isExternal) {
|
|
return [];
|
|
}
|
|
if (parsed?.kind === "markdown_link") {
|
|
return [trimmed];
|
|
}
|
|
return isLikelySingleModelReference(trimmed) ? [trimmed] : [];
|
|
}
|
|
function extractWikilinkReferences(value) {
|
|
const refs = [];
|
|
let index = 0;
|
|
while (index < value.length) {
|
|
const start = value.indexOf("[[", index);
|
|
if (start < 0) {
|
|
break;
|
|
}
|
|
const end = value.indexOf("]]", start + 2);
|
|
if (end < 0) {
|
|
break;
|
|
}
|
|
refs.push(value.slice(start, end + 2));
|
|
index = end + 2;
|
|
}
|
|
return refs;
|
|
}
|
|
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.colorSchemesById?.[candidate] ?? index.dataObjectsById[candidate] ?? index.dfdObjectsById[candidate] ?? index.erEntitiesById[candidate] ?? index.erEntitiesByPhysicalName[candidate] ?? index.relationsFilesById[candidate] ?? index.domainDiagramsById?.[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 "color-scheme":
|
|
case "dfd-object":
|
|
case "dfd-diagram":
|
|
case "domains":
|
|
case "domain-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":
|
|
case "color-scheme":
|
|
case "domains":
|
|
case "domain-diagram":
|
|
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 isLikelySingleModelReference(value) {
|
|
const trimmed = value.trim();
|
|
if (!trimmed || trimmed.startsWith("#") || /\s/.test(trimmed)) {
|
|
return false;
|
|
}
|
|
const parsed = parseReferenceValue(trimmed);
|
|
if (parsed?.isExternal) {
|
|
return false;
|
|
}
|
|
if (parsed?.kind && parsed.kind !== "raw") {
|
|
return true;
|
|
}
|
|
const target = parsed?.target ?? trimmed;
|
|
const basename = getBasename(target);
|
|
return /^(?:APP|CLASS|CLD|CLS|CODE|CODESET|CS|DATA|DFD|DFDO|DOMAIN|DOMAINS|ENT|ERD|MAP|MSG|PROC|REL|RULE|SCR|SRC)[-_][A-Z0-9][A-Z0-9_-]*$/.test(
|
|
basename
|
|
);
|
|
}
|
|
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/domain-diagram-resolver.ts
|
|
var DOMAIN_CONFLICT_FIELDS = ["parent", "kind", "name"];
|
|
function resolveDomainDiagram(diagram, index) {
|
|
const resolvedSources = resolveDomainSources(
|
|
diagram.path,
|
|
diagram.domainSources,
|
|
index,
|
|
{ warnWhenNoValidSources: true }
|
|
);
|
|
return {
|
|
diagram,
|
|
domains: resolvedSources.domains,
|
|
sourceSummaries: resolvedSources.sourceSummaries,
|
|
conflicts: resolvedSources.conflicts,
|
|
warnings: resolvedSources.warnings
|
|
};
|
|
}
|
|
function resolveDomainSources(ownerPath, sourceRefs, index, options = {}) {
|
|
const warnings = [];
|
|
const sourceSummaries = [];
|
|
const validSources = [];
|
|
for (const sourceRef of sourceRefs) {
|
|
const resolved = findModelByReference(sourceRef.ref, index);
|
|
if (!resolved) {
|
|
sourceSummaries.push({
|
|
ref: sourceRef,
|
|
status: "unresolved",
|
|
domainCount: 0
|
|
});
|
|
warnings.push(createSourceWarning(
|
|
ownerPath,
|
|
sourceRef.rowIndex,
|
|
formatDomainDiagramUnresolvedSourceMessage(sourceRef.ref),
|
|
"unresolved-reference",
|
|
"warning"
|
|
));
|
|
continue;
|
|
}
|
|
if (resolved.fileType !== "domains") {
|
|
sourceSummaries.push({
|
|
ref: sourceRef,
|
|
resolvedPath: resolved.path,
|
|
status: "invalid-type",
|
|
domainCount: 0
|
|
});
|
|
warnings.push(createSourceWarning(
|
|
ownerPath,
|
|
sourceRef.rowIndex,
|
|
formatDomainDiagramInvalidSourceTypeMessage(sourceRef.ref, resolved.fileType),
|
|
"invalid-structure",
|
|
"warning"
|
|
));
|
|
continue;
|
|
}
|
|
sourceSummaries.push({
|
|
ref: sourceRef,
|
|
resolvedPath: resolved.path,
|
|
resolvedId: resolved.id,
|
|
status: resolved.domains.length > 0 ? "ok" : "empty",
|
|
domainCount: resolved.domains.length
|
|
});
|
|
validSources.push({ source: resolved, ref: sourceRef.ref });
|
|
if (resolved.domains.length === 0) {
|
|
warnings.push(createSourceWarning(
|
|
ownerPath,
|
|
sourceRef.rowIndex,
|
|
formatDomainDiagramEmptySourceMessage(sourceRef.ref),
|
|
"invalid-structure",
|
|
"warning"
|
|
));
|
|
}
|
|
}
|
|
if (validSources.length === 0 && options.warnWhenNoValidSources) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatDomainDiagramNoValidSourcesMessage(),
|
|
severity: "warning",
|
|
path: ownerPath,
|
|
field: "Domain Sources"
|
|
});
|
|
}
|
|
const mergeResult = mergeDomainDiagramSources(validSources, ownerPath);
|
|
warnings.push(...mergeResult.warnings);
|
|
return {
|
|
domains: mergeResult.domains,
|
|
sourceSummaries,
|
|
conflicts: mergeResult.conflicts,
|
|
warnings
|
|
};
|
|
}
|
|
function mergeDomainDiagramSources(sources, diagramPath = "") {
|
|
const effectiveById = /* @__PURE__ */ new Map();
|
|
const order = [];
|
|
const conflicts = [];
|
|
const warnings = [];
|
|
for (const entry of sources) {
|
|
for (const domain of entry.source.domains) {
|
|
const previous = effectiveById.get(domain.id);
|
|
if (!previous) {
|
|
order.push(domain.id);
|
|
effectiveById.set(domain.id, {
|
|
domain: { ...domain },
|
|
sourcePath: entry.source.path
|
|
});
|
|
continue;
|
|
}
|
|
const duplicateConflict = createConflict(
|
|
domain.id,
|
|
"duplicate",
|
|
previous.sourcePath,
|
|
entry.source.path,
|
|
void 0,
|
|
void 0,
|
|
"error"
|
|
);
|
|
conflicts.push(duplicateConflict);
|
|
warnings.push(createMergeWarning(
|
|
diagramPath || entry.source.path,
|
|
formatDomainDiagramDuplicateDomainMessage(
|
|
domain.id,
|
|
previous.sourcePath,
|
|
entry.source.path
|
|
),
|
|
duplicateConflict.severity
|
|
));
|
|
for (const field of DOMAIN_CONFLICT_FIELDS) {
|
|
const previousValue = previous.domain[field]?.trim() ?? "";
|
|
const nextValue = domain[field]?.trim() ?? "";
|
|
if (previousValue === nextValue) {
|
|
continue;
|
|
}
|
|
const severity = field === "name" ? "warning" : "error";
|
|
const conflict = createConflict(
|
|
domain.id,
|
|
field,
|
|
previous.sourcePath,
|
|
entry.source.path,
|
|
previousValue,
|
|
nextValue,
|
|
severity
|
|
);
|
|
conflicts.push(conflict);
|
|
warnings.push(createMergeWarning(
|
|
diagramPath || entry.source.path,
|
|
formatDomainDiagramFieldConflictMessage(
|
|
domain.id,
|
|
field,
|
|
previous.sourcePath,
|
|
entry.source.path
|
|
),
|
|
severity
|
|
));
|
|
}
|
|
effectiveById.set(domain.id, {
|
|
domain: { ...domain },
|
|
sourcePath: entry.source.path
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
domains: order.map((id) => effectiveById.get(id)?.domain).filter((domain) => Boolean(domain)),
|
|
conflicts,
|
|
warnings
|
|
};
|
|
}
|
|
function createConflict(domainId, field, earlierSourcePath, laterSourcePath, earlierValue, laterValue, severity) {
|
|
return {
|
|
domainId,
|
|
field,
|
|
earlierSourcePath,
|
|
laterSourcePath,
|
|
earlierValue,
|
|
laterValue,
|
|
effectiveSourcePath: laterSourcePath,
|
|
severity
|
|
};
|
|
}
|
|
function createSourceWarning(path2, rowIndex, message, code, severity) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity,
|
|
path: path2,
|
|
field: "Domain Sources.ref",
|
|
context: { rowIndex: rowIndex + 1 }
|
|
};
|
|
}
|
|
function createMergeWarning(path2, message, severity) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity,
|
|
path: path2,
|
|
field: "Domain Sources"
|
|
};
|
|
}
|
|
|
|
// src/core/app-process-domain-resolver.ts
|
|
var APP_PROCESS_DOMAIN_CONFLICT_FIELDS = ["parent", "kind", "name"];
|
|
function resolveAppProcessDomainPlacement(process, index) {
|
|
const localDomains = process.domains ?? [];
|
|
const hasLocalDomains = localDomains.length > 0;
|
|
if (!hasLocalDomains && process.domainSources.length === 0) {
|
|
return {
|
|
process,
|
|
domains: [],
|
|
sourceSummaries: [],
|
|
conflicts: [],
|
|
placements: [],
|
|
warnings: []
|
|
};
|
|
}
|
|
const resolvedSources = process.domainSources.length > 0 ? resolveDomainSources(process.path, process.domainSources, index) : {
|
|
domains: [],
|
|
sourceSummaries: [],
|
|
conflicts: [],
|
|
warnings: []
|
|
};
|
|
const mergeResult = mergeAppProcessDomains(
|
|
resolvedSources.domains,
|
|
localDomains,
|
|
process.path
|
|
);
|
|
const domainsById = new Map(mergeResult.domains.map((domain) => [domain.id, domain]));
|
|
const warnings = [
|
|
...resolvedSources.warnings,
|
|
...mergeResult.warnings,
|
|
...validateMergedAppProcessDomainParents(process.path, mergeResult.domains)
|
|
];
|
|
const conflicts = [
|
|
...resolvedSources.conflicts,
|
|
...mergeResult.conflicts
|
|
];
|
|
const placements = (process.steps ?? []).map((step) => {
|
|
const domainId = step.domain?.trim() ?? "";
|
|
if (!domainId) {
|
|
return null;
|
|
}
|
|
const domain = domainsById.get(domainId);
|
|
if (!domain) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: hasLocalDomains && process.domainSources.length === 0 ? formatAppProcessUnknownLocalDomainMessage(step.id, domainId) : formatAppProcessUnknownDomainMessage(step.id, domainId),
|
|
severity: "warning",
|
|
path: process.path,
|
|
field: "Steps.domain",
|
|
context: { stepId: step.id, domainId }
|
|
});
|
|
}
|
|
return {
|
|
stepId: step.id,
|
|
stepLabel: step.label,
|
|
domainId,
|
|
lane: step.lane,
|
|
status: domain ? "resolved" : "unresolved",
|
|
domain
|
|
};
|
|
}).filter(
|
|
(placement) => Boolean(placement)
|
|
);
|
|
return {
|
|
process,
|
|
domains: mergeResult.domains,
|
|
sourceSummaries: resolvedSources.sourceSummaries,
|
|
conflicts,
|
|
placements,
|
|
warnings
|
|
};
|
|
}
|
|
function validateMergedAppProcessDomainParents(processPath, domains) {
|
|
const domainIds = new Set(domains.map((domain) => domain.id));
|
|
const warnings = [];
|
|
for (const domain of domains) {
|
|
if (!domain.parent || domain.parent === domain.id || domainIds.has(domain.parent)) {
|
|
continue;
|
|
}
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: formatDomainParentUnknownMessage(domain.parent),
|
|
severity: "warning",
|
|
path: processPath,
|
|
field: "Domains.parent",
|
|
context: { rowIndex: domain.rowIndex + 1 }
|
|
});
|
|
}
|
|
return warnings;
|
|
}
|
|
function mergeAppProcessDomains(externalDomains, localDomains, processPath) {
|
|
const domainsById = /* @__PURE__ */ new Map();
|
|
const order = [];
|
|
const conflicts = [];
|
|
const warnings = [];
|
|
for (const domain of externalDomains) {
|
|
if (!domainsById.has(domain.id)) {
|
|
order.push(domain.id);
|
|
}
|
|
domainsById.set(domain.id, { ...domain });
|
|
}
|
|
for (const localDomain of localDomains) {
|
|
const externalDomain = domainsById.get(localDomain.id);
|
|
if (!externalDomain) {
|
|
order.push(localDomain.id);
|
|
domainsById.set(localDomain.id, { ...localDomain });
|
|
continue;
|
|
}
|
|
for (const field of APP_PROCESS_DOMAIN_CONFLICT_FIELDS) {
|
|
const externalValue = externalDomain[field]?.trim() ?? "";
|
|
const localValue = localDomain[field]?.trim() ?? "";
|
|
if (externalValue === localValue) {
|
|
continue;
|
|
}
|
|
conflicts.push({
|
|
domainId: localDomain.id,
|
|
field,
|
|
earlierSourcePath: "Domain Sources",
|
|
laterSourcePath: processPath,
|
|
earlierValue: externalValue,
|
|
laterValue: localValue,
|
|
effectiveSourcePath: processPath,
|
|
severity: "warning"
|
|
});
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatAppProcessLocalDomainFieldOverrideMessage(localDomain.id, field),
|
|
severity: "warning",
|
|
path: processPath,
|
|
field: "Domains"
|
|
});
|
|
}
|
|
domainsById.set(localDomain.id, { ...localDomain });
|
|
}
|
|
return {
|
|
domains: order.map((id) => domainsById.get(id)).filter((domain) => Boolean(domain)),
|
|
conflicts,
|
|
warnings
|
|
};
|
|
}
|
|
|
|
// src/core/color-scheme.ts
|
|
var BUILT_IN_COLOR_ENTRIES = [
|
|
{ target: "domain", kind: "organization", fill: "#e3f2fd", stroke: "#1976d2", text: "#111111", rowIndex: 0 },
|
|
{ target: "domain", kind: "department", fill: "#e8f5e9", stroke: "#388e3c", text: "#111111", rowIndex: 1 },
|
|
{ target: "domain", kind: "location", fill: "#fff3e0", stroke: "#f57c00", text: "#111111", rowIndex: 2 },
|
|
{ target: "domain", kind: "system", fill: "#f3e5f5", stroke: "#7b1fa2", text: "#111111", rowIndex: 3 },
|
|
{ target: "domain", kind: "external", fill: "#eeeeee", stroke: "#616161", text: "#111111", rowIndex: 4 },
|
|
{ target: "domain", kind: "device", fill: "#e0f7fa", stroke: "#0097a7", text: "#111111", rowIndex: 5 },
|
|
{ kind: "default", fill: "#f5f5f5", stroke: "#9e9e9e", text: "#111111", rowIndex: 6 }
|
|
];
|
|
var BUILT_IN_COLOR_SCHEME = {
|
|
id: "built-in-default",
|
|
name: "Built-in default",
|
|
entries: BUILT_IN_COLOR_ENTRIES,
|
|
defaultStyle: {
|
|
fill: "#f5f5f5",
|
|
stroke: "#9e9e9e",
|
|
text: "#111111"
|
|
}
|
|
};
|
|
function resolveDefaultColorScheme(index, defaultColorSchemeRef) {
|
|
const ref = defaultColorSchemeRef?.trim();
|
|
if (!ref) {
|
|
return { colorScheme: BUILT_IN_COLOR_SCHEME, warnings: [] };
|
|
}
|
|
const resolved = findModelByReference(ref, index);
|
|
if (!resolved) {
|
|
return {
|
|
colorScheme: BUILT_IN_COLOR_SCHEME,
|
|
warnings: [createColorSchemeSettingWarning(formatColorSchemeSettingUnresolvedMessage(ref))]
|
|
};
|
|
}
|
|
if (resolved.fileType !== "color-scheme") {
|
|
return {
|
|
colorScheme: BUILT_IN_COLOR_SCHEME,
|
|
warnings: [
|
|
createColorSchemeSettingWarning(
|
|
formatColorSchemeSettingInvalidTypeMessage(ref, resolved.fileType)
|
|
)
|
|
]
|
|
};
|
|
}
|
|
return {
|
|
colorScheme: {
|
|
id: resolved.id,
|
|
name: resolved.name,
|
|
sourcePath: resolved.path,
|
|
entries: resolved.colors,
|
|
defaultStyle: resolveColorStyle(BUILT_IN_COLOR_SCHEME, "", "")
|
|
},
|
|
warnings: []
|
|
};
|
|
}
|
|
function resolveColorStyle(colorScheme, target, kind) {
|
|
const scheme = colorScheme ?? BUILT_IN_COLOR_SCHEME;
|
|
const normalizedTarget = target.trim().toLowerCase();
|
|
const normalizedKind = kind?.trim().toLowerCase() ?? "";
|
|
const targetKindMatch = scheme.entries.find(
|
|
(entry) => (entry.target?.trim().toLowerCase() ?? "") === normalizedTarget && entry.kind.trim().toLowerCase() === normalizedKind
|
|
);
|
|
if (targetKindMatch) {
|
|
return mergeStyle(scheme.defaultStyle, entryToStyle(targetKindMatch));
|
|
}
|
|
const globalKindMatch = scheme.entries.find(
|
|
(entry) => !entry.target?.trim() && entry.kind.trim().toLowerCase() === normalizedKind
|
|
);
|
|
if (globalKindMatch) {
|
|
return mergeStyle(scheme.defaultStyle, entryToStyle(globalKindMatch));
|
|
}
|
|
const builtInTargetKindMatch = BUILT_IN_COLOR_SCHEME.entries.find(
|
|
(entry) => (entry.target?.trim().toLowerCase() ?? "") === normalizedTarget && entry.kind.trim().toLowerCase() === normalizedKind
|
|
);
|
|
if (builtInTargetKindMatch) {
|
|
return mergeStyle(
|
|
BUILT_IN_COLOR_SCHEME.defaultStyle,
|
|
entryToStyle(builtInTargetKindMatch)
|
|
);
|
|
}
|
|
const defaultMatch = scheme.entries.find(
|
|
(entry) => !entry.target?.trim() && entry.kind.trim().toLowerCase() === "default"
|
|
);
|
|
if (defaultMatch) {
|
|
return mergeStyle(BUILT_IN_COLOR_SCHEME.defaultStyle, entryToStyle(defaultMatch));
|
|
}
|
|
return BUILT_IN_COLOR_SCHEME.defaultStyle;
|
|
}
|
|
function getAppliedColorSchemeRowsForTargets(colorScheme, targets) {
|
|
const scheme = colorScheme ?? BUILT_IN_COLOR_SCHEME;
|
|
const normalizedTargets = new Set(
|
|
targets.map((target) => target.trim().toLowerCase()).filter(Boolean)
|
|
);
|
|
const rowsByKey = /* @__PURE__ */ new Map();
|
|
const isBuiltInScheme = scheme.id === BUILT_IN_COLOR_SCHEME.id;
|
|
for (const entry of scheme.entries) {
|
|
if (isEntryRelevantForTargets(entry, normalizedTargets)) {
|
|
rowsByKey.set(entryKey(entry), {
|
|
entry,
|
|
source: isBuiltInScheme ? "built-in" : "configured"
|
|
});
|
|
}
|
|
}
|
|
if (!isBuiltInScheme) {
|
|
for (const entry of BUILT_IN_COLOR_SCHEME.entries) {
|
|
if (isEntryRelevantForTargets(entry, normalizedTargets)) {
|
|
const key = entryKey(entry);
|
|
if (!rowsByKey.has(key)) {
|
|
rowsByKey.set(key, { entry, source: "built-in" });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return [...rowsByKey.values()].sort(compareAppliedColorSchemeRows);
|
|
}
|
|
function formatColorSchemeKindRequiredMessage() {
|
|
return "Color Scheme kind is required.";
|
|
}
|
|
function formatColorSchemeInvalidColorMessage(field, value) {
|
|
return `Color Scheme ${field} "${value}" is not a supported hex color.`;
|
|
}
|
|
function formatColorSchemeDuplicateEntryMessage(target, kind) {
|
|
const targetLabel = target.trim() || "(default target)";
|
|
return `duplicate Color Scheme entry for target "${targetLabel}" and kind "${kind}"`;
|
|
}
|
|
function formatColorSchemeSettingUnresolvedMessage(ref) {
|
|
return `Default Color Scheme ref "${ref}" could not be resolved. Built-in colors will be used.`;
|
|
}
|
|
function formatColorSchemeSettingInvalidTypeMessage(ref, fileType) {
|
|
return `Default Color Scheme ref "${ref}" resolves to type "${fileType}", but expected type "color_scheme". Built-in colors will be used.`;
|
|
}
|
|
function entryToStyle(entry) {
|
|
return {
|
|
fill: entry.fill,
|
|
stroke: entry.stroke,
|
|
text: entry.text
|
|
};
|
|
}
|
|
function mergeStyle(base, override) {
|
|
return {
|
|
fill: override.fill ?? base.fill,
|
|
stroke: override.stroke ?? base.stroke,
|
|
text: override.text ?? base.text
|
|
};
|
|
}
|
|
function normalizeKind(kind) {
|
|
return kind.trim().toLowerCase();
|
|
}
|
|
function isEntryRelevantForTargets(entry, normalizedTargets) {
|
|
const entryTarget = entry.target?.trim().toLowerCase() ?? "";
|
|
return !entryTarget || normalizedTargets.has(entryTarget);
|
|
}
|
|
function entryKey(entry) {
|
|
const target = entry.target?.trim().toLowerCase() ?? "";
|
|
return `${target}::${normalizeKind(entry.kind)}`;
|
|
}
|
|
function compareAppliedColorSchemeRows(left, right) {
|
|
const leftTarget = left.entry.target?.trim().toLowerCase() ?? "";
|
|
const rightTarget = right.entry.target?.trim().toLowerCase() ?? "";
|
|
if (leftTarget !== rightTarget) {
|
|
if (!leftTarget) {
|
|
return -1;
|
|
}
|
|
if (!rightTarget) {
|
|
return 1;
|
|
}
|
|
return leftTarget.localeCompare(rightTarget);
|
|
}
|
|
return normalizeKind(left.entry.kind).localeCompare(normalizeKind(right.entry.kind));
|
|
}
|
|
function createColorSchemeSettingWarning(message) {
|
|
return {
|
|
code: "unresolved-reference",
|
|
message,
|
|
severity: "warning",
|
|
field: "defaultColorSchemeRef"
|
|
};
|
|
}
|
|
|
|
// 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 relatedIdentity = relatedObject ? void 0 : resolveReferenceIdentity(relatedReference, index);
|
|
const relatedObjectId = relatedObject ? getObjectId(relatedObject) : relatedIdentity?.resolvedId ?? relation.targetClass;
|
|
if (!relatedObject && !relatedIdentity?.resolvedModel) {
|
|
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 relatedIdentity = relatedObject ? void 0 : resolveReferenceIdentity(relatedReference, index);
|
|
const relatedObjectId = relatedObject ? getObjectId(relatedObject) : relatedIdentity?.resolvedId ?? relatedReference;
|
|
if (!relatedObject && !relatedIdentity?.resolvedModel) {
|
|
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 if (model.fileType === "domains") {
|
|
} 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 mappingRows = /* @__PURE__ */ new Set();
|
|
const targetMemberRows = /* @__PURE__ */ new Map();
|
|
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();
|
|
const rule = row.rule?.trim();
|
|
if (!targetRef) {
|
|
diagnostics.push(createSectionWarning(model.path, "Mappings", "target_ref is empty"));
|
|
}
|
|
if (sourceRef && targetRef) {
|
|
const duplicateKey = buildMappingRowDuplicateKey(sourceRef, targetRef, transform, rule);
|
|
if (mappingRows.has(duplicateKey)) {
|
|
diagnostics.push(
|
|
createSectionWarning(
|
|
model.path,
|
|
"Mappings",
|
|
`duplicate mapping row "${formatMappingReferenceForMessage(sourceRef)} -> ${formatMappingReferenceForMessage(targetRef)}"`
|
|
)
|
|
);
|
|
}
|
|
mappingRows.add(duplicateKey);
|
|
}
|
|
if (targetRef) {
|
|
const targetMember = getMappingTargetMemberReference(targetRef);
|
|
if (targetMember) {
|
|
const rowKey = buildMappingRowDuplicateKey(sourceRef, targetRef, transform, rule);
|
|
const existing = targetMemberRows.get(targetMember.key) ?? {
|
|
display: targetMember.display,
|
|
rowKeys: /* @__PURE__ */ new Set(),
|
|
warned: false
|
|
};
|
|
existing.rowKeys.add(rowKey);
|
|
if (!existing.warned && existing.rowKeys.size > 1) {
|
|
diagnostics.push(createDuplicateMappingTargetMemberWarning(model.path, targetMember.display));
|
|
existing.warned = true;
|
|
}
|
|
targetMemberRows.set(targetMember.key, existing);
|
|
}
|
|
}
|
|
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 (rule) {
|
|
diagnostics.push(
|
|
...buildReferenceWarnings(model.path, "Mappings", 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 buildMappingRowDuplicateKey(sourceRef, targetRef, transform, rule) {
|
|
return [
|
|
normalizeMappingReferenceKey(sourceRef),
|
|
normalizeMappingReferenceKey(targetRef),
|
|
normalizeMappingFreeTextKey(transform),
|
|
normalizeMappingReferenceKey(rule)
|
|
].join(" ");
|
|
}
|
|
function getMappingTargetMemberReference(reference) {
|
|
const qualified = parseQualifiedRef(reference);
|
|
if (!qualified?.hasMemberRef || !qualified.memberRef) {
|
|
return null;
|
|
}
|
|
const display = formatMappingReferenceForMessage(reference);
|
|
return {
|
|
key: normalizeMappingReferenceKey(reference),
|
|
display
|
|
};
|
|
}
|
|
function createDuplicateMappingTargetMemberWarning(path2, display) {
|
|
return {
|
|
code: "duplicate-mapping-target-member",
|
|
message: `mapping target member "${display}" is mapped from multiple sources.`,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "Mappings",
|
|
context: { section: "Mappings", reference: display }
|
|
};
|
|
}
|
|
function normalizeMappingReferenceKey(reference) {
|
|
return formatMappingReferenceForMessage(reference).toLowerCase();
|
|
}
|
|
function normalizeMappingFreeTextKey(value) {
|
|
return value?.trim().replace(/\s+/g, " ").toLowerCase() ?? "";
|
|
}
|
|
function formatMappingReferenceForMessage(reference) {
|
|
const trimmed = reference?.trim();
|
|
if (!trimmed) {
|
|
return "";
|
|
}
|
|
const qualified = parseQualifiedRef(trimmed);
|
|
if (qualified) {
|
|
const parsedBase = parseReferenceValue(qualified.baseRefRaw);
|
|
const base = parsedBase?.target?.trim() || qualified.baseRefRaw.trim();
|
|
return qualified.memberRef ? `${base}.${qualified.memberRef}` : base;
|
|
}
|
|
const parsed = parseReferenceValue(trimmed);
|
|
return parsed?.target?.trim() || trimmed;
|
|
}
|
|
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,
|
|
"transition target reference",
|
|
void 0,
|
|
{ useCouldNotResolveMessage: true }
|
|
)
|
|
);
|
|
}
|
|
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 actionSignatures = /* @__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 actionSignature = buildScreenActionDuplicateSignature(action);
|
|
if (actionSignature) {
|
|
if (actionSignatures.has(actionSignature)) {
|
|
diagnostics.push({
|
|
code: "invalid-structure",
|
|
message: `duplicate action definition "${target ?? ""}" + "${action.event?.trim() ?? ""}"`,
|
|
severity: "warning",
|
|
path: model.path,
|
|
field: "Actions",
|
|
context: { section: "Actions" }
|
|
});
|
|
}
|
|
actionSignatures.add(actionSignature);
|
|
}
|
|
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"
|
|
)
|
|
);
|
|
}
|
|
return diagnostics;
|
|
}
|
|
function buildScreenActionDuplicateSignature(action) {
|
|
const target = action.target?.trim();
|
|
const event = action.event?.trim();
|
|
if (!target || !event) {
|
|
return null;
|
|
}
|
|
return [
|
|
target,
|
|
event,
|
|
action.condition?.trim() ?? "",
|
|
action.kind?.trim() ?? "",
|
|
action.invoke?.trim() ?? "",
|
|
action.transition?.trim() ?? "",
|
|
action.rule?.trim() ?? ""
|
|
].join("\0");
|
|
}
|
|
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, options = {}) {
|
|
const value = ref?.trim();
|
|
if (!value) {
|
|
return [];
|
|
}
|
|
const candidates = extractModelReferenceCandidates(value);
|
|
if (candidates.length === 0) {
|
|
return [];
|
|
}
|
|
if (candidates.length > 1 || candidates[0] !== value) {
|
|
return candidates.flatMap(
|
|
(candidate) => buildSingleReferenceWarnings(
|
|
path2,
|
|
section,
|
|
candidate,
|
|
index,
|
|
messagePrefix,
|
|
expectedFileType,
|
|
options
|
|
)
|
|
);
|
|
}
|
|
return buildSingleReferenceWarnings(
|
|
path2,
|
|
section,
|
|
value,
|
|
index,
|
|
messagePrefix,
|
|
expectedFileType,
|
|
options
|
|
);
|
|
}
|
|
function buildSingleReferenceWarnings(path2, section, value, index, messagePrefix, expectedFileType, options = {}) {
|
|
const qualified = parseQualifiedRef(value);
|
|
if (qualified?.hasMemberRef) {
|
|
const resolved2 = resolveQualifiedMemberReference(value, index);
|
|
if (!resolved2.baseIdentity.resolvedModel) {
|
|
return [createSectionWarning(path2, section, formatReferenceWarningMessage(messagePrefix, value, options))];
|
|
}
|
|
if (expectedFileType && resolved2.baseIdentity.resolvedModel.fileType !== expectedFileType) {
|
|
return [createSectionWarning(path2, section, formatReferenceWarningMessage(messagePrefix, value, options))];
|
|
}
|
|
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, formatReferenceWarningMessage(messagePrefix, value, options))];
|
|
}
|
|
if (expectedFileType && resolved.resolvedModel.fileType !== expectedFileType) {
|
|
return [createSectionWarning(path2, section, formatReferenceWarningMessage(messagePrefix, value, options))];
|
|
}
|
|
return [];
|
|
}
|
|
function formatReferenceWarningMessage(messagePrefix, value, options) {
|
|
return options.useCouldNotResolveMessage ? `${messagePrefix} "${value}" could not be resolved. Check the ID or file name.` : `${messagePrefix} "${value}"`;
|
|
}
|
|
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) {
|
|
const targetObject = resolveObjectModelReference(relation.targetClass, index);
|
|
const targetIdentity = targetObject ? void 0 : resolveReferenceIdentity(relation.targetClass, index);
|
|
if (!targetObject && !targetIdentity?.resolvedModel) {
|
|
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"
|
|
}
|
|
});
|
|
} else if (!targetObject && targetIdentity?.resolvedModel) {
|
|
diagnostics.push({
|
|
code: "class-relation-target-not-diagram-compatible",
|
|
message: formatClassRelationTargetNotDiagramCompatibleMessage(
|
|
getReferenceDiagnosticLabel(relation.targetClass, targetIdentity)
|
|
),
|
|
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 getReferenceDiagnosticLabel(reference, identity) {
|
|
return identity?.resolvedId ?? identity?.target ?? parseReferenceValue(reference)?.target ?? reference.trim();
|
|
}
|
|
function formatClassRelationTargetNotDiagramCompatibleMessage(target) {
|
|
return `class relation target "${target}" exists, but is not compatible with Class Diagram rendering and was excluded. Consider representing non-structural cross-model relationships with Mapping.`;
|
|
}
|
|
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) {
|
|
if (warning.severity === "info" || warning.severity === "error") {
|
|
return warning;
|
|
}
|
|
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 { ...warning, severity: "error" };
|
|
}
|
|
if (warning.code === "invalid-structure" && typeof warning.field === "string" && ["type", "id", "name", "logical_name", "physical_name", "kind"].includes(warning.field)) {
|
|
return { ...warning, severity: "error" };
|
|
}
|
|
return warning;
|
|
}
|
|
function localizeDiagnosticMessage(message, language) {
|
|
if (!isJapaneseLanguage(language)) {
|
|
return message;
|
|
}
|
|
const replacements = [
|
|
[/^Mermaid overview: no outbound relations to display\.$/, "Mermaid\u6982\u8981: \u8868\u793A\u3059\u308B\u5916\u5411\u304D\u306E\u95A2\u4FC2\u306F\u3042\u308A\u307E\u305B\u3093\u3002"],
|
|
[/^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`],
|
|
[/^unresolved DFD flow source ""$/, "DFD flow \u306E source \u304C\u672A\u6307\u5B9A\u3067\u3059\u3002"],
|
|
[/^unresolved DFD flow target ""$/, "DFD flow \u306E target \u304C\u672A\u6307\u5B9A\u3067\u3059\u3002"],
|
|
[/^unresolved DFD flow source "([^"]+)"$/, (_match, source) => `DFD flow \u306E source "${source}" \u304C\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002`],
|
|
[/^unresolved DFD flow target "([^"]+)"$/, (_match, target) => `DFD flow \u306E target "${target}" \u304C\u89E3\u6C7A\u3067\u304D\u307E\u305B\u3093\u3002`],
|
|
[/^unresolved DFD object ref "([^"]+)"$/, (_match, ref) => `DFD\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306E\u53C2\u7167 "${ref}" \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`],
|
|
[/^DFD object ref "([^"]+)" could not be resolved\. Check the ID or file name\.$/, (_match, ref) => `DFD\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306E\u53C2\u7167 "${ref}" \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`],
|
|
[/^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 supported app_process Domain Sources 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 app_process Domain Sources \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 mapping row "([^"]+)"$/, (_match, value) => `mapping row "${value}" \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^mapping target member "([^"]+)" is mapped from multiple sources\.$/, (_match, value) => `mapping target member "${value}" \u304C\u8907\u6570\u306E source_ref \u304B\u3089\u5BFE\u5FDC\u4ED8\u3051\u3089\u308C\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 definition "([^"]+)" \+ "([^"]+)"$/, (_match, target, event) => `action \u5B9A\u7FA9 "${target}" + "${event}" \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\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`],
|
|
[/^Step "([^"]+)" has both domain and lane\. domain is used and lane is ignored\.$/, (_match, step) => `Step "${step}" \u306B\u306F domain \u3068 lane \u306E\u4E21\u65B9\u304C\u3042\u308A\u307E\u3059\u3002domain \u304C\u4F7F\u308F\u308C\u3001lane \u306F\u7121\u8996\u3055\u308C\u307E\u3059\u3002`],
|
|
[/^app_process Step "([^"]+)" references unknown Domain "([^"]+)"\.$/, (_match, step, domain) => `app_process Step "${step}" \u304C\u672A\u5B9A\u7FA9\u306E Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^app_process Step "([^"]+)" references unknown local Domain "([^"]+)"\.$/, (_match, step, domain) => `app_process Step "${step}" \u304C\u672A\u5B9A\u7FA9\u306E\u30ED\u30FC\u30AB\u30EB Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^app_process local Domain "([^"]+)" overrides external Domain (name|kind|parent)\.$/, (_match, domain, field) => `app_process \u30ED\u30FC\u30AB\u30EB Domain "${domain}" \u304C\u5916\u90E8 Domain \u306E ${field} \u3092\u4E0A\u66F8\u304D\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^(.+) "([^"]+)" could not be resolved\. Check the ID or file name\.$/, (_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 (.+) "([^"]+)"$/, (_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}`],
|
|
[/^class relation target "([^"]+)" exists, but is not compatible with Class Diagram rendering and was excluded\. Consider representing non-structural cross-model relationships with Mapping\.$/, (_match, target) => `class relation target "${target}" \u306F\u5B58\u5728\u3057\u307E\u3059\u304C\u3001Class Diagram \u306E\u63CF\u753B\u5BFE\u8C61\u3067\u306F\u306A\u3044\u305F\u3081\u9664\u5916\u3055\u308C\u307E\u3057\u305F\u3002\u30AF\u30E9\u30B9\u56F3\u306E\u69CB\u9020\u95A2\u4FC2\u3067\u306F\u306A\u3044\u5BFE\u5FDC\u306F Mapping \u3067\u306E\u8868\u73FE\u3092\u691C\u8A0E\u3057\u3066\u304F\u3060\u3055\u3044\u3002`],
|
|
[/^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`],
|
|
[/^Domain id is required\.$/, "Domain \u306E id \u304C\u5FC5\u8981\u3067\u3059\u3002"],
|
|
[/^duplicate Domain id "([^"]+)"$/, (_match, id) => `Domain id "${id}" \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^Domain parent "([^"]+)" is not defined\.$/, (_match, parent) => `Domain parent "${parent}" \u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002`],
|
|
[/^Domain "([^"]+)" cannot use itself as parent\.$/, (_match, domain) => `Domain "${domain}" \u306F\u81EA\u5206\u81EA\u8EAB\u3092 parent \u306B\u3067\u304D\u307E\u305B\u3093\u3002`],
|
|
[/^Domain parent cycle detected: (.+)$/, (_match, chain) => `Domain \u306E parent \u304C\u5FAA\u74B0\u3057\u3066\u3044\u307E\u3059: ${chain}`],
|
|
[/^Domain "([^"]+)" is defined in multiple Domains files\.$/, (_match, domain) => `Domain "${domain}" \u304C\u8907\u6570\u306E Domains \u30D5\u30A1\u30A4\u30EB\u3067\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^Domain "([^"]+)" has conflicting (name|kind|parent) values across Domains files\.$/, (_match, domain, field) => `Domain "${domain}" \u306E ${field} \u304C\u8907\u6570\u306E Domains \u30D5\u30A1\u30A4\u30EB\u3067\u4E00\u81F4\u3057\u3066\u3044\u307E\u305B\u3093\u3002`],
|
|
[/^Color Scheme kind is required\.$/, "Color Scheme \u306E kind \u304C\u5FC5\u8981\u3067\u3059\u3002"],
|
|
[/^Color Scheme (fill|stroke|text) "([^"]+)" is not a supported hex color\.$/, (_match, field, value) => `Color Scheme \u306E ${field} "${value}" \u306F\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u308B hex color \u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^duplicate Color Scheme entry for target "([^"]+)" and kind "([^"]+)"$/, (_match, target, kind) => `target "${target}" \u3068 kind "${kind}" \u306E Color Scheme entry \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^Default Color Scheme ref "([^"]+)" could not be resolved\. Built-in colors will be used\.$/, (_match, ref) => `\u65E2\u5B9A\u306E Color Scheme ref "${ref}" \u306E\u53C2\u7167\u5148\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u7D44\u307F\u8FBC\u307F\u8272\u3092\u4F7F\u3044\u307E\u3059\u3002`],
|
|
[/^Default Color Scheme ref "([^"]+)" resolves to type "([^"]+)", but expected type "color_scheme"\. Built-in colors will be used\.$/, (_match, ref, fileType) => `\u65E2\u5B9A\u306E Color Scheme ref "${ref}" \u306F type "${fileType}" \u306B\u89E3\u6C7A\u3055\u308C\u307E\u3057\u305F\u304C\u3001type "color_scheme" \u304C\u5FC5\u8981\u3067\u3059\u3002\u7D44\u307F\u8FBC\u307F\u8272\u3092\u4F7F\u3044\u307E\u3059\u3002`],
|
|
[/^Domain Source ref is required\.$/, "Domain Source \u306E ref \u304C\u5FC5\u8981\u3067\u3059\u3002"],
|
|
[/^Domain Source ref "([^"]+)" could not be resolved\. Check the ID or file name\.$/, (_match, ref) => `Domain Source ref "${ref}" \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`],
|
|
[/^Domain Source ref "([^"]+)" resolves to type "([^"]+)", but expected type "domains"\.$/, (_match, ref, fileType) => `Domain Source ref "${ref}" \u306F type "${fileType}" \u306B\u89E3\u6C7A\u3055\u308C\u307E\u3057\u305F\u304C\u3001type "domains" \u304C\u5FC5\u8981\u3067\u3059\u3002`],
|
|
[/^Domain Diagram has no valid Domain Sources\.$/, "Domain Diagram \u306B\u6709\u52B9\u306A Domain Sources \u304C\u3042\u308A\u307E\u305B\u3093\u3002"],
|
|
[/^Domain Source ref "([^"]+)" has no Domain rows\.$/, (_match, ref) => `Domain Source ref "${ref}" \u306B Domain \u884C\u304C\u3042\u308A\u307E\u305B\u3093\u3002`],
|
|
[/^Domain "([^"]+)" is defined by multiple Domain Diagram sources: "([^"]+)" and "([^"]+)"\.$/, (_match, domain, earlier, later) => `Domain "${domain}" \u304C\u8907\u6570\u306E Domain Diagram source \u3067\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u3059: "${earlier}" \u3068 "${later}"\u3002`],
|
|
[/^Domain "([^"]+)" has conflicting (name|kind|parent) values between Domain Diagram sources "([^"]+)" and "([^"]+)"\.$/, (_match, domain, field, earlier, later) => `Domain "${domain}" \u306E ${field} \u304C Domain Diagram source "${earlier}" \u3068 "${later}" \u3067\u4E00\u81F4\u3057\u3066\u3044\u307E\u305B\u3093\u3002`],
|
|
[/^DFD-local Domain "([^"]+)" is not defined in shared Domains\.$/, (_match, domain) => `DFD\u5185\u306E Domain "${domain}" \u306F\u5171\u901A Domains \u306B\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002`],
|
|
[/^DFD-local Domain "([^"]+)" has (name|kind|parent) "([^"]*)", but shared Domains define \2 "([^"]*)"\.$/, (_match, domain, field, local, shared) => `DFD\u5185\u306E Domain "${domain}" \u306E ${field} \u306F "${local}" \u3067\u3059\u304C\u3001\u5171\u901A Domains \u3067\u306F "${shared}" \u3068\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^DFD-local Domain "([^"]+)" overrides Domain Source (name|kind|parent) "([^"]*)" with "([^"]*)"\.$/, (_match, domain, field, source, local) => `DFD\u5185\u306E Domain "${domain}" \u306F Domain Source \u306E ${field} "${source}" \u3092 "${local}" \u3067\u4E0A\u66F8\u304D\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^DFD object "([^"]+)" references unknown local Domain "([^"]+)"\.$/, (_match, object, domain) => `DFD object "${object}" \u304C\u672A\u5B9A\u7FA9\u306E\u30ED\u30FC\u30AB\u30EB Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^DFD object "([^"]+)" references unknown Domain "([^"]+)"\.$/, (_match, object, domain) => `DFD object "${object}" \u304C\u672A\u5B9A\u7FA9\u306E Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`],
|
|
[/^DFD object "([^"]+)" references Domain "([^"]+)", but this DFD has no local Domains\.$/, (_match, object, domain) => `DFD object "${object}" \u304C Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u304C\u3001\u3053\u306E DFD \u306B\u306F\u30ED\u30FC\u30AB\u30EB Domains \u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002`],
|
|
[/^DFD local object "([^"]+)" is treated as an inline object 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 "([^"]+)" has no kind, and it could not be inferred 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) {
|
|
const title = summary.modelId ?? summary.modelLabel;
|
|
return [
|
|
`# Relationship summary: ${title}`,
|
|
"",
|
|
`Model: ${summary.modelLabel}`,
|
|
`Type: ${summary.modelType}`,
|
|
...summary.modelId ? [`ID: ${summary.modelId}`] : [],
|
|
"",
|
|
formatCategorizedRelationshipSection("## Used by", summary.inboundRelationships),
|
|
"",
|
|
formatCategorizedRelationshipSection("## References", summary.outboundRelationships),
|
|
"",
|
|
...summary.modelType === "codeset" ? [formatValueUsageSection("## Value usage", summary.valueUsages), ""] : [],
|
|
formatUnresolvedSection("## Unresolved", summary.unresolvedOutbound),
|
|
"",
|
|
formatSourceLinkCountSection("## Source links", summary.relatedSourceLinks)
|
|
].join("\n");
|
|
}
|
|
var IMPACT_RELATIONSHIP_CATEGORY_ORDER = [
|
|
"screens",
|
|
"processes",
|
|
"rules",
|
|
"mappings",
|
|
"diagrams",
|
|
"classes",
|
|
"dataEr",
|
|
"other"
|
|
];
|
|
function getImpactRelationshipCategoryKey(relationship) {
|
|
const type = relationship.modelType.replace(/_/g, "-");
|
|
if (type === "screen") {
|
|
return "screens";
|
|
}
|
|
if (type === "app-process" || type === "process") {
|
|
return "processes";
|
|
}
|
|
if (type === "rule") {
|
|
return "rules";
|
|
}
|
|
if (type === "mapping") {
|
|
return "mappings";
|
|
}
|
|
if (["dfd-diagram", "er-diagram", "class-diagram", "domain-diagram", "diagram"].includes(type)) {
|
|
return "diagrams";
|
|
}
|
|
if (type === "class" || type === "object") {
|
|
return "classes";
|
|
}
|
|
if (type === "data-object" || type === "er-entity") {
|
|
return "dataEr";
|
|
}
|
|
const id = relationship.modelId?.toUpperCase() ?? "";
|
|
if (id.startsWith("SCR-")) {
|
|
return "screens";
|
|
}
|
|
if (id.startsWith("PROC-")) {
|
|
return "processes";
|
|
}
|
|
if (id.startsWith("RULE-")) {
|
|
return "rules";
|
|
}
|
|
if (id.startsWith("MAP-")) {
|
|
return "mappings";
|
|
}
|
|
if (id.startsWith("DFD-") || id.startsWith("ERD-") || id.startsWith("CLD-") || id.startsWith("DOMAIN-DIAGRAM-")) {
|
|
return "diagrams";
|
|
}
|
|
if (id.startsWith("CLS-")) {
|
|
return "classes";
|
|
}
|
|
if (id.startsWith("DATA-") || id.startsWith("ENT-")) {
|
|
return "dataEr";
|
|
}
|
|
return "other";
|
|
}
|
|
function collectModelReferences(model) {
|
|
const references = [];
|
|
const add = (raw, relationKind, section, field, notes, sourceContext) => {
|
|
const trimmed = raw?.trim();
|
|
if (!trimmed) {
|
|
return;
|
|
}
|
|
for (const candidate of extractModelReferenceCandidates(trimmed)) {
|
|
references.push({
|
|
raw: candidate,
|
|
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 "domains":
|
|
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) {
|
|
return extractModelReferenceCandidates(reference).length > 0;
|
|
}
|
|
function formatCategorizedRelationshipSection(title, relationships) {
|
|
if (relationships.length === 0) {
|
|
return `${title}
|
|
- none`;
|
|
}
|
|
const lines = [title];
|
|
const groups = groupImpactRelationshipsByCategory(relationships);
|
|
for (const key of IMPACT_RELATIONSHIP_CATEGORY_ORDER) {
|
|
const group = groups.get(key) ?? [];
|
|
if (group.length === 0) {
|
|
continue;
|
|
}
|
|
lines.push("", `### ${getImpactRelationshipCategoryMarkdownLabel(key)}`);
|
|
for (const relationship of dedupeImpactRelationships(group)) {
|
|
const usageText = relationship.usageCount === 1 ? "1 usage" : `${relationship.usageCount} usages`;
|
|
const idText = relationship.modelId && relationship.modelId !== relationship.modelLabel ? ` (${relationship.modelId})` : "";
|
|
lines.push(`- ${relationship.modelLabel}${idText} \u2014 ${usageText}`);
|
|
}
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function groupImpactRelationshipsByCategory(relationships) {
|
|
const groups = /* @__PURE__ */ new Map();
|
|
for (const relationship of relationships) {
|
|
const key = getImpactRelationshipCategoryKey(relationship);
|
|
const group = groups.get(key) ?? [];
|
|
group.push(relationship);
|
|
groups.set(key, group);
|
|
}
|
|
return groups;
|
|
}
|
|
function dedupeImpactRelationships(relationships) {
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const deduped = [];
|
|
for (const relationship of relationships) {
|
|
const key = relationship.modelPath || relationship.modelId || `${relationship.modelType}:${relationship.modelLabel}`;
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
deduped.push(relationship);
|
|
}
|
|
return deduped;
|
|
}
|
|
function getImpactRelationshipCategoryMarkdownLabel(key) {
|
|
switch (key) {
|
|
case "screens":
|
|
return "Screens";
|
|
case "processes":
|
|
return "Processes";
|
|
case "rules":
|
|
return "Rules";
|
|
case "mappings":
|
|
return "Mappings";
|
|
case "diagrams":
|
|
return "Diagrams";
|
|
case "classes":
|
|
return "Classes";
|
|
case "dataEr":
|
|
return "Data / ER";
|
|
case "other":
|
|
default:
|
|
return "Other models";
|
|
}
|
|
}
|
|
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 formatSourceLinkCountSection(title, sourceLinks) {
|
|
return [
|
|
title,
|
|
`- total: ${sourceLinks.length}`
|
|
].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":
|
|
case "domains":
|
|
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/weave-map.ts
|
|
function buildWeaveMapModel(summary, options = {}) {
|
|
const sourceLinkMode = options.sourceLinkMode ?? "compact";
|
|
const focusNodeId = createFocusNodeId(summary);
|
|
const nodes = /* @__PURE__ */ new Map();
|
|
const edges = /* @__PURE__ */ new Map();
|
|
const countedNodes = /* @__PURE__ */ new Map();
|
|
const addNode = (node) => {
|
|
const existing = nodes.get(node.id);
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
nodes.set(node.id, node);
|
|
return node;
|
|
};
|
|
const addCountedNode = (node, notes) => {
|
|
const existing = nodes.get(node.id);
|
|
const accumulator = countedNodes.get(node.id);
|
|
if (existing && accumulator) {
|
|
accumulator.count += 1;
|
|
if (notes) {
|
|
accumulator.notes.add(notes);
|
|
}
|
|
existing.label = appendCount(accumulator.baseLabel, accumulator.count);
|
|
existing.notes = mergeNotes(accumulator.notes);
|
|
return existing;
|
|
}
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
const noteSet = /* @__PURE__ */ new Set();
|
|
if (notes) {
|
|
noteSet.add(notes);
|
|
}
|
|
countedNodes.set(node.id, {
|
|
node,
|
|
baseLabel: node.label,
|
|
count: 1,
|
|
notes: noteSet
|
|
});
|
|
nodes.set(node.id, node);
|
|
return node;
|
|
};
|
|
const addEdge = (edge) => {
|
|
const key = createEdgeAggregationKey(edge);
|
|
const existing = edges.get(key);
|
|
if (existing) {
|
|
existing.count += 1;
|
|
if (edge.notes) {
|
|
existing.notes.add(edge.notes);
|
|
}
|
|
existing.edge.label = appendCount(existing.baseLabel, existing.count);
|
|
existing.edge.notes = mergeNotes(existing.notes);
|
|
return;
|
|
}
|
|
const notes = /* @__PURE__ */ new Set();
|
|
if (edge.notes) {
|
|
notes.add(edge.notes);
|
|
}
|
|
edges.set(key, {
|
|
edge,
|
|
baseLabel: edge.label || edge.relationType,
|
|
count: 1,
|
|
notes
|
|
});
|
|
};
|
|
addNode({
|
|
id: focusNodeId,
|
|
label: summary.modelLabel || summary.modelId || summary.modelPath,
|
|
modelType: summary.modelType,
|
|
layer: getWeaveMapLayerForModelType(summary.modelType),
|
|
path: summary.modelPath,
|
|
modelId: summary.modelId,
|
|
status: "focus"
|
|
});
|
|
summary.outboundRelationships.forEach((relationship, index) => {
|
|
const targetNode = addNode(createModelNode(relationship));
|
|
const relationType = getRelationshipRelationType(relationship, "outbound");
|
|
addEdge({
|
|
id: createEdgeId("outbound", focusNodeId, targetNode.id, index),
|
|
from: focusNodeId,
|
|
to: targetNode.id,
|
|
relationType,
|
|
label: relationType,
|
|
status: "ok",
|
|
notes: formatRelationshipNotes(relationship)
|
|
});
|
|
});
|
|
summary.inboundRelationships.forEach((relationship, index) => {
|
|
const sourceNode = addNode(createModelNode(relationship));
|
|
const relationType = getRelationshipRelationType(relationship, "inbound");
|
|
addEdge({
|
|
id: createEdgeId("inbound", sourceNode.id, focusNodeId, index),
|
|
from: sourceNode.id,
|
|
to: focusNodeId,
|
|
relationType,
|
|
label: relationType,
|
|
status: "ok",
|
|
notes: formatRelationshipNotes(relationship)
|
|
});
|
|
});
|
|
summary.unresolvedOutbound.forEach((reference, index) => {
|
|
const unresolvedNodeId = createUnresolvedNodeId(reference);
|
|
const notes = formatReferenceNotes(reference);
|
|
addCountedNode({
|
|
id: unresolvedNodeId,
|
|
label: reference.targetLabel || reference.targetRaw,
|
|
modelType: "unresolved",
|
|
layer: "Warning",
|
|
status: "unresolved",
|
|
notes
|
|
}, notes);
|
|
addEdge({
|
|
id: createEdgeId("unresolved", focusNodeId, unresolvedNodeId, index),
|
|
from: focusNodeId,
|
|
to: unresolvedNodeId,
|
|
relationType: "unresolved",
|
|
label: reference.relationKind || "unresolved",
|
|
status: "unresolved",
|
|
notes
|
|
});
|
|
});
|
|
if (sourceLinkMode === "compact") {
|
|
const sourceLinksByNode = /* @__PURE__ */ new Map();
|
|
summary.relatedSourceLinks.forEach((sourceLink) => {
|
|
const sourceNodeId = createSourceNodeId(sourceLink);
|
|
const notes = formatSourceLinkNotes(sourceLink);
|
|
addCountedNode({
|
|
id: sourceNodeId,
|
|
label: sourceLink.label || sourceLink.path,
|
|
modelType: "source-link",
|
|
layer: "Source",
|
|
path: sourceLink.path,
|
|
status: "source",
|
|
notes
|
|
}, notes);
|
|
const entry = sourceLinksByNode.get(sourceNodeId);
|
|
if (entry) {
|
|
entry.count += 1;
|
|
if (notes) {
|
|
entry.notes.push(notes);
|
|
}
|
|
return;
|
|
}
|
|
sourceLinksByNode.set(sourceNodeId, {
|
|
nodeId: sourceNodeId,
|
|
notes: notes ? [notes] : [],
|
|
count: 1
|
|
});
|
|
});
|
|
Array.from(sourceLinksByNode.values()).forEach((entry, index) => {
|
|
const edgeLabel = appendCount("source links", entry.count);
|
|
addEdge({
|
|
id: createEdgeId("source", focusNodeId, entry.nodeId, index),
|
|
from: focusNodeId,
|
|
to: entry.nodeId,
|
|
relationType: "source-link",
|
|
label: edgeLabel,
|
|
status: "source",
|
|
notes: mergeNotes(new Set(entry.notes))
|
|
});
|
|
});
|
|
} else {
|
|
summary.relatedSourceLinks.forEach((sourceLink, index) => {
|
|
const sourceNodeId = createSourceNodeId(sourceLink);
|
|
const notes = formatSourceLinkNotes(sourceLink);
|
|
addCountedNode({
|
|
id: sourceNodeId,
|
|
label: sourceLink.label || sourceLink.path,
|
|
modelType: "source-link",
|
|
layer: "Source",
|
|
path: sourceLink.path,
|
|
status: "source",
|
|
notes
|
|
}, notes);
|
|
const ownerNodeId = findSourceOwnerNodeId(sourceLink, nodes) ?? focusNodeId;
|
|
addEdge({
|
|
id: createEdgeId("source", ownerNodeId, sourceNodeId, index),
|
|
from: ownerNodeId,
|
|
to: sourceNodeId,
|
|
relationType: "source-link",
|
|
label: sourceLink.relationKind,
|
|
status: "source",
|
|
notes
|
|
});
|
|
});
|
|
}
|
|
return {
|
|
focusNodeId,
|
|
nodes: Array.from(nodes.values()),
|
|
edges: Array.from(edges.values()).map((entry) => entry.edge)
|
|
};
|
|
}
|
|
function getWeaveMapLayerForModelType(modelType) {
|
|
switch (modelType) {
|
|
case "screen":
|
|
return "UI";
|
|
case "app-process":
|
|
case "app_process":
|
|
return "Process";
|
|
case "rule":
|
|
return "Rule";
|
|
case "codeset":
|
|
return "Rule / State";
|
|
case "message":
|
|
return "UI / Message";
|
|
case "data-object":
|
|
case "data_object":
|
|
case "er-entity":
|
|
case "er_entity":
|
|
return "Data";
|
|
case "mapping":
|
|
return "Mapping";
|
|
case "object":
|
|
case "class":
|
|
case "class-diagram":
|
|
case "class_diagram":
|
|
case "diagram":
|
|
return "Implementation";
|
|
case "dfd-object":
|
|
case "dfd_object":
|
|
case "dfd-diagram":
|
|
case "dfd_diagram":
|
|
return "Data Flow";
|
|
case "relations":
|
|
return "Relationship";
|
|
case "source-link":
|
|
case "source_link":
|
|
return "Source";
|
|
case "unresolved":
|
|
return "Warning";
|
|
default:
|
|
return "Other";
|
|
}
|
|
}
|
|
function createFocusNodeId(summary) {
|
|
return `node:focus:${summary.modelPath || summary.modelId || summary.modelLabel}`;
|
|
}
|
|
function createModelNode(relationship) {
|
|
return {
|
|
id: createModelNodeId(relationship.modelPath, relationship.modelId),
|
|
label: relationship.modelLabel || relationship.modelId || relationship.modelPath,
|
|
modelType: relationship.modelType,
|
|
layer: getWeaveMapLayerForModelType(relationship.modelType),
|
|
path: relationship.modelPath,
|
|
modelId: relationship.modelId,
|
|
status: "ok",
|
|
notes: formatRelationshipNotes(relationship)
|
|
};
|
|
}
|
|
function createModelNodeId(modelPath, modelId) {
|
|
return `node:model:${modelId || modelPath}`;
|
|
}
|
|
function createSourceNodeId(sourceLink) {
|
|
return `node:source:${sourceLink.path.trim()}`;
|
|
}
|
|
function createUnresolvedNodeId(reference) {
|
|
return `node:unresolved:${getReferenceTargetIdentity(reference)}`;
|
|
}
|
|
function createEdgeId(relation, from, to, index) {
|
|
return `edge:${relation}:${from}:${to}:${index}`;
|
|
}
|
|
function getRelationshipRelationType(relationship, fallback) {
|
|
return relationship.usages.find((usage) => usage.relationKind)?.relationKind ?? fallback;
|
|
}
|
|
function getReferenceTargetIdentity(reference) {
|
|
return (reference.targetPath || reference.targetId || reference.targetRaw || reference.targetLabel).trim();
|
|
}
|
|
function createEdgeAggregationKey(edge) {
|
|
return [
|
|
edge.from,
|
|
edge.to,
|
|
edge.status,
|
|
edge.relationType,
|
|
edge.label ?? ""
|
|
].join("\0");
|
|
}
|
|
function appendCount(label, count) {
|
|
return count > 1 ? `${label} \xD7 ${count}` : label;
|
|
}
|
|
function mergeNotes(notes) {
|
|
const merged = Array.from(notes).filter((note) => note.trim());
|
|
return merged.length > 0 ? merged.join("; ") : void 0;
|
|
}
|
|
function formatRelationshipNotes(relationship) {
|
|
const parts = [`${relationship.usageCount} usage${relationship.usageCount === 1 ? "" : "s"}`];
|
|
const sections = uniqueDefined(relationship.usages.map((usage) => usage.section));
|
|
if (sections.length > 0) {
|
|
parts.push(`sections: ${sections.join(", ")}`);
|
|
}
|
|
const fields = uniqueDefined(relationship.usages.map((usage) => usage.field));
|
|
if (fields.length > 0) {
|
|
parts.push(`fields: ${fields.join(", ")}`);
|
|
}
|
|
return parts.join("; ");
|
|
}
|
|
function formatReferenceNotes(reference) {
|
|
const parts = [
|
|
reference.section ? `section: ${reference.section}` : void 0,
|
|
reference.field ? `field: ${reference.field}` : void 0,
|
|
reference.sourceContext ? `context: ${reference.sourceContext}` : void 0,
|
|
reference.notes
|
|
].filter((part) => Boolean(part));
|
|
return parts.length > 0 ? parts.join("; ") : void 0;
|
|
}
|
|
function formatSourceLinkNotes(sourceLink) {
|
|
const parts = [
|
|
`owner: ${sourceLink.ownerLabel}`,
|
|
`path: ${sourceLink.path}`,
|
|
...sourceLink.notes
|
|
].filter((part) => part.trim());
|
|
return parts.length > 0 ? parts.join("; ") : void 0;
|
|
}
|
|
function findSourceOwnerNodeId(sourceLink, nodes) {
|
|
for (const node of nodes.values()) {
|
|
if (sourceLink.ownerPath && node.path === sourceLink.ownerPath || sourceLink.ownerId && node.modelId === sourceLink.ownerId) {
|
|
return node.id;
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
function uniqueDefined(values) {
|
|
return Array.from(new Set(values.filter((value) => Boolean(value))));
|
|
}
|
|
|
|
// 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",
|
|
"## Colors": "Colors",
|
|
"## 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",
|
|
"## Domain Sources": "Domain Sources",
|
|
"## Flows": "Flows",
|
|
"## Objects": "Objects",
|
|
"## Domains": "Domains",
|
|
"## 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/frontmatter-parser.ts
|
|
function parseFrontmatter(markdown) {
|
|
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
const warnings = [];
|
|
if (!normalized.startsWith("---\n")) {
|
|
return {
|
|
file: {
|
|
body: normalized
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
const lines = normalized.split("\n");
|
|
let closingIndex = -1;
|
|
for (let index = 1; index < lines.length; index += 1) {
|
|
if (lines[index].trim() === "---") {
|
|
closingIndex = index;
|
|
break;
|
|
}
|
|
}
|
|
if (closingIndex === -1) {
|
|
warnings.push(createWarning("frontmatter parse error: missing closing delimiter"));
|
|
return {
|
|
file: {
|
|
body: normalized
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
const frontmatterLines = lines.slice(1, closingIndex);
|
|
const body = lines.slice(closingIndex + 1).join("\n");
|
|
const parsed = parseYamlLikeFrontmatter(frontmatterLines);
|
|
warnings.push(...parsed.warnings);
|
|
return {
|
|
file: {
|
|
frontmatter: parsed.frontmatter,
|
|
body
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function parseYamlLikeFrontmatter(lines) {
|
|
const warnings = [];
|
|
const frontmatter = {};
|
|
let activeListKey = null;
|
|
for (const rawLine of lines) {
|
|
const trimmed = rawLine.trim();
|
|
if (!trimmed || trimmed.startsWith("#")) {
|
|
continue;
|
|
}
|
|
const listItemMatch = rawLine.match(/^\s*-\s+(.+)$/);
|
|
if (listItemMatch) {
|
|
if (!activeListKey) {
|
|
warnings.push(
|
|
createWarning(
|
|
`frontmatter parse error: unexpected list item "${trimmed}"`
|
|
)
|
|
);
|
|
continue;
|
|
}
|
|
const currentValue = frontmatter[activeListKey];
|
|
if (!Array.isArray(currentValue)) {
|
|
frontmatter[activeListKey] = [];
|
|
}
|
|
frontmatter[activeListKey].push(
|
|
parseScalarValue(listItemMatch[1].trim())
|
|
);
|
|
continue;
|
|
}
|
|
activeListKey = null;
|
|
const keyValueMatch = rawLine.match(/^\s*([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
if (!keyValueMatch) {
|
|
warnings.push(
|
|
createWarning(`frontmatter parse error: malformed line "${trimmed}"`)
|
|
);
|
|
continue;
|
|
}
|
|
const [, key, rawValue] = keyValueMatch;
|
|
const value = rawValue.trim();
|
|
if (!value) {
|
|
frontmatter[key] = [];
|
|
activeListKey = key;
|
|
continue;
|
|
}
|
|
frontmatter[key] = parseScalarValue(value);
|
|
}
|
|
return {
|
|
frontmatter,
|
|
warnings
|
|
};
|
|
}
|
|
function parseScalarValue(value) {
|
|
if (value === "true") {
|
|
return true;
|
|
}
|
|
if (value === "false") {
|
|
return false;
|
|
}
|
|
if (/^\[(.*)\]$/.test(value)) {
|
|
const inner = value.slice(1, -1).trim();
|
|
if (!inner) {
|
|
return [];
|
|
}
|
|
return inner.split(",").map((entry) => stripQuotes(entry.trim()));
|
|
}
|
|
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
return Number(value);
|
|
}
|
|
return stripQuotes(value);
|
|
}
|
|
function stripQuotes(value) {
|
|
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
return value.slice(1, -1);
|
|
}
|
|
return value;
|
|
}
|
|
function createWarning(message) {
|
|
return {
|
|
code: "frontmatter-parse-error",
|
|
message,
|
|
severity: "warning"
|
|
};
|
|
}
|
|
|
|
// src/parsers/markdown-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/domains-parser.ts
|
|
var DOMAIN_HEADERS = ["id", "name", "kind", "parent", "description"];
|
|
function parseDomainsFile(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 description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
|
|
if (frontmatter.type !== "domains") {
|
|
warnings.push(createWarning3(path2, "type", 'expected type "domains"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning3(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
const domainsTable = parseDomainEntries(sections.Domains, path2);
|
|
warnings.push(...domainsTable.warnings);
|
|
warnings.push(...validateDomainEntries(path2, domainsTable.rows));
|
|
const fallbackTitle = name || id || getFileStem(path2) || "Untitled Domains";
|
|
return {
|
|
file: {
|
|
fileType: "domains",
|
|
schema: "domains",
|
|
path: path2,
|
|
title: fallbackTitle,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: fallbackTitle,
|
|
description: description || void 0,
|
|
domains: domainsTable.rows
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function parseDomainEntries(lines, path2) {
|
|
const table = parseMarkdownTable(lines, DOMAIN_HEADERS, path2, "Domains");
|
|
const warnings = [...table.warnings];
|
|
const rows = [];
|
|
const seenIds = /* @__PURE__ */ new Set();
|
|
table.rows.forEach((row, rowIndex) => {
|
|
const id = row.id?.trim() ?? "";
|
|
const name = row.name?.trim() ?? "";
|
|
const kind = row.kind?.trim() ?? "";
|
|
const parent = row.parent?.trim() ?? "";
|
|
const description = row.description?.trim() ?? "";
|
|
if (!id) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatDomainIdRequiredMessage(),
|
|
severity: "error",
|
|
path: path2,
|
|
field: "Domains.id",
|
|
context: { rowIndex: rowIndex + 1 }
|
|
});
|
|
return;
|
|
}
|
|
if (seenIds.has(id)) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatDuplicateDomainIdMessage(id),
|
|
severity: "error",
|
|
path: path2,
|
|
field: "Domains.id",
|
|
context: { rowIndex: rowIndex + 1 }
|
|
});
|
|
return;
|
|
}
|
|
seenIds.add(id);
|
|
rows.push({
|
|
id,
|
|
name: name || void 0,
|
|
kind: kind || void 0,
|
|
parent: parent || void 0,
|
|
description: description || void 0,
|
|
rowIndex
|
|
});
|
|
});
|
|
return { rows, warnings };
|
|
}
|
|
function validateDomainEntries(path2, domains, options = {}) {
|
|
const warnings = [];
|
|
const domainIds = new Set(domains.map((domain) => domain.id));
|
|
for (const domain of domains) {
|
|
if (!domain.parent) {
|
|
continue;
|
|
}
|
|
if (domain.parent === domain.id) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatDomainSelfParentMessage(domain.id),
|
|
severity: "error",
|
|
path: path2,
|
|
field: "Domains.parent",
|
|
context: { rowIndex: domain.rowIndex + 1 }
|
|
});
|
|
continue;
|
|
}
|
|
if (!domainIds.has(domain.parent) && !options.skipUnknownParents) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: formatDomainParentUnknownMessage(domain.parent),
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "Domains.parent",
|
|
context: { rowIndex: domain.rowIndex + 1 }
|
|
});
|
|
}
|
|
}
|
|
warnings.push(...validateDomainCycles(path2, domains));
|
|
return warnings;
|
|
}
|
|
function validateDomainCycles(path2, domains) {
|
|
const warnings = [];
|
|
const byId = new Map(domains.map((domain) => [domain.id, domain]));
|
|
const reported = /* @__PURE__ */ new Set();
|
|
for (const domain of domains) {
|
|
if (domain.parent === domain.id) {
|
|
continue;
|
|
}
|
|
const chain = [];
|
|
const seen = /* @__PURE__ */ new Set();
|
|
let current = domain;
|
|
while (current?.parent) {
|
|
chain.push(current.id);
|
|
if (seen.has(current.parent)) {
|
|
const cycleStart = chain.indexOf(current.parent);
|
|
const cycleIds = cycleStart >= 0 ? chain.slice(cycleStart) : [current.parent, current.id];
|
|
const cycleKey = [...new Set(cycleIds)].sort().join(">");
|
|
if (!reported.has(cycleKey)) {
|
|
reported.add(cycleKey);
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatDomainParentCycleMessage([...cycleIds, current.parent]),
|
|
severity: "error",
|
|
path: path2,
|
|
field: "Domains.parent",
|
|
context: { rowIndex: domain.rowIndex + 1 }
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
seen.add(current.id);
|
|
current = byId.get(current.parent);
|
|
}
|
|
}
|
|
return warnings;
|
|
}
|
|
function createWarning3(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
function getFileStem(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
|
|
// 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 domainResolution = resolveDfdDiagramDomains(diagram, index);
|
|
warnings.push(...domainResolution.warnings);
|
|
const resolvedDiagram = {
|
|
...diagram,
|
|
domainSourceSummaries: domainResolution.sourceSummaries,
|
|
domains: domainResolution.domains
|
|
};
|
|
const objectResolution = resolveDfdDiagramObjects(resolvedDiagram, index, {
|
|
hasDomainSources: diagram.domainSources.length > 0
|
|
});
|
|
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: resolvedDiagram,
|
|
nodes: objectResolution.nodes,
|
|
edges,
|
|
missingObjects: objectResolution.missingObjects,
|
|
warnings: [...warnings, ...objectResolution.warnings]
|
|
};
|
|
}
|
|
function resolveDfdDiagramDomains(diagram, index) {
|
|
const warnings = [];
|
|
const sourceSummaries = [];
|
|
const effectiveById = /* @__PURE__ */ new Map();
|
|
const order = [];
|
|
const validSources = [];
|
|
for (const sourceRef of diagram.domainSources) {
|
|
const resolved = findModelByReference(sourceRef.ref, index);
|
|
if (!resolved) {
|
|
sourceSummaries.push({
|
|
ref: sourceRef,
|
|
status: "unresolved",
|
|
domainCount: 0
|
|
});
|
|
warnings.push(createDfdDomainSourceWarning(
|
|
diagram.path,
|
|
sourceRef.rowIndex,
|
|
formatDomainDiagramUnresolvedSourceMessage(sourceRef.ref),
|
|
"unresolved-reference"
|
|
));
|
|
continue;
|
|
}
|
|
if (resolved.fileType !== "domains") {
|
|
sourceSummaries.push({
|
|
ref: sourceRef,
|
|
resolvedPath: resolved.path,
|
|
status: "invalid-type",
|
|
domainCount: 0
|
|
});
|
|
warnings.push(createDfdDomainSourceWarning(
|
|
diagram.path,
|
|
sourceRef.rowIndex,
|
|
formatDomainDiagramInvalidSourceTypeMessage(sourceRef.ref, resolved.fileType),
|
|
"invalid-structure"
|
|
));
|
|
continue;
|
|
}
|
|
sourceSummaries.push({
|
|
ref: sourceRef,
|
|
resolvedPath: resolved.path,
|
|
resolvedId: resolved.id,
|
|
status: resolved.domains.length > 0 ? "ok" : "empty",
|
|
domainCount: resolved.domains.length
|
|
});
|
|
if (resolved.domains.length === 0) {
|
|
warnings.push(createDfdDomainSourceWarning(
|
|
diagram.path,
|
|
sourceRef.rowIndex,
|
|
formatDomainDiagramEmptySourceMessage(sourceRef.ref),
|
|
"invalid-structure"
|
|
));
|
|
}
|
|
validSources.push({ source: resolved, ref: sourceRef.ref });
|
|
}
|
|
const sourceMerge = mergeDomainDiagramSources(validSources, diagram.path);
|
|
warnings.push(...sourceMerge.warnings);
|
|
for (const domain of sourceMerge.domains) {
|
|
order.push(domain.id);
|
|
effectiveById.set(domain.id, {
|
|
domain: { ...domain },
|
|
sourcePath: diagram.path
|
|
});
|
|
}
|
|
for (const domain of diagram.domains ?? []) {
|
|
const previous = effectiveById.get(domain.id);
|
|
if (!previous) {
|
|
order.push(domain.id);
|
|
effectiveById.set(domain.id, { domain: { ...domain }, sourcePath: diagram.path });
|
|
continue;
|
|
}
|
|
for (const field of ["name", "kind", "parent"]) {
|
|
const localValue = domain[field]?.trim() ?? "";
|
|
const sourceValue = previous.domain[field]?.trim() ?? "";
|
|
if (localValue && sourceValue && localValue !== sourceValue) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatDfdLocalDomainOverridesSourceMessage(
|
|
domain.id,
|
|
field,
|
|
localValue,
|
|
sourceValue
|
|
),
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: `Domains.${field}`,
|
|
context: { rowIndex: domain.rowIndex + 1 }
|
|
});
|
|
}
|
|
}
|
|
effectiveById.set(domain.id, { domain: { ...domain }, sourcePath: diagram.path });
|
|
}
|
|
const domains = order.map((id) => effectiveById.get(id)?.domain).filter((domain) => Boolean(domain));
|
|
warnings.push(...validateDomainEntries(diagram.path, domains));
|
|
return { domains, sourceSummaries, warnings };
|
|
}
|
|
function createDfdDomainSourceWarning(path2, rowIndex, message, code) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "Domain Sources.ref",
|
|
context: { rowIndex: rowIndex + 1 }
|
|
};
|
|
}
|
|
function resolveDfdDiagramObjects(diagram, index, domainContext) {
|
|
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"
|
|
}));
|
|
const localDomainIds = new Set((diagram.domains ?? []).map((domain) => domain.id));
|
|
for (const entry of entries) {
|
|
const ref = entry.ref?.trim();
|
|
const resolvedObject = ref ? resolveDfdObjectReference(ref, index) ?? void 0 : void 0;
|
|
const resolvedIdentity = ref ? resolveReferenceIdentity(ref, index) : void 0;
|
|
if (!ref) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: `DFD local object "${entry.id ?? entry.label ?? entry.rowIndex + 1}" is treated as an inline object without ref.`,
|
|
severity: "info",
|
|
path: diagram.path,
|
|
field: "Objects",
|
|
context: { rowIndex: entry.rowIndex + 1 }
|
|
});
|
|
} else if (!resolvedObject && !resolvedIdentity?.resolvedModel) {
|
|
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}" has no kind, and it could not be inferred 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 domain = entry.domain?.trim();
|
|
if (domain && localDomainIds.size === 0 && !domainContext.hasDomainSources) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: formatDfdObjectDomainWithoutLocalDomainsMessage(
|
|
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
|
domain
|
|
),
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Objects.domain",
|
|
context: { rowIndex: entry.rowIndex + 1 }
|
|
});
|
|
} else if (domain && !localDomainIds.has(domain)) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: domainContext.hasDomainSources ? formatDfdObjectUnknownDomainMessage(
|
|
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
|
domain
|
|
) : formatDfdObjectUnknownLocalDomainMessage(
|
|
entry.id ?? ref ?? String(entry.rowIndex + 1),
|
|
domain
|
|
),
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Objects.domain",
|
|
context: { rowIndex: entry.rowIndex + 1 }
|
|
});
|
|
}
|
|
const node = {
|
|
id: nodeId,
|
|
ref,
|
|
label: resolvedLabel,
|
|
kind: effectiveKind,
|
|
metadata: {
|
|
notes: entry.notes,
|
|
domain,
|
|
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) {
|
|
const sourceIdentity = sourceObject ? void 0 : resolveReferenceIdentity(edge.source, index);
|
|
const targetIdentity = targetObject ? void 0 : resolveReferenceIdentity(edge.target, index);
|
|
const sourceEndpointExists = Boolean(sourceObject || sourceIdentity?.resolvedModel);
|
|
const targetEndpointExists = Boolean(targetObject || targetIdentity?.resolvedModel);
|
|
if (sourceEndpointExists && targetEndpointExists) {
|
|
pushClassRelationTargetNotDiagramCompatibleWarnings(
|
|
warnings,
|
|
diagram.path,
|
|
[
|
|
{ reference: edge.source, object: sourceObject, identity: sourceIdentity },
|
|
{ reference: edge.target, object: targetObject, identity: targetIdentity }
|
|
]
|
|
);
|
|
return false;
|
|
}
|
|
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) {
|
|
const sourceIdentity = sourceObject ? void 0 : resolveReferenceIdentity(relation.source, index);
|
|
const targetIdentity = targetObject ? void 0 : resolveReferenceIdentity(relation.target, index);
|
|
const sourceEndpointExists = Boolean(sourceObject || sourceIdentity?.resolvedModel);
|
|
const targetEndpointExists = Boolean(targetObject || targetIdentity?.resolvedModel);
|
|
if (sourceEndpointExists && targetEndpointExists) {
|
|
pushClassRelationTargetNotDiagramCompatibleWarnings(
|
|
warnings,
|
|
diagram.path,
|
|
[
|
|
{ reference: relation.source, object: sourceObject, identity: sourceIdentity },
|
|
{ reference: relation.target, object: targetObject, identity: targetIdentity }
|
|
]
|
|
);
|
|
seenRelationIds.add(relationKey);
|
|
continue;
|
|
}
|
|
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;
|
|
}
|
|
const sourceIdentity = sourceObject ? void 0 : resolveReferenceIdentity(relation.sourceClass, index);
|
|
const targetIdentity = targetObject ? void 0 : resolveReferenceIdentity(relation.targetClass, index);
|
|
const sourceEndpointExists = Boolean(sourceObject || sourceIdentity?.resolvedModel);
|
|
const targetEndpointExists = Boolean(targetObject || targetIdentity?.resolvedModel);
|
|
if (!sourceObject || !targetObject) {
|
|
if (sourceEndpointExists && targetEndpointExists) {
|
|
pushClassRelationTargetNotDiagramCompatibleWarnings(
|
|
warnings,
|
|
diagram.path,
|
|
[
|
|
{ reference: relation.sourceClass, object: sourceObject, identity: sourceIdentity },
|
|
{ reference: relation.targetClass, object: targetObject, identity: targetIdentity }
|
|
]
|
|
);
|
|
seenRelationIds.add(relationKey);
|
|
continue;
|
|
}
|
|
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 pushClassRelationTargetNotDiagramCompatibleWarnings(warnings, path2, endpoints) {
|
|
for (const endpoint of endpoints) {
|
|
if (endpoint.object || !endpoint.identity?.resolvedModel) {
|
|
continue;
|
|
}
|
|
warnings.push({
|
|
code: "class-relation-target-not-diagram-compatible",
|
|
message: formatClassRelationTargetNotDiagramCompatibleMessage2(
|
|
getReferenceDiagnosticLabel2(endpoint.reference, endpoint.identity)
|
|
),
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "relations"
|
|
});
|
|
}
|
|
}
|
|
function getReferenceDiagnosticLabel2(reference, identity) {
|
|
return identity?.resolvedId ?? identity?.target ?? parseReferenceValue(reference)?.target ?? reference.trim();
|
|
}
|
|
function formatClassRelationTargetNotDiagramCompatibleMessage2(target) {
|
|
return `class relation target "${target}" exists, but is not compatible with Class Diagram rendering and was excluded. Consider representing non-structural cross-model relationships with Mapping.`;
|
|
}
|
|
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 VALID_DOMAIN_RENDER_MODES = /* @__PURE__ */ new Set([
|
|
"mindmap",
|
|
"area",
|
|
"tree"
|
|
]);
|
|
var TABLE_TEXT_FORMATS = /* @__PURE__ */ new Set([
|
|
"data-object",
|
|
"app-process",
|
|
"rule",
|
|
"codeset",
|
|
"message",
|
|
"mapping"
|
|
]);
|
|
function resolveRenderMode(input) {
|
|
const diagnostics = [];
|
|
const toolbarMode = normalizeRenderModeForFormat(
|
|
input.toolbarOverride,
|
|
input.formatType
|
|
);
|
|
const frontmatterMode = normalizeFrontmatterRenderMode(
|
|
input.frontmatterRenderMode,
|
|
input.formatType,
|
|
input.filePath,
|
|
diagnostics
|
|
);
|
|
const settingsMode = normalizeRenderModeForFormat(
|
|
input.settingsDefaultRenderMode,
|
|
input.formatType
|
|
);
|
|
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";
|
|
case "domains":
|
|
case "domain-diagram":
|
|
return "mindmap";
|
|
default:
|
|
return "custom";
|
|
}
|
|
}
|
|
function getSupportedRenderModes(formatType, modelKind) {
|
|
return getForcedRenderModes(formatType, modelKind);
|
|
}
|
|
function getForcedRenderModes(formatType, modelKind) {
|
|
switch (formatType) {
|
|
case "domains":
|
|
case "domain-diagram":
|
|
return ["mindmap", "area", "tree"];
|
|
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 normalizeDomainRenderMode(value) {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
const normalized = value.trim().toLowerCase();
|
|
return VALID_DOMAIN_RENDER_MODES.has(normalized) ? normalized : null;
|
|
}
|
|
function normalizeRenderModeForFormat(value, formatType) {
|
|
if (formatType === "domains" || formatType === "domain-diagram") {
|
|
return normalizeDomainRenderMode(value);
|
|
}
|
|
return normalizeRenderMode(value);
|
|
}
|
|
function normalizeFrontmatterRenderMode(value, formatType, 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 = normalizeRenderModeForFormat(normalized, formatType);
|
|
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 (formatType === "domains" || formatType === "domain-diagram") {
|
|
return "mermaid";
|
|
}
|
|
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",
|
|
color_scheme: "color-scheme",
|
|
domains: "domains",
|
|
domain_diagram: "domain-diagram",
|
|
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/core/supported-formats.ts
|
|
var SUPPORTED_MODEL_WEAVE_FORMATS = [
|
|
"class",
|
|
"class_diagram",
|
|
"er_entity",
|
|
"er_diagram",
|
|
"dfd_object",
|
|
"dfd_diagram",
|
|
"data_object",
|
|
"app_process",
|
|
"screen",
|
|
"rule",
|
|
"codeset",
|
|
"message",
|
|
"mapping",
|
|
"color_scheme",
|
|
"domains",
|
|
"domain_diagram"
|
|
];
|
|
var SUPPORTED_MODEL_WEAVE_FORMAT_LIST = SUPPORTED_MODEL_WEAVE_FORMATS.join(" / ");
|
|
var SUPPORTED_MODEL_WEAVE_FILE_TYPES = /* @__PURE__ */ new Set([
|
|
"object",
|
|
"er-entity",
|
|
"diagram",
|
|
"dfd-object",
|
|
"dfd-diagram",
|
|
"data-object",
|
|
"app-process",
|
|
"screen",
|
|
"rule",
|
|
"codeset",
|
|
"message",
|
|
"mapping",
|
|
"color-scheme",
|
|
"domains",
|
|
"domain-diagram"
|
|
]);
|
|
function isModelWeavePreviewSupportedFileType(fileType) {
|
|
return SUPPORTED_MODEL_WEAVE_FILE_TYPES.has(fileType);
|
|
}
|
|
|
|
// src/editor/model-weave-editor-suggest.ts
|
|
var import_obsidian2 = require("obsidian");
|
|
|
|
// 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/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(
|
|
createWarning4(
|
|
"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 || getFileStem2(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 getFileStem2(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(
|
|
createWarning4(
|
|
"invalid-structure",
|
|
`ER relation id looks incomplete: ${currentId}`,
|
|
path2,
|
|
"Relations"
|
|
)
|
|
);
|
|
}
|
|
if (seenIds.has(currentId)) {
|
|
warnings.push(
|
|
createWarning4(
|
|
"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(
|
|
createWarning4(
|
|
"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(
|
|
createWarning4(
|
|
"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(
|
|
createWarning4(
|
|
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 createWarning4(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 = Array.from({ length: 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() || getFileStem3(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 getFileStem3(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() {
|
|
const mermaid = await (0, import_obsidian3.loadMermaid)();
|
|
if (!isMermaidAdapter(mermaid)) {
|
|
throw new Error("Obsidian Mermaid adapter is unavailable.");
|
|
}
|
|
return mermaid;
|
|
}
|
|
function isMermaidAdapter(value) {
|
|
return typeof value === "object" && value !== null && "render" in value && typeof value.render === "function";
|
|
}
|
|
|
|
// 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) => {
|
|
if (!event.ctrlKey && !event.metaKey) {
|
|
return;
|
|
}
|
|
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();
|
|
});
|
|
const targetWindow = canvas.ownerDocument.defaultView ?? window;
|
|
targetWindow.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, options = {}) {
|
|
const toolbar = activeDocument.createElement("div");
|
|
toolbar.className = "mdspec-zoom-toolbar model-weave-zoom-toolbar";
|
|
const leftGroup = activeDocument.createElement("div");
|
|
leftGroup.addClass("model-weave-zoom-toolbar-left");
|
|
const help = activeDocument.createElement("div");
|
|
help.addClass("model-weave-zoom-toolbar-help");
|
|
help.textContent = helpText;
|
|
leftGroup.appendChild(help);
|
|
const rightGroup = activeDocument.createElement("div");
|
|
rightGroup.addClass("model-weave-zoom-toolbar-right");
|
|
const controls = activeDocument.createElement("div");
|
|
controls.addClass("model-weave-zoom-toolbar-controls");
|
|
const zoomOutButton = createToolbarButton("\u2212");
|
|
const fitButton = createToolbarButton("Fit");
|
|
fitButton.addClass("model-weave-zoom-toolbar-fit");
|
|
const zoomLabel = activeDocument.createElement("span");
|
|
zoomLabel.addClass("model-weave-zoom-toolbar-label");
|
|
zoomLabel.textContent = "100%";
|
|
const zoomInButton = createToolbarButton("+");
|
|
const resetButton = createToolbarButton("100%");
|
|
const exportPngButton = options.onExportPng ? createToolbarButton("PNG") : null;
|
|
const exportAndOpenPngButton = options.onExportAndOpenPng ? createToolbarButton("PNG\u2197") : null;
|
|
if (exportPngButton) {
|
|
exportPngButton.addClass("model-weave-zoom-toolbar-export-png");
|
|
exportPngButton.setAttribute("aria-label", options.exportPngLabel ?? "Export as PNG");
|
|
exportPngButton.title = options.exportPngTitle ?? options.exportPngLabel ?? "Export as PNG";
|
|
exportPngButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
void options.onExportPng?.();
|
|
});
|
|
}
|
|
if (exportAndOpenPngButton) {
|
|
exportAndOpenPngButton.addClass("model-weave-zoom-toolbar-export-open-png");
|
|
exportAndOpenPngButton.setAttribute(
|
|
"aria-label",
|
|
options.exportAndOpenPngLabel ?? "Export PNG and open"
|
|
);
|
|
exportAndOpenPngButton.title = options.exportAndOpenPngTitle ?? options.exportAndOpenPngLabel ?? "Export PNG and open";
|
|
exportAndOpenPngButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
void options.onExportAndOpenPng?.();
|
|
});
|
|
}
|
|
controls.append(
|
|
zoomOutButton,
|
|
fitButton,
|
|
zoomLabel,
|
|
zoomInButton,
|
|
resetButton,
|
|
...exportPngButton ? [exportPngButton] : [],
|
|
...exportAndOpenPngButton ? [exportAndOpenPngButton] : []
|
|
);
|
|
rightGroup.appendChild(controls);
|
|
toolbar.append(leftGroup, rightGroup);
|
|
return {
|
|
root: toolbar,
|
|
zoomOutButton,
|
|
fitButton,
|
|
zoomLabel,
|
|
zoomInButton,
|
|
resetButton,
|
|
exportPngButton,
|
|
exportAndOpenPngButton,
|
|
leftGroup,
|
|
rightGroup
|
|
};
|
|
}
|
|
function createToolbarButton(label) {
|
|
const button = activeDocument.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 = activeDocument.createElement("section");
|
|
root.className = `${options.className} model-weave-mermaid-shell`;
|
|
if (options.title) {
|
|
const title = activeDocument.createElement("h2");
|
|
title.textContent = options.title;
|
|
title.title = options.title;
|
|
title.addClass("model-weave-mermaid-title");
|
|
root.appendChild(title);
|
|
}
|
|
const canvas = activeDocument.createElement("div");
|
|
canvas.addClass("model-weave-graph-canvas");
|
|
if (!options.forExport) {
|
|
canvas.addClass("model-weave-graph-canvas-interactive");
|
|
}
|
|
const toolbar = options.forExport ? null : createZoomToolbar("Ctrl/Meta + wheel: zoom / Drag background: pan", {
|
|
onExportPng: options.onExportPng,
|
|
onExportAndOpenPng: options.onExportAndOpenPng,
|
|
exportPngLabel: options.exportPngLabel,
|
|
exportPngTitle: options.exportPngTitle,
|
|
exportAndOpenPngLabel: options.exportAndOpenPngLabel,
|
|
exportAndOpenPngTitle: options.exportAndOpenPngTitle
|
|
});
|
|
if (toolbar) {
|
|
root.appendChild(toolbar.root);
|
|
}
|
|
const viewport = activeDocument.createElement("div");
|
|
viewport.addClass("model-weave-graph-viewport");
|
|
const surface = activeDocument.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(shell3, options) {
|
|
if (options.showSourcePanel !== false) {
|
|
appendMermaidSourcePanel(
|
|
options.sourcePanelContainer ?? shell3.root,
|
|
options.source,
|
|
options.sourcePanelPlacement,
|
|
{
|
|
title: options.sourcePanelTitle,
|
|
copyLabel: options.sourcePanelCopyLabel
|
|
}
|
|
);
|
|
}
|
|
const debug = options.showRenderDebug ? appendMermaidRenderDebugPanel(
|
|
options.renderDebugContainer ?? shell3.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 } = shell3;
|
|
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(shell3.surface)
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
function appendMermaidSourcePanel(container, source, placement = "append", labels) {
|
|
const fencedSource = `\`\`\`mermaid
|
|
${source}
|
|
\`\`\``;
|
|
const root = container.ownerDocument.createElement("details");
|
|
root.addClass("model-weave-preview-section");
|
|
root.addClass("model-weave-mermaid-source-panel");
|
|
const summary = container.ownerDocument.createElement("summary");
|
|
summary.textContent = labels?.title ?? modelWeaveText("Mermaid source", "Mermaid \u30BD\u30FC\u30B9");
|
|
summary.addClass("model-weave-preview-section-title");
|
|
root.appendChild(summary);
|
|
const actions = container.ownerDocument.createElement("div");
|
|
actions.addClass("model-weave-mermaid-source-actions");
|
|
const copyButton = container.ownerDocument.createElement("button");
|
|
copyButton.type = "button";
|
|
copyButton.textContent = labels?.copyLabel ?? modelWeaveText("Copy Mermaid", "Mermaid \u3092\u30B3\u30D4\u30FC");
|
|
copyButton.addClass("model-weave-secondary-button");
|
|
copyButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
void navigator.clipboard?.writeText(fencedSource);
|
|
});
|
|
actions.appendChild(copyButton);
|
|
root.appendChild(actions);
|
|
const pre = container.ownerDocument.createElement("pre");
|
|
pre.addClass("model-weave-mermaid-source-code");
|
|
const code = container.ownerDocument.createElement("code");
|
|
code.textContent = fencedSource;
|
|
pre.appendChild(code);
|
|
root.appendChild(pre);
|
|
placePanel(container, root, placement);
|
|
}
|
|
function appendMermaidRenderDebugPanel(container, placement = "append") {
|
|
const root = container.ownerDocument.createElement("details");
|
|
root.addClass("model-weave-preview-section");
|
|
root.addClass("model-weave-mermaid-render-debug");
|
|
const summary = container.ownerDocument.createElement("summary");
|
|
summary.textContent = modelWeaveText(
|
|
"Mermaid render debug",
|
|
"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);
|
|
if (!importedSvg.instanceOf(SVGSVGElement)) {
|
|
throw new Error("Mermaid SVG import did not produce an SVG element.");
|
|
}
|
|
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.instanceOf(SVGSVGElement) ? parsedSvg : null;
|
|
}
|
|
}
|
|
const htmlDocument = parser.parseFromString(svgMarkup, "text/html");
|
|
const htmlSvg = htmlDocument.body.querySelector("svg");
|
|
if (!htmlSvg) {
|
|
return null;
|
|
}
|
|
return htmlSvg.instanceOf(SVGSVGElement) ? htmlSvg : null;
|
|
}
|
|
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 = activeDocument.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 activeDocument.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 DOMAIN_VIEW_MODE_SETTING_OPTIONS = [
|
|
{ value: "mindmap", label: "Mindmap" },
|
|
{ value: "area", label: "Area" },
|
|
{ value: "tree", label: "Tree" }
|
|
];
|
|
var DEFAULT_MODEL_WEAVE_SETTINGS = {
|
|
defaultClassRenderMode: "custom",
|
|
defaultErRenderMode: "custom",
|
|
defaultDfdRenderMode: "mermaid",
|
|
defaultProcessRenderMode: "custom",
|
|
defaultScreenRenderMode: "custom",
|
|
defaultDomainsViewMode: "mindmap",
|
|
defaultDomainDiagramViewMode: "mindmap",
|
|
defaultZoom: "fit",
|
|
fontSize: "normal",
|
|
nodeDensity: "normal",
|
|
localSourceRoot: "",
|
|
defaultColorSchemeRef: "",
|
|
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_DOMAIN_VIEW_MODES = /* @__PURE__ */ new Set([
|
|
"mindmap",
|
|
"area",
|
|
"tree"
|
|
]);
|
|
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
|
|
),
|
|
defaultDomainsViewMode: normalizeEnumValue(
|
|
raw.defaultDomainsViewMode,
|
|
VALID_DOMAIN_VIEW_MODES,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.defaultDomainsViewMode
|
|
),
|
|
defaultDomainDiagramViewMode: normalizeEnumValue(
|
|
raw.defaultDomainDiagramViewMode,
|
|
VALID_DOMAIN_VIEW_MODES,
|
|
DEFAULT_MODEL_WEAVE_SETTINGS.defaultDomainDiagramViewMode
|
|
),
|
|
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),
|
|
defaultColorSchemeRef: normalizeStringValue(raw.defaultColorSchemeRef),
|
|
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;
|
|
}
|
|
const normalized = value.trim().toLowerCase();
|
|
return allowed.has(normalized) ? normalized : 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
|
|
|
|
| id | label | kind | ref | domain | notes |
|
|
|---|---|---|---|---|---|
|
|
| EXTERNAL | External System | external | | | Local object |
|
|
| PROCESS | Sample Process | process | [[DFD-PROC-SAMPLE]] | | Referenced reusable object |
|
|
| STORE | Sample Data Store | datastore | | | Local object |
|
|
|
|
## Flows
|
|
|
|
| id | from | to | data | notes |
|
|
|---|---|---|---|---|
|
|
| | | | | |
|
|
|
|
## Notes
|
|
|
|
- Add \`## Domain Sources\` only when referenced \`type: domains\` files already exist.
|
|
`,
|
|
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 |
|
|
|---|---|---|---|
|
|
| | | | |
|
|
|
|
## Domains
|
|
|
|
| id | name | kind | parent | description |
|
|
|---|---|---|---|---|
|
|
| user | User | external | | End user |
|
|
| system | System | application | | Application system |
|
|
| screen | Screen | application | | Screen/UI |
|
|
|
|
## Steps
|
|
|
|
| id | domain | 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
|
|
`,
|
|
domains: `---
|
|
type: domains
|
|
id: DOMAIN-SAMPLE
|
|
title: Domain Sample
|
|
---
|
|
|
|
# Domain Sample
|
|
|
|
## Domains
|
|
|
|
| id | name | kind | parent | description |
|
|
|---|---|---|---|---|
|
|
| business_domain | Business Domain | business | | Business capability area |
|
|
| application_domain | Application Domain | application | business_domain | Application capability area |
|
|
| data_domain | Data Domain | data | business_domain | Data management area |
|
|
| integration_domain | Integration Domain | integration | business_domain | External integration area |
|
|
|
|
## Notes
|
|
|
|
- \`kind\` is used by color_scheme when supported views apply colors.
|
|
- \`parent\` references another \`Domains.id\` in the same file.
|
|
- Domains Area/Tree views and Domain Diagram Area/Tree views can apply colors by kind.
|
|
`,
|
|
domainDiagram: `---
|
|
type: domain_diagram
|
|
id: DOMAIN-DIAGRAM-SAMPLE
|
|
title: Domain Diagram Sample
|
|
---
|
|
|
|
# Domain Diagram Sample
|
|
|
|
## Domain Sources
|
|
|
|
| ref |
|
|
|---|
|
|
| [[DOMAIN-SAMPLE]] |
|
|
|
|
## Notes
|
|
|
|
- Domain Diagram combines one or more \`domains\` files.
|
|
- Area/Tree views can use \`color_scheme\` for Domain kind colors.
|
|
- Add more rows to \`Domain Sources\` when combining multiple domain files.
|
|
`,
|
|
colorScheme: `---
|
|
type: color_scheme
|
|
id: COLOR-SCHEME-DEFAULT
|
|
name: DefaultColorScheme
|
|
tags:
|
|
- ColorScheme
|
|
---
|
|
|
|
# DefaultColorScheme
|
|
|
|
## Summary
|
|
|
|
Default color scheme for supported Model Weave views.
|
|
|
|
Set \`defaultColorSchemeRef\` to \`[[COLOR-SCHEME-DEFAULT]]\` in Model Weave settings to use this color scheme.
|
|
|
|
## Colors
|
|
|
|
| target | kind | fill | stroke | text | notes |
|
|
|---|---|---|---|---|---|
|
|
| | default | #f5f5f5 | #9e9e9e | #111111 | Global fallback |
|
|
| | business | #4f81bd | #2f5597 | #ffffff | Global business color |
|
|
| | application | #9bbb59 | #6f8a3f | #000000 | Global application color |
|
|
| | model | #8faadc | #5b7dbb | #000000 | Global model color |
|
|
| | renderer | #70ad47 | #507e32 | #000000 | Global renderer color |
|
|
| | data | #8064a2 | #60497a | #ffffff | Global data color |
|
|
| | integration | #f4b183 | #c55a11 | #000000 | Global integration color |
|
|
| | export | #ffd966 | #bf9000 | #000000 | Global export color |
|
|
| | ui | #76a5af | #45818e | #000000 | Global UI color |
|
|
| | operations | #7f7f7f | #595959 | #ffffff | Global operations color |
|
|
| | external | #bfbfbf | #7f7f7f | #000000 | Global external color |
|
|
| domain | default | #f5f5f5 | #9e9e9e | #111111 | Domain fallback |
|
|
| domain | business | #4f81bd | #2f5597 | #ffffff | Domain-specific business color |
|
|
| domain | application | #9bbb59 | #6f8a3f | #000000 | Domain-specific application color |
|
|
| domain | model | #8faadc | #5b7dbb | #000000 | Domain model color |
|
|
| domain | renderer | #70ad47 | #507e32 | #000000 | Domain renderer color |
|
|
| domain | data | #8064a2 | #60497a | #ffffff | Domain-specific data color |
|
|
| domain | integration | #f4b183 | #c55a11 | #000000 | Domain integration color |
|
|
| domain | export | #ffd966 | #bf9000 | #000000 | Domain export color |
|
|
| domain | ui | #76a5af | #45818e | #000000 | Domain UI color |
|
|
| domain | operations | #7f7f7f | #595959 | #ffffff | Domain operations color |
|
|
| domain | external | #bfbfbf | #7f7f7f | #000000 | Domain external color |
|
|
| dfd | default | #f5f5f5 | #9e9e9e | #111111 | DFD fallback |
|
|
| dfd | process | #9bbb59 | #6f8a3f | #000000 | DFD process |
|
|
| dfd | datastore | #8064a2 | #60497a | #ffffff | DFD datastore |
|
|
| dfd | external | #bfbfbf | #7f7f7f | #000000 | DFD external |
|
|
| dfd | other | #f4b183 | #c55a11 | #000000 | DFD other |
|
|
| app_process | default | #f5f5f5 | #9e9e9e | #111111 | Business Flow fallback |
|
|
| app_process | start | #4f81bd | #2f5597 | #ffffff | Start step |
|
|
| app_process | process | #9bbb59 | #6f8a3f | #000000 | Process step |
|
|
| app_process | decision | #f4b183 | #c55a11 | #000000 | Decision step |
|
|
| app_process | end | #bfbfbf | #7f7f7f | #000000 | End step |
|
|
|
|
## Notes
|
|
|
|
- Empty \`target\` means a global kind color.
|
|
- Target-specific rows override global rows for the same \`kind\`.
|
|
- Current runtime color application supports Domains Area/Tree, Domain Diagram Area/Tree, DFD, and app_process Business Flow.
|
|
- Mindmap is currently not colorized.
|
|
- Colors use HEX values.
|
|
- \`fill\` controls node background color.
|
|
- \`stroke\` controls node border color.
|
|
- \`text\` controls node text color.
|
|
- Do not define the same \`target + kind\` pair more than once.
|
|
`
|
|
};
|
|
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(
|
|
createWarning5(
|
|
"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(
|
|
createWarning5("missing-name", 'missing required field "name"', path2, "name")
|
|
);
|
|
}
|
|
if (!rawKind) {
|
|
warnings.push(
|
|
createWarning5("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(
|
|
createWarning5("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(
|
|
createWarning5(
|
|
"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(
|
|
createWarning5(
|
|
"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(
|
|
createWarning5(
|
|
"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(
|
|
createWarning5(
|
|
"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 ? [] : [
|
|
createWarning5(
|
|
"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(
|
|
createWarning5(
|
|
"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(
|
|
createWarning5(
|
|
"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 createWarning5(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(
|
|
createWarning6(
|
|
"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(
|
|
createWarning6(
|
|
"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(
|
|
createWarning6(
|
|
"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(
|
|
createWarning6(
|
|
"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 createWarning6(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(
|
|
createWarning7(
|
|
"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(
|
|
createWarning7("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(
|
|
createWarning7(
|
|
"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(
|
|
createWarning7(
|
|
"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(
|
|
createWarning7(
|
|
"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 createWarning7(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/domain-sources-parser.ts
|
|
var DOMAIN_SOURCE_HEADERS = ["ref"];
|
|
var DOMAIN_SOURCE_WITH_NOTES_HEADERS = ["ref", "notes"];
|
|
function parseDomainSourcesTable(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 ? [] : [createTableWarning(
|
|
"invalid-table-row",
|
|
path2,
|
|
"Domain Sources",
|
|
'table in section "Domain Sources" is incomplete'
|
|
)]
|
|
};
|
|
}
|
|
const headers = splitMarkdownTableRow(normalizedLines[0]) ?? [];
|
|
const warnings = [];
|
|
if (!isSupportedDomainSourceHeaders(headers)) {
|
|
warnings.push(
|
|
createTableWarning(
|
|
"invalid-table-column",
|
|
path2,
|
|
"Domain Sources",
|
|
'table columns in section "Domain Sources" do not match supported Domain Sources headers'
|
|
)
|
|
);
|
|
}
|
|
const rows = [];
|
|
normalizedLines.slice(2).forEach((rowLine, rowIndex) => {
|
|
const values = splitMarkdownTableRow(rowLine) ?? [];
|
|
if (values.length !== headers.length) {
|
|
warnings.push(
|
|
createTableWarning(
|
|
"invalid-table-row",
|
|
path2,
|
|
"Domain Sources",
|
|
`table row in section "Domain Sources" has ${values.length} columns, expected ${headers.length}`
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
const row = {};
|
|
for (const [index, header] of headers.entries()) {
|
|
row[header] = values[index] ?? "";
|
|
}
|
|
const ref = row.ref?.trim() ?? "";
|
|
if (!ref) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatDomainDiagramMissingRefMessage(),
|
|
severity: "error",
|
|
path: path2,
|
|
field: "Domain Sources.ref",
|
|
context: { rowIndex: rowIndex + 1 }
|
|
});
|
|
return;
|
|
}
|
|
rows.push({
|
|
ref,
|
|
notes: row.notes?.trim() || void 0,
|
|
rowIndex
|
|
});
|
|
});
|
|
return { rows, warnings };
|
|
}
|
|
function isSupportedDomainSourceHeaders(headers) {
|
|
return sameHeaders3(headers, DOMAIN_SOURCE_HEADERS) || sameHeaders3(headers, DOMAIN_SOURCE_WITH_NOTES_HEADERS);
|
|
}
|
|
function sameHeaders3(actual, expected) {
|
|
return actual.length === expected.length && actual.every((header, index) => header === expected[index]);
|
|
}
|
|
function createTableWarning(code, path2, field, message) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "warning",
|
|
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(createWarning8(path2, "type", 'expected type "dfd_diagram"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning8(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning8(path2, "name", 'required frontmatter "name" is missing'));
|
|
}
|
|
const objectsTable = parseDfdObjectsTable(sections.Objects, path2);
|
|
const domainsTable = parseDomainEntries(sections.Domains, path2);
|
|
const domainSourcesTable = parseDomainSourcesTable(sections["Domain Sources"], path2);
|
|
const flowsTable = parseMarkdownTable(sections.Flows, FLOW_HEADERS, path2, "Flows");
|
|
warnings.push(
|
|
...domainsTable.warnings,
|
|
...validateDomainEntries(path2, domainsTable.rows, {
|
|
skipUnknownParents: domainSourcesTable.rows.length > 0
|
|
}),
|
|
...domainSourcesTable.warnings,
|
|
...objectsTable.warnings,
|
|
...flowsTable.warnings
|
|
);
|
|
const fallbackTitle = name || id || getFileStem4(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,
|
|
metadata: {
|
|
domain: entry.domain,
|
|
rowIndex: entry.rowIndex
|
|
}
|
|
}));
|
|
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),
|
|
domainSources: domainSourcesTable.rows,
|
|
domains: domainsTable.rows,
|
|
objectRefs,
|
|
objectEntries,
|
|
nodes,
|
|
edges,
|
|
flows
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function getFileStem4(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
function joinSectionLines2(lines) {
|
|
const value = (lines ?? []).join("\n").trim();
|
|
return value || void 0;
|
|
}
|
|
function createWarning8(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 ? [] : [createWarning8(path2, "Objects", 'table in section "Objects" is incomplete')]
|
|
};
|
|
}
|
|
const headers = splitMarkdownTableRow(normalizedLines[0]) ?? [];
|
|
const warnings = [];
|
|
const hasLegacyHeaders = sameHeaders4(headers, LEGACY_OBJECT_HEADERS);
|
|
const hasLocalHeaders = headers.includes("id") && headers.includes("label") && headers.includes("kind") && headers.includes("ref");
|
|
if (!hasLegacyHeaders && !hasLocalHeaders) {
|
|
warnings.push(
|
|
createWarning8(
|
|
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(
|
|
createWarning8(
|
|
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 domain = row.domain?.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,
|
|
domain: domain || 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 sameHeaders4(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(createWarning9(path2, "type", 'expected type "dfd_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'));
|
|
}
|
|
if (!rawKind) {
|
|
warnings.push(createWarning9(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 || getFileStem5(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 getFileStem5(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 createWarning9(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(createWarning10(path2, "type", 'expected type "data_object"'));
|
|
}
|
|
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 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 || getFileStem6(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 getFileStem6(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 createWarning10(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/domain-diagram-parser.ts
|
|
function parseDomainDiagramFile(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 title = typeof frontmatter.title === "string" ? frontmatter.title.trim() : "";
|
|
if (frontmatter.type !== "domain_diagram") {
|
|
warnings.push(createWarning11(path2, "type", 'expected type "domain_diagram"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning11(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
const sourcesTable = parseDomainSourcesTable(sections["Domain Sources"], path2);
|
|
warnings.push(...sourcesTable.warnings);
|
|
const domainSources = sourcesTable.rows;
|
|
if (domainSources.length === 0) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatDomainDiagramNoValidSourcesMessage(),
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "Domain Sources"
|
|
});
|
|
}
|
|
const fallbackTitle = name || title || id || getFileStem7(path2) || "Untitled Domain Diagram";
|
|
return {
|
|
file: {
|
|
fileType: "domain-diagram",
|
|
schema: "domain_diagram",
|
|
path: path2,
|
|
title: fallbackTitle,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: name || title || fallbackTitle,
|
|
domainSources
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function createWarning11(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
function getFileStem7(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
|
|
// 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 LEGACY_STEP_HEADERS = ["id", "lane", "label", "kind", "input", "output", "rule", "invoke", "screen", "notes"];
|
|
var DOMAIN_STEP_HEADERS = ["id", "domain", "label", "kind", "input", "output", "rule", "invoke", "screen", "notes"];
|
|
var TRANSITIONAL_STEP_HEADERS = ["id", "domain", "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(createWarning12(path2, "type", 'expected type "app_process"'));
|
|
}
|
|
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 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 domainsTable = parseDomainEntries(sections.Domains, path2);
|
|
const hasStructuredSteps = hasMarkdownTable(sections.Steps);
|
|
const stepsTable = hasStructuredSteps ? parseAppProcessStepsTable(sections.Steps, path2) : { rows: [], warnings: [] };
|
|
const flowsTable = hasMarkdownTable(sections.Flows) ? parseMarkdownTable(sections.Flows, FLOW_HEADERS2, path2, "Flows") : { rows: [], warnings: [] };
|
|
const domainSourcesTable = "Domain Sources" in sections ? parseDomainSourcesTable(sections["Domain Sources"], path2) : { rows: [], warnings: [] };
|
|
const steps = stepsTable.rows.map((row) => ({
|
|
id: row.id?.trim() ?? "",
|
|
domain: row.domain?.trim() || void 0,
|
|
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,
|
|
...domainsTable.warnings,
|
|
...validateDomainEntries(path2, domainsTable.rows, {
|
|
skipUnknownParents: domainSourcesTable.rows.length > 0
|
|
}),
|
|
...stepsTable.warnings,
|
|
...flowsTable.warnings,
|
|
...domainSourcesTable.warnings
|
|
);
|
|
const fallbackName = name || id || getFileStem8(path2) || "Untitled App Process";
|
|
return {
|
|
file: {
|
|
fileType: "app-process",
|
|
schema: "app_process",
|
|
path: path2,
|
|
title: fallbackName,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
domains: domainsTable.rows,
|
|
domainSources: domainSourcesTable.rows,
|
|
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 getFileStem8(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 parseAppProcessStepsTable(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 ? [] : [createTableWarning2("invalid-table-row", path2, "Steps", 'table in section "Steps" is incomplete')]
|
|
};
|
|
}
|
|
const headers = splitMarkdownTableRow(normalizedLines[0]) ?? [];
|
|
const warnings = [];
|
|
if (!isSupportedStepHeaders(headers)) {
|
|
warnings.push(
|
|
createTableWarning2(
|
|
"invalid-table-column",
|
|
path2,
|
|
"Steps",
|
|
'table columns in section "Steps" do not match supported app_process step headers'
|
|
)
|
|
);
|
|
}
|
|
const rows = [];
|
|
normalizedLines.slice(2).forEach((rowLine, rowIndex) => {
|
|
const values = splitMarkdownTableRow(rowLine) ?? [];
|
|
if (values.length !== headers.length) {
|
|
warnings.push(
|
|
createTableWarning2(
|
|
"invalid-table-row",
|
|
path2,
|
|
"Steps",
|
|
`table row in section "Steps" has ${values.length} columns, expected ${headers.length}`
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
const row = {};
|
|
for (const [index, header] of headers.entries()) {
|
|
row[header] = values[index] ?? "";
|
|
}
|
|
const domain = row.domain?.trim() ?? "";
|
|
const lane = row.lane?.trim() ?? "";
|
|
if (domain && lane) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: `Step "${row.id?.trim() || row.label?.trim() || rowIndex + 1}" has both domain and lane. domain is used and lane is ignored.`,
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "Steps.domain",
|
|
context: { rowIndex: rowIndex + 1 }
|
|
});
|
|
}
|
|
rows.push(row);
|
|
});
|
|
return { rows, warnings };
|
|
}
|
|
function isSupportedStepHeaders(headers) {
|
|
return sameHeaders5(headers, LEGACY_STEP_HEADERS) || sameHeaders5(headers, DOMAIN_STEP_HEADERS) || sameHeaders5(headers, TRANSITIONAL_STEP_HEADERS);
|
|
}
|
|
function sameHeaders5(actual, expected) {
|
|
return actual.length === expected.length && actual.every((header, index) => header === expected[index]);
|
|
}
|
|
function isEmptyRow(values) {
|
|
return values.every((value) => !value?.trim());
|
|
}
|
|
function createTableWarning2(code, path2, field, message) {
|
|
return {
|
|
code,
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
function createWarning12(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(createWarning13(path2, "type", 'expected type "screen"'));
|
|
}
|
|
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 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 && !sameHeaders6(layoutHeaders, LAYOUT_HEADERS)) {
|
|
warnings.push(createWarning13(path2, "Layout", 'table columns in section "Layout" do not match expected headers'));
|
|
}
|
|
const fieldHeaders = fieldsTable.headers;
|
|
const isCanonicalFields = sameHeaders6(fieldHeaders, FIELD_HEADERS);
|
|
const isCanonicalFieldsWithCondition = sameHeaders6(fieldHeaders, FIELD_HEADERS_WITH_CONDITION);
|
|
const isCanonicalFieldsWithRuleBeforeCondition = sameHeaders6(fieldHeaders, FIELD_HEADERS_WITH_RULE_BEFORE_CONDITION);
|
|
const isLegacyFields = sameHeaders6(fieldHeaders, LEGACY_FIELD_HEADERS);
|
|
const isLegacyFieldsWithCondition = sameHeaders6(fieldHeaders, LEGACY_FIELD_HEADERS_WITH_CONDITION);
|
|
const isLegacyFieldsWithRuleBeforeCondition = sameHeaders6(fieldHeaders, LEGACY_FIELD_HEADERS_WITH_RULE_BEFORE_CONDITION);
|
|
if (fieldHeaders.length > 0 && !isCanonicalFields && !isCanonicalFieldsWithCondition && !isCanonicalFieldsWithRuleBeforeCondition && !isLegacyFields && !isLegacyFieldsWithCondition && !isLegacyFieldsWithRuleBeforeCondition) {
|
|
warnings.push(createWarning13(path2, "Fields", 'table columns in section "Fields" do not match expected screen field headers'));
|
|
}
|
|
const actionHeaders = actionsTable.headers;
|
|
if (actionHeaders.length > 0 && !sameHeaders6(actionHeaders, ACTION_HEADERS) && !sameHeaders6(actionHeaders, ACTION_HEADERS_WITH_CONDITION) && !sameHeaders6(actionHeaders, ACTION_HEADERS_WITH_CONDITION_AFTER_RULE)) {
|
|
warnings.push(createWarning13(path2, "Actions", 'table columns in section "Actions" do not match expected headers'));
|
|
}
|
|
const messageHeaders = messagesTable.headers;
|
|
const isCanonicalMessages = sameHeaders6(messageHeaders, MESSAGE_HEADERS);
|
|
const isCanonicalMessagesWithCondition = sameHeaders6(messageHeaders, MESSAGE_HEADERS_WITH_CONDITION);
|
|
const isLegacyMessages = sameHeaders6(messageHeaders, LEGACY_MESSAGE_HEADERS);
|
|
if (messageHeaders.length > 0 && !isCanonicalMessages && !isCanonicalMessagesWithCondition && !isLegacyMessages) {
|
|
warnings.push(createWarning13(path2, "Messages", 'table columns in section "Messages" do not match expected headers'));
|
|
}
|
|
const transitionHeaders = transitionsTable.headers;
|
|
if (transitionHeaders.length > 0 && !sameHeaders6(transitionHeaders, LEGACY_TRANSITION_HEADERS)) {
|
|
warnings.push(createWarning13(path2, "Transitions", 'table columns in section "Transitions" do not match expected legacy headers'));
|
|
}
|
|
const fallbackName = name || id || getFileStem9(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 && !sameHeaders6(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 sameHeaders6(actual, expected) {
|
|
if (actual.length !== expected.length) {
|
|
return false;
|
|
}
|
|
return actual.every((header, index) => header === expected[index]);
|
|
}
|
|
function getFileStem9(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) => {
|
|
if (value === void 0 || value === null) {
|
|
return true;
|
|
}
|
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
return !String(value).trim();
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
function createWarning13(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(createWarning14(path2, "type", 'expected type "codeset"'));
|
|
}
|
|
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 valuesTable = parseMarkdownTable(sections.Values, VALUE_HEADERS, path2, "Values");
|
|
warnings.push(...valuesTable.warnings);
|
|
const fallbackName = name || id || getFileStem10(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 getFileStem10(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 createWarning14(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(createWarning15(path2, "type", 'expected type "message"'));
|
|
}
|
|
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 messagesTable = parseMarkdownTable(sections.Messages, MESSAGE_HEADERS2, path2, "Messages");
|
|
warnings.push(...messagesTable.warnings);
|
|
const fallbackName = name || id || getFileStem11(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 getFileStem11(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 createWarning15(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(createWarning16(path2, "type", 'expected type "rule"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning16(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning16(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 || getFileStem12(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 getFileStem12(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 createWarning16(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(createWarning17(path2, "type", 'expected type "mapping"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning17(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
if (!name) {
|
|
warnings.push(createWarning17(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 || getFileStem13(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 getFileStem13(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 createWarning17(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
|
|
// src/parsers/color-scheme-parser.ts
|
|
var COLOR_HEADERS = ["target", "kind", "fill", "stroke", "text", "notes"];
|
|
var HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
function parseColorSchemeFile(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 title = typeof frontmatter.title === "string" ? frontmatter.title.trim() : "";
|
|
if (frontmatter.type !== "color_scheme") {
|
|
warnings.push(createWarning18(path2, "type", 'expected type "color_scheme"'));
|
|
}
|
|
if (!id) {
|
|
warnings.push(createWarning18(path2, "id", 'required frontmatter "id" is missing'));
|
|
}
|
|
const colorTable = parseMarkdownTable(sections.Colors, COLOR_HEADERS, path2, "Colors");
|
|
warnings.push(...colorTable.warnings);
|
|
const colors = [];
|
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
colorTable.rows.forEach((row, rowIndex) => {
|
|
const target = row.target?.trim() ?? "";
|
|
const kind = row.kind?.trim() ?? "";
|
|
const fill = row.fill?.trim() ?? "";
|
|
const stroke = row.stroke?.trim() ?? "";
|
|
const text = row.text?.trim() ?? "";
|
|
const notes = row.notes?.trim() ?? "";
|
|
if (!kind) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatColorSchemeKindRequiredMessage(),
|
|
severity: "error",
|
|
path: path2,
|
|
field: "Colors.kind",
|
|
context: { rowIndex: rowIndex + 1 }
|
|
});
|
|
return;
|
|
}
|
|
for (const [field, value] of [
|
|
["fill", fill],
|
|
["stroke", stroke],
|
|
["text", text]
|
|
]) {
|
|
if (value && !HEX_COLOR_PATTERN.test(value)) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatColorSchemeInvalidColorMessage(field, value),
|
|
severity: "warning",
|
|
path: path2,
|
|
field: `Colors.${field}`,
|
|
context: { rowIndex: rowIndex + 1 }
|
|
});
|
|
}
|
|
}
|
|
const key = `${target.toLowerCase()}::${kind.toLowerCase()}`;
|
|
if (seenKeys.has(key)) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatColorSchemeDuplicateEntryMessage(target, kind),
|
|
severity: "warning",
|
|
path: path2,
|
|
field: "Colors.kind",
|
|
context: { rowIndex: rowIndex + 1 }
|
|
});
|
|
}
|
|
seenKeys.add(key);
|
|
colors.push({
|
|
target: target || void 0,
|
|
kind,
|
|
fill: fill || void 0,
|
|
stroke: stroke || void 0,
|
|
text: text || void 0,
|
|
notes: notes || void 0,
|
|
rowIndex
|
|
});
|
|
});
|
|
const fallbackTitle = name || title || id || getFileStem14(path2) || "Untitled Color Scheme";
|
|
return {
|
|
file: {
|
|
fileType: "color-scheme",
|
|
schema: "color_scheme",
|
|
path: path2,
|
|
title: fallbackTitle,
|
|
frontmatter,
|
|
sections,
|
|
sourceLinks: parseSourceLinks(sections["Source Links"]),
|
|
id,
|
|
name: name || title || fallbackTitle,
|
|
colors
|
|
},
|
|
warnings
|
|
};
|
|
}
|
|
function createWarning18(path2, field, message) {
|
|
return {
|
|
code: "invalid-structure",
|
|
message,
|
|
severity: "warning",
|
|
path: path2,
|
|
field
|
|
};
|
|
}
|
|
function getFileStem14(path2) {
|
|
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
|
|
}
|
|
|
|
// 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"]);
|
|
var STANDALONE_DOMAIN_CANONICAL_FIELDS = [
|
|
"name",
|
|
"kind",
|
|
"parent"
|
|
];
|
|
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"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
validateStandaloneDomains(index, warnings);
|
|
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 validateStandaloneDomains(index, warnings) {
|
|
const entriesByDomainId = /* @__PURE__ */ new Map();
|
|
for (const model of Object.values(index.modelsByFilePath)) {
|
|
if (model.fileType !== "domains") {
|
|
continue;
|
|
}
|
|
for (const domain of model.domains) {
|
|
if (!entriesByDomainId.has(domain.id)) {
|
|
entriesByDomainId.set(domain.id, []);
|
|
}
|
|
entriesByDomainId.get(domain.id).push({
|
|
domain,
|
|
path: model.path
|
|
});
|
|
}
|
|
}
|
|
validateStandaloneDomainParents(entriesByDomainId, warnings);
|
|
for (const entries of entriesByDomainId.values()) {
|
|
if (entries.length < 2) {
|
|
continue;
|
|
}
|
|
const sortedEntries = [...entries].sort((left, right) => {
|
|
const pathOrder = left.path.localeCompare(right.path);
|
|
if (pathOrder !== 0) {
|
|
return pathOrder;
|
|
}
|
|
return left.domain.rowIndex - right.domain.rowIndex;
|
|
});
|
|
const canonical = sortedEntries[0];
|
|
if (!canonical) {
|
|
continue;
|
|
}
|
|
for (const entry of sortedEntries) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatStandaloneDomainDuplicateMessage(entry.domain.id),
|
|
severity: "warning",
|
|
path: entry.path,
|
|
field: "Domains.id",
|
|
context: { rowIndex: entry.domain.rowIndex + 1 }
|
|
});
|
|
}
|
|
compareStandaloneDomainFields(sortedEntries, warnings);
|
|
}
|
|
}
|
|
function validateStandaloneDomainParents(entriesByDomainId, warnings) {
|
|
for (const entries of entriesByDomainId.values()) {
|
|
for (const entry of entries) {
|
|
const parent = entry.domain.parent?.trim();
|
|
if (!parent || parent === entry.domain.id || entriesByDomainId.has(parent)) {
|
|
continue;
|
|
}
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: formatDomainParentUnknownMessage(parent),
|
|
severity: "warning",
|
|
path: entry.path,
|
|
field: "Domains.parent",
|
|
context: { rowIndex: entry.domain.rowIndex + 1 }
|
|
});
|
|
}
|
|
}
|
|
}
|
|
function compareStandaloneDomainFields(entries, warnings) {
|
|
for (const field of STANDALONE_DOMAIN_CANONICAL_FIELDS) {
|
|
const values = new Set(entries.map((entry) => entry.domain[field]?.trim() ?? ""));
|
|
if (values.size < 2) {
|
|
continue;
|
|
}
|
|
for (const entry of entries) {
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatStandaloneDomainFieldConflictMessage(
|
|
entry.domain.id,
|
|
field
|
|
),
|
|
severity: "warning",
|
|
path: entry.path,
|
|
field: `Domains.${field}`,
|
|
context: { rowIndex: entry.domain.rowIndex + 1 }
|
|
});
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
validateDfdLocalDomains(dfdDiagram, index, warnings);
|
|
validateDfdObjectDomains(dfdDiagram, warnings);
|
|
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 (!identity.resolvedModel) {
|
|
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 && Boolean(sourceIdentity?.resolvedModel);
|
|
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 && Boolean(targetIdentity?.resolvedModel);
|
|
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 validateDfdObjectDomains(diagram, warnings) {
|
|
const localDomains = diagram.domains ?? [];
|
|
const localDomainIds = new Set(localDomains.map((domain) => domain.id));
|
|
for (const entry of diagram.objectEntries) {
|
|
const domain = entry.domain?.trim();
|
|
if (!domain) {
|
|
continue;
|
|
}
|
|
const objectId = entry.id?.trim() || entry.ref?.trim() || String(entry.rowIndex + 1);
|
|
if (localDomainIds.size === 0) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: formatDfdObjectDomainWithoutLocalDomainsMessage(objectId, domain),
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Objects.domain",
|
|
context: { rowIndex: entry.rowIndex + 1 }
|
|
});
|
|
} else if (!localDomainIds.has(domain)) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: formatDfdObjectUnknownLocalDomainMessage(objectId, domain),
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Objects.domain",
|
|
context: { rowIndex: entry.rowIndex + 1 }
|
|
});
|
|
}
|
|
}
|
|
}
|
|
function validateDfdLocalDomains(diagram, index, warnings) {
|
|
const localDomains = diagram.domains ?? [];
|
|
if (localDomains.length === 0) {
|
|
return;
|
|
}
|
|
const sharedDomains = buildSharedDomainLookup(index);
|
|
for (const localDomain of localDomains) {
|
|
const sharedDomain = sharedDomains.get(localDomain.id);
|
|
if (!sharedDomain) {
|
|
warnings.push({
|
|
code: "unresolved-reference",
|
|
message: formatDfdLocalDomainMissingSharedMessage(localDomain.id),
|
|
severity: "warning",
|
|
path: diagram.path,
|
|
field: "Domains.id",
|
|
context: { rowIndex: localDomain.rowIndex + 1 }
|
|
});
|
|
continue;
|
|
}
|
|
compareDfdLocalDomainField(diagram.path, localDomain, sharedDomain, "name", warnings);
|
|
compareDfdLocalDomainField(diagram.path, localDomain, sharedDomain, "kind", warnings);
|
|
compareDfdLocalDomainField(diagram.path, localDomain, sharedDomain, "parent", warnings);
|
|
}
|
|
}
|
|
function buildSharedDomainLookup(index) {
|
|
const sharedDomains = /* @__PURE__ */ new Map();
|
|
for (const domainsModel of Object.values(index.domainsById)) {
|
|
for (const domain of domainsModel.domains) {
|
|
if (!sharedDomains.has(domain.id)) {
|
|
sharedDomains.set(domain.id, domain);
|
|
}
|
|
}
|
|
}
|
|
return sharedDomains;
|
|
}
|
|
function compareDfdLocalDomainField(path2, localDomain, sharedDomain, field, warnings) {
|
|
const localValue = localDomain[field]?.trim();
|
|
if (!localValue) {
|
|
return;
|
|
}
|
|
const sharedValue = sharedDomain[field]?.trim() ?? "";
|
|
if (localValue === sharedValue) {
|
|
return;
|
|
}
|
|
warnings.push({
|
|
code: "invalid-structure",
|
|
message: formatDfdLocalDomainFieldMismatchMessage(
|
|
localDomain.id,
|
|
field,
|
|
localValue,
|
|
sharedValue
|
|
),
|
|
severity: "warning",
|
|
path: path2,
|
|
field: `Domains.${field}`,
|
|
context: { rowIndex: localDomain.rowIndex + 1 }
|
|
});
|
|
}
|
|
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: {},
|
|
colorSchemesById: {},
|
|
domainsById: {},
|
|
domainDiagramsById: {},
|
|
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;
|
|
}
|
|
clearVaultValidationWarnings(index);
|
|
clearDomainParentNotDefinedWarnings(index);
|
|
for (const warning of validateVaultIndex(index)) {
|
|
pushWarning(index.warningsByFilePath, warning.path ?? "vault", {
|
|
...warning,
|
|
context: {
|
|
...warning.context ?? {},
|
|
vaultValidation: true
|
|
}
|
|
});
|
|
}
|
|
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 "color-scheme": {
|
|
addModelById(
|
|
index.colorSchemesById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "domains": {
|
|
addModelById(
|
|
index.domainsById,
|
|
parseResult.file.id,
|
|
parseResult.file,
|
|
index.warningsByFilePath,
|
|
file.path,
|
|
{ suppressDuplicateWarning: true }
|
|
);
|
|
break;
|
|
}
|
|
case "domain-diagram": {
|
|
addModelById(
|
|
index.domainDiagramsById,
|
|
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 clearVaultValidationWarnings(index) {
|
|
for (const [path2, warnings] of Object.entries(index.warningsByFilePath)) {
|
|
index.warningsByFilePath[path2] = warnings.filter(
|
|
(warning) => warning.context?.vaultValidation !== true
|
|
);
|
|
}
|
|
}
|
|
function clearDomainParentNotDefinedWarnings(index) {
|
|
for (const [path2, warnings] of Object.entries(index.warningsByFilePath)) {
|
|
const model = index.modelsByFilePath[path2];
|
|
if (model?.fileType !== "domains") {
|
|
continue;
|
|
}
|
|
index.warningsByFilePath[path2] = warnings.filter(
|
|
(warning) => !getDomainParentNotDefinedValue(warning)
|
|
);
|
|
}
|
|
}
|
|
function getDomainParentNotDefinedValue(warning) {
|
|
if (warning.code !== "unresolved-reference" || warning.field !== "Domains.parent") {
|
|
return null;
|
|
}
|
|
const match = warning.message.match(/^Domain parent "([^"]+)" is not defined\.$/);
|
|
return match?.[1] ?? null;
|
|
}
|
|
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);
|
|
}
|
|
if (frontmatter?.type === "color_scheme") {
|
|
return parseColorSchemeFile(content, file.path);
|
|
}
|
|
if (frontmatter?.type === "domains") {
|
|
return parseDomainsFile(content, file.path);
|
|
}
|
|
if (frontmatter?.type === "domain_diagram") {
|
|
return parseDomainDiagramFile(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 "color-scheme":
|
|
return parseColorSchemeFile(content, file.path);
|
|
case "domains":
|
|
return parseDomainsFile(content, file.path);
|
|
case "domain-diagram":
|
|
return parseDomainDiagramFile(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,
|
|
domains: [],
|
|
domainSources: [],
|
|
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 "color-scheme":
|
|
return {
|
|
...common,
|
|
fileType: "color-scheme",
|
|
schema: "color_scheme",
|
|
id,
|
|
name,
|
|
colors: []
|
|
};
|
|
case "domains":
|
|
return {
|
|
...common,
|
|
fileType: "domains",
|
|
schema: "domains",
|
|
id,
|
|
name,
|
|
description: getFrontmatterString(frontmatter, "description"),
|
|
domains: []
|
|
};
|
|
case "domain-diagram":
|
|
return {
|
|
...common,
|
|
fileType: "domain-diagram",
|
|
schema: "domain_diagram",
|
|
id,
|
|
name,
|
|
domainSources: []
|
|
};
|
|
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",
|
|
domainSources: [],
|
|
domains: [],
|
|
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 "color-scheme":
|
|
delete index.colorSchemesById[model.id];
|
|
break;
|
|
case "domains":
|
|
delete index.domainsById[model.id];
|
|
break;
|
|
case "domain-diagram":
|
|
delete index.domainDiagramsById[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/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/weave-map-mermaid.ts
|
|
var WEAVE_MAP_LAYER_ORDER = [
|
|
"UI",
|
|
"Process",
|
|
"Rule",
|
|
"Rule / State",
|
|
"UI / Message",
|
|
"Data",
|
|
"Mapping",
|
|
"Implementation",
|
|
"Data Flow",
|
|
"Relationship",
|
|
"Source",
|
|
"Warning",
|
|
"Other"
|
|
];
|
|
var DEFAULT_WEAVE_MAP_LAYER_STYLES = {
|
|
UI: { fill: "#eef6ff", stroke: "#b8d4f0", color: "#1f2937" },
|
|
Process: { fill: "#eefaf1", stroke: "#b7dfc2", color: "#1f2937" },
|
|
Rule: { fill: "#fff8e6", stroke: "#ead38a", color: "#1f2937" },
|
|
"Rule / State": { fill: "#fff8e6", stroke: "#ead38a", color: "#1f2937" },
|
|
"UI / Message": { fill: "#fdf2f8", stroke: "#f0b8d4", color: "#1f2937" },
|
|
Data: { fill: "#eef6ff", stroke: "#b8d4f0", color: "#1f2937" },
|
|
Mapping: { fill: "#f5efff", stroke: "#d6c2f0", color: "#1f2937" },
|
|
Implementation: { fill: "#f3f4f6", stroke: "#cbd5e1", color: "#1f2937" },
|
|
"Data Flow": { fill: "#ecfeff", stroke: "#a5dbe2", color: "#1f2937" },
|
|
Relationship: { fill: "#f8fafc", stroke: "#cbd5e1", color: "#1f2937" },
|
|
Source: { fill: "#effaf0", stroke: "#9fd3a8", color: "#1f2937" },
|
|
Warning: { fill: "#fff1f1", stroke: "#f0b4b4", color: "#1f2937" },
|
|
Other: { fill: "#f7f7f7", stroke: "#d4d4d8", color: "#1f2937" }
|
|
};
|
|
function buildWeaveMapMermaidSource(model, options = {}) {
|
|
const nodeIds = createWeaveMapNodeMermaidIds(model.nodes);
|
|
const orderedLayers = getOrderedLayers(model.nodes);
|
|
const lines = [
|
|
"flowchart LR",
|
|
` ${buildWeaveMapClassDef("weaveFocus", "#fff3cd", "#d39e00", 2.4)}`,
|
|
` ${buildWeaveMapClassDef("weaveNode", "#f5f7fb", "#7c8a9a")}`,
|
|
` ${buildWeaveMapClassDef("weaveSource", "#e8f5e9", "#388e3c")}`,
|
|
` ${buildWeaveMapClassDef("weaveUnresolved", "#ffebee", "#c62828", 2)}`,
|
|
` ${buildWeaveMapClassDef("weaveWarning", "#fff8e1", "#f57f17", 2)}`,
|
|
""
|
|
];
|
|
for (const layer of orderedLayers) {
|
|
const layerNodes = model.nodes.filter((node) => node.layer === layer);
|
|
if (layerNodes.length === 0) {
|
|
continue;
|
|
}
|
|
const subgraphId = `layer_${sanitizeMermaidId(layer)}`;
|
|
lines.push(` subgraph ${subgraphId}["${escapeMermaidLabel(layer)}"]`);
|
|
for (const node of layerNodes) {
|
|
const mermaidId = nodeIds.get(node.id) ?? sanitizeMermaidId(node.id);
|
|
lines.push(` ${mermaidId}["${buildNodeLabel(node)}"]`);
|
|
}
|
|
lines.push(" end", "");
|
|
}
|
|
for (const layer of orderedLayers) {
|
|
lines.push(` ${buildLayerStyleLine(layer, options.colorScheme)}`);
|
|
}
|
|
if (orderedLayers.length > 0) {
|
|
lines.push("");
|
|
}
|
|
for (const edge of model.edges) {
|
|
const from = nodeIds.get(edge.from) ?? sanitizeMermaidId(edge.from);
|
|
const to = nodeIds.get(edge.to) ?? sanitizeMermaidId(edge.to);
|
|
const label = sanitizeEdgeLabel(edge.label || edge.relationType);
|
|
const arrow = edge.status === "unresolved" ? "-.->" : "-->";
|
|
lines.push(` ${from} ${arrow}|${label}| ${to}`);
|
|
}
|
|
if (model.edges.length > 0) {
|
|
lines.push("");
|
|
}
|
|
for (const node of model.nodes) {
|
|
const mermaidId = nodeIds.get(node.id) ?? sanitizeMermaidId(node.id);
|
|
lines.push(` class ${mermaidId} ${getNodeClassName(node)}`);
|
|
}
|
|
return lines.join("\n").trimEnd();
|
|
}
|
|
function createWeaveMapNodeMermaidIds(nodes) {
|
|
const usedIds = /* @__PURE__ */ new Set();
|
|
const ids = /* @__PURE__ */ new Map();
|
|
for (const node of nodes) {
|
|
ids.set(node.id, ensureUniqueMermaidId(`n_${sanitizeMermaidId(node.id)}`, usedIds));
|
|
}
|
|
return ids;
|
|
}
|
|
function getOrderedLayers(nodes) {
|
|
const presentLayers = new Set(nodes.map((node) => node.layer));
|
|
const ordered = WEAVE_MAP_LAYER_ORDER.filter((layer) => presentLayers.has(layer));
|
|
const extra = Array.from(presentLayers).filter((layer) => !WEAVE_MAP_LAYER_ORDER.includes(layer));
|
|
return [...ordered, ...extra];
|
|
}
|
|
function buildNodeLabel(node) {
|
|
return escapeMermaidLabel(`${node.layer}
|
|
${node.label}`);
|
|
}
|
|
function sanitizeEdgeLabel(label) {
|
|
return escapeMermaidEdgeLabel(label) || "relates";
|
|
}
|
|
function buildLayerStyleLine(layer, colorScheme) {
|
|
const style = resolveWeaveMapLayerStyle(layer, colorScheme);
|
|
return `style layer_${sanitizeMermaidId(layer)} fill:${style.fill},stroke:${style.stroke},stroke-width:1px,color:${style.color}`;
|
|
}
|
|
function resolveWeaveMapLayerStyle(layer, colorScheme) {
|
|
const fallback = DEFAULT_WEAVE_MAP_LAYER_STYLES[layer] ?? DEFAULT_WEAVE_MAP_LAYER_STYLES.Other;
|
|
const override = colorScheme?.entries.find(
|
|
(entry) => (entry.target?.trim().toLowerCase() ?? "") === "weave_map" && entry.kind.trim().toLowerCase() === getWeaveMapLayerColorKind(layer)
|
|
);
|
|
if (!override) {
|
|
return fallback;
|
|
}
|
|
return {
|
|
fill: override.fill ?? fallback.fill,
|
|
stroke: override.stroke ?? fallback.stroke,
|
|
color: override.text ?? fallback.color
|
|
};
|
|
}
|
|
function getWeaveMapLayerColorKind(layer) {
|
|
switch (layer) {
|
|
case "UI":
|
|
return "ui";
|
|
case "Process":
|
|
return "process";
|
|
case "Rule":
|
|
return "rule";
|
|
case "Rule / State":
|
|
return "rule_state";
|
|
case "UI / Message":
|
|
return "ui_message";
|
|
case "Data":
|
|
return "data";
|
|
case "Mapping":
|
|
return "mapping";
|
|
case "Implementation":
|
|
return "implementation";
|
|
case "Data Flow":
|
|
return "data_flow";
|
|
case "Relationship":
|
|
return "relationship";
|
|
case "Source":
|
|
return "source";
|
|
case "Warning":
|
|
return "warning";
|
|
case "Other":
|
|
return "other";
|
|
}
|
|
}
|
|
function getNodeClassName(node) {
|
|
if (node.status === "focus") {
|
|
return "weaveFocus";
|
|
}
|
|
if (node.status === "source") {
|
|
return "weaveSource";
|
|
}
|
|
if (node.status === "unresolved") {
|
|
return "weaveUnresolved";
|
|
}
|
|
if (node.status === "warning") {
|
|
return "weaveWarning";
|
|
}
|
|
return "weaveNode";
|
|
}
|
|
function buildWeaveMapClassDef(className, fill, stroke, strokeWidth = 1.4) {
|
|
return `classDef ${className} fill:${fill},stroke:${stroke},color:#111111,stroke-width:${strokeWidth}px`;
|
|
}
|
|
|
|
// src/views/modeling-preview-view.ts
|
|
var import_obsidian7 = require("obsidian");
|
|
var import_electron2 = require("electron");
|
|
|
|
// 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/core/domain-tree.ts
|
|
function buildDomainTree(domains) {
|
|
const nodes = /* @__PURE__ */ new Map();
|
|
const roots = [];
|
|
for (const domain of domains) {
|
|
nodes.set(domain.id, {
|
|
domain,
|
|
children: []
|
|
});
|
|
}
|
|
for (const domain of domains) {
|
|
const node = nodes.get(domain.id);
|
|
if (!node) {
|
|
continue;
|
|
}
|
|
const parent = domain.parent ? nodes.get(domain.parent) : void 0;
|
|
if (!parent || domain.parent === domain.id || hasAncestor(parent, domain.id, nodes)) {
|
|
roots.push(node);
|
|
continue;
|
|
}
|
|
parent.children.push(node);
|
|
}
|
|
return roots;
|
|
}
|
|
function hasAncestor(node, targetId, nodes) {
|
|
const seen = /* @__PURE__ */ new Set();
|
|
let current = node;
|
|
while (current?.domain.parent) {
|
|
if (current.domain.id === targetId || seen.has(current.domain.id)) {
|
|
return true;
|
|
}
|
|
seen.add(current.domain.id);
|
|
current = nodes.get(current.domain.parent);
|
|
}
|
|
return current?.domain.id === targetId;
|
|
}
|
|
|
|
// 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/views/mermaid-node-interactions.ts
|
|
function attachMermaidNodeInteractions(options) {
|
|
if (options.targets.length === 0) {
|
|
return () => void 0;
|
|
}
|
|
const svg = options.svg ?? options.rootEl.querySelector("svg");
|
|
if (!svg) {
|
|
return () => void 0;
|
|
}
|
|
const controller = new AbortController();
|
|
const dragThreshold = options.dragThreshold ?? 6;
|
|
const hoverIntervalMs = options.hoverIntervalMs ?? 350;
|
|
const source = options.source ?? "model-weave";
|
|
const nodeSelector = options.nodeSelector ?? "g.node";
|
|
const interactions = buildMermaidNodeInteractions(svg, options.targets, nodeSelector, options.findNodeElements);
|
|
let pointerStart = null;
|
|
let pendingOpen = null;
|
|
let lastPointerupOpen = null;
|
|
let lastHoverMermaidId = "";
|
|
let lastHoverAt = 0;
|
|
for (const interaction of interactions) {
|
|
if (options.nodeClassName) {
|
|
interaction.nodeEl.classList.add(options.nodeClassName);
|
|
}
|
|
setMermaidNodeTitle(interaction.nodeEl, interaction.target, options.formatTitle);
|
|
}
|
|
options.rootEl.addEventListener("pointerdown", (event) => {
|
|
const interaction = getMermaidNodeInteractionFromEvent(event, interactions);
|
|
pointerStart = interaction ? {
|
|
x: event.clientX,
|
|
y: event.clientY,
|
|
mermaidId: interaction.target.mermaidId
|
|
} : null;
|
|
pendingOpen = interaction ? {
|
|
pointerId: event.pointerId,
|
|
startX: event.clientX,
|
|
startY: event.clientY,
|
|
target: interaction.target,
|
|
opened: false,
|
|
canceled: false
|
|
} : null;
|
|
logMermaidInteractionClickDebug(options, "pointerdown", event, interaction, void 0, false, Boolean(interaction), false);
|
|
}, { signal: controller.signal });
|
|
const documentEl = options.rootEl.ownerDocument;
|
|
documentEl.addEventListener("pointermove", (event) => {
|
|
if (!pendingOpen || pendingOpen.pointerId !== event.pointerId) {
|
|
return;
|
|
}
|
|
const distance = getPendingMermaidOpenDistance(pendingOpen, event);
|
|
if (distance > dragThreshold) {
|
|
pendingOpen.canceled = true;
|
|
}
|
|
}, { signal: controller.signal });
|
|
documentEl.addEventListener("pointerup", (event) => {
|
|
const pending = pendingOpen;
|
|
if (!pending || pending.pointerId !== event.pointerId) {
|
|
return;
|
|
}
|
|
const distance = getPendingMermaidOpenDistance(pending, event);
|
|
const isDrag = distance > dragThreshold;
|
|
const willOpen = Boolean(!pending.canceled && !pending.opened && !isDrag);
|
|
const interaction = findMermaidNodeInteractionByTarget(interactions, pending.target);
|
|
logMermaidInteractionClickDebug(options, "pointerup", event, interaction, distance, isDrag, willOpen, pending.opened);
|
|
if (willOpen) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
pending.opened = true;
|
|
lastPointerupOpen = { mermaidId: pending.target.mermaidId, at: event.timeStamp };
|
|
openMermaidNodeTarget(options, pending.target, event);
|
|
}
|
|
pendingOpen = null;
|
|
}, { signal: controller.signal });
|
|
documentEl.addEventListener("pointercancel", (event) => {
|
|
if (!pendingOpen || pendingOpen.pointerId !== event.pointerId) {
|
|
return;
|
|
}
|
|
pendingOpen.canceled = true;
|
|
pendingOpen = null;
|
|
}, { signal: controller.signal });
|
|
options.rootEl.addEventListener("pointermove", (event) => {
|
|
const interaction = getMermaidNodeInteractionFromEvent(event, interactions);
|
|
if (!interaction) {
|
|
lastHoverMermaidId = "";
|
|
return;
|
|
}
|
|
const shouldTrigger = interaction.target.mermaidId !== lastHoverMermaidId || event.timeStamp - lastHoverAt >= hoverIntervalMs;
|
|
if (!shouldTrigger) {
|
|
return;
|
|
}
|
|
lastHoverMermaidId = interaction.target.mermaidId;
|
|
lastHoverAt = event.timeStamp;
|
|
triggerMermaidNodeHoverPreview(options, source, interaction.nodeEl, interaction.target, event);
|
|
}, { signal: controller.signal });
|
|
options.rootEl.addEventListener("click", (event) => {
|
|
const interaction = getMermaidNodeInteractionFromEvent(event, interactions);
|
|
const dragDistance = getMermaidNodeDragDistance(pointerStart, event);
|
|
const willOpen = Boolean(
|
|
interaction && isMermaidNodeClick(pointerStart, event, interaction.target.mermaidId, dragThreshold)
|
|
);
|
|
const alreadyOpenedByPointerup = Boolean(
|
|
interaction && lastPointerupOpen?.mermaidId === interaction.target.mermaidId && event.timeStamp - lastPointerupOpen.at < 1e3
|
|
);
|
|
logMermaidInteractionClickDebug(
|
|
options,
|
|
"click",
|
|
event,
|
|
interaction,
|
|
dragDistance,
|
|
!willOpen && dragDistance !== void 0 && dragDistance > dragThreshold,
|
|
willOpen && !alreadyOpenedByPointerup,
|
|
alreadyOpenedByPointerup || (pendingOpen?.opened ?? false)
|
|
);
|
|
if (!interaction || !willOpen || alreadyOpenedByPointerup) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (pendingOpen && pendingOpen.target === interaction.target) {
|
|
if (pendingOpen.opened) {
|
|
pendingOpen = null;
|
|
return;
|
|
}
|
|
pendingOpen.opened = true;
|
|
}
|
|
openMermaidNodeTarget(options, interaction.target, event);
|
|
pendingOpen = null;
|
|
}, { signal: controller.signal });
|
|
return () => controller.abort();
|
|
}
|
|
function attachGraphElementHoverPreview(options) {
|
|
const fallback = options.rootEl ?? getElementHoverFallback(options.targetEl);
|
|
if (!fallback || !options.target.linktext || !options.target.sourcePath) {
|
|
return () => void 0;
|
|
}
|
|
const controller = new AbortController();
|
|
const source = options.source ?? "model-weave";
|
|
const hoverIntervalMs = options.hoverIntervalMs ?? 350;
|
|
let lastHoverAt = 0;
|
|
options.targetEl.addEventListener("pointermove", (event) => {
|
|
if (event.timeStamp - lastHoverAt < hoverIntervalMs) {
|
|
return;
|
|
}
|
|
lastHoverAt = event.timeStamp;
|
|
triggerGraphInteractionHoverPreview(
|
|
options.app,
|
|
source,
|
|
getGraphHoverParent(options.hoverParent, options.targetEl, fallback),
|
|
options.targetEl,
|
|
options.target,
|
|
event
|
|
);
|
|
}, { signal: controller.signal });
|
|
return () => controller.abort();
|
|
}
|
|
function buildMermaidNodeInteractions(svg, targets, nodeSelector, findNodeElements) {
|
|
if (findNodeElements) {
|
|
return findNodeElements(svg, targets).map((match) => ({
|
|
nodeEl: match.element,
|
|
target: match.target
|
|
}));
|
|
}
|
|
return targets.map((target) => {
|
|
const nodeEl = findMermaidSvgNode(svg, target.mermaidId, nodeSelector);
|
|
return nodeEl ? { nodeEl, target } : null;
|
|
}).filter((interaction) => Boolean(interaction));
|
|
}
|
|
function findMermaidSvgNode(svg, mermaidId, nodeSelector) {
|
|
const nodes = Array.from(svg.querySelectorAll(nodeSelector));
|
|
return nodes.find((node) => node.id.includes(mermaidId)) ?? null;
|
|
}
|
|
function getMermaidNodeInteractionFromEvent(event, interactions) {
|
|
const eventTarget = event.target;
|
|
if (!(eventTarget instanceof Element)) {
|
|
return null;
|
|
}
|
|
return interactions.find(
|
|
(interaction) => interaction.nodeEl === eventTarget || interaction.nodeEl.contains(eventTarget)
|
|
) ?? null;
|
|
}
|
|
function findMermaidNodeInteractionByTarget(interactions, target) {
|
|
return interactions.find((interaction) => interaction.target === target) ?? null;
|
|
}
|
|
function setMermaidNodeTitle(nodeEl, target, formatTitle) {
|
|
const titleText = formatTitle?.(target) ?? target.label ?? target.linktext;
|
|
if (!titleText) {
|
|
return;
|
|
}
|
|
const doc = nodeEl.ownerDocument;
|
|
const title = nodeEl.querySelector("title") ?? doc.createElementNS("http://www.w3.org/2000/svg", "title");
|
|
title.textContent = titleText;
|
|
if (!title.parentElement) {
|
|
nodeEl.prepend(title);
|
|
}
|
|
}
|
|
function triggerMermaidNodeHoverPreview(options, source, targetEl, target, event) {
|
|
if (!target.linktext || !target.sourcePath) {
|
|
return;
|
|
}
|
|
const hoverParent = getGraphHoverParent(options.hoverParent, targetEl, options.rootEl);
|
|
triggerGraphInteractionHoverPreview(
|
|
options.app,
|
|
source,
|
|
hoverParent,
|
|
targetEl,
|
|
target,
|
|
event
|
|
);
|
|
}
|
|
function triggerGraphInteractionHoverPreview(app, source, hoverParent, targetEl, target, event) {
|
|
if (!target.linktext || !target.sourcePath) {
|
|
return;
|
|
}
|
|
try {
|
|
app.workspace.trigger("hover-link", {
|
|
event,
|
|
source,
|
|
hoverParent,
|
|
targetEl,
|
|
linktext: target.linktext,
|
|
sourcePath: target.sourcePath
|
|
});
|
|
} catch {
|
|
}
|
|
}
|
|
function getGraphHoverParent(hoverParent, targetEl, fallback) {
|
|
return typeof hoverParent === "function" ? hoverParent(targetEl, fallback) : hoverParent ?? fallback;
|
|
}
|
|
function getElementHoverFallback(targetEl) {
|
|
return targetEl instanceof HTMLElement ? targetEl : targetEl.ownerSVGElement?.parentElement ?? null;
|
|
}
|
|
function isMermaidNodeClick(pointerStart, event, mermaidId, dragThreshold) {
|
|
if (!pointerStart) {
|
|
return true;
|
|
}
|
|
if (mermaidId && pointerStart.mermaidId !== mermaidId) {
|
|
return false;
|
|
}
|
|
const distance = getMermaidNodeDragDistance(pointerStart, event);
|
|
return distance === void 0 || distance <= dragThreshold;
|
|
}
|
|
function getMermaidNodeDragDistance(pointerStart, event) {
|
|
if (!pointerStart) {
|
|
return void 0;
|
|
}
|
|
return Math.hypot(event.clientX - pointerStart.x, event.clientY - pointerStart.y);
|
|
}
|
|
function getPendingMermaidOpenDistance(pendingOpen, event) {
|
|
return Math.hypot(event.clientX - pendingOpen.startX, event.clientY - pendingOpen.startY);
|
|
}
|
|
function openMermaidNodeTarget(options, target, event) {
|
|
logMermaidInteractionOpenDebug(options, target);
|
|
if (options.openLinkText) {
|
|
void options.openLinkText(target, event);
|
|
return;
|
|
}
|
|
void options.app.workspace.openLinkText(
|
|
target.linktext,
|
|
target.sourcePath,
|
|
event.ctrlKey || event.metaKey
|
|
);
|
|
}
|
|
function logMermaidInteractionClickDebug(options, phase, event, interaction, dragDistance, isDrag, willOpen, opened = false) {
|
|
if (options.isDebugEnabled?.() !== true) {
|
|
return;
|
|
}
|
|
const eventTarget = event.target instanceof Element ? event.target : null;
|
|
const currentTarget = event.currentTarget instanceof Element ? event.currentTarget : null;
|
|
const target = interaction?.target;
|
|
console.debug("Model Weave mermaid interaction click debug", {
|
|
debugName: options.debugName,
|
|
phase,
|
|
eventTargetTag: eventTarget?.tagName,
|
|
eventTargetId: eventTarget?.id,
|
|
currentTargetTag: currentTarget?.tagName,
|
|
currentTargetId: currentTarget?.id,
|
|
resolvedTarget: target ? {
|
|
mermaidId: target.mermaidId,
|
|
linktext: target.linktext,
|
|
sourcePath: target.sourcePath,
|
|
filePath: target.filePath,
|
|
kind: target.kind,
|
|
targetType: target.targetType
|
|
} : null,
|
|
dragDistance,
|
|
isDrag,
|
|
willOpen,
|
|
opened
|
|
});
|
|
}
|
|
function logMermaidInteractionOpenDebug(options, target) {
|
|
if (options.isDebugEnabled?.() !== true) {
|
|
return;
|
|
}
|
|
console.debug("Model Weave mermaid interaction open link", {
|
|
debugName: options.debugName,
|
|
linktext: target.linktext,
|
|
sourcePath: target.sourcePath,
|
|
filePath: target.filePath
|
|
});
|
|
}
|
|
|
|
// 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 = activeDocument.createElement("section");
|
|
root.addClass("model-weave-diagram-shell");
|
|
if (!options?.hideTitle) {
|
|
const title = activeDocument.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 = activeDocument.createElement("div");
|
|
canvas.addClass("model-weave-diagram-canvas");
|
|
if (!options?.forExport) {
|
|
canvas.addClass("model-weave-diagram-canvas-interactive");
|
|
}
|
|
const toolbar = options?.forExport ? null : createZoomToolbar("Ctrl/Meta + wheel: zoom / Drag background: pan", {
|
|
onExportPng: options?.onExportPng,
|
|
onExportAndOpenPng: options?.onExportAndOpenPng,
|
|
exportPngLabel: options?.exportPngLabel,
|
|
exportPngTitle: options?.exportPngTitle,
|
|
exportAndOpenPngLabel: options?.exportAndOpenPngLabel,
|
|
exportAndOpenPngTitle: options?.exportAndOpenPngTitle
|
|
});
|
|
if (toolbar) {
|
|
root.appendChild(toolbar.root);
|
|
}
|
|
const viewport = activeDocument.createElement("div");
|
|
viewport.addClass("model-weave-diagram-viewport");
|
|
const surface = activeDocument.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, options?.classDetailLabels));
|
|
}
|
|
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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.createElementNS(SVG_NS, "g");
|
|
const { startX, startY, endX, endY, midX, midY } = getConnectionPoints(
|
|
source,
|
|
target
|
|
);
|
|
const line = activeDocument.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 = activeDocument.createElementNS(SVG_NS, "g");
|
|
const width = Math.max(52, value.length * 8 + 12);
|
|
const height = 20;
|
|
const rect = activeDocument.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 = activeDocument.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 = activeDocument.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;
|
|
}
|
|
attachClassNodeHoverPreview(box, layout, options);
|
|
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 = activeDocument.createElement("header");
|
|
header.addClass("model-weave-node-header");
|
|
header.addClass(getHeaderModifierClass(object.kind));
|
|
const kind = activeDocument.createElement("div");
|
|
kind.addClass("model-weave-node-kind");
|
|
kind.textContent = object.kind;
|
|
const title = activeDocument.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 attachClassNodeHoverPreview(box, layout, options) {
|
|
if (!options?.app || options.forExport) {
|
|
return;
|
|
}
|
|
const target = createClassNodeInteractionTarget(layout, options.interactionSourcePath);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
attachGraphElementHoverPreview({
|
|
app: options.app,
|
|
targetEl: box,
|
|
target,
|
|
source: "model-weave"
|
|
});
|
|
}
|
|
function createClassNodeInteractionTarget(layout, sourcePath) {
|
|
const object = layout.node.object;
|
|
if (!object?.path) {
|
|
return null;
|
|
}
|
|
const label = object.fileType === "er-entity" ? object.logicalName || object.physicalName || layout.node.label || layout.node.id : object.name || layout.node.label || layout.node.id;
|
|
const resolvedSourcePath = sourcePath ?? object.path;
|
|
const targetType = object.fileType === "er-entity" ? "er_entity" : "class";
|
|
const kind = object.path === resolvedSourcePath ? targetType === "er_entity" ? "er-current" : "class-current" : targetType === "er_entity" ? "er-reference" : "class-reference";
|
|
return {
|
|
mermaidId: layout.node.id,
|
|
linktext: object.path,
|
|
sourcePath: resolvedSourcePath,
|
|
label,
|
|
kind,
|
|
targetType,
|
|
filePath: object.path,
|
|
modelId: layout.node.id,
|
|
modelType: object.fileType
|
|
};
|
|
}
|
|
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 = activeDocument.createElement("section");
|
|
section.addClass("model-weave-node-section");
|
|
const heading = activeDocument.createElement("div");
|
|
heading.addClass("model-weave-node-section-heading");
|
|
heading.textContent = title;
|
|
section.appendChild(heading);
|
|
if (items.length === 0) {
|
|
const empty = activeDocument.createElement("div");
|
|
empty.addClass("model-weave-node-empty");
|
|
empty.textContent = "None";
|
|
section.appendChild(empty);
|
|
return section;
|
|
}
|
|
const list = activeDocument.createElement("ul");
|
|
list.addClass("model-weave-node-list");
|
|
for (const item of items) {
|
|
const entry = activeDocument.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, labels) {
|
|
const section = activeDocument.createElement("details");
|
|
section.addClass("model-weave-diagram-details");
|
|
section.open = false;
|
|
const summary = activeDocument.createElement("summary");
|
|
summary.textContent = `${labels?.displayedRelations ?? "Displayed relations"} (${diagram.edges.length})`;
|
|
summary.addClass("model-weave-diagram-details-summary");
|
|
section.appendChild(summary);
|
|
if (diagram.edges.length === 0) {
|
|
const empty = activeDocument.createElement("p");
|
|
empty.textContent = labels?.noRelationsUsed ?? "No relations are currently used for rendering.";
|
|
empty.addClass("model-weave-diagram-details-empty");
|
|
section.appendChild(empty);
|
|
return section;
|
|
}
|
|
const list = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.createElementNS(SVG_NS2, "g");
|
|
const width = Math.max(34, value.length * 8 + 12);
|
|
const height = 20;
|
|
const rect = activeDocument.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 = activeDocument.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 = activeDocument.createElement("section");
|
|
root.addClass("model-weave-diagram-shell");
|
|
if (!options?.hideTitle) {
|
|
const title = activeDocument.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 = activeDocument.createElement("div");
|
|
canvas.addClass("model-weave-diagram-canvas");
|
|
if (!options?.forExport) {
|
|
canvas.addClass("model-weave-diagram-canvas-interactive");
|
|
}
|
|
const toolbar = options?.forExport ? null : createZoomToolbar("Ctrl/Meta + wheel: zoom / Drag background: pan", {
|
|
onExportPng: options?.onExportPng,
|
|
onExportAndOpenPng: options?.onExportAndOpenPng,
|
|
exportPngLabel: options?.exportPngLabel,
|
|
exportPngTitle: options?.exportPngTitle,
|
|
exportAndOpenPngLabel: options?.exportAndOpenPngLabel,
|
|
exportAndOpenPngTitle: options?.exportAndOpenPngTitle
|
|
});
|
|
if (toolbar) {
|
|
root.appendChild(toolbar.root);
|
|
}
|
|
const viewport = activeDocument.createElement("div");
|
|
viewport.addClass("model-weave-diagram-viewport");
|
|
const surface = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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;
|
|
}
|
|
attachErNodeHoverPreview(box, layout, options);
|
|
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 = activeDocument.createElement("header");
|
|
header.addClass("model-weave-node-header");
|
|
header.addClass("model-weave-node-header-er");
|
|
const kind = activeDocument.createElement("div");
|
|
kind.addClass("model-weave-node-kind");
|
|
kind.textContent = object.fileType === "er-entity" ? "er_entity" : "entity";
|
|
const title = activeDocument.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 = activeDocument.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 attachErNodeHoverPreview(box, layout, options) {
|
|
if (!options?.app || options.forExport) {
|
|
return;
|
|
}
|
|
const target = createErNodeInteractionTarget(layout, options.interactionSourcePath);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
attachGraphElementHoverPreview({
|
|
app: options.app,
|
|
targetEl: box,
|
|
target,
|
|
source: "model-weave"
|
|
});
|
|
}
|
|
function createErNodeInteractionTarget(layout, sourcePath) {
|
|
const object = layout.node.object;
|
|
if (!object?.path) {
|
|
return null;
|
|
}
|
|
const label = object.fileType === "er-entity" ? object.logicalName || object.physicalName || layout.node.label || layout.node.id : object.name || layout.node.label || layout.node.id;
|
|
const resolvedSourcePath = sourcePath ?? object.path;
|
|
const targetType = object.fileType === "er-entity" ? "er_entity" : "class";
|
|
const kind = object.path === resolvedSourcePath ? targetType === "er_entity" ? "er-current" : "class-current" : targetType === "er_entity" ? "er-reference" : "class-reference";
|
|
return {
|
|
mermaidId: layout.node.id,
|
|
linktext: object.path,
|
|
sourcePath: resolvedSourcePath,
|
|
label,
|
|
kind,
|
|
targetType,
|
|
filePath: object.path,
|
|
modelId: layout.node.id,
|
|
modelType: object.fileType
|
|
};
|
|
}
|
|
function createAttributeSection(items) {
|
|
const section = activeDocument.createElement("section");
|
|
section.addClass("model-weave-node-section");
|
|
const heading = activeDocument.createElement("div");
|
|
heading.addClass("model-weave-node-section-heading");
|
|
heading.textContent = "Columns";
|
|
section.appendChild(heading);
|
|
if (items.length === 0) {
|
|
const empty = activeDocument.createElement("div");
|
|
empty.addClass("model-weave-node-empty");
|
|
empty.textContent = "None";
|
|
section.appendChild(empty);
|
|
return section;
|
|
}
|
|
const list = activeDocument.createElement("ul");
|
|
list.addClass("model-weave-node-list");
|
|
for (const item of items) {
|
|
const entry = activeDocument.createElement("li");
|
|
entry.textContent = item;
|
|
list.appendChild(entry);
|
|
}
|
|
section.appendChild(list);
|
|
return section;
|
|
}
|
|
function createRelationTable(diagram) {
|
|
const section = activeDocument.createElement("details");
|
|
section.addClass("model-weave-diagram-details");
|
|
section.open = false;
|
|
const summary = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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;
|
|
var ER_MERMAID_READABLE_STYLE_ID = "model-weave-er-mermaid-readable-style";
|
|
var ER_DETAIL_INTERACTION_NODE_SELECTOR = "g.node, g.entityBox";
|
|
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),
|
|
interactionTargets: buildClassOverviewInteractionTargets(
|
|
diagram,
|
|
options?.interactionSourcePath ?? diagram.diagram.path
|
|
),
|
|
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),
|
|
interactionTargets: buildClassDetailInteractionTargets(
|
|
diagram,
|
|
options?.interactionSourcePath ?? diagram.diagram.path
|
|
),
|
|
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),
|
|
interactionTargets: buildErOverviewInteractionTargets(
|
|
diagram,
|
|
options?.interactionSourcePath ?? diagram.diagram.path
|
|
),
|
|
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),
|
|
interactionTargets: buildErDetailInteractionTargets(
|
|
diagram,
|
|
options?.interactionSourcePath ?? diagram.diagram.path
|
|
),
|
|
interactionNodeSelector: ER_DETAIL_INTERACTION_NODE_SELECTOR,
|
|
interactionFindNodeElements: findErDetailMermaidInteractionNodes,
|
|
interactionDebugName: "ER detail",
|
|
options,
|
|
fallback: () => renderErDiagram(diagram, options),
|
|
afterRenderSvg: applyErMermaidReadableSvgStyle,
|
|
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 shell3 = createMermaidShell({
|
|
className: config.className,
|
|
title: config.title,
|
|
forExport: config.options?.forExport,
|
|
onExportPng: config.options?.onExportPng,
|
|
onExportAndOpenPng: config.options?.onExportAndOpenPng,
|
|
exportPngLabel: config.options?.exportPngLabel,
|
|
exportPngTitle: config.options?.exportPngTitle,
|
|
exportAndOpenPngLabel: config.options?.exportAndOpenPngLabel,
|
|
exportAndOpenPngTitle: config.options?.exportAndOpenPngTitle
|
|
});
|
|
const ready = renderMermaidSourceIntoShell(shell3, {
|
|
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,
|
|
sourcePanelTitle: config.options?.sourcePanelTitle,
|
|
sourcePanelCopyLabel: config.options?.sourcePanelCopyLabel,
|
|
showRenderDebug: !config.options?.forExport && config.options?.showMermaidRenderDebug === true
|
|
}).then(() => {
|
|
const svg = shell3.surface.querySelector("svg");
|
|
if (svg?.instanceOf(SVGSVGElement)) {
|
|
config.afterRenderSvg?.(svg);
|
|
}
|
|
const interactionSelector = config.interactionNodeSelector ?? "g.node";
|
|
if (!config.options?.forExport && config.options?.showMermaidRenderDebug === true && config.interactionDebugName) {
|
|
logMermaidInteractionDebug(
|
|
config.interactionDebugName,
|
|
svg ?? null,
|
|
config.interactionTargets ?? [],
|
|
interactionSelector,
|
|
config.interactionFindNodeElements,
|
|
Boolean(config.options?.app)
|
|
);
|
|
}
|
|
if (!config.options?.forExport && config.options?.app && (config.interactionTargets?.length ?? 0) > 0) {
|
|
attachMermaidNodeInteractions({
|
|
app: config.options.app,
|
|
rootEl: shell3.surface,
|
|
targets: config.interactionTargets ?? [],
|
|
source: "model-weave",
|
|
nodeClassName: "model-weave-mermaid-interactive-node",
|
|
nodeSelector: interactionSelector,
|
|
findNodeElements: config.interactionFindNodeElements,
|
|
dragThreshold: 6,
|
|
isDebugEnabled: () => config.options?.showMermaidRenderDebug === true && Boolean(config.interactionDebugName),
|
|
debugName: config.interactionDebugName,
|
|
hoverParent: (nodeEl, fallback) => nodeEl.closest(
|
|
".model-weave-view-only-stage, .mdspec-diagram--class, .mdspec-diagram--er, .model-weave-mermaid-shell"
|
|
) ?? fallback,
|
|
formatTitle: (target) => target.label ? `${target.label} (${target.targetType ?? "model"})` : target.linktext
|
|
});
|
|
}
|
|
}).catch(() => {
|
|
const fallback = config.fallback();
|
|
const notice = createMermaidFallbackNotice(config.fallbackMessage);
|
|
shell3.root.replaceChildren(notice, ...Array.from(fallback.childNodes));
|
|
});
|
|
setMermaidRenderReadyPromise(shell3.root, ready);
|
|
return shell3.root;
|
|
}
|
|
function logMermaidInteractionDebug(debugName, svg, targets, selector, findNodeElements, hasApp) {
|
|
const matches = svg ? findNodeElements?.(svg, targets) ?? Array.from(svg.querySelectorAll(selector)).map((element) => ({
|
|
element,
|
|
target: targets.find((candidate) => element.id === candidate.mermaidId)
|
|
})).filter((match) => Boolean(match.target)) : [];
|
|
const targetIds = targets.map((target) => target.mermaidId);
|
|
const matchedTargetIds = new Set(matches.map((match) => match.target.mermaidId));
|
|
const unmatchedTargetIds = targetIds.filter((targetId) => !matchedTargetIds.has(targetId));
|
|
console.debug("Model Weave ER detail interaction debug", {
|
|
debugName,
|
|
hasSvg: Boolean(svg),
|
|
hasApp,
|
|
attachEligible: Boolean(svg && hasApp && targets.length > 0),
|
|
targetCount: targets.length,
|
|
targetIds,
|
|
matchedCount: matches.length,
|
|
selector: findNodeElements ? void 0 : selector,
|
|
resolver: findNodeElements ? "er-detail-svg-id" : void 0,
|
|
matchedIds: matches.map((match) => ({
|
|
targetId: match.target.mermaidId,
|
|
elementId: match.element.id,
|
|
tagName: match.element.tagName,
|
|
className: match.element.getAttribute("class")
|
|
})),
|
|
unmatchedTargetIds
|
|
});
|
|
}
|
|
function findErDetailMermaidInteractionNodes(svg, targets) {
|
|
const entityGroups = getErDetailEntityGroupElements(svg);
|
|
const matches = [];
|
|
const matchedElements = /* @__PURE__ */ new Set();
|
|
for (const target of targets) {
|
|
const match = entityGroups.find(
|
|
(element) => !matchedElements.has(element) && isErDetailEntityElementForTarget(element, target.mermaidId)
|
|
);
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
matchedElements.add(match);
|
|
matches.push({
|
|
element: match,
|
|
target
|
|
});
|
|
}
|
|
return matches;
|
|
}
|
|
function getErDetailEntityGroupElements(svg) {
|
|
return Array.from(svg.querySelectorAll(
|
|
'g[id^="entity-"], g[id^="er-entity-"]'
|
|
));
|
|
}
|
|
function isErDetailEntityElementForTarget(element, targetId) {
|
|
const targetCanonicalId = canonicalizeErDetailEntityId(targetId);
|
|
const candidates = getErDetailSvgIdCandidates(element.id);
|
|
const matched = candidates.some(
|
|
(candidate) => canonicalizeErDetailEntityId(candidate) === targetCanonicalId
|
|
);
|
|
if (!matched) {
|
|
return false;
|
|
}
|
|
return getErDetailSvgElementExclusionReason(element) === null;
|
|
}
|
|
function getErDetailSvgElementExclusionReason(element) {
|
|
const className = element.getAttribute("class") ?? "";
|
|
const identity = `${element.id} ${className}`.toLowerCase();
|
|
if (/edge|relationship|cardinality|label|attr|attribute|marker|style/.test(identity)) {
|
|
return "edge/relationship/cardinality/label/attribute/marker/style";
|
|
}
|
|
if (element.id.toLowerCase().includes("-attr-")) {
|
|
return "attribute row";
|
|
}
|
|
if (element.tagName.toLowerCase() !== "g") {
|
|
return "not entity group";
|
|
}
|
|
if (!/entity|er/.test(identity)) {
|
|
return "missing entity/er id or class hint";
|
|
}
|
|
return null;
|
|
}
|
|
function getErDetailSvgIdCandidates(id) {
|
|
const stripped = stripErDetailEntitySvgId(id);
|
|
const candidates = /* @__PURE__ */ new Set();
|
|
candidates.add(id);
|
|
candidates.add(stripped);
|
|
candidates.add(canonicalizeErDetailEntityId(stripped));
|
|
return Array.from(candidates).filter((candidate) => candidate.length > 0);
|
|
}
|
|
function stripErDetailEntitySvgId(value) {
|
|
return value.replace(/^text-entity-/, "").replace(/^er-entity-/, "").replace(/^entity-/, "").replace(/^er-/, "").replace(
|
|
/-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?:-.+)?$/i,
|
|
""
|
|
);
|
|
}
|
|
function canonicalizeErDetailEntityId(value) {
|
|
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
}
|
|
function buildClassOverviewInteractionTargets(diagram, sourcePath) {
|
|
return buildDiagramNodeInteractionTargets(
|
|
diagram,
|
|
sourcePath,
|
|
(node, usedIds) => ensureUniqueMermaidId(sanitizeMermaidId(node.id), usedIds),
|
|
"class"
|
|
);
|
|
}
|
|
function buildClassDetailInteractionTargets(diagram, sourcePath) {
|
|
return buildDiagramNodeInteractionTargets(
|
|
diagram,
|
|
sourcePath,
|
|
(node, usedIds) => ensureUniqueMermaidId(sanitizeMermaidId(node.id), usedIds),
|
|
"class"
|
|
);
|
|
}
|
|
function buildErOverviewInteractionTargets(diagram, sourcePath) {
|
|
return buildDiagramNodeInteractionTargets(
|
|
diagram,
|
|
sourcePath,
|
|
(node, usedIds) => ensureUniqueMermaidId(sanitizeMermaidId(node.id), usedIds),
|
|
"er"
|
|
);
|
|
}
|
|
function buildErDetailInteractionTargets(diagram, sourcePath) {
|
|
return buildDiagramNodeInteractionTargets(
|
|
diagram,
|
|
sourcePath,
|
|
(node, usedIds) => {
|
|
const entity = node.object && node.object.fileType === "er-entity" ? node.object : void 0;
|
|
return ensureUniqueMermaidId(
|
|
sanitizeMermaidId(entity?.physicalName || entity?.id || node.id),
|
|
usedIds
|
|
);
|
|
},
|
|
"er"
|
|
);
|
|
}
|
|
function buildDiagramNodeInteractionTargets(diagram, sourcePath, getMermaidId, mode) {
|
|
const usedIds = /* @__PURE__ */ new Set();
|
|
return diagram.nodes.map((node) => {
|
|
const mermaidId = getMermaidId(node, usedIds);
|
|
return mode === "class" ? buildClassNodeInteractionTarget(node, mermaidId, sourcePath) : buildErNodeInteractionTarget(node, mermaidId, sourcePath);
|
|
}).filter((target) => Boolean(target));
|
|
}
|
|
function buildClassNodeInteractionTarget(node, mermaidId, sourcePath) {
|
|
const object = node.object && node.object.fileType === "object" ? node.object : void 0;
|
|
if (!object?.path) {
|
|
return null;
|
|
}
|
|
const label = node.label ?? object.name ?? node.id;
|
|
return {
|
|
mermaidId,
|
|
linktext: object.path,
|
|
sourcePath,
|
|
label,
|
|
kind: object.path === sourcePath ? "class-current" : "class-node",
|
|
targetType: "class",
|
|
filePath: object.path,
|
|
modelId: getClassObjectId2(object) ?? node.id,
|
|
modelType: object.fileType
|
|
};
|
|
}
|
|
function buildErNodeInteractionTarget(node, mermaidId, sourcePath) {
|
|
const entity = node.object && node.object.fileType === "er-entity" ? node.object : void 0;
|
|
if (!entity?.path) {
|
|
return null;
|
|
}
|
|
const label = node.label ?? entity.logicalName ?? entity.physicalName ?? node.id;
|
|
return {
|
|
mermaidId,
|
|
linktext: entity.path,
|
|
sourcePath,
|
|
label,
|
|
kind: entity.path === sourcePath ? "er-current" : "er-entity-node",
|
|
targetType: "er_entity",
|
|
filePath: entity.path,
|
|
modelId: entity.id || node.id,
|
|
modelType: entity.fileType
|
|
};
|
|
}
|
|
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 = sanitizeEdgeLabel2(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 = sanitizeEdgeLabel2(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 applyErMermaidReadableSvgStyle(svg) {
|
|
if (svg.querySelector(`#${ER_MERMAID_READABLE_STYLE_ID}`)) {
|
|
return;
|
|
}
|
|
const style = svg.ownerDocument.createElementNS(
|
|
"http://www.w3.org/2000/svg",
|
|
"style"
|
|
);
|
|
style.setAttribute("id", ER_MERMAID_READABLE_STYLE_ID);
|
|
style.textContent = buildErMermaidReadableSvgStyle();
|
|
svg.prepend(style);
|
|
}
|
|
function buildErMermaidReadableSvgStyle() {
|
|
return [
|
|
".er.entityBox,.entityBox,.er.attributeBoxOdd,.attributeBoxOdd,.er.attributeBoxEven,.attributeBoxEven{fill:#f8fafc!important;stroke:#64748b!important;}",
|
|
".er.relationshipLabelBox,.relationshipLabelBox{fill:#f8fafc!important;stroke:#cbd5e1!important;}",
|
|
".er.entityLabel,.entityLabel,.er.attribute-type,.attribute-type,.er.attribute-name,.attribute-name,.er.attribute-key,.attribute-key,.er.relationshipLabel,.relationshipLabel{fill:#111827!important;color:#111827!important;}",
|
|
"text,tspan{fill:#111827!important;color:#111827!important;}"
|
|
].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 sanitizeEdgeLabel2(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 = sanitizeEdgeLabel2(
|
|
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 = sanitizeEdgeLabel2(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 = activeDocument.createElement("section");
|
|
root.className = "mdspec-diagram mdspec-diagram--component";
|
|
const title = activeDocument.createElement("h2");
|
|
title.textContent = `${diagram.diagram.name} (component)`;
|
|
root.appendChild(title);
|
|
const grid = activeDocument.createElement("div");
|
|
grid.className = "mdspec-component-grid";
|
|
for (const node of diagram.nodes) {
|
|
const box = activeDocument.createElement("article");
|
|
box.className = "mdspec-component";
|
|
const heading = activeDocument.createElement("h3");
|
|
heading.textContent = getNodeLabel(node);
|
|
box.appendChild(heading);
|
|
const description = activeDocument.createElement("p");
|
|
description.textContent = getNodeDescription(node);
|
|
box.appendChild(description);
|
|
grid.appendChild(box);
|
|
}
|
|
if (grid.childElementCount === 0) {
|
|
const empty = activeDocument.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 shell3 = createMermaidShell({
|
|
className: "mdspec-diagram mdspec-diagram--dfd",
|
|
title: options?.hideTitle ? void 0 : `${diagram.diagram.name} (dfd)`,
|
|
forExport: options?.forExport,
|
|
onExportPng: options?.onExportPng,
|
|
onExportAndOpenPng: options?.onExportAndOpenPng,
|
|
exportPngLabel: options?.exportPngLabel,
|
|
exportPngTitle: options?.exportPngTitle,
|
|
exportAndOpenPngLabel: options?.exportAndOpenPngLabel,
|
|
exportAndOpenPngTitle: options?.exportAndOpenPngTitle
|
|
});
|
|
if (!options?.hideDetails) {
|
|
const domainDetails = createDomainPlacementDetails(diagram, options?.dfdDetailLabels);
|
|
if (domainDetails) {
|
|
shell3.root.appendChild(domainDetails);
|
|
}
|
|
shell3.root.appendChild(createObjectDetails(diagram, options?.dfdDetailLabels));
|
|
shell3.root.appendChild(createFlowDetails(diagram.edges, options?.dfdDetailLabels));
|
|
}
|
|
const interactionTargets = buildDfdMermaidInteractionTargets(
|
|
diagram,
|
|
options?.interactionSourcePath ?? diagram.diagram.path
|
|
);
|
|
const ready = renderMermaidSourceIntoShell(shell3, {
|
|
source: buildDfdMermaidSource(diagram, options?.colorScheme),
|
|
renderIdPrefix: "model_weave_dfd",
|
|
fitVerticalAlign: options?.fitVerticalAlign,
|
|
viewportState: options?.viewportState,
|
|
onViewportStateChange: options?.onViewportStateChange,
|
|
showSourcePanel: !options?.forExport,
|
|
sourcePanelContainer: options?.sourcePanelContainer,
|
|
sourcePanelPlacement: options?.sourcePanelPlacement,
|
|
sourcePanelTitle: options?.sourcePanelTitle,
|
|
sourcePanelCopyLabel: options?.sourcePanelCopyLabel,
|
|
showRenderDebug: !options?.forExport && options?.showMermaidRenderDebug === true
|
|
}).then(() => {
|
|
if (!options?.forExport && options?.app && interactionTargets.length > 0) {
|
|
attachMermaidNodeInteractions({
|
|
app: options.app,
|
|
rootEl: shell3.surface,
|
|
targets: interactionTargets,
|
|
source: "model-weave",
|
|
nodeClassName: "model-weave-mermaid-interactive-node",
|
|
dragThreshold: 6,
|
|
hoverParent: (nodeEl, fallback) => nodeEl.closest(
|
|
".model-weave-view-only-stage, .mdspec-diagram--dfd, .model-weave-mermaid-shell"
|
|
) ?? fallback,
|
|
formatTitle: (target) => target.label ? `${target.label} (${target.targetType ?? "model"})` : target.linktext
|
|
});
|
|
}
|
|
}).catch(() => {
|
|
shell3.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(shell3.root, ready);
|
|
return shell3.root;
|
|
}
|
|
function buildDfdMermaidInteractionTargets(diagram, sourcePath) {
|
|
return diagram.nodes.map((node) => {
|
|
const object = getDfdObject(node);
|
|
if (!object?.path) {
|
|
return null;
|
|
}
|
|
const target = {
|
|
mermaidId: toMermaidNodeId(node.id),
|
|
linktext: object.path,
|
|
sourcePath,
|
|
label: node.label ?? object.name ?? node.id,
|
|
kind: "dfd-object",
|
|
targetType: object.fileType,
|
|
filePath: object.path,
|
|
modelId: object.id,
|
|
modelType: object.fileType
|
|
};
|
|
return target;
|
|
}).filter((target) => Boolean(target));
|
|
}
|
|
function buildDfdMermaidSource(diagram, colorScheme) {
|
|
const palette = getModelWeaveMermaidPalette();
|
|
const lines = ["flowchart LR"];
|
|
const colorClasses = /* @__PURE__ */ new Map();
|
|
const domainStyles = [];
|
|
if (!colorScheme) {
|
|
lines.push(
|
|
` ${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();
|
|
const localDomains = getDfdLocalDomains(diagram);
|
|
const localDomainsById = new Map(localDomains.map((domain) => [domain.id, domain]));
|
|
const groupedNodes = /* @__PURE__ */ new Map();
|
|
const ungroupedNodes = [];
|
|
for (const node of diagram.nodes) {
|
|
const domainId = getNodeDomainId(node);
|
|
if (domainId && localDomainsById.has(domainId)) {
|
|
if (!groupedNodes.has(domainId)) {
|
|
groupedNodes.set(domainId, []);
|
|
}
|
|
groupedNodes.get(domainId).push(node);
|
|
} else {
|
|
ungroupedNodes.push(node);
|
|
}
|
|
}
|
|
for (const root of buildDomainTree(localDomains)) {
|
|
appendDfdDomainSubgraph(
|
|
lines,
|
|
root,
|
|
groupedNodes,
|
|
nodeIds,
|
|
1,
|
|
colorScheme,
|
|
colorClasses,
|
|
domainStyles
|
|
);
|
|
}
|
|
for (const node of ungroupedNodes) {
|
|
const mermaidId = toMermaidNodeId(node.id);
|
|
nodeIds.set(node.id, mermaidId);
|
|
lines.push(` ${mermaidId}${toMermaidNodeDeclaration(
|
|
node,
|
|
getDfdObject(node),
|
|
colorScheme,
|
|
colorClasses
|
|
)}`);
|
|
}
|
|
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}`);
|
|
}
|
|
}
|
|
if (colorClasses.size > 0) {
|
|
lines.push("");
|
|
for (const [className, style] of colorClasses) {
|
|
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
|
|
}
|
|
}
|
|
if (domainStyles.length > 0) {
|
|
lines.push("", ...domainStyles);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function appendDfdDomainSubgraph(lines, domainNode, groupedNodes, nodeIds, depth, colorScheme, colorClasses, domainStyles) {
|
|
const childLines = [];
|
|
for (const child of domainNode.children) {
|
|
appendDfdDomainSubgraph(
|
|
childLines,
|
|
child,
|
|
groupedNodes,
|
|
nodeIds,
|
|
depth + 1,
|
|
colorScheme,
|
|
colorClasses,
|
|
domainStyles
|
|
);
|
|
}
|
|
const nodes = groupedNodes.get(domainNode.domain.id) ?? [];
|
|
if (nodes.length === 0 && childLines.length === 0) {
|
|
return false;
|
|
}
|
|
const indent = " ".repeat(depth);
|
|
const domainMermaidId = toMermaidDomainId(domainNode.domain.id);
|
|
lines.push(`${indent}subgraph ${domainMermaidId}["${buildDomainLabel(domainNode.domain)}"]`);
|
|
lines.push(...childLines);
|
|
for (const node of nodes) {
|
|
const mermaidId = toMermaidNodeId(node.id);
|
|
nodeIds.set(node.id, mermaidId);
|
|
lines.push(`${indent} ${mermaidId}${toMermaidNodeDeclaration(
|
|
node,
|
|
getDfdObject(node),
|
|
colorScheme,
|
|
colorClasses
|
|
)}`);
|
|
}
|
|
lines.push(`${indent}end`);
|
|
if (colorScheme) {
|
|
domainStyles.push(
|
|
` style ${domainMermaidId} ${formatMermaidClassDefStyle(
|
|
resolveColorStyle(colorScheme, "domain", domainNode.domain.kind)
|
|
)}`
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
function getDfdLocalDomains(diagram) {
|
|
return isDfdDiagramModel(diagram.diagram) ? diagram.diagram.domains ?? [] : [];
|
|
}
|
|
function isDfdDiagramModel(diagram) {
|
|
return diagram.schema === "dfd_diagram";
|
|
}
|
|
function getDfdObject(node) {
|
|
return node.object && typeof node.object === "object" && "fileType" in node.object && node.object.fileType === "dfd-object" ? node.object : void 0;
|
|
}
|
|
function getNodeDomainId(node) {
|
|
const domain = node.metadata?.domain;
|
|
return typeof domain === "string" && domain.trim() ? domain.trim() : void 0;
|
|
}
|
|
function toMermaidDomainId(value) {
|
|
return `domain_${toMermaidNodeId(value)}`;
|
|
}
|
|
function buildDomainLabel(domain) {
|
|
const displayName = domain.name?.trim() || domain.id;
|
|
const label = domain.kind?.trim() ? `${displayName} [${domain.kind.trim()}]` : displayName;
|
|
return escapeMermaidLabel2(label);
|
|
}
|
|
function createDomainPlacementDetails(diagram, labels) {
|
|
if (!isDfdDiagramModel(diagram.diagram)) {
|
|
return null;
|
|
}
|
|
const sources = diagram.diagram.domainSourceSummaries ?? [];
|
|
const domainsById = new Map(getDfdLocalDomains(diagram).map((domain) => [domain.id, domain]));
|
|
const placed = diagram.nodes.map((node) => ({ node, domainId: getNodeDomainId(node) })).filter(
|
|
(entry) => Boolean(entry.domainId)
|
|
);
|
|
if (sources.length === 0 && placed.length === 0) {
|
|
return null;
|
|
}
|
|
const section = activeDocument.createElement("details");
|
|
section.className = "mdspec-section";
|
|
section.addClass("model-weave-diagram-details");
|
|
section.open = false;
|
|
const resolvedCount = placed.filter((entry) => domainsById.has(entry.domainId)).length;
|
|
const summary = activeDocument.createElement("summary");
|
|
summary.textContent = `${labels?.domainPlacement ?? "Domain placement"} (${resolvedCount}/${placed.length} ${labels?.resolved ?? "resolved"})`;
|
|
summary.addClass("model-weave-diagram-details-summary");
|
|
section.appendChild(summary);
|
|
const list = activeDocument.createElement("ul");
|
|
list.addClass("model-weave-diagram-details-list");
|
|
for (const source of sources) {
|
|
const item = activeDocument.createElement("li");
|
|
item.addClass("model-weave-diagram-details-item");
|
|
item.textContent = [
|
|
modelWeaveText("Source", "Source"),
|
|
source.ref.ref,
|
|
source.status,
|
|
source.resolvedPath ?? "-",
|
|
`${source.domainCount} domains`
|
|
].join(" / ");
|
|
list.appendChild(item);
|
|
}
|
|
for (const entry of placed) {
|
|
const domain = domainsById.get(entry.domainId);
|
|
const item = activeDocument.createElement("li");
|
|
item.addClass("model-weave-diagram-details-item");
|
|
item.textContent = [
|
|
modelWeaveText("Object", "Object"),
|
|
entry.node.id,
|
|
entry.domainId,
|
|
domain ? labels?.resolved ?? "resolved" : labels?.unresolved ?? "unresolved"
|
|
].join(" / ");
|
|
list.appendChild(item);
|
|
}
|
|
section.appendChild(list);
|
|
return section;
|
|
}
|
|
function createFlowDetails(edges, labels) {
|
|
const section = activeDocument.createElement("details");
|
|
section.className = "mdspec-section";
|
|
section.addClass("model-weave-diagram-details");
|
|
section.open = false;
|
|
const summary = activeDocument.createElement("summary");
|
|
summary.textContent = `${labels?.displayedFlows ?? "Displayed flows"} (${edges.length})`;
|
|
summary.addClass("model-weave-diagram-details-summary");
|
|
section.appendChild(summary);
|
|
if (edges.length === 0) {
|
|
const empty = activeDocument.createElement("p");
|
|
empty.textContent = labels?.noFlows ?? "No flows are used for rendering.";
|
|
empty.addClass("model-weave-diagram-details-empty");
|
|
section.appendChild(empty);
|
|
return section;
|
|
}
|
|
const list = activeDocument.createElement("ul");
|
|
list.addClass("model-weave-diagram-details-list");
|
|
for (const edge of edges) {
|
|
const item = activeDocument.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 createObjectDetails(diagram, labels) {
|
|
const section = activeDocument.createElement("details");
|
|
section.className = "mdspec-section";
|
|
section.addClass("model-weave-diagram-details");
|
|
section.open = false;
|
|
const summary = activeDocument.createElement("summary");
|
|
summary.textContent = `${labels?.displayedObjects ?? "Displayed objects"} (${diagram.nodes.length})`;
|
|
summary.addClass("model-weave-diagram-details-summary");
|
|
section.appendChild(summary);
|
|
if (diagram.nodes.length === 0) {
|
|
const empty = activeDocument.createElement("p");
|
|
empty.textContent = labels?.noObjects ?? "No objects are used for rendering.";
|
|
empty.addClass("model-weave-diagram-details-empty");
|
|
section.appendChild(empty);
|
|
return section;
|
|
}
|
|
const domainsById = new Map(getDfdLocalDomains(diagram).map((domain) => [domain.id, domain]));
|
|
const list = activeDocument.createElement("ul");
|
|
list.addClass("model-weave-diagram-details-list");
|
|
for (const node of diagram.nodes) {
|
|
const item = activeDocument.createElement("li");
|
|
item.addClass("model-weave-diagram-details-item");
|
|
const domainId = getNodeDomainId(node);
|
|
const domain = domainId ? domainsById.get(domainId) : void 0;
|
|
const domainLabel = domain ? domain.name || domain.id : domainId;
|
|
item.textContent = [
|
|
node.id,
|
|
node.label ?? node.ref ?? "-",
|
|
modelWeaveText("Domain", "Domain") + `: ${domainLabel ?? "-"}`
|
|
].join(" / ");
|
|
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, colorScheme, colorClasses) {
|
|
const label = escapeMermaidLabel2(node.label ?? object?.name ?? node.ref ?? node.id);
|
|
const kind = object?.kind ?? node.kind;
|
|
const className = colorScheme ? registerDfdColorClass(kind, colorScheme, colorClasses) : toBuiltInDfdClassName(kind);
|
|
switch (kind) {
|
|
case "datastore":
|
|
return `[("${label}")]:::${className}`;
|
|
case "process":
|
|
return `["${label}"]:::${className}`;
|
|
case "other":
|
|
return `["${label}"]:::${className}`;
|
|
case "external":
|
|
default:
|
|
return `["${label}"]:::${className}`;
|
|
}
|
|
}
|
|
function registerDfdColorClass(kind, colorScheme, colorClasses) {
|
|
const className = toDfdColorClassName(kind);
|
|
colorClasses?.set(className, resolveColorStyle(colorScheme, "dfd", kind));
|
|
return className;
|
|
}
|
|
function toBuiltInDfdClassName(kind) {
|
|
switch (kind) {
|
|
case "datastore":
|
|
return "dfdDatastore";
|
|
case "process":
|
|
return "dfdProcess";
|
|
case "other":
|
|
return "dfdOther";
|
|
case "external":
|
|
default:
|
|
return "dfdExternal";
|
|
}
|
|
}
|
|
function toDfdColorClassName(kind) {
|
|
const suffix = kind?.trim() ? kind.trim() : "default";
|
|
return `kind_dfd_${sanitizeMermaidId(suffix)}`;
|
|
}
|
|
function formatMermaidClassDefStyle(style) {
|
|
return [
|
|
style.fill ? `fill:${style.fill}` : void 0,
|
|
style.stroke ? `stroke:${style.stroke}` : void 0,
|
|
style.text ? `color:${style.text}` : void 0
|
|
].filter((entry) => Boolean(entry)).join(",");
|
|
}
|
|
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 = activeDocument.createElement("section");
|
|
root.className = "mdspec-diagram mdspec-diagram--flow";
|
|
const title = activeDocument.createElement("h2");
|
|
title.textContent = `${diagram.diagram.name} (flow)`;
|
|
root.appendChild(title);
|
|
const list = activeDocument.createElement("ol");
|
|
list.className = "mdspec-flow";
|
|
for (const node of diagram.nodes) {
|
|
const item = activeDocument.createElement("li");
|
|
item.textContent = getNodeLabel2(node);
|
|
list.appendChild(item);
|
|
}
|
|
if (list.childElementCount === 0) {
|
|
const empty = activeDocument.createElement("p");
|
|
empty.textContent = "No objects referenced.";
|
|
root.appendChild(empty);
|
|
} else {
|
|
root.appendChild(list);
|
|
}
|
|
if (diagram.edges.length > 0) {
|
|
const relations = activeDocument.createElement("ul");
|
|
for (const edge of diagram.edges) {
|
|
const item = activeDocument.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 = activeDocument.createElement("section");
|
|
root.className = "mdspec-fallback";
|
|
const title = activeDocument.createElement("h2");
|
|
title.textContent = modelWeaveText(
|
|
"Diagram preview is not available",
|
|
"Diagram preview \u306F\u5229\u7528\u3067\u304D\u307E\u305B\u3093"
|
|
);
|
|
const message = activeDocument.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 shell3 = createMermaidShell({
|
|
className: "model-weave-app-process-business-flow",
|
|
title: `${model.title} (app_process / business flow)`,
|
|
forExport: options.forExport,
|
|
onExportPng: options.onExportPng,
|
|
onExportAndOpenPng: options.onExportAndOpenPng,
|
|
exportPngLabel: options.exportPngLabel,
|
|
exportPngTitle: options.exportPngTitle,
|
|
exportAndOpenPngLabel: options.exportAndOpenPngLabel,
|
|
exportAndOpenPngTitle: options.exportAndOpenPngTitle
|
|
});
|
|
const sourcePath = options.interactionSourcePath ?? "";
|
|
const interactionTargets = buildAppProcessBusinessFlowInteractionTargets(
|
|
model,
|
|
sourcePath
|
|
);
|
|
const source = buildAppProcessBusinessFlowMermaidSource(
|
|
model,
|
|
options.colorScheme
|
|
);
|
|
const ready = renderMermaidSourceIntoShell(shell3, {
|
|
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 ?? shell3.root,
|
|
sourcePanelPlacement: options.sourcePanelPlacement,
|
|
sourcePanelTitle: options.sourcePanelTitle,
|
|
sourcePanelCopyLabel: options.sourcePanelCopyLabel,
|
|
showRenderDebug: !options.forExport && options.debug !== false && options.showMermaidRenderDebug === true
|
|
}).then(() => {
|
|
if (!options.forExport && options.app && interactionTargets.length > 0) {
|
|
attachMermaidNodeInteractions({
|
|
app: options.app,
|
|
rootEl: shell3.surface,
|
|
targets: interactionTargets,
|
|
source: "model-weave",
|
|
nodeClassName: "model-weave-mermaid-interactive-node",
|
|
dragThreshold: 6,
|
|
hoverParent: (nodeEl, fallback) => nodeEl.closest(
|
|
".model-weave-view-only-stage, .model-weave-app-process-business-flow, .model-weave-mermaid-shell"
|
|
) ?? fallback,
|
|
formatTitle: (target) => target.label ? `${target.label} (${target.targetType ?? "model"})` : target.linktext
|
|
});
|
|
}
|
|
}).catch((error) => {
|
|
shell3.root.addClass("model-weave-mermaid-fallback-shell");
|
|
shell3.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(shell3.root, ready);
|
|
return shell3.root;
|
|
}
|
|
function buildAppProcessBusinessFlowInteractionTargets(model, sourcePath) {
|
|
if (!sourcePath) {
|
|
return [];
|
|
}
|
|
return model.steps.map((step, index) => ({
|
|
mermaidId: `S${index + 1}`,
|
|
linktext: sourcePath,
|
|
sourcePath,
|
|
label: getStepLabel(step),
|
|
kind: "app-process-step",
|
|
targetType: "app_process",
|
|
filePath: sourcePath,
|
|
modelId: step.id,
|
|
modelType: "app-process"
|
|
}));
|
|
}
|
|
function buildAppProcessBusinessFlowMermaidSource(model, colorScheme) {
|
|
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 colorClasses = /* @__PURE__ */ new Map();
|
|
const domainStyles = [];
|
|
const nodeClasses = [];
|
|
const localDomains = model.domains ?? [];
|
|
const localDomainsById = new Map(localDomains.map((domain) => [domain.id, domain]));
|
|
const domainStepGroups = /* @__PURE__ */ new Map();
|
|
const flatPlacementGroups = /* @__PURE__ */ new Map();
|
|
const ungrouped = [];
|
|
for (const step of model.steps) {
|
|
const domainId = step.domain?.trim();
|
|
if (domainId && localDomainsById.has(domainId)) {
|
|
const group2 = domainStepGroups.get(domainId) ?? [];
|
|
group2.push(step);
|
|
domainStepGroups.set(domainId, group2);
|
|
continue;
|
|
}
|
|
const flatPlacement = getStepFlatPlacementGroup(step);
|
|
if (!flatPlacement) {
|
|
ungrouped.push(step);
|
|
continue;
|
|
}
|
|
const group = flatPlacementGroups.get(flatPlacement) ?? [];
|
|
group.push(step);
|
|
flatPlacementGroups.set(flatPlacement, group);
|
|
}
|
|
const includedDomains = getIncludedDomains(localDomains, domainStepGroups);
|
|
for (const root of buildDomainTree(includedDomains)) {
|
|
appendAppProcessDomainSubgraph(
|
|
lines,
|
|
root,
|
|
domainStepGroups,
|
|
stepNodeIds,
|
|
nodeClasses,
|
|
colorClasses,
|
|
domainStyles,
|
|
colorScheme,
|
|
1
|
|
);
|
|
}
|
|
let groupIndex = 0;
|
|
for (const [placement, steps] of flatPlacementGroups) {
|
|
groupIndex += 1;
|
|
lines.push(` subgraph L${groupIndex}["${escapeMermaidLabel(placement)}"]`);
|
|
for (const step of steps) {
|
|
lines.push(` ${buildStepNodeDeclaration(stepNodeIds.get(step), step)}`);
|
|
appendStepColorClass(
|
|
nodeClasses,
|
|
colorClasses,
|
|
stepNodeIds.get(step),
|
|
step,
|
|
colorScheme
|
|
);
|
|
}
|
|
lines.push(" end");
|
|
}
|
|
for (const step of ungrouped) {
|
|
lines.push(` ${buildStepNodeDeclaration(stepNodeIds.get(step), step)}`);
|
|
appendStepColorClass(
|
|
nodeClasses,
|
|
colorClasses,
|
|
stepNodeIds.get(step),
|
|
step,
|
|
colorScheme
|
|
);
|
|
}
|
|
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}`
|
|
);
|
|
}
|
|
if (colorClasses.size > 0) {
|
|
lines.push("");
|
|
for (const [className, style] of colorClasses) {
|
|
lines.push(` classDef ${className} ${formatMermaidClassDefStyle2(style)}`);
|
|
}
|
|
lines.push("", ...nodeClasses);
|
|
}
|
|
if (domainStyles.length > 0) {
|
|
lines.push("", ...domainStyles);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function getAppProcessBusinessFlowColorSchemeTargets(model) {
|
|
const targets = ["app_process"];
|
|
if (hasResolvedAppProcessDomainGroups(model)) {
|
|
targets.push("domain");
|
|
}
|
|
return targets;
|
|
}
|
|
function hasResolvedAppProcessDomainGroups(model) {
|
|
const domainIds = new Set(
|
|
(model.domains ?? []).map((domain) => domain.id.trim()).filter(Boolean)
|
|
);
|
|
if (domainIds.size === 0) {
|
|
return false;
|
|
}
|
|
return model.steps.some((step) => {
|
|
const domainId = step.domain?.trim();
|
|
return Boolean(domainId && domainIds.has(domainId));
|
|
});
|
|
}
|
|
function getIncludedDomains(localDomains, domainStepGroups) {
|
|
const domainsById = new Map(localDomains.map((domain) => [domain.id, domain]));
|
|
const includedIds = /* @__PURE__ */ new Set();
|
|
for (const domainId of domainStepGroups.keys()) {
|
|
let current = domainsById.get(domainId);
|
|
const seen = /* @__PURE__ */ new Set();
|
|
while (current && !seen.has(current.id)) {
|
|
includedIds.add(current.id);
|
|
seen.add(current.id);
|
|
current = current.parent ? domainsById.get(current.parent) : void 0;
|
|
}
|
|
}
|
|
return localDomains.filter((domain) => includedIds.has(domain.id));
|
|
}
|
|
function appendAppProcessDomainSubgraph(lines, domainNode, domainStepGroups, stepNodeIds, nodeClasses, colorClasses, domainStyles, colorScheme, depth) {
|
|
const childLines = [];
|
|
for (const child of domainNode.children) {
|
|
appendAppProcessDomainSubgraph(
|
|
childLines,
|
|
child,
|
|
domainStepGroups,
|
|
stepNodeIds,
|
|
nodeClasses,
|
|
colorClasses,
|
|
domainStyles,
|
|
colorScheme,
|
|
depth + 1
|
|
);
|
|
}
|
|
const steps = domainStepGroups.get(domainNode.domain.id) ?? [];
|
|
if (steps.length === 0 && childLines.length === 0) {
|
|
return false;
|
|
}
|
|
const indent = " ".repeat(depth);
|
|
lines.push(`${indent}subgraph ${toAppProcessDomainSubgraphId(domainNode.domain.id)}["${buildAppProcessDomainLabel(domainNode.domain)}"]`);
|
|
lines.push(...childLines);
|
|
for (const step of steps) {
|
|
lines.push(`${indent} ${buildStepNodeDeclaration(stepNodeIds.get(step), step)}`);
|
|
appendStepColorClass(
|
|
nodeClasses,
|
|
colorClasses,
|
|
stepNodeIds.get(step),
|
|
step,
|
|
colorScheme
|
|
);
|
|
}
|
|
lines.push(`${indent}end`);
|
|
if (colorScheme) {
|
|
domainStyles.push(
|
|
` style ${toAppProcessDomainSubgraphId(domainNode.domain.id)} ${formatMermaidClassDefStyle2(
|
|
resolveColorStyle(colorScheme, "domain", domainNode.domain.kind)
|
|
)}`
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
function toAppProcessDomainSubgraphId(domainId) {
|
|
return `domain_${sanitizeMermaidId(domainId)}`;
|
|
}
|
|
function buildAppProcessDomainLabel(domain) {
|
|
return escapeMermaidLabel(domain.name?.trim() || domain.id);
|
|
}
|
|
function getStepFlatPlacementGroup(step) {
|
|
const domain = step.domain?.trim();
|
|
if (domain) {
|
|
return domain;
|
|
}
|
|
const lane = step.lane?.trim();
|
|
return lane || null;
|
|
}
|
|
function appendStepColorClass(nodeClasses, colorClasses, nodeId, step, colorScheme) {
|
|
if (!colorScheme || !nodeId) {
|
|
return;
|
|
}
|
|
const className = toAppProcessColorClassName(step.kind);
|
|
colorClasses.set(
|
|
className,
|
|
resolveColorStyle(colorScheme, "app_process", step.kind)
|
|
);
|
|
nodeClasses.push(` class ${nodeId} ${className}`);
|
|
}
|
|
function toAppProcessColorClassName(kind) {
|
|
const suffix = kind?.trim() ? kind.trim() : "default";
|
|
return `kind_app_process_${sanitizeMermaidId(suffix)}`;
|
|
}
|
|
function formatMermaidClassDefStyle2(style) {
|
|
return [
|
|
style.fill ? `fill:${style.fill}` : void 0,
|
|
style.stroke ? `stroke:${style.stroke}` : void 0,
|
|
style.text ? `color:${style.text}` : void 0
|
|
].filter((entry) => Boolean(entry)).join(",");
|
|
}
|
|
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 = activeDocument.createElement("section");
|
|
root.addClass("model-weave-object-context");
|
|
root.addClass("model-weave-preview-section");
|
|
const titleRow = activeDocument.createElement("div");
|
|
titleRow.addClass("model-weave-object-context-title-row");
|
|
const title = activeDocument.createElement("h3");
|
|
title.textContent = options?.labels?.title ?? "Related objects";
|
|
title.addClass("model-weave-object-context-title");
|
|
title.addClass("model-weave-preview-section-title");
|
|
titleRow.appendChild(title);
|
|
const count = activeDocument.createElement("span");
|
|
count.textContent = options?.labels?.linked(context.relatedObjects.length) ?? `${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,
|
|
app: options?.app,
|
|
interactionSourcePath: options?.interactionSourcePath ?? context.object.path,
|
|
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 = activeDocument.createElement("details");
|
|
details.addClass("model-weave-object-context-list");
|
|
details.addClass("model-weave-preview-section");
|
|
const summary = activeDocument.createElement("summary");
|
|
summary.textContent = context.object.fileType === "er-entity" ? `${options?.labels?.relationDetails ?? "Relation details"} (${sortedEntries.length})` : `${options?.labels?.connectionDetails ?? "Connection details"} (${sortedEntries.length})`;
|
|
summary.addClass("model-weave-object-context-summary");
|
|
summary.addClass("model-weave-preview-section-title");
|
|
details.appendChild(summary);
|
|
const tableWrap = activeDocument.createElement("div");
|
|
tableWrap.addClass("model-weave-object-context-table-wrap");
|
|
tableWrap.addClass("model-weave-table-wrap");
|
|
if (sortedEntries.length === 0) {
|
|
const empty = activeDocument.createElement("p");
|
|
empty.textContent = options?.labels?.noDirectlyRelated ?? "No directly related objects.";
|
|
empty.addClass("model-weave-object-context-empty");
|
|
details.appendChild(empty);
|
|
return details;
|
|
}
|
|
const table = activeDocument.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 = activeDocument.createElement("thead");
|
|
const headRow = activeDocument.createElement("tr");
|
|
for (const header of headers) {
|
|
const cell = activeDocument.createElement("th");
|
|
cell.textContent = header;
|
|
cell.addClass("model-weave-object-context-th");
|
|
headRow.appendChild(cell);
|
|
}
|
|
thead.appendChild(headRow);
|
|
table.appendChild(thead);
|
|
const tbody = activeDocument.createElement("tbody");
|
|
for (const entry of sortedEntries) {
|
|
const row = activeDocument.createElement("tr");
|
|
const values = context.object.fileType === "er-entity" ? buildErListRow(entry) : buildClassListRow(entry);
|
|
values.forEach((value, index) => {
|
|
const cell = activeDocument.createElement("td");
|
|
cell.addClass("model-weave-object-context-td");
|
|
if (index === 0 && options?.onOpenObject) {
|
|
const wrapper = activeDocument.createElement("div");
|
|
wrapper.addClass("model-weave-object-context-link-wrap");
|
|
const badge = createDirectionBadge(entry.direction);
|
|
wrapper.appendChild(badge);
|
|
const button = activeDocument.createElement("button");
|
|
button.type = "button";
|
|
button.textContent = value;
|
|
button.addClass("model-weave-object-context-link");
|
|
button.addEventListener("click", () => {
|
|
options.onOpenObject?.(entry.relatedObjectId, { openInNewLeaf: false });
|
|
});
|
|
attachRelatedObjectHoverPreview(button, context, entry, options);
|
|
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 attachRelatedObjectHoverPreview(element, context, entry, options) {
|
|
if (!options?.app || !entry.relatedObject?.path) {
|
|
return;
|
|
}
|
|
const target = createRelatedObjectInteractionTarget(
|
|
context,
|
|
entry,
|
|
options.interactionSourcePath ?? context.object.path
|
|
);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
attachGraphElementHoverPreview({
|
|
app: options.app,
|
|
targetEl: element,
|
|
target,
|
|
source: "model-weave"
|
|
});
|
|
}
|
|
function createRelatedObjectInteractionTarget(context, entry, sourcePath) {
|
|
const related = entry.relatedObject;
|
|
if (!related?.path) {
|
|
return null;
|
|
}
|
|
const isEr = related.fileType === "er-entity";
|
|
const label = isEr ? related.logicalName || related.physicalName || entry.relatedObjectId : related.name || entry.relatedObjectId;
|
|
const targetType = isEr ? "er_entity" : "class";
|
|
return {
|
|
mermaidId: `related:${entry.relatedObjectId}`,
|
|
linktext: related.path,
|
|
sourcePath,
|
|
label,
|
|
kind: isEr ? "er-reference" : "class-reference",
|
|
targetType,
|
|
filePath: related.path,
|
|
modelId: entry.relatedObjectId,
|
|
modelType: related.fileType
|
|
};
|
|
}
|
|
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 = activeDocument.createElement("span");
|
|
badge.textContent = formatDirection(direction);
|
|
badge.addClass("model-weave-badge");
|
|
badge.addClass(getDirectionBadgeClass(direction));
|
|
return badge;
|
|
}
|
|
function createKindBadge(kind) {
|
|
const badge = activeDocument.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_electron = require("electron");
|
|
var import_obsidian6 = require("obsidian");
|
|
|
|
// 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.weaveMap.title": "Weave Map",
|
|
"relationship.weaveMap.description": "Visual map of related models.",
|
|
"relationship.weaveMap.viewMode": "View",
|
|
"relationship.weaveMap.compact": "Compact",
|
|
"relationship.weaveMap.full": "Full",
|
|
"relationship.overview.outgoing": "Outgoing",
|
|
"relationship.overview.incoming": "Incoming",
|
|
"relationship.overview.unresolved": "Unresolved",
|
|
"relationship.overview.sourceLinks": "Source links",
|
|
"relationship.overview.valueUsage": "Value usage",
|
|
"relationship.usedBy.screens": "Used by screens",
|
|
"relationship.usedBy.processes": "Used by processes",
|
|
"relationship.usedBy.rules": "Used by rules",
|
|
"relationship.usedBy.mappings": "Used by mappings",
|
|
"relationship.usedBy.diagrams": "Used by diagrams",
|
|
"relationship.usedBy.classes": "Used by classes",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"relationship.usedBy.dataEr": "Used by data / ER",
|
|
"relationship.usedBy.other": "Used by other models",
|
|
"relationship.references.screens": "References screens",
|
|
"relationship.references.processes": "References processes",
|
|
"relationship.references.rules": "References rules",
|
|
"relationship.references.mappings": "References mappings",
|
|
"relationship.references.diagrams": "References diagrams",
|
|
"relationship.references.classes": "References classes",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"relationship.references.dataEr": "References data / ER",
|
|
"relationship.references.other": "References other models",
|
|
"review.summary.title": "Review summary",
|
|
"review.summary.model": "Model",
|
|
"review.summary.modelType": "Model type",
|
|
"review.summary.modelId": "Model ID",
|
|
"review.summary.errors": "Errors",
|
|
"review.summary.warnings": "Warnings",
|
|
"review.summary.notes": "Notes",
|
|
"review.summary.incoming": "Incoming",
|
|
"review.summary.outgoing": "Outgoing",
|
|
"review.summary.unresolved": "Unresolved",
|
|
"review.summary.sourceLinks": "Source links",
|
|
"review.summary.weaveMap": "Weave map",
|
|
"review.summary.available": "Available",
|
|
"review.summary.notAvailable": "Not available",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"relationship.usage.one": "usage",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"relationship.usage.other": "usages",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"relationship.note.one": "note",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"relationship.note.other": "notes",
|
|
"domains.preview.title": "Domains",
|
|
"domains.preview.overview": "Overview",
|
|
"domains.preview.count": "Domains",
|
|
"domains.preview.list": "Domain list",
|
|
"domains.preview.tree": "Domain hierarchy",
|
|
"domains.preview.details": "Details",
|
|
"domains.preview.relationships": "Domain relationships",
|
|
"domains.preview.diagram": "Domain hierarchy diagram",
|
|
"domains.preview.mindmap": "Mindmap",
|
|
"domains.preview.area": "Area",
|
|
"domains.preview.treeMode": "Tree",
|
|
"domains.preview.viewMode": "Domain view mode",
|
|
"domains.preview.diagramEmpty": "No domain hierarchy to display.",
|
|
"domains.preview.diagramRenderFailed": "Domain hierarchy diagram could not be rendered.",
|
|
"domains.preview.empty": "No domains defined.",
|
|
"mermaid.source.title": "Mermaid source",
|
|
"mermaid.source.copy": "Copy Mermaid",
|
|
"graph.exportPng": "Export as PNG",
|
|
"graph.exportPngOpen": "Export PNG and open",
|
|
"graph.exportPngOpenUnavailable": "Opening exported PNG is only available on desktop vaults.",
|
|
"graph.exportPngOpenFailed": "Failed to open exported PNG: {message}",
|
|
"graph.focusModeEnter": "Enter focus mode",
|
|
"graph.focusModeExit": "Exit focus mode",
|
|
"graph.viewOnlyEnter": "View only",
|
|
"graph.viewOnlyExit": "Exit view only",
|
|
"preview.openInMainPane": "Open preview in main pane",
|
|
"preview.openInMainPane.short": "Main pane",
|
|
"preview.openInNewPane": "Open preview in new pane",
|
|
"preview.openInNewPane.short": "New pane",
|
|
"diagnostics.notes": "Notes",
|
|
"diagnostics.warnings": "Warnings",
|
|
"diagnostics.errors": "Errors",
|
|
"diagnostics.openInEditor": "Open this diagnostic in the editor",
|
|
"diagnostics.summary": "Diagnostics summary",
|
|
"diagnostics.openSource": "Open location",
|
|
"diagnostics.openLocation": "Open location",
|
|
"diagnostics.openLocationTooltip": "Open the Markdown location for this diagnostic",
|
|
"diagnostics.copyMessage": "Copy message",
|
|
"diagnostics.copyMarkdown": "Copy diagnostic as Markdown",
|
|
"diagnostics.copyReference": "Copy reference",
|
|
"diagnostics.copyExpectedHeader": "Copy expected header",
|
|
"diagnostics.copyFrontmatterExample": "Copy frontmatter example",
|
|
"diagnostics.severity.error": "Error",
|
|
"diagnostics.severity.warning": "Warning",
|
|
"diagnostics.severity.note": "Note",
|
|
"diagnostics.meta.file": "File",
|
|
"diagnostics.meta.section": "Section",
|
|
"diagnostics.meta.line": "Line",
|
|
"diagnostics.meta.row": "Row",
|
|
"diagnostics.meta.reference": "Reference",
|
|
"diagnostics.meta.field": "Field",
|
|
"diagnostics.meta.severity": "Severity",
|
|
"diagnostics.meta.code": "Code",
|
|
"diagnostics.meta.message": "Message",
|
|
"diagnostics.details.title": "Diagnostic details",
|
|
"diagnostics.details.expectedHeader": "Expected header",
|
|
"diagnostics.details.actualHeader": "Actual header",
|
|
"diagnostics.details.missingColumns": "Missing columns",
|
|
"diagnostics.details.extraColumns": "Extra columns",
|
|
"diagnostics.details.unresolvedReference": "Unresolved reference",
|
|
"diagnostics.details.duplicateTarget": "Duplicate target",
|
|
"diagnostics.details.mappingRow": "Mapping row",
|
|
"diagnostics.details.frontmatterKey": "Missing frontmatter key",
|
|
"diagnostics.details.frontmatterExample": "Frontmatter example",
|
|
"diagnostics.details.requestedRenderMode": "Requested render mode",
|
|
"diagnostics.details.effectiveRenderMode": "Effective render mode",
|
|
"diagnostics.details.formatDefault": "Format default",
|
|
"diagnostics.details.diagramCompatibility": "Diagram compatibility",
|
|
"diagnostics.details.notClassDiagramCompatible": "Exists, but is excluded from class diagram rendering",
|
|
"diagnostics.details.targetType": "Target type",
|
|
"objectContext.title": "Related objects",
|
|
"objectContext.linked": "{count} linked",
|
|
"objectContext.connectionDetails": "Connection details",
|
|
"objectContext.relationDetails": "Relation details",
|
|
"objectContext.noDirectlyRelated": "No directly related objects.",
|
|
"class.preview.displayedRelations": "Displayed relations",
|
|
"class.preview.noRelationsUsed": "No relations are currently used for rendering.",
|
|
"summary.counts": "Counts",
|
|
"summary.count.triggers": "Triggers",
|
|
"summary.count.inputs": "Inputs",
|
|
"summary.count.outputs": "Outputs",
|
|
"summary.count.transitions": "Transitions",
|
|
"summary.count.steps": "Steps",
|
|
"summary.count.flows": "Flows",
|
|
"summary.count.domains": "Domains",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.count.domainSources": "Domain Sources",
|
|
"summary.count.layouts": "Layouts",
|
|
"summary.count.fields": "Fields",
|
|
"summary.count.actions": "Actions",
|
|
"summary.count.messages": "Messages",
|
|
"summary.count.localProcesses": "Local processes",
|
|
"summary.count.invokedProcesses": "Invoked processes",
|
|
"summary.count.outgoingScreens": "Outgoing screens",
|
|
"summary.detectedSections": "Detected sections",
|
|
"summary.noRows": "No rows",
|
|
"summary.section.summary": "Summary",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.domainSourcesSummary": "Domain Sources Summary",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.domainsSummary": "Domains Summary",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.triggersSummary": "Triggers Summary",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.inputsSummary": "Inputs Summary",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.outputsSummary": "Outputs Summary",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.stepsSummary": "Steps Summary",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.flowsSummary": "Flows Summary",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.transitionsSummary": "Transitions Summary",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.structureLayout": "Structure / Layout",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.uiElementsFields": "UI Elements / Fields",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.behaviorActions": "Behavior / Actions",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.localProcesses": "Local Processes",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.invokedProcesses": "Invoked Processes",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"summary.section.transitionsOutgoingScreens": "Transitions / Outgoing Screens",
|
|
"summary.section.messages": "Messages",
|
|
"summary.section.notes": "Notes",
|
|
"summary.section.layout": "Layout",
|
|
"summary.section.fields": "Fields",
|
|
"summary.section.actions": "Actions",
|
|
"summary.section.transitionsLegacy": "Transitions (legacy)",
|
|
"summary.unit.rows": "{count} rows",
|
|
"summary.unit.headings": "{count} headings",
|
|
"screen.preview.unassigned": "Unassigned",
|
|
"screen.preview.layoutMissing": "Layout is missing or undefined",
|
|
/* eslint-disable obsidianmd/ui/sentence-case-locale-module -- Source Links table labels and status text intentionally match UI/spec wording. */
|
|
"sourceLinks.title": "Source links",
|
|
"sourceLinks.help": "Open uses your OS/default app and may fail for UNC/WSL paths or unsupported file associations.",
|
|
"sourceLinks.path": "Path",
|
|
"sourceLinks.status": "Status",
|
|
"sourceLinks.resolvedPath": "Resolved Path",
|
|
"sourceLinks.notes": "Notes",
|
|
"sourceLinks.action": "Action",
|
|
"sourceLinks.copyPath": "Copy Path",
|
|
"sourceLinks.copyAllPaths": "Copy all paths",
|
|
"sourceLinks.copyAvailablePaths": "Copy available paths",
|
|
"sourceLinks.copyAsMarkdown": "Copy as Markdown",
|
|
"sourceLinks.copyMissingPaths": "Copy missing paths",
|
|
"sourceLinks.open": "Open",
|
|
"sourceLinks.openWithDefaultApp": "Open with default app",
|
|
"sourceLinks.summary.total": "Total",
|
|
"sourceLinks.summary.available": "Available",
|
|
"sourceLinks.summary.missing": "Missing",
|
|
"sourceLinks.summary.rootNotConfigured": "Source root not configured",
|
|
"sourceLinks.noLinks": "No source links are defined for this model.",
|
|
"sourceLinks.unsupportedFileUri": "unsupported file URI",
|
|
"sourceLinks.useFilesystemPath": "Use a filesystem path instead of a file URI",
|
|
"sourceLinks.unsupportedSourceRoot": "unsupported source root",
|
|
"sourceLinks.outsideSourceRoot": "outside source root",
|
|
"sourceLinks.localSourceRootNotConfigured": "Local source root is not configured",
|
|
"sourceLinks.missing": "missing",
|
|
"sourceLinks.available": "available",
|
|
"sourceLinks.availableDirectory": "available directory",
|
|
"sourceLinks.uncPathNote": "UNC/WSL path. Open may depend on your OS and app support.",
|
|
"sourceLinks.openUnavailable": "Could not open Source Link: OS open is not available.",
|
|
"sourceLinks.openFailed": "Could not open Source Link: {message}",
|
|
/* eslint-enable obsidianmd/ui/sentence-case-locale-module */
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domains.field.type": "type",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domains.field.id": "id",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domains.field.name": "name",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domains.field.kind": "kind",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domains.field.parent": "parent",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domains.field.description": "description",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domains.field.path": "path",
|
|
"domains.relationship.definedIn": "Defined in",
|
|
"domains.relationship.conflicts": "Conflicts",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domains.relationship.dfdLocalDomains": "Referenced by DFD-local Domains",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domains.relationship.dfdObjects": "Referenced by DFD objects",
|
|
"domains.relationship.parent": "Parent",
|
|
"domains.relationship.children": "Children",
|
|
"domains.relationship.none": "None",
|
|
"domains.relationship.conflictField": "{field} differs across Domains files",
|
|
"domains.value.none": "-",
|
|
"domainDiagram.preview.sources": "Domain sources",
|
|
"domainDiagram.preview.noSources": "No domain sources defined.",
|
|
"domainDiagram.preview.conflicts": "Domain source conflicts",
|
|
"domainDiagram.preview.noConflicts": "No domain source conflicts.",
|
|
"domainDiagram.preview.sourceCount": "Domain sources",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"appProcess.preview.domainSourcesPlacement": "Domain Sources / Placement",
|
|
"appProcess.preview.legacyLaneLayoutOnly": "Legacy lane placement remains layout-only and is used only when steps.domain is empty.",
|
|
"appProcess.preview.localDomains": "Local domains",
|
|
"appProcess.preview.domainPlacement": "Domain placement",
|
|
"dfd.preview.displayedObjects": "Displayed objects",
|
|
"dfd.preview.displayedFlows": "Displayed flows",
|
|
"dfd.preview.noObjects": "No objects are used for rendering.",
|
|
"dfd.preview.noFlows": "No flows are used for rendering.",
|
|
"dfd.preview.domainPlacement": "Domain placement",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"dfd.preview.resolved": "resolved",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"dfd.preview.unresolved": "unresolved",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domainDiagram.field.ref": "ref",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domainDiagram.field.status": "status",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domainDiagram.field.notes": "notes",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domainDiagram.field.conflict": "conflict",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domainDiagram.field.earlier": "earlier",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domainDiagram.field.later": "later",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"domainDiagram.field.effective": "effective",
|
|
"domainDiagram.status.ok": "OK",
|
|
"domainDiagram.status.unresolved": "Unresolved",
|
|
"domainDiagram.status.invalid-type": "Invalid type",
|
|
"domainDiagram.status.empty": "Empty",
|
|
"colorScheme.preview.title": "Color scheme",
|
|
"colorScheme.preview.applied": "Applied color scheme",
|
|
"colorScheme.preview.builtIn": "Built-in default",
|
|
"colorScheme.preview.configured": "Configured color scheme",
|
|
"colorScheme.preview.colors": "Colors",
|
|
"colorScheme.preview.swatch": "Color preview",
|
|
"colorScheme.preview.targets": "Targets",
|
|
"colorScheme.preview.empty": "No colors defined.",
|
|
"colorScheme.field.target": "Target",
|
|
"colorScheme.field.kind": "Kind",
|
|
"colorScheme.field.fill": "Fill",
|
|
"colorScheme.field.stroke": "Stroke",
|
|
"colorScheme.field.text": "Text",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"colorScheme.field.notes": "notes",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"colorScheme.field.source": "source",
|
|
"settings.section.viewer": "Viewer",
|
|
"settings.defaultClassRenderMode.name": "Default class render mode",
|
|
"settings.defaultClassRenderMode.desc": "Used for class and class_diagram files when frontmatter.render_mode is not set.",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"settings.defaultErRenderMode.name": "Default ER render mode",
|
|
"settings.defaultErRenderMode.desc": "Used for er_entity and er_diagram files when frontmatter.render_mode is not set.",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"settings.defaultDfdRenderMode.name": "Default DFD render mode",
|
|
"settings.defaultDfdRenderMode.desc": "Used for dfd_diagram files when frontmatter.render_mode is not set.",
|
|
"settings.defaultProcessRenderMode.name": "Default process render mode",
|
|
"settings.defaultProcessRenderMode.desc": "Used for app_process files when frontmatter.render_mode is not set.",
|
|
"settings.defaultScreenRenderMode.name": "Default screen render mode",
|
|
"settings.defaultScreenRenderMode.desc": "Used for screen files when frontmatter.render_mode is not set.",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"settings.defaultDomainsViewMode.name": "Default Domains view mode",
|
|
"settings.defaultDomainsViewMode.desc": "Used when frontmatter.render_mode is not set for domains files.",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"settings.defaultDomainDiagramViewMode.name": "Default Domain Diagram view mode",
|
|
"settings.defaultDomainDiagramViewMode.desc": "Used when frontmatter.render_mode is not set for domain_diagram files.",
|
|
"settings.defaultZoom.name": "Default zoom",
|
|
"settings.defaultZoom.desc": "Initial diagram zoom when no saved viewport state exists. Fit uses fit-to-view; 100% opens at actual scale.",
|
|
"settings.fontSize.name": "Font size",
|
|
"settings.fontSize.desc": "Adjusts the base preview text size across viewers.",
|
|
"settings.nodeDensity.name": "Node density",
|
|
"settings.nodeDensity.desc": "Controls diagram compactness where supported. Compact reduces padding and gaps; relaxed gives more breathing room.",
|
|
"settings.relationshipView.name": "Relationship view",
|
|
"settings.relationshipView.desc": "Show object-level inbound/outbound relationships in previews. Disable this for large vaults or reverse engineering workflows when preview speed matters more.",
|
|
"settings.showMermaidRenderDebug.name": "Show Mermaid render debug",
|
|
"settings.showMermaidRenderDebug.desc": "Show collapsed Mermaid rendering diagnostics under Mermaid diagrams. Mermaid source remains available regardless of this setting.",
|
|
"settings.uiLanguage.name": "UI language",
|
|
// eslint-disable-next-line obsidianmd/ui/sentence-case-locale-module
|
|
"settings.uiLanguage.desc": "Language for Model Weave viewer and settings captions. Auto follows Obsidian when available.",
|
|
"settings.localSourceRoot.name": "Local source root",
|
|
"settings.localSourceRoot.desc": "Base directory used to resolve relative source links outside the Obsidian vault.",
|
|
"settings.defaultColorScheme.name": "Default color scheme",
|
|
"settings.defaultColorScheme.desc": "Vault ref or path to a color_scheme file used by supported diagrams.",
|
|
"settings.refreshOpenViews.name": "Refresh open views",
|
|
"settings.refreshOpenViews.desc": "Re-render open previews using the current settings.",
|
|
"settings.refreshOpenViews.button": "Refresh",
|
|
"settings.refreshOpenViews.notice": "Refreshed open views",
|
|
"settings.option.auto": "Auto",
|
|
"settings.option.english": "English",
|
|
"settings.option.japanese": "Japanese",
|
|
"settings.option.custom": "Custom",
|
|
"settings.option.mermaid": "Mermaid",
|
|
"settings.option.mermaidDetail": "Mermaid detail",
|
|
"settings.option.fit": "Fit",
|
|
"settings.option.small": "Small",
|
|
"settings.option.normal": "Normal",
|
|
"settings.option.large": "Large",
|
|
"settings.option.compact": "Compact",
|
|
"settings.option.relaxed": "Relaxed"
|
|
};
|
|
|
|
// 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.weaveMap.title": "Weave Map",
|
|
"relationship.weaveMap.description": "\u95A2\u9023\u30E2\u30C7\u30EB\u3092\u8996\u899A\u7684\u306B\u8868\u793A\u3059\u308B\u30DE\u30C3\u30D7\u3067\u3059\u3002",
|
|
"relationship.weaveMap.viewMode": "\u8868\u793A",
|
|
"relationship.weaveMap.compact": "\u96C6\u7D04",
|
|
"relationship.weaveMap.full": "\u8A73\u7D30",
|
|
"relationship.overview.outgoing": "\u53C2\u7167\u5148",
|
|
"relationship.overview.incoming": "\u88AB\u53C2\u7167",
|
|
"relationship.overview.unresolved": "\u672A\u89E3\u6C7A",
|
|
"relationship.overview.sourceLinks": "\u30BD\u30FC\u30B9\u30EA\u30F3\u30AF",
|
|
"relationship.overview.valueUsage": "\u5024\u306E\u5229\u7528",
|
|
"relationship.usedBy.screens": "Screens\u3067\u4F7F\u7528",
|
|
"relationship.usedBy.processes": "Processes\u3067\u4F7F\u7528",
|
|
"relationship.usedBy.rules": "Rules\u3067\u4F7F\u7528",
|
|
"relationship.usedBy.mappings": "Mappings\u3067\u4F7F\u7528",
|
|
"relationship.usedBy.diagrams": "Diagrams\u3067\u4F7F\u7528",
|
|
"relationship.usedBy.classes": "Classes\u3067\u4F7F\u7528",
|
|
"relationship.usedBy.dataEr": "Data / ER\u3067\u4F7F\u7528",
|
|
"relationship.usedBy.other": "\u305D\u306E\u4ED6\u30E2\u30C7\u30EB\u3067\u4F7F\u7528",
|
|
"relationship.references.screens": "Screens\u3092\u53C2\u7167",
|
|
"relationship.references.processes": "Processes\u3092\u53C2\u7167",
|
|
"relationship.references.rules": "Rules\u3092\u53C2\u7167",
|
|
"relationship.references.mappings": "Mappings\u3092\u53C2\u7167",
|
|
"relationship.references.diagrams": "Diagrams\u3092\u53C2\u7167",
|
|
"relationship.references.classes": "Classes\u3092\u53C2\u7167",
|
|
"relationship.references.dataEr": "Data / ER\u3092\u53C2\u7167",
|
|
"relationship.references.other": "\u305D\u306E\u4ED6\u30E2\u30C7\u30EB\u3092\u53C2\u7167",
|
|
"review.summary.title": "\u30EC\u30D3\u30E5\u30FC\u6982\u8981",
|
|
"review.summary.model": "\u30E2\u30C7\u30EB",
|
|
"review.summary.modelType": "\u30E2\u30C7\u30EB\u7A2E\u5225",
|
|
"review.summary.modelId": "\u30E2\u30C7\u30EBID",
|
|
"review.summary.errors": "\u30A8\u30E9\u30FC",
|
|
"review.summary.warnings": "\u8B66\u544A",
|
|
"review.summary.notes": "\u30CE\u30FC\u30C8",
|
|
"review.summary.incoming": "\u88AB\u53C2\u7167",
|
|
"review.summary.outgoing": "\u53C2\u7167\u5148",
|
|
"review.summary.unresolved": "\u672A\u89E3\u6C7A",
|
|
"review.summary.sourceLinks": "\u30BD\u30FC\u30B9\u30EA\u30F3\u30AF",
|
|
"review.summary.weaveMap": "Weave Map",
|
|
"review.summary.available": "\u5229\u7528\u53EF\u80FD",
|
|
"review.summary.notAvailable": "\u5229\u7528\u4E0D\u53EF",
|
|
"relationship.usage.one": "\u4EF6",
|
|
"relationship.usage.other": "\u4EF6",
|
|
"relationship.note.one": "\u4EF6\u306E\u30E1\u30E2",
|
|
"relationship.note.other": "\u4EF6\u306E\u30E1\u30E2",
|
|
"domains.preview.title": "Domains",
|
|
"domains.preview.overview": "\u6982\u8981",
|
|
"domains.preview.count": "Domain \u6570",
|
|
"domains.preview.list": "Domain \u4E00\u89A7",
|
|
"domains.preview.tree": "Domain \u968E\u5C64",
|
|
"domains.preview.details": "\u8A73\u7D30\u60C5\u5831",
|
|
"domains.preview.relationships": "Domain \u95A2\u4FC2",
|
|
"domains.preview.diagram": "Domain \u968E\u5C64\u56F3",
|
|
"domains.preview.mindmap": "\u30DE\u30A4\u30F3\u30C9\u30DE\u30C3\u30D7",
|
|
"domains.preview.area": "\u9818\u57DF",
|
|
"domains.preview.treeMode": "\u30C4\u30EA\u30FC",
|
|
"domains.preview.viewMode": "Domain \u8868\u793A\u30E2\u30FC\u30C9",
|
|
"domains.preview.diagramEmpty": "\u8868\u793A\u3067\u304D\u308B Domain \u968E\u5C64\u304C\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
"domains.preview.diagramRenderFailed": "Domain \u968E\u5C64\u56F3\u3092\u63CF\u753B\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002",
|
|
"domains.preview.empty": "Domain \u306F\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
"mermaid.source.title": "Mermaid \u30BD\u30FC\u30B9",
|
|
"mermaid.source.copy": "Mermaid \u3092\u30B3\u30D4\u30FC",
|
|
"graph.exportPng": "PNG\u3068\u3057\u3066\u66F8\u304D\u51FA\u3057",
|
|
"graph.exportPngOpen": "PNG\u3092\u66F8\u304D\u51FA\u3057\u3066\u958B\u304F",
|
|
"graph.exportPngOpenUnavailable": "\u66F8\u304D\u51FA\u3057\u305F PNG \u3092\u958B\u304F\u6A5F\u80FD\u306F\u30C7\u30B9\u30AF\u30C8\u30C3\u30D7 Vault \u3067\u306E\u307F\u5229\u7528\u3067\u304D\u307E\u3059\u3002",
|
|
"graph.exportPngOpenFailed": "\u66F8\u304D\u51FA\u3057\u305F PNG \u3092\u958B\u3051\u307E\u305B\u3093\u3067\u3057\u305F: {message}",
|
|
"graph.focusModeEnter": "\u30D5\u30A9\u30FC\u30AB\u30B9\u30E2\u30FC\u30C9\u306B\u5207\u308A\u66FF\u3048",
|
|
"graph.focusModeExit": "\u30D5\u30A9\u30FC\u30AB\u30B9\u30E2\u30FC\u30C9\u3092\u7D42\u4E86",
|
|
"graph.viewOnlyEnter": "View Only \u306B\u5207\u308A\u66FF\u3048",
|
|
"graph.viewOnlyExit": "View Only \u3092\u7D42\u4E86",
|
|
"preview.openInMainPane": "\u30E1\u30A4\u30F3\u30DA\u30A4\u30F3\u3067\u958B\u304F",
|
|
"preview.openInMainPane.short": "\u30E1\u30A4\u30F3\u30DA\u30A4\u30F3",
|
|
"preview.openInNewPane": "\u65B0\u3057\u3044\u30DA\u30A4\u30F3\u3067\u958B\u304F",
|
|
"preview.openInNewPane.short": "\u65B0\u3057\u3044\u30DA\u30A4\u30F3",
|
|
"diagnostics.notes": "\u30CE\u30FC\u30C8",
|
|
"diagnostics.warnings": "\u8B66\u544A",
|
|
"diagnostics.errors": "\u30A8\u30E9\u30FC",
|
|
"diagnostics.openInEditor": "\u3053\u306E\u8A3A\u65AD\u3092\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u958B\u304F",
|
|
"diagnostics.summary": "\u8A3A\u65AD\u30B5\u30DE\u30EA",
|
|
"diagnostics.openSource": "\u8A72\u5F53\u7B87\u6240\u3092\u958B\u304F",
|
|
"diagnostics.openLocation": "\u8A72\u5F53\u7B87\u6240\u3092\u958B\u304F",
|
|
"diagnostics.openLocationTooltip": "\u3053\u306E\u8A3A\u65AD\u304C\u767A\u751F\u3057\u305FMarkdown\u4E0A\u306E\u4F4D\u7F6E\u3092\u958B\u304D\u307E\u3059",
|
|
"diagnostics.copyMessage": "\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u30B3\u30D4\u30FC",
|
|
"diagnostics.copyMarkdown": "\u8A3A\u65AD\u3092Markdown\u3067\u30B3\u30D4\u30FC",
|
|
"diagnostics.copyReference": "\u53C2\u7167\u5024\u3092\u30B3\u30D4\u30FC",
|
|
"diagnostics.copyExpectedHeader": "\u671F\u5F85\u30D8\u30C3\u30C0\u30FC\u3092\u30B3\u30D4\u30FC",
|
|
"diagnostics.copyFrontmatterExample": "frontmatter \u4F8B\u3092\u30B3\u30D4\u30FC",
|
|
"diagnostics.severity.error": "\u30A8\u30E9\u30FC",
|
|
"diagnostics.severity.warning": "\u8B66\u544A",
|
|
"diagnostics.severity.note": "\u30CE\u30FC\u30C8",
|
|
"diagnostics.meta.file": "\u30D5\u30A1\u30A4\u30EB",
|
|
"diagnostics.meta.section": "\u30BB\u30AF\u30B7\u30E7\u30F3",
|
|
"diagnostics.meta.line": "\u884C",
|
|
"diagnostics.meta.row": "\u884C\u756A\u53F7",
|
|
"diagnostics.meta.reference": "\u53C2\u7167\u5024",
|
|
"diagnostics.meta.field": "\u30D5\u30A3\u30FC\u30EB\u30C9",
|
|
"diagnostics.meta.severity": "\u91CD\u8981\u5EA6",
|
|
"diagnostics.meta.code": "\u30B3\u30FC\u30C9",
|
|
"diagnostics.meta.message": "\u30E1\u30C3\u30BB\u30FC\u30B8",
|
|
"diagnostics.details.title": "\u8A3A\u65AD\u8A73\u7D30",
|
|
"diagnostics.details.expectedHeader": "\u671F\u5F85\u3055\u308C\u308B\u30D8\u30C3\u30C0\u30FC",
|
|
"diagnostics.details.actualHeader": "\u5B9F\u969B\u306E\u30D8\u30C3\u30C0\u30FC",
|
|
"diagnostics.details.missingColumns": "\u4E0D\u8DB3\u3057\u3066\u3044\u308B\u5217",
|
|
"diagnostics.details.extraColumns": "\u4F59\u5206\u306A\u5217",
|
|
"diagnostics.details.unresolvedReference": "\u672A\u89E3\u6C7A\u306E\u53C2\u7167",
|
|
"diagnostics.details.duplicateTarget": "\u91CD\u8907\u3057\u3066\u3044\u308B\u5BFE\u8C61",
|
|
"diagnostics.details.mappingRow": "Mapping row",
|
|
"diagnostics.details.frontmatterKey": "\u4E0D\u8DB3\u3057\u3066\u3044\u308B frontmatter key",
|
|
"diagnostics.details.frontmatterExample": "frontmatter \u4F8B",
|
|
"diagnostics.details.requestedRenderMode": "\u8981\u6C42\u3055\u308C\u305F\u63CF\u753B\u30E2\u30FC\u30C9",
|
|
"diagnostics.details.effectiveRenderMode": "\u5B9F\u969B\u306E\u63CF\u753B\u30E2\u30FC\u30C9",
|
|
"diagnostics.details.formatDefault": "format \u65E2\u5B9A",
|
|
"diagnostics.details.diagramCompatibility": "\u56F3\u3068\u306E\u4E92\u63DB\u6027",
|
|
"diagnostics.details.notClassDiagramCompatible": "\u5B58\u5728\u3057\u307E\u3059\u304C\u3001Class Diagram \u306E\u63CF\u753B\u5BFE\u8C61\u5916\u3067\u3059",
|
|
"diagnostics.details.targetType": "\u53C2\u7167\u5148 type",
|
|
"objectContext.title": "\u95A2\u9023\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8",
|
|
"objectContext.linked": "{count} \u4EF6\u306E\u95A2\u9023",
|
|
"objectContext.connectionDetails": "\u63A5\u7D9A\u8A73\u7D30",
|
|
"objectContext.relationDetails": "Relation \u8A73\u7D30",
|
|
"objectContext.noDirectlyRelated": "\u76F4\u63A5\u95A2\u4FC2\u3059\u308B\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
"class.preview.displayedRelations": "\u8868\u793A\u4E2D\u306E\u95A2\u4FC2",
|
|
"class.preview.noRelationsUsed": "\u63CF\u753B\u306B\u4F7F\u308F\u308C\u3066\u3044\u308B\u95A2\u4FC2\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
"summary.counts": "\u4EF6\u6570",
|
|
"summary.count.triggers": "\u30C8\u30EA\u30AC\u30FC",
|
|
"summary.count.inputs": "\u5165\u529B",
|
|
"summary.count.outputs": "\u51FA\u529B",
|
|
"summary.count.transitions": "\u9077\u79FB",
|
|
"summary.count.steps": "\u30B9\u30C6\u30C3\u30D7",
|
|
"summary.count.flows": "\u30D5\u30ED\u30FC",
|
|
"summary.count.domains": "Domains",
|
|
"summary.count.domainSources": "Domain Sources",
|
|
"summary.count.layouts": "\u30EC\u30A4\u30A2\u30A6\u30C8",
|
|
"summary.count.fields": "\u30D5\u30A3\u30FC\u30EB\u30C9",
|
|
"summary.count.actions": "\u30A2\u30AF\u30B7\u30E7\u30F3",
|
|
"summary.count.messages": "\u30E1\u30C3\u30BB\u30FC\u30B8",
|
|
"summary.count.localProcesses": "\u30ED\u30FC\u30AB\u30EB\u30D7\u30ED\u30BB\u30B9",
|
|
"summary.count.invokedProcesses": "\u547C\u3073\u51FA\u3057\u5148\u30D7\u30ED\u30BB\u30B9",
|
|
"summary.count.outgoingScreens": "\u9077\u79FB\u5148\u753B\u9762",
|
|
"summary.detectedSections": "\u691C\u51FA\u3055\u308C\u305F\u30BB\u30AF\u30B7\u30E7\u30F3",
|
|
"summary.noRows": "\u884C\u306F\u3042\u308A\u307E\u305B\u3093",
|
|
"summary.section.summary": "\u6982\u8981",
|
|
"summary.section.domainSourcesSummary": "\u30C9\u30E1\u30A4\u30F3\u30BD\u30FC\u30B9\u6982\u8981",
|
|
"summary.section.domainsSummary": "\u30C9\u30E1\u30A4\u30F3\u6982\u8981",
|
|
"summary.section.triggersSummary": "\u30C8\u30EA\u30AC\u30FC\u6982\u8981",
|
|
"summary.section.inputsSummary": "\u5165\u529B\u6982\u8981",
|
|
"summary.section.outputsSummary": "\u51FA\u529B\u6982\u8981",
|
|
"summary.section.stepsSummary": "\u30B9\u30C6\u30C3\u30D7\u6982\u8981",
|
|
"summary.section.flowsSummary": "\u30D5\u30ED\u30FC\u6982\u8981",
|
|
"summary.section.transitionsSummary": "\u9077\u79FB\u6982\u8981",
|
|
"summary.section.structureLayout": "\u69CB\u9020 / \u30EC\u30A4\u30A2\u30A6\u30C8",
|
|
"summary.section.uiElementsFields": "UI\u8981\u7D20 / \u30D5\u30A3\u30FC\u30EB\u30C9",
|
|
"summary.section.behaviorActions": "\u632F\u308B\u821E\u3044 / \u30A2\u30AF\u30B7\u30E7\u30F3",
|
|
"summary.section.localProcesses": "\u30ED\u30FC\u30AB\u30EB\u30D7\u30ED\u30BB\u30B9",
|
|
"summary.section.invokedProcesses": "\u547C\u3073\u51FA\u3057\u5148\u30D7\u30ED\u30BB\u30B9",
|
|
"summary.section.transitionsOutgoingScreens": "\u9077\u79FB / \u9077\u79FB\u5148\u753B\u9762",
|
|
"summary.section.messages": "\u30E1\u30C3\u30BB\u30FC\u30B8",
|
|
"summary.section.notes": "\u30CE\u30FC\u30C8",
|
|
"summary.section.layout": "\u30EC\u30A4\u30A2\u30A6\u30C8",
|
|
"summary.section.fields": "\u30D5\u30A3\u30FC\u30EB\u30C9",
|
|
"summary.section.actions": "\u30A2\u30AF\u30B7\u30E7\u30F3",
|
|
"summary.section.transitionsLegacy": "\u9077\u79FB\uFF08\u65E7\u5F62\u5F0F\uFF09",
|
|
"summary.unit.rows": "{count}\u884C",
|
|
"summary.unit.headings": "{count}\u898B\u51FA\u3057",
|
|
"screen.preview.unassigned": "\u672A\u5206\u985E",
|
|
"screen.preview.layoutMissing": "layout \u304C\u672A\u6307\u5B9A\u307E\u305F\u306F\u672A\u5B9A\u7FA9\u3067\u3059",
|
|
"sourceLinks.title": "\u30BD\u30FC\u30B9\u30EA\u30F3\u30AF",
|
|
"sourceLinks.help": "\u958B\u304F\u64CD\u4F5C\u306F OS \u306E\u65E2\u5B9A\u30A2\u30D7\u30EA\u3092\u4F7F\u7528\u3057\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",
|
|
"sourceLinks.path": "\u30D1\u30B9",
|
|
"sourceLinks.status": "\u72B6\u614B",
|
|
"sourceLinks.resolvedPath": "\u89E3\u6C7A\u6E08\u307F\u30D1\u30B9",
|
|
"sourceLinks.notes": "\u5099\u8003",
|
|
"sourceLinks.action": "\u64CD\u4F5C",
|
|
"sourceLinks.copyPath": "\u30D1\u30B9\u3092\u30B3\u30D4\u30FC",
|
|
"sourceLinks.copyAllPaths": "\u3059\u3079\u3066\u30B3\u30D4\u30FC",
|
|
"sourceLinks.copyAvailablePaths": "\u5229\u7528\u53EF\u80FD\u306A\u30D1\u30B9\u3092\u30B3\u30D4\u30FC",
|
|
"sourceLinks.copyAsMarkdown": "Markdown\u3067\u30B3\u30D4\u30FC",
|
|
"sourceLinks.copyMissingPaths": "\u898B\u3064\u304B\u3089\u306A\u3044\u30D1\u30B9\u3092\u30B3\u30D4\u30FC",
|
|
"sourceLinks.open": "\u958B\u304F",
|
|
"sourceLinks.openWithDefaultApp": "\u65E2\u5B9A\u30A2\u30D7\u30EA\u3067\u958B\u304F",
|
|
"sourceLinks.summary.total": "\u5408\u8A08",
|
|
"sourceLinks.summary.available": "\u5229\u7528\u53EF\u80FD",
|
|
"sourceLinks.summary.missing": "\u898B\u3064\u304B\u308A\u307E\u305B\u3093",
|
|
"sourceLinks.summary.rootNotConfigured": "Source root \u672A\u8A2D\u5B9A",
|
|
"sourceLinks.noLinks": "\u3053\u306E\u30E2\u30C7\u30EB\u306B\u306F\u30BD\u30FC\u30B9\u30EA\u30F3\u30AF\u304C\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
"sourceLinks.unsupportedFileUri": "file URI \u306F\u672A\u5BFE\u5FDC\u3067\u3059",
|
|
"sourceLinks.useFilesystemPath": "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",
|
|
"sourceLinks.unsupportedSourceRoot": "source root \u304C\u672A\u5BFE\u5FDC\u3067\u3059",
|
|
"sourceLinks.outsideSourceRoot": "source root \u306E\u5916\u3067\u3059",
|
|
"sourceLinks.localSourceRootNotConfigured": "Local source root \u304C\u672A\u8A2D\u5B9A\u3067\u3059",
|
|
"sourceLinks.missing": "\u898B\u3064\u304B\u308A\u307E\u305B\u3093",
|
|
"sourceLinks.available": "\u5229\u7528\u53EF\u80FD",
|
|
"sourceLinks.availableDirectory": "\u5229\u7528\u53EF\u80FD\u306A\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA",
|
|
"sourceLinks.uncPathNote": "UNC/WSL \u30D1\u30B9\u3067\u3059\u3002\u958B\u304F\u64CD\u4F5C\u306F OS \u3084\u30A2\u30D7\u30EA\u306E\u5BFE\u5FDC\u72B6\u6CC1\u306B\u4F9D\u5B58\u3057\u307E\u3059\u3002",
|
|
"sourceLinks.openUnavailable": "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",
|
|
"sourceLinks.openFailed": "Source Link \u3092\u958B\u3051\u307E\u305B\u3093\u3067\u3057\u305F: {message}",
|
|
"domains.field.type": "type",
|
|
"domains.field.id": "id",
|
|
"domains.field.name": "name",
|
|
"domains.field.kind": "kind",
|
|
"domains.field.parent": "parent",
|
|
"domains.field.description": "description",
|
|
"domains.field.path": "path",
|
|
"domains.relationship.definedIn": "\u5B9A\u7FA9\u30D5\u30A1\u30A4\u30EB",
|
|
"domains.relationship.conflicts": "\u4E0D\u6574\u5408",
|
|
"domains.relationship.dfdLocalDomains": "DFD\u5185 Domain \u304B\u3089\u306E\u53C2\u7167",
|
|
"domains.relationship.dfdObjects": "DFD object \u304B\u3089\u306E\u53C2\u7167",
|
|
"domains.relationship.parent": "\u89AA",
|
|
"domains.relationship.children": "\u5B50",
|
|
"domains.relationship.none": "\u306A\u3057",
|
|
"domains.relationship.conflictField": "{field} \u304C\u8907\u6570\u306E Domains \u30D5\u30A1\u30A4\u30EB\u3067\u4E00\u81F4\u3057\u3066\u3044\u307E\u305B\u3093",
|
|
"domains.value.none": "-",
|
|
"domainDiagram.preview.sources": "Domain Sources",
|
|
"domainDiagram.preview.noSources": "Domain Source \u306F\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
"domainDiagram.preview.conflicts": "Domain Source \u306E\u4E0D\u6574\u5408",
|
|
"domainDiagram.preview.noConflicts": "Domain Source \u306E\u4E0D\u6574\u5408\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
"domainDiagram.preview.sourceCount": "Domain Source \u6570",
|
|
"appProcess.preview.domainSourcesPlacement": "Domain Sources / \u914D\u7F6E",
|
|
"appProcess.preview.legacyLaneLayoutOnly": "legacy lane \u914D\u7F6E\u306F layout-only \u3067\u3059\u3002steps.domain \u304C\u7A7A\u306E\u5834\u5408\u3060\u3051\u4F7F\u308F\u308C\u307E\u3059\u3002",
|
|
"appProcess.preview.localDomains": "\u30ED\u30FC\u30AB\u30EB Domains",
|
|
"appProcess.preview.domainPlacement": "Domain \u914D\u7F6E",
|
|
"dfd.preview.displayedObjects": "\u8868\u793A\u4E2D\u306E\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8",
|
|
"dfd.preview.displayedFlows": "\u8868\u793A\u4E2D\u306E\u30D5\u30ED\u30FC",
|
|
"dfd.preview.noObjects": "\u63CF\u753B\u306B\u4F7F\u308F\u308C\u3066\u3044\u308B\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
"dfd.preview.noFlows": "\u63CF\u753B\u306B\u4F7F\u308F\u308C\u3066\u3044\u308B\u30D5\u30ED\u30FC\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
|
|
"dfd.preview.domainPlacement": "Domain \u914D\u7F6E",
|
|
"dfd.preview.resolved": "\u89E3\u6C7A\u6E08\u307F",
|
|
"dfd.preview.unresolved": "\u672A\u89E3\u6C7A",
|
|
"domainDiagram.field.ref": "ref",
|
|
"domainDiagram.field.status": "\u72B6\u614B",
|
|
"domainDiagram.field.notes": "notes",
|
|
"domainDiagram.field.conflict": "\u4E0D\u6574\u5408",
|
|
"domainDiagram.field.earlier": "\u524D\u306E\u5B9A\u7FA9",
|
|
"domainDiagram.field.later": "\u5F8C\u306E\u5B9A\u7FA9",
|
|
"domainDiagram.field.effective": "\u6709\u52B9\u306A\u5B9A\u7FA9",
|
|
"domainDiagram.status.ok": "OK",
|
|
"domainDiagram.status.unresolved": "\u672A\u89E3\u6C7A",
|
|
"domainDiagram.status.invalid-type": "type \u4E0D\u4E00\u81F4",
|
|
"domainDiagram.status.empty": "\u7A7A",
|
|
"colorScheme.preview.title": "Color Scheme",
|
|
"colorScheme.preview.applied": "\u9069\u7528\u4E2D\u306E Color Scheme",
|
|
"colorScheme.preview.builtIn": "\u7D44\u307F\u8FBC\u307F\u65E2\u5B9A",
|
|
"colorScheme.preview.configured": "\u8A2D\u5B9A\u3055\u308C\u305F Color Scheme",
|
|
"colorScheme.preview.colors": "Colors",
|
|
"colorScheme.preview.swatch": "\u30AB\u30E9\u30FC\u78BA\u8A8D",
|
|
"colorScheme.preview.targets": "\u5BFE\u8C61",
|
|
"colorScheme.preview.empty": "Color \u306F\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
|
|
"colorScheme.field.target": "\u5BFE\u8C61",
|
|
"colorScheme.field.kind": "kind",
|
|
"colorScheme.field.fill": "\u5857\u308A",
|
|
"colorScheme.field.stroke": "\u7DDA",
|
|
"colorScheme.field.text": "\u6587\u5B57",
|
|
"colorScheme.field.notes": "notes",
|
|
"colorScheme.field.source": "source",
|
|
"settings.section.viewer": "\u30D3\u30E5\u30FC\u30A2\u30FC",
|
|
"settings.defaultClassRenderMode.name": "Class \u306E\u521D\u671F render_mode",
|
|
"settings.defaultClassRenderMode.desc": "class \u3068 class_diagram \u30D5\u30A1\u30A4\u30EB\u3067 frontmatter.render_mode \u304C\u672A\u8A2D\u5B9A\u306E\u5834\u5408\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
|
|
"settings.defaultErRenderMode.name": "ER \u306E\u521D\u671F render_mode",
|
|
"settings.defaultErRenderMode.desc": "er_entity \u3068 er_diagram \u30D5\u30A1\u30A4\u30EB\u3067 frontmatter.render_mode \u304C\u672A\u8A2D\u5B9A\u306E\u5834\u5408\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
|
|
"settings.defaultDfdRenderMode.name": "DFD \u306E\u521D\u671F render_mode",
|
|
"settings.defaultDfdRenderMode.desc": "dfd_diagram \u30D5\u30A1\u30A4\u30EB\u3067 frontmatter.render_mode \u304C\u672A\u8A2D\u5B9A\u306E\u5834\u5408\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
|
|
"settings.defaultProcessRenderMode.name": "Process \u306E\u521D\u671F render_mode",
|
|
"settings.defaultProcessRenderMode.desc": "app_process \u30D5\u30A1\u30A4\u30EB\u3067 frontmatter.render_mode \u304C\u672A\u8A2D\u5B9A\u306E\u5834\u5408\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
|
|
"settings.defaultScreenRenderMode.name": "Screen \u306E\u521D\u671F render_mode",
|
|
"settings.defaultScreenRenderMode.desc": "screen \u30D5\u30A1\u30A4\u30EB\u3067 frontmatter.render_mode \u304C\u672A\u8A2D\u5B9A\u306E\u5834\u5408\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
|
|
"settings.defaultDomainsViewMode.name": "Domains \u306E\u521D\u671F\u8868\u793A\u30E2\u30FC\u30C9",
|
|
"settings.defaultDomainsViewMode.desc": "domains \u30D5\u30A1\u30A4\u30EB\u3067 frontmatter.render_mode \u304C\u672A\u8A2D\u5B9A\u306E\u5834\u5408\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
|
|
"settings.defaultDomainDiagramViewMode.name": "Domain Diagram \u306E\u521D\u671F\u8868\u793A\u30E2\u30FC\u30C9",
|
|
"settings.defaultDomainDiagramViewMode.desc": "domain_diagram \u30D5\u30A1\u30A4\u30EB\u3067 frontmatter.render_mode \u304C\u672A\u8A2D\u5B9A\u306E\u5834\u5408\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002",
|
|
"settings.defaultZoom.name": "\u521D\u671F\u30BA\u30FC\u30E0",
|
|
"settings.defaultZoom.desc": "\u4FDD\u5B58\u6E08\u307F viewport state \u304C\u306A\u3044\u5834\u5408\u306E diagram zoom \u3067\u3059\u3002Fit \u306F\u5168\u4F53\u8868\u793A\u3001100% \u306F\u7B49\u500D\u3067\u958B\u304D\u307E\u3059\u3002",
|
|
"settings.fontSize.name": "\u6587\u5B57\u30B5\u30A4\u30BA",
|
|
"settings.fontSize.desc": "\u30D3\u30E5\u30FC\u30A2\u30FC\u5168\u4F53\u306E\u57FA\u672C preview \u6587\u5B57\u30B5\u30A4\u30BA\u3092\u8ABF\u6574\u3057\u307E\u3059\u3002",
|
|
"settings.nodeDensity.name": "\u30CE\u30FC\u30C9\u5BC6\u5EA6",
|
|
"settings.nodeDensity.desc": "\u5BFE\u5FDC diagram \u306E\u5BC6\u5EA6\u3092\u8ABF\u6574\u3057\u307E\u3059\u3002Compact \u306F\u4F59\u767D\u3068 gap \u3092\u5C0F\u3055\u304F\u3057\u3001Relaxed \u306F\u4F59\u767D\u3092\u5E83\u304F\u3057\u307E\u3059\u3002",
|
|
"settings.relationshipView.name": "\u95A2\u9023\u8868\u793A",
|
|
"settings.relationshipView.desc": "preview \u306B object-level \u306E inbound/outbound relationships \u3092\u8868\u793A\u3057\u307E\u3059\u3002\u5927\u304D\u306A vault \u3084 reverse engineering \u3067\u901F\u5EA6\u3092\u512A\u5148\u3059\u308B\u5834\u5408\u306F\u7121\u52B9\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
"settings.showMermaidRenderDebug.name": "Mermaid render debug \u3092\u8868\u793A",
|
|
"settings.showMermaidRenderDebug.desc": "Mermaid diagrams \u306E\u4E0B\u306B\u6298\u308A\u305F\u305F\u307F\u5F0F\u306E Mermaid rendering diagnostics \u3092\u8868\u793A\u3057\u307E\u3059\u3002Mermaid source \u306F\u3053\u306E\u8A2D\u5B9A\u306B\u95A2\u4FC2\u306A\u304F\u5229\u7528\u3067\u304D\u307E\u3059\u3002",
|
|
"settings.uiLanguage.name": "UI \u8A00\u8A9E",
|
|
"settings.uiLanguage.desc": "Model Weave viewer \u3068 settings \u306E caption \u8A00\u8A9E\u3067\u3059\u3002Auto \u306F\u53EF\u80FD\u306A\u5834\u5408 Obsidian \u306B\u5F93\u3044\u307E\u3059\u3002",
|
|
"settings.localSourceRoot.name": "Local source root",
|
|
"settings.localSourceRoot.desc": "Obsidian vault \u5916\u306E relative source links \u3092\u89E3\u6C7A\u3059\u308B\u305F\u3081\u306E base directory \u3067\u3059\u3002",
|
|
"settings.defaultColorScheme.name": "Default color scheme",
|
|
"settings.defaultColorScheme.desc": "\u5BFE\u5FDC diagrams \u3067\u4F7F\u3046 color_scheme \u30D5\u30A1\u30A4\u30EB\u3078\u306E vault ref \u307E\u305F\u306F path \u3067\u3059\u3002",
|
|
"settings.refreshOpenViews.name": "\u958B\u3044\u3066\u3044\u308B view \u3092\u66F4\u65B0",
|
|
"settings.refreshOpenViews.desc": "\u73FE\u5728\u306E\u8A2D\u5B9A\u3092\u4F7F\u3063\u3066\u3001\u958B\u3044\u3066\u3044\u308B preview \u3092\u518D\u63CF\u753B\u3057\u307E\u3059\u3002",
|
|
"settings.refreshOpenViews.button": "\u66F4\u65B0",
|
|
"settings.refreshOpenViews.notice": "\u958B\u3044\u3066\u3044\u308B view \u3092\u66F4\u65B0\u3057\u307E\u3057\u305F",
|
|
"settings.option.auto": "Auto",
|
|
"settings.option.english": "English",
|
|
"settings.option.japanese": "\u65E5\u672C\u8A9E",
|
|
"settings.option.custom": "Custom",
|
|
"settings.option.mermaid": "Mermaid",
|
|
"settings.option.mermaidDetail": "Mermaid detail",
|
|
"settings.option.fit": "Fit",
|
|
"settings.option.small": "Small",
|
|
"settings.option.normal": "Normal",
|
|
"settings.option.large": "Large",
|
|
"settings.option.compact": "Compact",
|
|
"settings.option.relaxed": "Relaxed"
|
|
};
|
|
|
|
// 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/renderers/source-links-renderer.ts
|
|
function renderSourceLinks(sourceLinks, localSourceRoot, language = "auto") {
|
|
const validSourceLinks = (sourceLinks ?? []).filter(
|
|
(sourceLink) => sourceLink.path.trim()
|
|
);
|
|
const t = createModelWeaveTranslator(language);
|
|
const section = activeDocument.createElement("section");
|
|
section.addClass("model-weave-source-links");
|
|
section.addClass("model-weave-preview-section");
|
|
const title = activeDocument.createElement("h3");
|
|
title.textContent = t("sourceLinks.title");
|
|
title.addClass("model-weave-source-links-title");
|
|
title.addClass("model-weave-preview-section-title");
|
|
section.appendChild(title);
|
|
const statuses = validSourceLinks.map(
|
|
(sourceLink) => resolveSourceLinkStatus(sourceLink, localSourceRoot, t)
|
|
);
|
|
const copyEntries = validSourceLinks.map((sourceLink, index) => ({
|
|
sourceLink,
|
|
status: statuses[index]
|
|
}));
|
|
renderSourceLinksSummary(section, statuses, t);
|
|
renderSourceLinksBulkActions(section, copyEntries, t);
|
|
if (validSourceLinks.length === 0) {
|
|
section.createEl("p", {
|
|
text: t("sourceLinks.noLinks"),
|
|
cls: "model-weave-source-links-help"
|
|
});
|
|
return section;
|
|
}
|
|
section.createEl("p", {
|
|
text: t("sourceLinks.help"),
|
|
cls: "model-weave-source-links-help"
|
|
});
|
|
const tableWrap = activeDocument.createElement("div");
|
|
tableWrap.addClass("model-weave-table-wrap");
|
|
const table = activeDocument.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 [
|
|
t("sourceLinks.path"),
|
|
t("sourceLinks.status"),
|
|
t("sourceLinks.resolvedPath"),
|
|
t("sourceLinks.notes"),
|
|
t("sourceLinks.action")
|
|
]) {
|
|
headRow.createEl("th", {
|
|
text: header,
|
|
cls: "model-weave-source-links-th"
|
|
});
|
|
}
|
|
const tbody = table.createEl("tbody");
|
|
validSourceLinks.forEach((sourceLink, index) => {
|
|
const status = statuses[index];
|
|
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: t("sourceLinks.copyPath"),
|
|
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: t("sourceLinks.open"),
|
|
cls: "model-weave-source-links-open"
|
|
});
|
|
button.type = "button";
|
|
button.disabled = !status.openable;
|
|
button.title = t("sourceLinks.openWithDefaultApp");
|
|
button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
void openResolvedSourcePath(status.resolvedPath, t);
|
|
});
|
|
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 renderSourceLinksSummary(section, statuses, t) {
|
|
const counts = {
|
|
total: statuses.length,
|
|
available: statuses.filter((status) => status.kind === "available").length,
|
|
missing: statuses.filter((status) => status.kind === "missing").length,
|
|
rootNotConfigured: statuses.filter((status) => status.kind === "root-not-configured").length
|
|
};
|
|
const summary = section.createDiv({ cls: "model-weave-source-links-summary" });
|
|
renderSourceLinksSummaryChip(summary, t("sourceLinks.summary.total"), counts.total);
|
|
renderSourceLinksSummaryChip(summary, t("sourceLinks.summary.available"), counts.available, counts.available > 0 ? "available" : void 0);
|
|
renderSourceLinksSummaryChip(summary, t("sourceLinks.summary.missing"), counts.missing, counts.missing > 0 ? "missing" : void 0);
|
|
renderSourceLinksSummaryChip(
|
|
summary,
|
|
t("sourceLinks.summary.rootNotConfigured"),
|
|
counts.rootNotConfigured,
|
|
counts.rootNotConfigured > 0 ? "warning" : void 0
|
|
);
|
|
}
|
|
function renderSourceLinksSummaryChip(container, label, value, modifier) {
|
|
const chip = container.createDiv({ cls: "model-weave-source-links-summary-chip" });
|
|
if (modifier) {
|
|
chip.addClass(`model-weave-source-links-summary-chip-${modifier}`);
|
|
}
|
|
chip.createSpan({ text: label, cls: "model-weave-source-links-summary-label" });
|
|
chip.createSpan({ text: String(value), cls: "model-weave-source-links-summary-value" });
|
|
}
|
|
function renderSourceLinksBulkActions(section, entries, t) {
|
|
const actions = section.createDiv({ cls: "model-weave-source-links-bulk-actions" });
|
|
appendBulkCopyButton(
|
|
actions,
|
|
t("sourceLinks.copyAllPaths"),
|
|
entries.map((entry) => entry.sourceLink.path)
|
|
);
|
|
appendBulkCopyButton(
|
|
actions,
|
|
t("sourceLinks.copyAvailablePaths"),
|
|
entries.filter((entry) => entry.status.kind === "available").map((entry) => entry.status.resolvedPath || entry.sourceLink.path)
|
|
);
|
|
appendBulkCopyButton(
|
|
actions,
|
|
t("sourceLinks.copyAsMarkdown"),
|
|
entries.map((entry) => formatSourceLinkMarkdownLine(entry.sourceLink))
|
|
);
|
|
appendBulkCopyButton(
|
|
actions,
|
|
t("sourceLinks.copyMissingPaths"),
|
|
entries.filter((entry) => entry.status.kind === "missing").map((entry) => entry.sourceLink.path)
|
|
);
|
|
}
|
|
function appendBulkCopyButton(container, label, lines) {
|
|
const button = container.createEl("button", {
|
|
text: label,
|
|
cls: "model-weave-source-links-bulk-copy"
|
|
});
|
|
button.type = "button";
|
|
button.disabled = lines.length === 0;
|
|
button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (lines.length === 0) {
|
|
return;
|
|
}
|
|
void navigator.clipboard?.writeText(lines.join("\n"));
|
|
});
|
|
}
|
|
function formatSourceLinkMarkdownLine(sourceLink) {
|
|
const note = (sourceLink.notes ?? sourceLink.label ?? "").replace(/\s+/g, " ").trim();
|
|
return note ? `- ${sourceLink.path} \u2014 ${note}` : `- ${sourceLink.path}`;
|
|
}
|
|
function resolveSourceLinkStatus(sourceLink, localSourceRoot, t) {
|
|
const resolved = resolveSourceLinkPath(localSourceRoot, sourceLink.path);
|
|
if (resolved.kind === "fileUri") {
|
|
return {
|
|
kind: "neutral",
|
|
label: t("sourceLinks.unsupportedFileUri"),
|
|
modifierClass: "model-weave-source-links-status-neutral",
|
|
resolvedPath: resolved.resolvedPath,
|
|
openable: false,
|
|
actionNote: t("sourceLinks.useFilesystemPath")
|
|
};
|
|
}
|
|
const { kind, rootPath, resolvedPath } = resolved;
|
|
if (resolved.unsupportedSourceRoot) {
|
|
return {
|
|
kind: "neutral",
|
|
label: t("sourceLinks.unsupportedSourceRoot"),
|
|
modifierClass: "model-weave-source-links-status-neutral",
|
|
resolvedPath,
|
|
openable: true,
|
|
actionNote: getPathKindNote(kind, t)
|
|
};
|
|
}
|
|
if (resolved.usedSourceRoot && !isResolvedPathInsideRoot(kind, rootPath, resolvedPath)) {
|
|
return {
|
|
kind: "neutral",
|
|
label: t("sourceLinks.outsideSourceRoot"),
|
|
modifierClass: "model-weave-source-links-status-neutral",
|
|
resolvedPath,
|
|
openable: true
|
|
};
|
|
}
|
|
const unconfiguredRelative = kind === "relative" && !resolved.usedSourceRoot && !localSourceRoot.trim();
|
|
if (!sourcePathExists(resolvedPath)) {
|
|
return {
|
|
kind: unconfiguredRelative ? "root-not-configured" : "missing",
|
|
label: unconfiguredRelative ? t("sourceLinks.localSourceRootNotConfigured") : t("sourceLinks.missing"),
|
|
modifierClass: unconfiguredRelative ? "model-weave-source-links-status-neutral" : "model-weave-source-links-status-missing",
|
|
resolvedPath,
|
|
openable: true,
|
|
actionNote: getPathKindNote(kind, t)
|
|
};
|
|
}
|
|
const stats = (0, import_fs.statSync)(resolvedPath);
|
|
return {
|
|
kind: "available",
|
|
label: stats.isFile() ? t("sourceLinks.available") : t("sourceLinks.availableDirectory"),
|
|
modifierClass: "model-weave-source-links-status-available",
|
|
resolvedPath,
|
|
openable: true,
|
|
actionNote: getPathKindNote(kind, t)
|
|
};
|
|
}
|
|
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, t) {
|
|
return isUncPathKind(kind) ? t("sourceLinks.uncPathNote") : void 0;
|
|
}
|
|
async function openResolvedSourcePath(resolvedPath, t) {
|
|
try {
|
|
if (typeof import_electron.shell.openPath !== "function") {
|
|
new import_obsidian6.Notice(t("sourceLinks.openUnavailable"));
|
|
return;
|
|
}
|
|
const result = await import_electron.shell.openPath(resolvedPath);
|
|
if (result) {
|
|
new import_obsidian6.Notice(t("sourceLinks.openFailed", { message: result }));
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
new import_obsidian6.Notice(t("sourceLinks.openFailed", { message }));
|
|
}
|
|
}
|
|
|
|
// src/renderers/object-renderer.ts
|
|
function renderObjectModel(model, context, localSourceRoot = "", language = "auto", options = {}) {
|
|
const root = activeDocument.createElement("section");
|
|
root.addClass("model-weave-object-focus");
|
|
root.addClass("model-weave-summary-details");
|
|
root.addClass("model-weave-preview-section");
|
|
const title = activeDocument.createElement("h2");
|
|
title.textContent = getPrimaryTitle(model);
|
|
title.addClass("model-weave-object-title");
|
|
title.addClass("model-weave-preview-section-title");
|
|
root.appendChild(title);
|
|
const meta = activeDocument.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);
|
|
if (options.includeSourceLinks !== false) {
|
|
const sourceLinks = renderSourceLinks(model.sourceLinks, localSourceRoot, language);
|
|
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 = activeDocument.createElement("div");
|
|
row.addClass("model-weave-detail-card-row");
|
|
const key = activeDocument.createElement("div");
|
|
key.textContent = label;
|
|
key.addClass("model-weave-object-meta-key");
|
|
key.addClass("model-weave-detail-card-label");
|
|
const val = activeDocument.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/renderers/domains-mermaid.ts
|
|
function renderDomainsMermaidDiagram(domains, options) {
|
|
const shell3 = createMermaidShell({
|
|
className: "model-weave-domains-mermaid",
|
|
title: options.title,
|
|
forExport: options.forExport === true,
|
|
onExportPng: options.onExportPng,
|
|
onExportAndOpenPng: options.onExportAndOpenPng,
|
|
exportPngLabel: options.exportPngLabel,
|
|
exportPngTitle: options.exportPngTitle,
|
|
exportAndOpenPngLabel: options.exportAndOpenPngLabel,
|
|
exportAndOpenPngTitle: options.exportAndOpenPngTitle
|
|
});
|
|
const mode = options.mode ?? "area";
|
|
shell3.root.addClass(`model-weave-domains-mermaid-mode-${mode}`);
|
|
const interactionTargets = buildDomainsMermaidInteractionTargets(
|
|
domains,
|
|
mode,
|
|
options.interactionSourcePath ?? ""
|
|
);
|
|
const ready = renderMermaidSourceIntoShell(shell3, {
|
|
source: buildDomainsMermaidSource(domains, mode, options.colorScheme),
|
|
renderIdPrefix: getDomainsMermaidRenderIdPrefix(mode),
|
|
fitHorizontalAlign: "left",
|
|
fitVerticalAlign: options.fitVerticalAlign,
|
|
minZoom: 0.08,
|
|
minFitScale: 0.08,
|
|
viewportState: options.viewportState,
|
|
onViewportStateChange: options.onViewportStateChange,
|
|
staticRender: options.forExport === true,
|
|
showSourcePanel: options.forExport === true ? false : void 0,
|
|
sourcePanelContainer: options.sourcePanelContainer,
|
|
sourcePanelPlacement: options.sourcePanelPlacement,
|
|
sourcePanelTitle: options.sourcePanelTitle,
|
|
sourcePanelCopyLabel: options.sourcePanelCopyLabel,
|
|
showRenderDebug: options.forExport === true ? false : options.showMermaidRenderDebug === true
|
|
}).then(() => {
|
|
if (options.forExport !== true && options.app && interactionTargets.length > 0) {
|
|
attachMermaidNodeInteractions({
|
|
app: options.app,
|
|
rootEl: shell3.surface,
|
|
targets: interactionTargets,
|
|
source: "model-weave",
|
|
nodeClassName: "model-weave-mermaid-interactive-node",
|
|
dragThreshold: 6,
|
|
hoverParent: (nodeEl, fallback) => nodeEl.closest(
|
|
".model-weave-view-only-stage, .model-weave-domains-mermaid, .model-weave-mermaid-shell"
|
|
) ?? fallback,
|
|
formatTitle: (target) => target.label ? `${target.label} (${target.targetType ?? "model"})` : target.linktext
|
|
});
|
|
}
|
|
}).catch(() => {
|
|
shell3.root.addClass("model-weave-mermaid-fallback-shell");
|
|
shell3.canvas.replaceChildren(
|
|
createMermaidFallbackNotice(
|
|
options.renderFailedMessage ?? modelWeaveText(
|
|
"Domain hierarchy diagram could not be rendered.",
|
|
"Domain \u968E\u5C64\u56F3\u3092\u63CF\u753B\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002"
|
|
)
|
|
)
|
|
);
|
|
});
|
|
setMermaidRenderReadyPromise(shell3.root, ready);
|
|
return shell3.root;
|
|
}
|
|
function buildDomainsMermaidInteractionTargets(domains, mode, sourcePath) {
|
|
if (!sourcePath) {
|
|
return [];
|
|
}
|
|
if (mode === "mindmap") {
|
|
return domains.map((domain) => ({
|
|
mermaidId: toDomainMermaidId(domain.id),
|
|
linktext: sourcePath,
|
|
sourcePath,
|
|
label: getDomainMindmapLabel(domain),
|
|
kind: "domain-node",
|
|
targetType: "domain",
|
|
filePath: sourcePath,
|
|
modelId: domain.id,
|
|
modelType: "domain"
|
|
}));
|
|
}
|
|
const idMap = createDomainMermaidIds(domains);
|
|
return domains.map((domain) => ({
|
|
mermaidId: idMap.get(domain) ?? toDomainMermaidId(domain.id),
|
|
linktext: sourcePath,
|
|
sourcePath,
|
|
label: getDomainMermaidLabel(domain),
|
|
kind: "domain-node",
|
|
targetType: "domain",
|
|
filePath: sourcePath,
|
|
modelId: domain.id,
|
|
modelType: "domain"
|
|
}));
|
|
}
|
|
function buildDomainsMermaidSource(domains, mode, colorScheme) {
|
|
if (mode === "mindmap") {
|
|
return buildDomainMindmapMermaid(domains);
|
|
}
|
|
if (mode === "tree") {
|
|
return buildDomainTreeViewMermaid(domains, colorScheme);
|
|
}
|
|
return buildDomainHierarchyMermaid(domains, colorScheme);
|
|
}
|
|
function getDomainsMermaidRenderIdPrefix(mode) {
|
|
if (mode === "mindmap") {
|
|
return "model_weave_domains_mindmap";
|
|
}
|
|
if (mode === "tree") {
|
|
return "model_weave_domains_tree";
|
|
}
|
|
return "model_weave_domains";
|
|
}
|
|
function buildDomainHierarchyMermaid(domains, colorScheme) {
|
|
const roots = buildDomainTree(domains);
|
|
const idMap = createDomainMermaidIds(domains);
|
|
const lines = ["flowchart TB", ""];
|
|
const nodeStyles = [];
|
|
for (const root of roots) {
|
|
appendDomainNodeLines(lines, root, idMap, 0, colorScheme, nodeStyles);
|
|
}
|
|
if (nodeStyles.length > 0) {
|
|
lines.push("", ...nodeStyles);
|
|
}
|
|
return lines.join("\n").trimEnd();
|
|
}
|
|
function buildDomainTreeViewMermaid(domains, colorScheme) {
|
|
const roots = buildDomainTree(domains);
|
|
const idMap = createDomainMermaidIds(domains);
|
|
const lines = ["flowchart TB"];
|
|
const edges = [];
|
|
const colorClasses = /* @__PURE__ */ new Map();
|
|
const nodeClasses = [];
|
|
for (const domain of domains) {
|
|
const mermaidId = idMap.get(domain) ?? toDomainMermaidId(domain.id);
|
|
const label = escapeDomainMermaidLabel(getDomainMermaidLabel(domain));
|
|
lines.push(` ${mermaidId}["${label}"]`);
|
|
if (colorScheme) {
|
|
const className = toDomainColorClassName(domain.kind);
|
|
colorClasses.set(
|
|
className,
|
|
resolveColorStyle(colorScheme, "domain", domain.kind)
|
|
);
|
|
nodeClasses.push(` class ${mermaidId} ${className}`);
|
|
}
|
|
}
|
|
for (const root of roots) {
|
|
appendDomainTreeViewEdgeLines(edges, root, idMap, /* @__PURE__ */ new Set());
|
|
}
|
|
if (edges.length > 0) {
|
|
lines.push("", ...edges);
|
|
}
|
|
if (colorClasses.size > 0) {
|
|
lines.push("");
|
|
for (const [className, style] of colorClasses) {
|
|
lines.push(` classDef ${className} ${formatMermaidClassDefStyle3(style)}`);
|
|
}
|
|
lines.push("", ...nodeClasses);
|
|
}
|
|
return lines.join("\n").trimEnd();
|
|
}
|
|
function buildDomainMindmapMermaid(domains) {
|
|
const roots = buildDomainTree(domains);
|
|
const lines = ["mindmap"];
|
|
if (roots.length === 0) {
|
|
return lines.join("\n");
|
|
}
|
|
if (roots.length === 1) {
|
|
appendDomainMindmapRootLines(lines, roots[0]);
|
|
return lines.join("\n");
|
|
}
|
|
lines.push(" root((Domains))");
|
|
for (const root of roots) {
|
|
appendDomainMindmapNodeLines(lines, root, 2, /* @__PURE__ */ new Set());
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function appendDomainMindmapRootLines(lines, root) {
|
|
lines.push(` root((${escapeDomainMindmapLabel(getDomainMindmapLabel(root.domain))}))`);
|
|
const visited = /* @__PURE__ */ new Set([root.domain.id]);
|
|
for (const child of root.children) {
|
|
appendDomainMindmapNodeLines(lines, child, 2, visited);
|
|
}
|
|
}
|
|
function appendDomainMindmapNodeLines(lines, node, depth, visited) {
|
|
if (visited.has(node.domain.id)) {
|
|
return;
|
|
}
|
|
const indent = " ".repeat(depth);
|
|
lines.push(`${indent}${escapeDomainMindmapLabel(getDomainMindmapLabel(node.domain))}`);
|
|
const nextVisited = new Set(visited);
|
|
nextVisited.add(node.domain.id);
|
|
for (const child of node.children) {
|
|
appendDomainMindmapNodeLines(lines, child, depth + 1, nextVisited);
|
|
}
|
|
}
|
|
function appendDomainTreeViewEdgeLines(lines, node, idMap, visited) {
|
|
if (visited.has(node.domain.id)) {
|
|
return;
|
|
}
|
|
const parentId = idMap.get(node.domain) ?? toDomainMermaidId(node.domain.id);
|
|
const nextVisited = new Set(visited);
|
|
nextVisited.add(node.domain.id);
|
|
for (const child of node.children) {
|
|
const childId = idMap.get(child.domain) ?? toDomainMermaidId(child.domain.id);
|
|
lines.push(` ${parentId} --> ${childId}`);
|
|
appendDomainTreeViewEdgeLines(lines, child, idMap, nextVisited);
|
|
}
|
|
}
|
|
function appendDomainNodeLines(lines, node, idMap, depth, colorScheme, nodeStyles) {
|
|
const indent = " ".repeat(depth);
|
|
const mermaidId = idMap.get(node.domain) ?? toDomainMermaidId(node.domain.id);
|
|
const label = escapeDomainMermaidLabel(getDomainMermaidLabel(node.domain));
|
|
if (colorScheme) {
|
|
nodeStyles.push(
|
|
` style ${mermaidId} ${formatMermaidClassDefStyle3(
|
|
resolveColorStyle(colorScheme, "domain", node.domain.kind)
|
|
)}`
|
|
);
|
|
}
|
|
if (node.children.length === 0) {
|
|
lines.push(`${indent}${mermaidId}["${label}"]`);
|
|
return;
|
|
}
|
|
lines.push(`${indent}subgraph ${mermaidId}["${label}"]`);
|
|
for (const child of node.children) {
|
|
appendDomainNodeLines(lines, child, idMap, depth + 1, colorScheme, nodeStyles);
|
|
}
|
|
lines.push(`${indent}end`);
|
|
}
|
|
function createDomainMermaidIds(domains) {
|
|
const usedIds = /* @__PURE__ */ new Set();
|
|
const idMap = /* @__PURE__ */ new Map();
|
|
for (const domain of domains) {
|
|
idMap.set(
|
|
domain,
|
|
ensureUniqueMermaidId(toDomainMermaidId(domain.id), usedIds)
|
|
);
|
|
}
|
|
return idMap;
|
|
}
|
|
function toDomainMermaidId(id) {
|
|
return `domain_${sanitizeMermaidId(id)}`;
|
|
}
|
|
function toDomainColorClassName(kind) {
|
|
const suffix = kind?.trim() ? kind.trim() : "default";
|
|
return `kind_domain_${sanitizeMermaidId(suffix)}`;
|
|
}
|
|
function formatMermaidClassDefStyle3(style) {
|
|
return [
|
|
style.fill ? `fill:${style.fill}` : void 0,
|
|
style.stroke ? `stroke:${style.stroke}` : void 0,
|
|
style.text ? `color:${style.text}` : void 0
|
|
].filter((entry) => Boolean(entry)).join(",");
|
|
}
|
|
function getDomainMermaidLabel(domain) {
|
|
const label = domain.name?.trim() || domain.id;
|
|
return domain.kind?.trim() ? `${label} [${domain.kind.trim()}]` : label;
|
|
}
|
|
function getDomainMindmapLabel(domain) {
|
|
const label = domain.name?.trim() || domain.id;
|
|
return domain.kind?.trim() ? `${label}\uFF08${domain.kind.trim()}\uFF09` : label;
|
|
}
|
|
function escapeDomainMermaidLabel(value) {
|
|
return value.replace(/\r\n?/g, "\n").split("\n").map(
|
|
(line) => line.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/</g, "<").replace(/>/g, ">")
|
|
).join("<br/>");
|
|
}
|
|
function escapeDomainMindmapLabel(value) {
|
|
return value.replace(/\r\n?/g, "\n").replace(/\n/g, " ").replace(/\(/g, "\uFF08").replace(/\)/g, "\uFF09").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
// 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/applied-color-scheme-renderer.ts
|
|
function renderAppliedColorSchemeSectionContent(container, colorScheme, rows, targets, t) {
|
|
container.createEl("p", {
|
|
text: formatAppliedColorSchemeSummary(colorScheme, targets, t),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
const tableWrap = container.createDiv({ cls: "model-weave-table-wrap" });
|
|
const table = tableWrap.createEl("table", {
|
|
cls: "model-weave-summary-table model-weave-data-table"
|
|
});
|
|
const headerRow = table.createEl("thead").createEl("tr");
|
|
for (const key of [
|
|
"colorScheme.field.target",
|
|
"colorScheme.field.kind",
|
|
"colorScheme.field.fill",
|
|
"colorScheme.field.stroke",
|
|
"colorScheme.field.text",
|
|
"colorScheme.preview.swatch",
|
|
"colorScheme.field.notes",
|
|
"colorScheme.field.source"
|
|
]) {
|
|
headerRow.createEl("th", {
|
|
text: t(key),
|
|
cls: "model-weave-summary-th"
|
|
});
|
|
}
|
|
const tbody = table.createEl("tbody");
|
|
if (rows.length === 0) {
|
|
const row = tbody.createEl("tr");
|
|
row.createEl("td", {
|
|
text: t("colorScheme.preview.empty"),
|
|
attr: { colspan: "8" },
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
return;
|
|
}
|
|
for (const row of rows) {
|
|
renderAppliedColorSchemeTableRow(tbody, row, t);
|
|
}
|
|
}
|
|
function formatAppliedColorSchemeSummary(colorScheme, targets, t) {
|
|
const schemeType = colorScheme.sourcePath ? t("colorScheme.preview.configured") : t("colorScheme.preview.builtIn");
|
|
const schemeName = colorScheme.sourcePath ? `${colorScheme.name} (${colorScheme.sourcePath})` : colorScheme.name;
|
|
const targetList = targets.length > 0 ? targets.join(", ") : t("domains.value.none");
|
|
return `${schemeType}: ${schemeName} / ${t("colorScheme.preview.targets")}: ${targetList}`;
|
|
}
|
|
function renderAppliedColorSchemeTableRow(tbody, row, t) {
|
|
const tableRow = tbody.createEl("tr");
|
|
const color = row.entry;
|
|
for (const value of [
|
|
color.target ?? t("domains.value.none"),
|
|
color.kind,
|
|
color.fill ?? t("domains.value.none"),
|
|
color.stroke ?? t("domains.value.none"),
|
|
color.text ?? t("domains.value.none")
|
|
]) {
|
|
tableRow.createEl("td", { text: value });
|
|
}
|
|
const swatchCell = tableRow.createEl("td");
|
|
const swatch = swatchCell.createSpan({
|
|
cls: "model-weave-color-swatch"
|
|
});
|
|
if (color.fill) {
|
|
swatch.style.backgroundColor = color.fill;
|
|
}
|
|
if (color.stroke) {
|
|
swatch.style.borderColor = color.stroke;
|
|
}
|
|
if (color.text) {
|
|
swatch.style.color = color.text;
|
|
}
|
|
swatch.textContent = "Aa";
|
|
tableRow.createEl("td", { text: color.notes ?? "" });
|
|
tableRow.createEl("td", {
|
|
text: row.source === "built-in" ? t("colorScheme.preview.builtIn") : t("colorScheme.preview.configured")
|
|
});
|
|
}
|
|
|
|
// src/views/view-icon.ts
|
|
var MODELING_VIEW_ICON = "git-branch";
|
|
|
|
// src/views/modeling-preview-view.ts
|
|
var MODELING_PREVIEW_VIEW_TYPE = "mdspec-preview";
|
|
function isDomainRenderMode(value) {
|
|
return value === "mindmap" || value === "area" || value === "tree";
|
|
}
|
|
function isStandardRenderMode(value) {
|
|
return value === "custom" || value === "mermaid" || value === "mermaid-detail";
|
|
}
|
|
function getStandardRenderMode(selection, fallback) {
|
|
const mode = selection?.effectiveMode;
|
|
return isStandardRenderMode(mode) ? mode : fallback;
|
|
}
|
|
function getDomainRenderModeFromSelection(selection) {
|
|
return isDomainRenderMode(selection?.effectiveMode) ? selection.effectiveMode : null;
|
|
}
|
|
function getMermaidSourceLabels(t) {
|
|
return {
|
|
sourcePanelTitle: t("mermaid.source.title"),
|
|
sourcePanelCopyLabel: t("mermaid.source.copy")
|
|
};
|
|
}
|
|
function getGraphExportLabels(t) {
|
|
const exportLabel = t("graph.exportPng");
|
|
const exportAndOpenLabel = t("graph.exportPngOpen");
|
|
return {
|
|
exportPngLabel: exportLabel,
|
|
exportPngTitle: exportLabel,
|
|
exportAndOpenPngLabel: exportAndOpenLabel,
|
|
exportAndOpenPngTitle: exportAndOpenLabel
|
|
};
|
|
}
|
|
function isDesktopVaultAdapter(adapter) {
|
|
return typeof adapter === "object" && adapter !== null && "getFullPath" in adapter && typeof adapter.getFullPath === "function";
|
|
}
|
|
function getDfdDetailLabels(t) {
|
|
return {
|
|
dfdDetailLabels: {
|
|
displayedObjects: t("dfd.preview.displayedObjects"),
|
|
displayedFlows: t("dfd.preview.displayedFlows"),
|
|
noObjects: t("dfd.preview.noObjects"),
|
|
noFlows: t("dfd.preview.noFlows"),
|
|
domainPlacement: t("dfd.preview.domainPlacement"),
|
|
resolved: t("dfd.preview.resolved"),
|
|
unresolved: t("dfd.preview.unresolved")
|
|
}
|
|
};
|
|
}
|
|
function getObjectContextLabels(t) {
|
|
return {
|
|
title: t("objectContext.title"),
|
|
linked: (count) => t("objectContext.linked", { count }),
|
|
connectionDetails: t("objectContext.connectionDetails"),
|
|
relationDetails: t("objectContext.relationDetails"),
|
|
noDirectlyRelated: t("objectContext.noDirectlyRelated")
|
|
};
|
|
}
|
|
function getClassDetailLabels(t) {
|
|
return {
|
|
classDetailLabels: {
|
|
displayedRelations: t("class.preview.displayedRelations"),
|
|
noRelationsUsed: t("class.preview.noRelationsUsed")
|
|
}
|
|
};
|
|
}
|
|
var VIEWPORT_STATE_CACHE_LIMIT = 50;
|
|
var DEFAULT_VIEWER_PREFERENCES = {
|
|
defaultZoom: "fit",
|
|
fontSize: "normal",
|
|
nodeDensity: "normal",
|
|
defaultDomainsViewMode: "mindmap",
|
|
defaultDomainDiagramViewMode: "mindmap",
|
|
localSourceRoot: "",
|
|
uiLanguage: "auto",
|
|
showMermaidRenderDebug: false
|
|
};
|
|
var ModelingPreviewView = class extends import_obsidian7.ItemView {
|
|
constructor(leaf, viewerPreferences = DEFAULT_VIEWER_PREFERENCES, paneActions = {}) {
|
|
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.domainsMermaidViewportState = {
|
|
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.domainsDiagramMode = "mindmap";
|
|
this.domainsDiagramModeFilePath = null;
|
|
this.domainsDiagramModeState = null;
|
|
this.activeScrollContainer = null;
|
|
this.focusModeEnabled = false;
|
|
this.focusModePlaceholder = null;
|
|
this.viewOnlyEnabled = false;
|
|
this.viewOnlyTarget = null;
|
|
this.viewOnlyPlaceholder = null;
|
|
this.viewOnlyStage = null;
|
|
this.handleFocusModeKeydown = (event) => {
|
|
if (event.key !== "Escape" || !this.focusModeEnabled) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
this.setFocusMode(false);
|
|
};
|
|
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);
|
|
this.paneActions = paneActions;
|
|
}
|
|
getViewType() {
|
|
return MODELING_PREVIEW_VIEW_TYPE;
|
|
}
|
|
getDisplayText() {
|
|
return "Modeling preview";
|
|
}
|
|
getIcon() {
|
|
return MODELING_VIEW_ICON;
|
|
}
|
|
onOpen() {
|
|
this.contentEl.ownerDocument.addEventListener("keydown", this.handleFocusModeKeydown);
|
|
this.renderCurrentState();
|
|
return Promise.resolve();
|
|
}
|
|
onClose() {
|
|
this.contentEl.ownerDocument.removeEventListener("keydown", this.handleFocusModeKeydown);
|
|
this.setFocusMode(false, { skipFit: true });
|
|
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);
|
|
}
|
|
async exportCurrentDiagramAsPngWithNotice() {
|
|
try {
|
|
const exportPath = await this.exportCurrentDiagramAsPng();
|
|
if (!exportPath) {
|
|
new import_obsidian7.Notice("The current view is not ready for export.");
|
|
return;
|
|
}
|
|
new import_obsidian7.Notice(`Diagram exported: ${exportPath}`);
|
|
} catch (error) {
|
|
this.showPngExportFailureNotice(error);
|
|
}
|
|
}
|
|
async exportCurrentDiagramAsPngAndOpenWithNotice() {
|
|
try {
|
|
const exportPath = await this.exportCurrentDiagramAsPng();
|
|
if (!exportPath) {
|
|
new import_obsidian7.Notice("The current view is not ready for export.");
|
|
return;
|
|
}
|
|
new import_obsidian7.Notice(`Diagram exported: ${exportPath}`);
|
|
await this.openExportedPng(exportPath);
|
|
} catch (error) {
|
|
this.showPngExportFailureNotice(error);
|
|
}
|
|
}
|
|
async exportWeaveMapPng(container, filePath) {
|
|
const snapshot = buildDomDiagramExportSnapshot(
|
|
container,
|
|
filePath,
|
|
"weave-map"
|
|
);
|
|
if (!snapshot) {
|
|
return null;
|
|
}
|
|
return exportDiagramSnapshotAsPng(this.app, snapshot);
|
|
}
|
|
async exportWeaveMapAsPng(container, filePath) {
|
|
try {
|
|
const exportPath = await this.exportWeaveMapPng(container, filePath);
|
|
if (!exportPath) {
|
|
new import_obsidian7.Notice("The current diagram has no measurable export bounds.");
|
|
return;
|
|
}
|
|
new import_obsidian7.Notice(`Diagram exported: ${exportPath}`);
|
|
} catch (error) {
|
|
this.showPngExportFailureNotice(error);
|
|
}
|
|
}
|
|
async exportWeaveMapAsPngAndOpen(container, filePath) {
|
|
try {
|
|
const exportPath = await this.exportWeaveMapPng(container, filePath);
|
|
if (!exportPath) {
|
|
new import_obsidian7.Notice("The current diagram has no measurable export bounds.");
|
|
return;
|
|
}
|
|
new import_obsidian7.Notice(`Diagram exported: ${exportPath}`);
|
|
await this.openExportedPng(exportPath);
|
|
} catch (error) {
|
|
this.showPngExportFailureNotice(error);
|
|
}
|
|
}
|
|
async openExportedPng(exportPath) {
|
|
const adapter = this.app.vault.adapter;
|
|
if (!isDesktopVaultAdapter(adapter)) {
|
|
new import_obsidian7.Notice(this.t("graph.exportPngOpenUnavailable"));
|
|
return;
|
|
}
|
|
try {
|
|
if (typeof import_electron2.shell.openPath !== "function") {
|
|
new import_obsidian7.Notice(this.t("graph.exportPngOpenUnavailable"));
|
|
return;
|
|
}
|
|
const result = await import_electron2.shell.openPath(adapter.getFullPath(exportPath));
|
|
if (result) {
|
|
new import_obsidian7.Notice(this.t("graph.exportPngOpenFailed", { message: result }));
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
new import_obsidian7.Notice(this.t("graph.exportPngOpenFailed", { message }));
|
|
}
|
|
}
|
|
showPngExportFailureNotice(error) {
|
|
if (error instanceof DiagramExportError && error.code === "bounds-invalid") {
|
|
new import_obsidian7.Notice("The current diagram has no measurable export bounds.");
|
|
return;
|
|
}
|
|
new import_obsidian7.Notice("Failed to export the current diagram as PNG.");
|
|
}
|
|
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.prepareDomainsDiagramMode(state, nextFilePath);
|
|
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 "domains":
|
|
return state.model.path;
|
|
case "domain-diagram":
|
|
return state.resolved.diagram.path;
|
|
case "color-scheme":
|
|
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") {
|
|
return;
|
|
}
|
|
if (reason === "renderer-switch") {
|
|
this.viewportStateCache.delete(nextFilePath);
|
|
resetGraphViewportState(state);
|
|
return;
|
|
}
|
|
if (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;
|
|
}
|
|
}
|
|
prepareDomainsDiagramMode(state, nextFilePath) {
|
|
if (state.mode !== "domains" && state.mode !== "domain-diagram") {
|
|
this.domainsDiagramModeFilePath = null;
|
|
this.domainsDiagramModeState = null;
|
|
return;
|
|
}
|
|
if (this.domainsDiagramModeFilePath === nextFilePath && this.domainsDiagramModeState === state.mode) {
|
|
return;
|
|
}
|
|
this.domainsDiagramMode = getDomainRenderModeFromSelection(state.rendererSelection) ?? (state.mode === "domains" ? this.viewerPreferences.defaultDomainsViewMode : this.viewerPreferences.defaultDomainDiagramViewMode);
|
|
this.domainsDiagramModeFilePath = nextFilePath;
|
|
this.domainsDiagramModeState = state.mode;
|
|
}
|
|
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: getStandardRenderMode(state.rendererSelection),
|
|
colorScheme: state.colorScheme,
|
|
...getMermaidSourceLabels(this.t)
|
|
})
|
|
};
|
|
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: getStandardRenderMode(state.rendererSelection, "mermaid"),
|
|
...getMermaidSourceLabels(this.t)
|
|
})
|
|
};
|
|
}
|
|
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 "domains":
|
|
return this.buildDomainsDiagramExportRenderable(
|
|
state.model.path,
|
|
state.model.domains,
|
|
state.colorScheme
|
|
);
|
|
case "domain-diagram":
|
|
return this.buildDomainsDiagramExportRenderable(
|
|
state.resolved.diagram.path,
|
|
state.resolved.domains,
|
|
state.colorScheme
|
|
);
|
|
case "summary":
|
|
if ((state.layoutBlocks?.length ?? 0) > 0) {
|
|
return {
|
|
filePath: state.filePath,
|
|
renderer: "custom",
|
|
render: () => createScreenPreviewDiagram(buildScreenPreviewData(state, this.t), {
|
|
forExport: true
|
|
})
|
|
};
|
|
}
|
|
if ((state.businessFlow?.steps.length ?? 0) > 0) {
|
|
return {
|
|
filePath: state.filePath,
|
|
renderer: "business-flow",
|
|
render: () => renderAppProcessBusinessFlow(state.businessFlow, {
|
|
forExport: true,
|
|
debug: false,
|
|
colorScheme: state.colorScheme
|
|
})
|
|
};
|
|
}
|
|
return null;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
buildDomainsDiagramExportRenderable(filePath, domains, colorScheme) {
|
|
if (domains.length === 0) {
|
|
return null;
|
|
}
|
|
const mode = this.domainsDiagramMode;
|
|
return {
|
|
filePath,
|
|
renderer: mode,
|
|
render: () => renderDomainsMermaidDiagram(domains, {
|
|
title: this.getDomainDiagramModeLabel(mode),
|
|
mode,
|
|
renderFailedMessage: this.t("domains.preview.diagramRenderFailed"),
|
|
fitVerticalAlign: "top",
|
|
colorScheme,
|
|
forExport: true
|
|
})
|
|
};
|
|
}
|
|
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 "domains":
|
|
this.renderDomainsState(this.state);
|
|
return;
|
|
case "domain-diagram":
|
|
this.renderDomainDiagramState(this.state);
|
|
return;
|
|
case "color-scheme":
|
|
this.renderColorSchemeState(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.setViewOnlyMode(false, { skipFit: true });
|
|
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",
|
|
"model-weave-viewer-view-only"
|
|
);
|
|
this.contentEl.classList.add("model-weave-viewer-root");
|
|
this.contentEl.classList.toggle("model-weave-viewer-focus-mode", this.focusModeEnabled);
|
|
this.contentEl.classList.toggle("model-weave-viewer-view-only", this.viewOnlyEnabled);
|
|
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`
|
|
});
|
|
this.appendViewerFocusToolbar();
|
|
this.ensureViewOnlyStage();
|
|
}
|
|
renderEmptyState(message) {
|
|
const doc = this.contentEl.ownerDocument;
|
|
const section = doc.createElement("section");
|
|
section.addClass("model-weave-viewer-empty");
|
|
const text = doc.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 shell3 = this.createViewerSplitShell(`object:${objectPath}`, 0.62);
|
|
shell3.bottomPane.addClass("model-weave-summary-details");
|
|
this.activeScrollContainer = shell3.bottomPane;
|
|
this.renderReviewSummaryPanel(shell3.bottomPane, {
|
|
model: state.model,
|
|
warnings: state.warnings,
|
|
impactSummary: state.impactSummary,
|
|
sourceLinks: state.model.sourceLinks,
|
|
weaveMapAvailable: Boolean(state.weaveMapMermaidSource)
|
|
});
|
|
renderDiagnostics(
|
|
shell3.bottomPane,
|
|
state.warnings,
|
|
state.onOpenDiagnostic ?? void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState,
|
|
this.getDiagnosticLanguage()
|
|
);
|
|
const objectDetails = renderObjectModel(
|
|
state.model,
|
|
state.context,
|
|
this.viewerPreferences.localSourceRoot,
|
|
this.viewerPreferences.uiLanguage,
|
|
{ includeSourceLinks: false }
|
|
);
|
|
this.renderImpactSummarySection(
|
|
shell3.bottomPane,
|
|
state.impactSummary,
|
|
state.onCopyImpactSummary,
|
|
state.onOpenImpactModel,
|
|
state.weaveMapMermaidSource,
|
|
state.colorScheme
|
|
);
|
|
this.renderSourceLinksSection(shell3.bottomPane, state.model.sourceLinks);
|
|
shell3.bottomPane.appendChild(objectDetails);
|
|
if (!state.context) {
|
|
return;
|
|
}
|
|
if (state.rendererSelection?.actualRenderer === "mermaid") {
|
|
const contextRoot2 = renderObjectContext(state.context, {
|
|
onOpenObject: state.onOpenObject ?? void 0,
|
|
app: this.app,
|
|
interactionSourcePath: objectPath,
|
|
viewportState: this.objectGraphViewportState,
|
|
onViewportStateChange: this.createObjectViewportStateHandler(objectPath),
|
|
labels: getObjectContextLabels(this.t)
|
|
});
|
|
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();
|
|
shell3.bottomPane.appendChild(relatedList2);
|
|
}
|
|
const subgraph = buildObjectSubgraphScene(state.context);
|
|
const mermaidRoot = renderDiagramModel(subgraph, {
|
|
app: this.app,
|
|
interactionSourcePath: objectPath,
|
|
hideTitle: true,
|
|
hideDetails: true,
|
|
renderMode: getStandardRenderMode(state.rendererSelection, "mermaid"),
|
|
fitVerticalAlign: "top",
|
|
viewportState: this.objectGraphViewportState,
|
|
onViewportStateChange: this.createObjectViewportStateHandler(objectPath),
|
|
sourcePanelContainer: shell3.bottomPane,
|
|
sourcePanelPlacement: "prepend",
|
|
...getMermaidSourceLabels(this.t),
|
|
...getGraphExportLabels(this.t),
|
|
onExportPng: () => this.exportCurrentDiagramAsPngWithNotice(),
|
|
onExportAndOpenPng: () => this.exportCurrentDiagramAsPngAndOpenWithNotice(),
|
|
...getDfdDetailLabels(this.t),
|
|
...getClassDetailLabels(this.t),
|
|
showMermaidRenderDebug: this.viewerPreferences.showMermaidRenderDebug
|
|
});
|
|
ensureGraphIdentityTitle(mermaidRoot, buildGraphIdentityTitle(state.model));
|
|
this.appendRendererSelection(mermaidRoot, state.rendererSelection);
|
|
this.appendViewerToolbarControls(mermaidRoot);
|
|
shell3.topPane.appendChild(mermaidRoot);
|
|
return;
|
|
}
|
|
const contextRoot = renderObjectContext(state.context, {
|
|
onOpenObject: state.onOpenObject ?? void 0,
|
|
app: this.app,
|
|
interactionSourcePath: objectPath,
|
|
viewportState: this.objectGraphViewportState,
|
|
onViewportStateChange: this.createObjectViewportStateHandler(objectPath),
|
|
labels: getObjectContextLabels(this.t)
|
|
});
|
|
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();
|
|
shell3.bottomPane.appendChild(relatedList);
|
|
}
|
|
ensureGraphIdentityTitle(contextRoot, buildGraphIdentityTitle(state.model));
|
|
this.appendRendererSelection(contextRoot, state.rendererSelection);
|
|
this.appendViewerToolbarControls(contextRoot);
|
|
shell3.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}`
|
|
});
|
|
}
|
|
}
|
|
renderDomainsState(state) {
|
|
const shell3 = this.createViewerSplitShell(`domains:${state.model.path}`, 0.62);
|
|
shell3.bottomPane.addClass("model-weave-summary-details");
|
|
this.activeScrollContainer = shell3.bottomPane;
|
|
this.renderReviewSummaryPanel(shell3.bottomPane, {
|
|
model: state.model,
|
|
warnings: state.warnings,
|
|
sourceLinks: state.model.sourceLinks,
|
|
weaveMapAvailable: false
|
|
});
|
|
this.renderDomainMermaidDiagram(
|
|
shell3.topPane,
|
|
state.model.domains,
|
|
shell3.bottomPane,
|
|
state.colorScheme,
|
|
buildGraphIdentityTitle(state.model),
|
|
state.model.path
|
|
);
|
|
this.renderDomainTree(shell3.bottomPane, buildDomainTree(state.model.domains));
|
|
renderDiagnostics(
|
|
shell3.bottomPane,
|
|
state.warnings,
|
|
state.onOpenDiagnostic ?? void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState,
|
|
this.getDiagnosticLanguage()
|
|
);
|
|
this.renderDomainRelationships(shell3.bottomPane, state.relationships);
|
|
this.renderSourceLinksSection(shell3.bottomPane, state.model.sourceLinks);
|
|
this.renderDomainDetails(shell3.bottomPane, state.model);
|
|
this.renderAppliedColorScheme(shell3.bottomPane, state.colorScheme, ["domain"]);
|
|
}
|
|
renderDomainDiagramState(state) {
|
|
const shell3 = this.createViewerSplitShell(
|
|
`domain-diagram:${state.resolved.diagram.path}`,
|
|
0.62
|
|
);
|
|
shell3.bottomPane.addClass("model-weave-summary-details");
|
|
this.activeScrollContainer = shell3.bottomPane;
|
|
this.renderReviewSummaryPanel(shell3.bottomPane, {
|
|
model: state.resolved.diagram,
|
|
warnings: state.warnings,
|
|
sourceLinks: state.resolved.diagram.sourceLinks,
|
|
weaveMapAvailable: false
|
|
});
|
|
this.renderDomainMermaidDiagram(
|
|
shell3.topPane,
|
|
state.resolved.domains,
|
|
shell3.bottomPane,
|
|
state.colorScheme,
|
|
buildGraphIdentityTitle(state.resolved.diagram),
|
|
state.resolved.diagram.path
|
|
);
|
|
this.renderDomainTree(shell3.bottomPane, buildDomainTree(state.resolved.domains));
|
|
renderDiagnostics(
|
|
shell3.bottomPane,
|
|
state.warnings,
|
|
state.onOpenDiagnostic ?? void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState,
|
|
this.getDiagnosticLanguage()
|
|
);
|
|
this.renderDomainDiagramSourceSummary(
|
|
shell3.bottomPane,
|
|
state.resolved.sourceSummaries
|
|
);
|
|
this.renderDomainDiagramConflictSummary(
|
|
shell3.bottomPane,
|
|
state.resolved.conflicts
|
|
);
|
|
this.renderDomainRelationships(shell3.bottomPane, state.relationships);
|
|
this.renderSourceLinksSection(shell3.bottomPane, state.resolved.diagram.sourceLinks);
|
|
this.renderDomainDiagramDetails(shell3.bottomPane, state.resolved);
|
|
this.renderAppliedColorScheme(shell3.bottomPane, state.colorScheme, ["domain"]);
|
|
}
|
|
renderDomainDiagramSourceSummary(container, sources) {
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
"domain-diagram:sources",
|
|
this.t("domainDiagram.preview.sources"),
|
|
true
|
|
);
|
|
if (sources.length === 0) {
|
|
section.createEl("p", {
|
|
text: this.t("domainDiagram.preview.noSources"),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
return;
|
|
}
|
|
const list = section.createEl("div", { cls: "model-weave-summary-list" });
|
|
for (const source of sources) {
|
|
const card = list.createDiv({
|
|
cls: "model-weave-preview-section model-weave-summary-metadata"
|
|
});
|
|
card.createEl("h3", {
|
|
text: source.resolvedPath ?? source.ref.ref,
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
this.renderDetailCard(card, [
|
|
{ label: this.t("domainDiagram.field.ref"), value: source.ref.ref },
|
|
{
|
|
label: this.t("domainDiagram.field.status"),
|
|
value: this.t(`domainDiagram.status.${source.status}`)
|
|
},
|
|
{
|
|
label: this.t("domains.preview.count"),
|
|
value: String(source.domainCount)
|
|
},
|
|
...source.ref.notes ? [{ label: this.t("domainDiagram.field.notes"), value: source.ref.notes }] : []
|
|
]);
|
|
}
|
|
}
|
|
renderDomainDiagramConflictSummary(container, conflicts) {
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
"domain-diagram:conflicts",
|
|
this.t("domainDiagram.preview.conflicts"),
|
|
true
|
|
);
|
|
if (conflicts.length === 0) {
|
|
section.createEl("p", {
|
|
text: this.t("domainDiagram.preview.noConflicts"),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
return;
|
|
}
|
|
const list = section.createEl("div", { cls: "model-weave-summary-list" });
|
|
for (const conflict of conflicts) {
|
|
const card = list.createDiv({
|
|
cls: "model-weave-preview-section model-weave-summary-metadata"
|
|
});
|
|
card.createEl("h3", {
|
|
text: `${this.t("domains.field.id")}: ${conflict.domainId}`,
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
this.renderDetailCard(card, [
|
|
{
|
|
label: this.t("domainDiagram.field.conflict"),
|
|
value: conflict.field
|
|
},
|
|
{
|
|
label: this.t("domainDiagram.field.earlier"),
|
|
value: this.formatDomainConflictValue(conflict.earlierSourcePath, conflict.earlierValue)
|
|
},
|
|
{
|
|
label: this.t("domainDiagram.field.later"),
|
|
value: this.formatDomainConflictValue(conflict.laterSourcePath, conflict.laterValue)
|
|
},
|
|
{
|
|
label: this.t("domainDiagram.field.effective"),
|
|
value: conflict.effectiveSourcePath
|
|
}
|
|
]);
|
|
}
|
|
}
|
|
renderAppProcessDomainPlacementSummary(container, resolved) {
|
|
if (resolved.process.domains.length === 0 && resolved.sourceSummaries.length === 0 && resolved.placements.length === 0) {
|
|
return;
|
|
}
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
"app-process:domain-placement",
|
|
this.t("appProcess.preview.domainSourcesPlacement"),
|
|
true
|
|
);
|
|
section.createEl("p", {
|
|
text: this.t("appProcess.preview.legacyLaneLayoutOnly"),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
if (resolved.process.domains.length > 0) {
|
|
const localHeading = section.createEl("h3", {
|
|
text: this.t("appProcess.preview.localDomains"),
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
localHeading.addClass("model-weave-summary-subtitle");
|
|
const localList = section.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const domain of resolved.process.domains) {
|
|
const label = [
|
|
domain.name || domain.id,
|
|
domain.kind ? `[${domain.kind}]` : "",
|
|
domain.parent ? `parent: ${domain.parent}` : ""
|
|
].filter(Boolean).join(" ");
|
|
localList.createEl("li", { text: `${domain.id}: ${label}` });
|
|
}
|
|
}
|
|
if (resolved.sourceSummaries.length > 0) {
|
|
const sourcesHeading = section.createEl("h3", {
|
|
text: this.t("domainDiagram.preview.sources"),
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
sourcesHeading.addClass("model-weave-summary-subtitle");
|
|
const sourceList = section.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const source of resolved.sourceSummaries) {
|
|
const label = [
|
|
source.resolvedPath ?? source.ref.ref,
|
|
`status: ${source.status}`,
|
|
`domains: ${source.domainCount}`
|
|
].join(" / ");
|
|
sourceList.createEl("li", { text: label });
|
|
}
|
|
}
|
|
if (resolved.placements.length > 0) {
|
|
const placementsHeading = section.createEl("h3", {
|
|
text: this.t("appProcess.preview.domainPlacement"),
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
placementsHeading.addClass("model-weave-summary-subtitle");
|
|
const placementList = section.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const placement of resolved.placements) {
|
|
const domainLabel = placement.domain ? [
|
|
placement.domain.name || placement.domain.id,
|
|
placement.domain.kind ? `[${placement.domain.kind}]` : ""
|
|
].filter(Boolean).join(" ") : "unresolved";
|
|
const stepLabel = placement.stepLabel ? `${placement.stepLabel} [${placement.stepId}]` : placement.stepId;
|
|
placementList.createEl("li", {
|
|
text: `${stepLabel}: ${placement.domainId} (${domainLabel})`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
renderDomainDiagramDetails(container, resolved) {
|
|
const details = this.createCollapsibleSection(
|
|
container,
|
|
"domain-diagram:details",
|
|
this.t("domains.preview.details"),
|
|
true
|
|
);
|
|
const diagram = resolved.diagram;
|
|
const overview = details.createDiv({
|
|
cls: "model-weave-preview-section model-weave-summary-metadata"
|
|
});
|
|
overview.createEl("h3", {
|
|
text: this.t("domains.preview.overview"),
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
this.renderDetailCard(overview, [
|
|
{ label: this.t("domains.field.type"), value: "domain_diagram" },
|
|
{ label: this.t("domains.field.id"), value: diagram.id || this.t("domains.value.none") },
|
|
{ label: this.t("domains.field.name"), value: diagram.name || this.t("domains.value.none") },
|
|
{
|
|
label: this.t("domainDiagram.preview.sourceCount"),
|
|
value: String(resolved.sourceSummaries.length)
|
|
},
|
|
{ label: this.t("domains.preview.count"), value: String(resolved.domains.length) },
|
|
{ label: this.t("domains.field.path"), value: diagram.path }
|
|
]);
|
|
this.renderDomainTable(details, resolved.domains);
|
|
}
|
|
formatDomainConflictValue(sourcePath, value) {
|
|
const displayValue = value?.trim() || this.t("domains.value.none");
|
|
return `${sourcePath}: ${displayValue}`;
|
|
}
|
|
renderDomainRelationships(container, relationships) {
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
"domains:relationships",
|
|
this.t("domains.preview.relationships"),
|
|
true
|
|
);
|
|
if (relationships.length === 0) {
|
|
section.createEl("p", {
|
|
text: this.t("domains.preview.empty"),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
return;
|
|
}
|
|
const list = section.createEl("div", { cls: "model-weave-summary-list" });
|
|
for (const relationship of relationships) {
|
|
const card = list.createDiv({
|
|
cls: "model-weave-preview-section model-weave-summary-metadata"
|
|
});
|
|
card.createEl("h3", {
|
|
text: `${this.t("domains.field.id")}: ${relationship.domain.id}`,
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
this.renderDetailCard(card, [
|
|
{
|
|
label: this.t("domains.field.name"),
|
|
value: relationship.domain.name || relationship.domain.id
|
|
},
|
|
{
|
|
label: this.t("domains.field.kind"),
|
|
value: relationship.domain.kind || this.t("domains.value.none")
|
|
},
|
|
{
|
|
label: this.t("domains.relationship.parent"),
|
|
value: relationship.parentId || this.t("domains.relationship.none")
|
|
},
|
|
{
|
|
label: this.t("domains.relationship.children"),
|
|
value: this.formatDomainRelationshipValues(relationship.childIds)
|
|
}
|
|
]);
|
|
if (relationship.domain.description) {
|
|
this.renderDomainRelationshipList(
|
|
card,
|
|
this.t("domains.field.description"),
|
|
[relationship.domain.description]
|
|
);
|
|
}
|
|
this.renderDomainRelationshipList(
|
|
card,
|
|
this.t("domains.relationship.definedIn"),
|
|
relationship.definedIn.map((entry) => entry.path)
|
|
);
|
|
this.renderDomainRelationshipList(
|
|
card,
|
|
this.t("domains.relationship.conflicts"),
|
|
relationship.conflicts.map(
|
|
(field) => this.t("domains.relationship.conflictField", { field })
|
|
)
|
|
);
|
|
this.renderDomainRelationshipList(
|
|
card,
|
|
this.t("domains.relationship.dfdLocalDomains"),
|
|
relationship.dfdLocalDomainReferences.map((entry) => entry.path)
|
|
);
|
|
this.renderDomainRelationshipList(
|
|
card,
|
|
this.t("domains.relationship.dfdObjects"),
|
|
relationship.dfdObjectReferences.map(
|
|
(entry) => entry.label ? `${entry.path} / ${entry.objectId}: ${entry.label}` : `${entry.path} / ${entry.objectId}`
|
|
)
|
|
);
|
|
}
|
|
}
|
|
renderDomainRelationshipList(container, label, values) {
|
|
const block = container.createDiv({ cls: "model-weave-summary-metadata" });
|
|
block.createEl("h4", {
|
|
text: label,
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
if (values.length === 0) {
|
|
block.createEl("p", {
|
|
text: this.t("domains.relationship.none"),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
return;
|
|
}
|
|
const list = block.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const value of values) {
|
|
list.createEl("li", { text: value });
|
|
}
|
|
}
|
|
formatDomainRelationshipValues(values) {
|
|
return values.length > 0 ? values.join(", ") : this.t("domains.relationship.none");
|
|
}
|
|
renderDomainDetails(container, model) {
|
|
const details = this.createCollapsibleSection(
|
|
container,
|
|
"domains:details",
|
|
this.t("domains.preview.details"),
|
|
true
|
|
);
|
|
const overview = details.createDiv({
|
|
cls: "model-weave-preview-section model-weave-summary-metadata"
|
|
});
|
|
overview.createEl("h3", {
|
|
text: this.t("domains.preview.overview"),
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
this.renderDetailCard(overview, [
|
|
{ label: this.t("domains.field.type"), value: "domains" },
|
|
{ label: this.t("domains.field.id"), value: model.id || this.t("domains.value.none") },
|
|
{ label: this.t("domains.field.name"), value: model.name || this.t("domains.value.none") },
|
|
{ label: this.t("domains.preview.count"), value: String(model.domains.length) },
|
|
{ label: this.t("domains.field.path"), value: model.path }
|
|
]);
|
|
this.renderDomainTable(details, model.domains);
|
|
}
|
|
renderColorSchemeState(state) {
|
|
this.contentEl.createEl("h2", {
|
|
text: state.model.title ?? state.model.name
|
|
});
|
|
renderDiagnostics(
|
|
this.contentEl,
|
|
state.warnings,
|
|
state.onOpenDiagnostic ?? void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState,
|
|
this.getDiagnosticLanguage()
|
|
);
|
|
const section = this.createCollapsibleSection(
|
|
this.contentEl,
|
|
"color-scheme:colors",
|
|
this.t("colorScheme.preview.colors"),
|
|
true
|
|
);
|
|
this.renderColorSchemeTable(section, state.model.colors);
|
|
}
|
|
renderColorSchemeTable(container, colors) {
|
|
const tableWrap = container.createDiv({ cls: "model-weave-table-wrap" });
|
|
const table = tableWrap.createEl("table", {
|
|
cls: "model-weave-summary-table model-weave-data-table"
|
|
});
|
|
const headerRow = table.createEl("thead").createEl("tr");
|
|
for (const key of [
|
|
"colorScheme.field.target",
|
|
"colorScheme.field.kind",
|
|
"colorScheme.field.fill",
|
|
"colorScheme.field.stroke",
|
|
"colorScheme.field.text",
|
|
"colorScheme.preview.swatch",
|
|
"colorScheme.field.notes"
|
|
]) {
|
|
headerRow.createEl("th", {
|
|
text: this.t(key),
|
|
cls: "model-weave-summary-th"
|
|
});
|
|
}
|
|
const tbody = table.createEl("tbody");
|
|
if (colors.length === 0) {
|
|
const row = tbody.createEl("tr");
|
|
row.createEl("td", {
|
|
text: this.t("colorScheme.preview.empty"),
|
|
attr: { colspan: "7" },
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
return;
|
|
}
|
|
for (const color of colors) {
|
|
this.renderColorSchemeTableRow(tbody, color);
|
|
}
|
|
}
|
|
renderAppliedColorScheme(container, colorScheme, targets) {
|
|
if (!colorScheme) {
|
|
return;
|
|
}
|
|
const normalizedTargets = targets.map((target) => target.trim()).filter(Boolean);
|
|
if (normalizedTargets.length === 0) {
|
|
return;
|
|
}
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
`color-scheme:applied:${normalizedTargets.join(":")}`,
|
|
this.t("colorScheme.preview.applied"),
|
|
false
|
|
);
|
|
renderAppliedColorSchemeSectionContent(
|
|
section,
|
|
colorScheme,
|
|
getAppliedColorSchemeRowsForTargets(colorScheme, normalizedTargets),
|
|
normalizedTargets,
|
|
this.t
|
|
);
|
|
}
|
|
renderColorSchemeTableRow(tbody, color) {
|
|
const row = tbody.createEl("tr");
|
|
for (const value of [
|
|
color.target ?? this.t("domains.value.none"),
|
|
color.kind,
|
|
color.fill ?? this.t("domains.value.none"),
|
|
color.stroke ?? this.t("domains.value.none"),
|
|
color.text ?? this.t("domains.value.none")
|
|
]) {
|
|
row.createEl("td", { text: value });
|
|
}
|
|
const swatchCell = row.createEl("td");
|
|
const swatch = swatchCell.createSpan({
|
|
cls: "model-weave-color-swatch"
|
|
});
|
|
if (color.fill) {
|
|
swatch.style.backgroundColor = color.fill;
|
|
}
|
|
if (color.stroke) {
|
|
swatch.style.borderColor = color.stroke;
|
|
}
|
|
if (color.text) {
|
|
swatch.style.color = color.text;
|
|
}
|
|
swatch.textContent = "Aa";
|
|
row.createEl("td", { text: color.notes ?? "" });
|
|
}
|
|
renderDomainTable(container, domains) {
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
"domains:list",
|
|
this.t("domains.preview.list"),
|
|
true
|
|
);
|
|
section.addClass("model-weave-table-wrap");
|
|
const table = section.createEl("table", {
|
|
cls: "model-weave-summary-table model-weave-data-table"
|
|
});
|
|
const headerRow = table.createEl("thead").createEl("tr");
|
|
for (const key of [
|
|
"domains.field.id",
|
|
"domains.field.name",
|
|
"domains.field.kind",
|
|
"domains.field.parent",
|
|
"domains.field.description"
|
|
]) {
|
|
headerRow.createEl("th", {
|
|
text: this.t(key),
|
|
cls: "model-weave-summary-th"
|
|
});
|
|
}
|
|
const tbody = table.createEl("tbody");
|
|
if (domains.length === 0) {
|
|
const row = tbody.createEl("tr");
|
|
row.createEl("td", {
|
|
text: this.t("domains.preview.empty"),
|
|
cls: "model-weave-summary-td model-weave-summary-empty-cell",
|
|
attr: { colspan: "5" }
|
|
});
|
|
return;
|
|
}
|
|
for (const domain of domains) {
|
|
const row = tbody.createEl("tr");
|
|
for (const value of [
|
|
domain.id,
|
|
domain.name || domain.id,
|
|
domain.kind,
|
|
domain.parent,
|
|
domain.description
|
|
]) {
|
|
row.createEl("td", {
|
|
text: value || this.t("domains.value.none"),
|
|
cls: "model-weave-summary-td"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
renderDomainTree(container, roots) {
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
"domains:tree",
|
|
this.t("domains.preview.tree"),
|
|
true
|
|
);
|
|
if (roots.length === 0) {
|
|
section.createEl("p", {
|
|
text: this.t("domains.preview.empty"),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
return;
|
|
}
|
|
const list = section.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const root of roots) {
|
|
this.renderDomainTreeNode(list, root, /* @__PURE__ */ new Set());
|
|
}
|
|
}
|
|
renderDomainTreeNode(list, node, visited) {
|
|
const item = list.createEl("li", {
|
|
text: this.getDomainLabel(node.domain)
|
|
});
|
|
if (visited.has(node.domain.id) || node.children.length === 0) {
|
|
return;
|
|
}
|
|
const nextVisited = new Set(visited);
|
|
nextVisited.add(node.domain.id);
|
|
const childList = item.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const child of node.children) {
|
|
this.renderDomainTreeNode(childList, child, nextVisited);
|
|
}
|
|
}
|
|
getDomainLabel(domain) {
|
|
const displayName = domain.name || domain.id;
|
|
return domain.kind ? `${displayName} (${domain.kind})` : displayName;
|
|
}
|
|
getDiagnosticLanguage() {
|
|
return this.viewerPreferences.uiLanguage === "auto" ? void 0 : this.viewerPreferences.uiLanguage;
|
|
}
|
|
renderDomainMermaidDiagram(container, domains, sourcePanelContainer, colorScheme, graphTitle, interactionSourcePath) {
|
|
if (domains.length === 0) {
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
"domains:diagram",
|
|
this.t("domains.preview.diagram"),
|
|
true
|
|
);
|
|
section.createEl("p", {
|
|
text: this.t("domains.preview.diagramEmpty"),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
return;
|
|
}
|
|
const diagramRoot = renderDomainsMermaidDiagram(domains, {
|
|
title: graphTitle ?? this.getDomainDiagramModeLabel(this.domainsDiagramMode),
|
|
mode: this.domainsDiagramMode,
|
|
renderFailedMessage: this.t("domains.preview.diagramRenderFailed"),
|
|
fitVerticalAlign: "top",
|
|
sourcePanelContainer,
|
|
sourcePanelPlacement: sourcePanelContainer ? "prepend" : void 0,
|
|
...getMermaidSourceLabels(this.t),
|
|
...getGraphExportLabels(this.t),
|
|
onExportPng: () => this.exportCurrentDiagramAsPngWithNotice(),
|
|
onExportAndOpenPng: () => this.exportCurrentDiagramAsPngAndOpenWithNotice(),
|
|
viewportState: this.domainsMermaidViewportState,
|
|
showMermaidRenderDebug: this.viewerPreferences.showMermaidRenderDebug,
|
|
colorScheme,
|
|
app: this.app,
|
|
interactionSourcePath
|
|
});
|
|
ensureGraphIdentityTitle(diagramRoot, graphTitle ?? this.getDomainDiagramModeLabel(this.domainsDiagramMode));
|
|
this.appendDomainDiagramModeSelector(diagramRoot);
|
|
this.appendViewerToolbarControls(diagramRoot);
|
|
container.appendChild(diagramRoot);
|
|
}
|
|
appendDomainDiagramModeSelector(container) {
|
|
const toolbar = container.querySelector(".mdspec-zoom-toolbar");
|
|
if (!toolbar) {
|
|
return;
|
|
}
|
|
toolbar.addClass("model-weave-render-mode-toolbar-host");
|
|
toolbar.querySelector(".model-weave-domain-mode-select-group")?.remove();
|
|
const doc = container.ownerDocument;
|
|
const wrapper = doc.createElement("div");
|
|
wrapper.className = "model-weave-domain-mode-select-group model-weave-render-mode-row";
|
|
const label = doc.createElement("span");
|
|
label.addClass("model-weave-render-mode-label");
|
|
label.textContent = this.t("domains.preview.viewMode");
|
|
wrapper.appendChild(label);
|
|
const select = doc.createElement("select");
|
|
select.addClass("model-weave-domain-mode-select");
|
|
for (const mode of ["mindmap", "area", "tree"]) {
|
|
const option = doc.createElement("option");
|
|
option.value = mode;
|
|
option.textContent = this.getDomainDiagramModeLabel(mode);
|
|
option.selected = this.domainsDiagramMode === mode;
|
|
select.appendChild(option);
|
|
}
|
|
select.addEventListener("change", () => {
|
|
const nextMode = select.value;
|
|
if (this.domainsDiagramMode === nextMode) {
|
|
return;
|
|
}
|
|
const shouldRestoreViewOnly = this.viewOnlyEnabled && this.viewOnlyTarget?.classList.contains("model-weave-domains-mermaid");
|
|
this.domainsDiagramMode = nextMode;
|
|
if (this.domainsMermaidViewportState.viewMode === "fit") {
|
|
resetGraphViewportState(this.domainsMermaidViewportState);
|
|
}
|
|
this.renderCurrentState();
|
|
this.restoreCurrentScrollPosition();
|
|
if (shouldRestoreViewOnly) {
|
|
const view = this.contentEl.ownerDocument.defaultView;
|
|
view?.requestAnimationFrame(() => {
|
|
view.requestAnimationFrame(() => {
|
|
const nextTarget = this.contentEl.querySelector(
|
|
".model-weave-domains-mermaid"
|
|
);
|
|
if (nextTarget) {
|
|
this.setViewOnlyMode(true, { target: nextTarget });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
wrapper.appendChild(select);
|
|
const rightGroup = toolbar.querySelector(".model-weave-zoom-toolbar-right") ?? toolbar;
|
|
rightGroup.appendChild(wrapper);
|
|
}
|
|
getDomainDiagramModeLabel(mode) {
|
|
if (mode === "mindmap") {
|
|
return this.t("domains.preview.mindmap");
|
|
}
|
|
if (mode === "tree") {
|
|
return this.t("domains.preview.treeMode");
|
|
}
|
|
return this.t("domains.preview.area");
|
|
}
|
|
renderSummaryState(state) {
|
|
const hasScreenPreview = (state.layoutBlocks?.length ?? 0) > 0;
|
|
const hasBusinessFlow = (state.businessFlow?.steps.length ?? 0) > 0;
|
|
if (hasScreenPreview || hasBusinessFlow) {
|
|
const shell3 = this.createViewerSplitShell(`summary:${state.filePath}`, 0.48);
|
|
this.activeScrollContainer = shell3.bottomPane;
|
|
if (hasScreenPreview) {
|
|
const screenRoot = createScreenPreviewDiagram(
|
|
buildScreenPreviewData(state, this.t),
|
|
{
|
|
app: this.app,
|
|
viewportState: this.screenPreviewViewportState,
|
|
onViewportStateChange: this.createScreenPreviewViewportStateHandler(
|
|
state.filePath
|
|
),
|
|
onNavigateToLocation: state.onNavigateToLocation,
|
|
onOpenLinkedFile: state.onOpenLinkedFile
|
|
}
|
|
);
|
|
ensureGraphIdentityTitle(screenRoot, buildSummaryGraphTitle(state));
|
|
this.appendViewerToolbarControls(screenRoot);
|
|
shell3.topPane.appendChild(screenRoot);
|
|
} else if (state.businessFlow) {
|
|
if (this.screenPreviewViewportState.viewMode === "fit") {
|
|
resetGraphViewportState(this.screenPreviewViewportState);
|
|
}
|
|
const businessFlowRoot = renderAppProcessBusinessFlow(state.businessFlow, {
|
|
sourcePanelContainer: shell3.bottomPane,
|
|
sourcePanelPlacement: "prepend",
|
|
...getMermaidSourceLabels(this.t),
|
|
...getGraphExportLabels(this.t),
|
|
onExportPng: () => this.exportCurrentDiagramAsPngWithNotice(),
|
|
onExportAndOpenPng: () => this.exportCurrentDiagramAsPngAndOpenWithNotice(),
|
|
showMermaidRenderDebug: this.viewerPreferences.showMermaidRenderDebug,
|
|
colorScheme: state.colorScheme,
|
|
app: this.app,
|
|
interactionSourcePath: state.filePath,
|
|
viewportState: this.screenPreviewViewportState,
|
|
onViewportStateChange: this.createScreenPreviewViewportStateHandler(
|
|
state.filePath
|
|
)
|
|
});
|
|
ensureGraphIdentityTitle(businessFlowRoot, buildSummaryGraphTitle(state));
|
|
this.appendViewerToolbarControls(businessFlowRoot);
|
|
shell3.topPane.appendChild(businessFlowRoot);
|
|
this.renderSummaryDetails(shell3.bottomPane, state, {
|
|
suppressBusinessFlowChart: true
|
|
});
|
|
return;
|
|
}
|
|
this.renderSummaryDetails(shell3.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 });
|
|
this.renderReviewSummaryPanel(container, {
|
|
model: {
|
|
title: state.title,
|
|
fileType: state.businessFlow ? "app_process" : state.summaryKind,
|
|
id: findSummaryMetadataValue(state, ["id", "model id", "model_id"])
|
|
},
|
|
warnings: state.warnings,
|
|
impactSummary: state.impactSummary,
|
|
sourceLinks: state.sourceLinks,
|
|
weaveMapAvailable: Boolean(state.weaveMapMermaidSource)
|
|
});
|
|
if (state.message) {
|
|
container.createEl("p", {
|
|
text: state.message,
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
}
|
|
renderDiagnostics(
|
|
container,
|
|
state.warnings,
|
|
void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState,
|
|
this.getDiagnosticLanguage()
|
|
);
|
|
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);
|
|
}
|
|
this.renderImpactSummarySection(
|
|
container,
|
|
state.impactSummary,
|
|
state.onCopyImpactSummary,
|
|
state.onOpenImpactModel,
|
|
state.weaveMapMermaidSource,
|
|
state.colorScheme
|
|
);
|
|
this.renderSourceLinksSection(container, state.sourceLinks);
|
|
if (state.appProcessDomainPlacement) {
|
|
this.renderAppProcessDomainPlacementSummary(
|
|
container,
|
|
state.appProcessDomainPlacement
|
|
);
|
|
}
|
|
if (state.counts.length > 0) {
|
|
const counts = container.createDiv({
|
|
cls: "model-weave-preview-section model-weave-summary-counts"
|
|
});
|
|
counts.createEl("h3", {
|
|
text: this.t("summary.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: `${this.localizeSummaryCountLabel(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);
|
|
}
|
|
const businessFlowRoot = renderAppProcessBusinessFlow(state.businessFlow, {
|
|
viewportState: this.screenPreviewViewportState,
|
|
colorScheme: state.colorScheme,
|
|
app: this.app,
|
|
interactionSourcePath: state.filePath,
|
|
...getGraphExportLabels(this.t),
|
|
onExportPng: () => this.exportCurrentDiagramAsPngWithNotice(),
|
|
onExportAndOpenPng: () => this.exportCurrentDiagramAsPngAndOpenWithNotice(),
|
|
onViewportStateChange: this.createScreenPreviewViewportStateHandler(
|
|
state.filePath
|
|
)
|
|
});
|
|
ensureGraphIdentityTitle(businessFlowRoot, buildSummaryGraphTitle(state));
|
|
this.appendViewerToolbarControls(businessFlowRoot);
|
|
section.appendChild(businessFlowRoot);
|
|
}
|
|
if (state.sections.length > 0) {
|
|
const sections = this.createCollapsibleSection(
|
|
container,
|
|
"detectedSections",
|
|
this.t("summary.detectedSections"),
|
|
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}`,
|
|
this.localizeSummarySectionTitle(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);
|
|
}
|
|
}
|
|
if (state.businessFlow && state.businessFlow.steps.length > 0) {
|
|
this.renderAppliedColorScheme(
|
|
container,
|
|
state.colorScheme,
|
|
this.getImpactColorSchemeTargets(
|
|
getAppProcessBusinessFlowColorSchemeTargets(state.businessFlow),
|
|
state.impactSummary
|
|
)
|
|
);
|
|
}
|
|
}
|
|
renderScreenSummaryDetails(container, state) {
|
|
container.createEl("h2", { text: state.title });
|
|
this.renderReviewSummaryPanel(container, {
|
|
model: {
|
|
title: state.title,
|
|
fileType: "screen",
|
|
id: findSummaryMetadataValue(state, ["id", "model id", "model_id"])
|
|
},
|
|
warnings: state.warnings,
|
|
impactSummary: state.impactSummary,
|
|
sourceLinks: state.sourceLinks,
|
|
weaveMapAvailable: Boolean(state.weaveMapMermaidSource)
|
|
});
|
|
renderDiagnostics(
|
|
container,
|
|
state.warnings,
|
|
void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState,
|
|
this.getDiagnosticLanguage()
|
|
);
|
|
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);
|
|
}
|
|
this.renderImpactSummarySection(
|
|
container,
|
|
state.impactSummary,
|
|
state.onCopyImpactSummary,
|
|
state.onOpenImpactModel,
|
|
state.weaveMapMermaidSource,
|
|
state.colorScheme
|
|
);
|
|
this.renderSourceLinksSection(container, state.sourceLinks);
|
|
if (state.counts.length > 0) {
|
|
const counts = container.createDiv({
|
|
cls: "model-weave-preview-section model-weave-screen-preview-section-counts"
|
|
});
|
|
counts.createEl("h3", {
|
|
text: this.t("summary.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: `${this.localizeSummaryCountLabel(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",
|
|
this.t("summary.detectedSections"),
|
|
false
|
|
);
|
|
const list = sections.createEl("ul", { cls: "model-weave-summary-list" });
|
|
for (const section of state.sections) {
|
|
const item = list.createEl("li", {
|
|
text: this.localizeDetectedSectionLabel(section.label)
|
|
});
|
|
this.bindLocationNavigation(item, state.onNavigateToLocation, section);
|
|
}
|
|
}
|
|
}
|
|
renderSummaryTable(container, state, table, defaultOpen) {
|
|
if (!table) {
|
|
return;
|
|
}
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
`summary:${table.title}`,
|
|
this.localizeSummarySectionTitle(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: this.t("summary.noRows"),
|
|
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"
|
|
});
|
|
}
|
|
}
|
|
localizeSummaryCountLabel(label) {
|
|
const keyByLabel = {
|
|
Triggers: "summary.count.triggers",
|
|
Inputs: "summary.count.inputs",
|
|
Outputs: "summary.count.outputs",
|
|
Transitions: "summary.count.transitions",
|
|
Steps: "summary.count.steps",
|
|
Flows: "summary.count.flows",
|
|
Domains: "summary.count.domains",
|
|
"Domain Sources": "summary.count.domainSources",
|
|
Layouts: "summary.count.layouts",
|
|
Fields: "summary.count.fields",
|
|
Actions: "summary.count.actions",
|
|
Messages: "summary.count.messages",
|
|
"Local processes": "summary.count.localProcesses",
|
|
"Local Processes": "summary.count.localProcesses",
|
|
"Invoked processes": "summary.count.invokedProcesses",
|
|
"Invoked Processes": "summary.count.invokedProcesses",
|
|
"Outgoing screens": "summary.count.outgoingScreens"
|
|
};
|
|
const key = keyByLabel[label];
|
|
return key ? this.t(key) : label;
|
|
}
|
|
localizeSummarySectionTitle(title) {
|
|
const keyByTitle = {
|
|
Summary: "summary.section.summary",
|
|
"Domain Sources Summary": "summary.section.domainSourcesSummary",
|
|
"Domains Summary": "summary.section.domainsSummary",
|
|
"Triggers Summary": "summary.section.triggersSummary",
|
|
"Inputs Summary": "summary.section.inputsSummary",
|
|
"Outputs Summary": "summary.section.outputsSummary",
|
|
"Steps Summary": "summary.section.stepsSummary",
|
|
"Flows Summary": "summary.section.flowsSummary",
|
|
"Transitions Summary": "summary.section.transitionsSummary",
|
|
"Structure / Layout": "summary.section.structureLayout",
|
|
"UI Elements / Fields": "summary.section.uiElementsFields",
|
|
"Behavior / Actions": "summary.section.behaviorActions",
|
|
"Local processes": "summary.section.localProcesses",
|
|
"Local Processes": "summary.section.localProcesses",
|
|
"Invoked processes": "summary.section.invokedProcesses",
|
|
"Invoked Processes": "summary.section.invokedProcesses",
|
|
"Transitions / Outgoing screens": "summary.section.transitionsOutgoingScreens",
|
|
"Transitions / Outgoing Screens": "summary.section.transitionsOutgoingScreens",
|
|
"Transitions (legacy)": "summary.section.transitionsLegacy",
|
|
Layout: "summary.section.layout",
|
|
Fields: "summary.section.fields",
|
|
Actions: "summary.section.actions",
|
|
Messages: "summary.section.messages",
|
|
Notes: "summary.section.notes"
|
|
};
|
|
const key = keyByTitle[title];
|
|
return key ? this.t(key) : title;
|
|
}
|
|
localizeDetectedSectionLabel(label) {
|
|
const countMatch = label.match(/^(.+):\s+(\d+)\s+(rows|headings)$/);
|
|
if (countMatch) {
|
|
const [, rawName, rawCount, rawUnit] = countMatch;
|
|
const localizedName = this.localizeSummarySectionTitle(rawName);
|
|
const unitKey = rawUnit === "headings" ? "summary.unit.headings" : "summary.unit.rows";
|
|
return `${localizedName}: ${this.t(unitKey, { count: Number(rawCount) })}`;
|
|
}
|
|
const legacyMatch = label.match(/^Transitions \(legacy\):\s+(\d+)\s+rows$/);
|
|
if (legacyMatch) {
|
|
return `${this.localizeSummarySectionTitle("Transitions (legacy)")}: ${this.t("summary.unit.rows", { count: Number(legacyMatch[1]) })}`;
|
|
}
|
|
return this.localizeSummarySectionTitle(label);
|
|
}
|
|
renderSummaryNavigationList(container, state, title, items, defaultOpen) {
|
|
if (items.length === 0) {
|
|
return;
|
|
}
|
|
const section = this.createCollapsibleSection(
|
|
container,
|
|
`navigation:${title}`,
|
|
this.localizeSummarySectionTitle(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 ?? {});
|
|
}
|
|
}
|
|
renderReviewSummaryPanel(container, options) {
|
|
const errors = options.warnings.filter((warning) => warning.severity === "error").length;
|
|
const warnings = options.warnings.filter((warning) => warning.severity === "warning").length;
|
|
const notes = options.warnings.filter((warning) => warning.severity === "info").length;
|
|
const modelName = getModelDisplayName(options.model);
|
|
const modelId = getModelId3(options.model);
|
|
const modelType = getModelType(options.model);
|
|
const sourceLinkCount = options.impactSummary?.relatedSourceLinks.length ?? options.sourceLinks?.length ?? 0;
|
|
const section = container.createDiv({
|
|
cls: "model-weave-preview-section model-weave-review-summary"
|
|
});
|
|
section.createEl("h3", {
|
|
text: this.t("review.summary.title"),
|
|
cls: "model-weave-preview-section-title"
|
|
});
|
|
const chips = section.createDiv({ cls: "model-weave-review-summary-chips" });
|
|
const addChip = (label, value, modifier) => {
|
|
const chip = chips.createDiv({ cls: "model-weave-review-summary-chip" });
|
|
if (modifier) {
|
|
chip.addClass(`model-weave-review-summary-chip-${modifier}`);
|
|
}
|
|
chip.createSpan({ text: label, cls: "model-weave-review-summary-chip-label" });
|
|
chip.createSpan({ text: String(value), cls: "model-weave-review-summary-chip-value" });
|
|
};
|
|
if (modelName) {
|
|
addChip(this.t("review.summary.model"), modelName);
|
|
}
|
|
if (modelType) {
|
|
addChip(this.t("review.summary.modelType"), modelType);
|
|
}
|
|
if (modelId && modelId !== modelName) {
|
|
addChip(this.t("review.summary.modelId"), modelId);
|
|
}
|
|
addChip(this.t("review.summary.errors"), errors, errors > 0 ? "error" : void 0);
|
|
addChip(this.t("review.summary.warnings"), warnings, warnings > 0 ? "warning" : void 0);
|
|
addChip(this.t("review.summary.notes"), notes);
|
|
if (options.impactSummary) {
|
|
addChip(this.t("review.summary.incoming"), options.impactSummary.inboundRelationships.length);
|
|
addChip(this.t("review.summary.outgoing"), options.impactSummary.outboundRelationships.length);
|
|
addChip(
|
|
this.t("review.summary.unresolved"),
|
|
options.impactSummary.unresolvedOutbound.length,
|
|
options.impactSummary.unresolvedOutbound.length > 0 ? "warning" : void 0
|
|
);
|
|
}
|
|
addChip(this.t("review.summary.sourceLinks"), sourceLinkCount);
|
|
addChip(
|
|
this.t("review.summary.weaveMap"),
|
|
options.weaveMapAvailable ? this.t("review.summary.available") : this.t("review.summary.notAvailable"),
|
|
options.weaveMapAvailable ? "available" : void 0
|
|
);
|
|
}
|
|
renderSourceLinksSection(container, sourceLinks) {
|
|
const sourceLinksSection = renderSourceLinks(
|
|
sourceLinks,
|
|
this.viewerPreferences.localSourceRoot,
|
|
this.viewerPreferences.uiLanguage
|
|
);
|
|
if (sourceLinksSection) {
|
|
container.appendChild(sourceLinksSection);
|
|
}
|
|
}
|
|
renderImpactSummarySection(container, summary, onCopyImpactSummary, onOpenImpactModel, weaveMapMermaidSource, colorScheme) {
|
|
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.renderImpactOverviewCards(section, summary);
|
|
this.renderWeaveMapBlock(section, summary, weaveMapMermaidSource, colorScheme);
|
|
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()
|
|
);
|
|
}
|
|
renderImpactOverviewCards(container, summary) {
|
|
const cards = container.createDiv({ cls: "model-weave-impact-overview-cards" });
|
|
const addCard = (label, value, description, modifier) => {
|
|
const card = cards.createDiv({ cls: "model-weave-impact-overview-card" });
|
|
if (modifier) {
|
|
card.addClass(`model-weave-impact-overview-card-${modifier}`);
|
|
}
|
|
card.createDiv({ text: label, cls: "model-weave-impact-overview-card-label" });
|
|
card.createDiv({ text: String(value), cls: "model-weave-impact-overview-card-value" });
|
|
card.createDiv({ text: description, cls: "model-weave-impact-overview-card-description" });
|
|
};
|
|
for (const group of this.getImpactRelationshipGroupCounts(summary.inboundRelationships, "incoming")) {
|
|
addCard(group.label, group.count, this.t("relationship.referencedByThisObject"));
|
|
}
|
|
for (const group of this.getImpactRelationshipGroupCounts(summary.outboundRelationships, "outgoing")) {
|
|
addCard(group.label, group.count, this.t("relationship.referencesFromThisObject"));
|
|
}
|
|
if (summary.unresolvedOutbound.length > 0) {
|
|
addCard(
|
|
this.t("relationship.overview.unresolved"),
|
|
summary.unresolvedOutbound.length,
|
|
this.t("relationship.unresolvedReferences"),
|
|
"warning"
|
|
);
|
|
}
|
|
if (summary.relatedSourceLinks.length > 0) {
|
|
addCard(
|
|
this.t("relationship.overview.sourceLinks"),
|
|
summary.relatedSourceLinks.length,
|
|
this.t("relationship.relatedSourceLinks")
|
|
);
|
|
}
|
|
if (summary.modelType === "codeset" && summary.valueUsages.length > 0) {
|
|
addCard(
|
|
this.t("relationship.overview.valueUsage"),
|
|
summary.valueUsages.length,
|
|
this.t("relationship.valueUsage")
|
|
);
|
|
}
|
|
}
|
|
renderWeaveMapBlock(container, summary, initialMermaidSource, colorScheme) {
|
|
let sourceLinkMode = "compact";
|
|
let weaveMapModel = this.buildWeaveMapModel(summary, sourceLinkMode);
|
|
let source = (weaveMapModel ? buildWeaveMapMermaidSource(weaveMapModel, { colorScheme }) : initialMermaidSource)?.trim();
|
|
if (!source) {
|
|
return;
|
|
}
|
|
let interactionTargets = weaveMapModel ? this.buildWeaveMapInteractionTargets(weaveMapModel, summary) : [];
|
|
const details = container.createEl("details", {
|
|
cls: "model-weave-preview-section model-weave-impact-weave-map"
|
|
});
|
|
details.open = this.getCollapsibleOpenState("impactWeaveMap", false);
|
|
details.addEventListener("toggle", () => {
|
|
this.setCollapsibleOpenState("impactWeaveMap", details.open);
|
|
if (details.open) {
|
|
renderWeaveMap();
|
|
}
|
|
});
|
|
details.createEl("summary", {
|
|
text: `${this.t("relationship.weaveMap.title")} \u2014 ${summary.modelId || summary.modelLabel}`,
|
|
cls: "model-weave-summary-heading model-weave-preview-section-title"
|
|
});
|
|
const section = details.createDiv({ cls: "model-weave-impact-weave-map-content" });
|
|
section.createEl("p", {
|
|
text: this.t("relationship.weaveMap.description"),
|
|
cls: "model-weave-muted"
|
|
});
|
|
const modeSelector = section.createDiv({
|
|
cls: "model-weave-render-mode-toolbar-host model-weave-impact-weave-map-mode"
|
|
});
|
|
modeSelector.createEl("span", {
|
|
text: this.t("relationship.weaveMap.viewMode"),
|
|
cls: "model-weave-summary-muted"
|
|
});
|
|
const modeButtons = /* @__PURE__ */ new Map();
|
|
const updateModeButtons = () => {
|
|
for (const [mode, button] of modeButtons) {
|
|
button.setAttribute("aria-pressed", String(sourceLinkMode === mode));
|
|
button.toggleClass("is-active", sourceLinkMode === mode);
|
|
}
|
|
};
|
|
const renderCurrentMode = () => {
|
|
weaveMapModel = this.buildWeaveMapModel(summary, sourceLinkMode);
|
|
source = weaveMapModel ? buildWeaveMapMermaidSource(weaveMapModel, { colorScheme }).trim() : void 0;
|
|
interactionTargets = weaveMapModel ? this.buildWeaveMapInteractionTargets(weaveMapModel, summary) : [];
|
|
if (!source) {
|
|
renderContainer.empty();
|
|
sourcePanelContainer.empty();
|
|
return;
|
|
}
|
|
rendered = false;
|
|
rendering = false;
|
|
renderContainer.empty();
|
|
sourcePanelContainer.empty();
|
|
renderWeaveMap();
|
|
};
|
|
for (const mode of ["compact", "full"]) {
|
|
const button = modeSelector.createEl("button", {
|
|
text: mode === "compact" ? this.t("relationship.weaveMap.compact") : this.t("relationship.weaveMap.full"),
|
|
cls: "model-weave-secondary-button"
|
|
});
|
|
button.type = "button";
|
|
modeButtons.set(mode, button);
|
|
button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
if (sourceLinkMode === mode) {
|
|
return;
|
|
}
|
|
sourceLinkMode = mode;
|
|
updateModeButtons();
|
|
renderCurrentMode();
|
|
});
|
|
}
|
|
updateModeButtons();
|
|
const renderContainer = section.createDiv({
|
|
cls: "model-weave-impact-weave-map-body"
|
|
});
|
|
renderContainer.setCssStyles({
|
|
height: "420px",
|
|
minHeight: "360px",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
position: "relative",
|
|
overflow: "hidden"
|
|
});
|
|
const sourcePanelContainer = section.createDiv({
|
|
cls: "model-weave-impact-weave-map-source"
|
|
});
|
|
let rendered = false;
|
|
let rendering = false;
|
|
const renderWeaveMap = () => {
|
|
const currentSource = source;
|
|
if (!currentSource || rendered || rendering) {
|
|
return;
|
|
}
|
|
rendering = true;
|
|
renderContainer.empty();
|
|
const shell3 = createMermaidShell({
|
|
className: "model-weave-impact-weave-map-render",
|
|
title: buildWeaveMapGraphTitle(this.t, summary),
|
|
...getGraphExportLabels(this.t),
|
|
onExportPng: () => this.exportWeaveMapAsPng(renderContainer, summary.modelPath),
|
|
onExportAndOpenPng: () => this.exportWeaveMapAsPngAndOpen(renderContainer, summary.modelPath)
|
|
});
|
|
shell3.root.setCssStyles({
|
|
flex: "1 1 auto",
|
|
minHeight: "0",
|
|
width: "100%",
|
|
height: "100%"
|
|
});
|
|
shell3.canvas.setCssStyles({
|
|
minHeight: "0"
|
|
});
|
|
this.appendViewerToolbarControls(shell3.root, shell3.root);
|
|
renderContainer.appendChild(shell3.root);
|
|
sourcePanelContainer.empty();
|
|
void this.renderWeaveMapMermaid(shell3, currentSource, sourcePanelContainer, interactionTargets).then(
|
|
() => {
|
|
rendered = true;
|
|
rendering = false;
|
|
},
|
|
() => {
|
|
rendering = false;
|
|
}
|
|
);
|
|
};
|
|
if (details.open) {
|
|
renderWeaveMap();
|
|
}
|
|
}
|
|
buildWeaveMapModel(summary, sourceLinkMode) {
|
|
try {
|
|
return buildWeaveMapModel(summary, { sourceLinkMode });
|
|
} catch {
|
|
return void 0;
|
|
}
|
|
}
|
|
buildWeaveMapInteractionTargets(model, summary) {
|
|
const mermaidIds = createWeaveMapNodeMermaidIds(model.nodes);
|
|
return model.nodes.filter((node) => this.isResolvedWeaveMapModelNode(node)).map((node) => ({
|
|
nodeId: node.id,
|
|
mermaidId: mermaidIds.get(node.id) ?? node.id,
|
|
label: node.label,
|
|
filePath: node.path ?? "",
|
|
linktext: node.path || node.modelId || node.label,
|
|
sourcePath: summary.modelPath,
|
|
modelId: node.modelId,
|
|
modelType: node.modelType,
|
|
status: "resolved"
|
|
})).filter((target) => target.filePath.length > 0 && target.linktext.length > 0);
|
|
}
|
|
isResolvedWeaveMapModelNode(node) {
|
|
return (node.status === "focus" || node.status === "ok") && Boolean(node.path);
|
|
}
|
|
async renderWeaveMapMermaid(shell3, source, container, interactionTargets) {
|
|
try {
|
|
await this.waitForWeaveMapContainerReady(shell3.root);
|
|
await renderMermaidSourceIntoShell(shell3, {
|
|
source,
|
|
renderIdPrefix: "model_weave_impact_weave_map",
|
|
fitVerticalAlign: "top",
|
|
sourcePanelContainer: container,
|
|
sourcePanelPlacement: "append",
|
|
...getMermaidSourceLabels(this.t),
|
|
showRenderDebug: this.viewerPreferences.showMermaidRenderDebug
|
|
});
|
|
await this.waitForWeaveMapSvgReady(shell3.surface);
|
|
attachMermaidNodeInteractions({
|
|
app: this.app,
|
|
rootEl: shell3.surface,
|
|
targets: interactionTargets,
|
|
source: "model-weave",
|
|
nodeClassName: "model-weave-weave-map-interactive-node",
|
|
dragThreshold: 6,
|
|
hoverParent: (nodeEl, fallback) => nodeEl.closest(
|
|
".model-weave-view-only-stage, .model-weave-impact-weave-map-render, .model-weave-impact-weave-map-body"
|
|
) ?? fallback,
|
|
formatTitle: (target) => target.modelId ? `${target.label ?? target.linktext} (${target.modelType ?? "model"} / ${target.modelId})` : target.label ?? target.linktext
|
|
});
|
|
await this.waitForNextAnimationFrame(shell3.root);
|
|
await this.waitForNextAnimationFrame(shell3.root);
|
|
shell3.toolbar?.fitButton.click();
|
|
} catch (error) {
|
|
shell3.root.addClass("model-weave-mermaid-fallback-shell");
|
|
shell3.surface.empty();
|
|
shell3.surface.createEl("p", {
|
|
text: error instanceof Error ? error.message : String(error),
|
|
cls: "model-weave-muted"
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
async waitForWeaveMapSvgReady(surface) {
|
|
for (let index = 0; index < 6; index += 1) {
|
|
await this.waitForNextAnimationFrame(surface);
|
|
const svg2 = surface.querySelector("svg");
|
|
if (!svg2) {
|
|
continue;
|
|
}
|
|
const rect = svg2.getBoundingClientRect();
|
|
const width = Number(svg2.getAttribute("width") ?? 0);
|
|
const height = Number(svg2.getAttribute("height") ?? 0);
|
|
const hasMeasuredSize = Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0 || rect.width > 0 && rect.height > 0;
|
|
if (hasMeasuredSize) {
|
|
return;
|
|
}
|
|
}
|
|
const svg = surface.querySelector("svg");
|
|
if (!svg) {
|
|
throw new Error("Mermaid SVG was not rendered.");
|
|
}
|
|
throw new Error("Mermaid SVG size could not be measured.");
|
|
}
|
|
async waitForWeaveMapContainerReady(element) {
|
|
for (let index = 0; index < 6; index += 1) {
|
|
await this.waitForNextAnimationFrame(element);
|
|
const rect = element.getBoundingClientRect();
|
|
if (element.isConnected && rect.width > 0 && rect.height > 0) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
waitForNextAnimationFrame(element) {
|
|
const view = element.ownerDocument.defaultView;
|
|
return new Promise((resolve) => {
|
|
if (view?.requestAnimationFrame) {
|
|
view.requestAnimationFrame(() => resolve());
|
|
return;
|
|
}
|
|
globalThis.setTimeout(resolve, 0);
|
|
});
|
|
}
|
|
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 [
|
|
...this.createGroupedImpactUsageSections(
|
|
"incoming",
|
|
summary.inboundRelationships,
|
|
"impactInbound",
|
|
this.t("relationship.referencedByThisObject"),
|
|
this.t("relationship.noInbound")
|
|
),
|
|
...this.createGroupedImpactUsageSections(
|
|
"outgoing",
|
|
summary.outboundRelationships,
|
|
"impactOutbound",
|
|
this.t("relationship.referencesFromThisObject"),
|
|
this.t("relationship.noOutbound")
|
|
)
|
|
];
|
|
}
|
|
createGroupedImpactUsageSections(direction, relationships, idPrefix, emptyTitle, emptyText) {
|
|
if (relationships.length === 0) {
|
|
return [{ id: idPrefix, title: emptyTitle, emptyText, items: [] }];
|
|
}
|
|
return this.groupImpactRelationships(relationships).map(({ key, relationships: groupedRelationships }) => ({
|
|
id: `${idPrefix}:${key}`,
|
|
title: this.getImpactRelationshipGroupLabel(direction, key),
|
|
emptyText,
|
|
items: groupedRelationships.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)
|
|
)
|
|
}))
|
|
}));
|
|
}
|
|
getImpactRelationshipGroupCounts(relationships, direction) {
|
|
return this.groupImpactRelationships(relationships).map(({ key, relationships: groupedRelationships }) => ({
|
|
label: this.getImpactRelationshipGroupLabel(direction, key),
|
|
count: groupedRelationships.length
|
|
}));
|
|
}
|
|
groupImpactRelationships(relationships) {
|
|
const groups = /* @__PURE__ */ new Map();
|
|
for (const relationship of relationships) {
|
|
const key = this.getImpactRelationshipGroupKey(relationship);
|
|
const group = groups.get(key) ?? [];
|
|
group.push(relationship);
|
|
groups.set(key, group);
|
|
}
|
|
return IMPACT_RELATIONSHIP_CATEGORY_ORDER.map((key) => ({ key, relationships: groups.get(key) ?? [] })).filter((group) => group.relationships.length > 0);
|
|
}
|
|
getImpactRelationshipGroupKey(relationship) {
|
|
return getImpactRelationshipCategoryKey(relationship);
|
|
}
|
|
getImpactRelationshipGroupLabel(direction, key) {
|
|
const prefix = direction === "incoming" ? "relationship.usedBy" : "relationship.references";
|
|
const suffix = ["screens", "processes", "rules", "mappings", "diagrams", "classes", "dataEr"].includes(key) ? key : "other";
|
|
return this.t(`${prefix}.${suffix}`);
|
|
}
|
|
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 shell3 = this.createViewerSplitShell(`dfd-object:${state.model.path}`, 0.62);
|
|
this.activeScrollContainer = shell3.bottomPane;
|
|
this.renderReviewSummaryPanel(shell3.bottomPane, {
|
|
model: state.model,
|
|
warnings: state.warnings,
|
|
impactSummary: state.impactSummary,
|
|
sourceLinks: state.model.sourceLinks,
|
|
weaveMapAvailable: Boolean(state.weaveMapMermaidSource)
|
|
});
|
|
renderDiagnostics(
|
|
shell3.bottomPane,
|
|
state.warnings,
|
|
state.onOpenDiagnostic ?? void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState,
|
|
this.getDiagnosticLanguage()
|
|
);
|
|
const objectDetails = renderObjectModel(
|
|
state.model,
|
|
void 0,
|
|
this.viewerPreferences.localSourceRoot,
|
|
this.viewerPreferences.uiLanguage,
|
|
{ includeSourceLinks: false }
|
|
);
|
|
this.renderImpactSummarySection(
|
|
shell3.bottomPane,
|
|
state.impactSummary,
|
|
state.onCopyImpactSummary,
|
|
state.onOpenImpactModel,
|
|
state.weaveMapMermaidSource,
|
|
state.colorScheme
|
|
);
|
|
this.renderSourceLinksSection(shell3.bottomPane, state.model.sourceLinks);
|
|
shell3.bottomPane.appendChild(objectDetails);
|
|
const diagramRoot = renderDiagramModel(state.diagram, {
|
|
app: this.app,
|
|
interactionSourcePath: state.model.path,
|
|
hideTitle: true,
|
|
hideDetails: false,
|
|
fitVerticalAlign: "top",
|
|
onOpenObject: state.onOpenObject ?? void 0,
|
|
viewportState: this.objectGraphViewportState,
|
|
onViewportStateChange: this.createObjectViewportStateHandler(state.model.path),
|
|
sourcePanelContainer: shell3.bottomPane,
|
|
sourcePanelPlacement: "prepend",
|
|
...getMermaidSourceLabels(this.t),
|
|
...getGraphExportLabels(this.t),
|
|
onExportPng: () => this.exportCurrentDiagramAsPngWithNotice(),
|
|
onExportAndOpenPng: () => this.exportCurrentDiagramAsPngAndOpenWithNotice(),
|
|
...getClassDetailLabels(this.t),
|
|
showMermaidRenderDebug: this.viewerPreferences.showMermaidRenderDebug
|
|
});
|
|
ensureGraphIdentityTitle(diagramRoot, buildGraphIdentityTitle(state.model));
|
|
this.appendViewerToolbarControls(diagramRoot);
|
|
this.moveDetailSections(diagramRoot, shell3.bottomPane);
|
|
shell3.topPane.appendChild(diagramRoot);
|
|
}
|
|
renderDiagramState(state) {
|
|
const filePath = state.diagram.diagram.path;
|
|
const shell3 = this.createViewerSplitShell(`diagram:${filePath}`, 0.64);
|
|
shell3.bottomPane.addClass("model-weave-collection-diagram-lower-pane");
|
|
const lowerSlots = this.createCollectionDiagramLowerPaneSlots(shell3.bottomPane);
|
|
this.activeScrollContainer = shell3.bottomPane;
|
|
this.renderReviewSummaryPanel(lowerSlots.review, {
|
|
model: state.diagram.diagram,
|
|
warnings: state.warnings,
|
|
impactSummary: state.impactSummary,
|
|
sourceLinks: state.diagram.diagram.sourceLinks,
|
|
weaveMapAvailable: Boolean(state.weaveMapMermaidSource)
|
|
});
|
|
renderDiagnostics(
|
|
lowerSlots.diagnostics,
|
|
state.warnings,
|
|
state.onOpenDiagnostic ?? void 0,
|
|
this.getCollapsibleOpenState,
|
|
this.setCollapsibleOpenState,
|
|
this.getDiagnosticLanguage()
|
|
);
|
|
const diagramRoot = renderDiagramModel(state.diagram, {
|
|
onOpenObject: state.onOpenObject ?? void 0,
|
|
app: this.app,
|
|
interactionSourcePath: filePath,
|
|
renderMode: getStandardRenderMode(state.rendererSelection),
|
|
colorScheme: state.colorScheme,
|
|
viewportState: this.diagramViewportState,
|
|
onViewportStateChange: this.createDiagramViewportStateHandler(filePath),
|
|
sourcePanelContainer: lowerSlots.source,
|
|
...getMermaidSourceLabels(this.t),
|
|
...getGraphExportLabels(this.t),
|
|
onExportPng: () => this.exportCurrentDiagramAsPngWithNotice(),
|
|
onExportAndOpenPng: () => this.exportCurrentDiagramAsPngAndOpenWithNotice(),
|
|
...getDfdDetailLabels(this.t),
|
|
...getClassDetailLabels(this.t),
|
|
showMermaidRenderDebug: this.viewerPreferences.showMermaidRenderDebug
|
|
});
|
|
ensureGraphIdentityTitle(diagramRoot, buildGraphIdentityTitle(state.diagram.diagram));
|
|
this.appendRendererSelection(diagramRoot, state.rendererSelection);
|
|
this.appendViewerToolbarControls(diagramRoot);
|
|
this.moveDetailSections(diagramRoot, lowerSlots.details);
|
|
this.renderImpactSummarySection(
|
|
lowerSlots.impact,
|
|
state.impactSummary,
|
|
state.onCopyImpactSummary,
|
|
state.onOpenImpactModel,
|
|
state.weaveMapMermaidSource,
|
|
state.colorScheme
|
|
);
|
|
this.renderSourceLinksSection(lowerSlots.sourceLinks, state.diagram.diagram.sourceLinks);
|
|
this.renderAppliedColorScheme(
|
|
lowerSlots.impact,
|
|
state.colorScheme,
|
|
this.getImpactColorSchemeTargets(
|
|
this.getDiagramColorSchemeTargets(state.diagram),
|
|
state.impactSummary
|
|
)
|
|
);
|
|
shell3.topPane.appendChild(diagramRoot);
|
|
}
|
|
getDiagramColorSchemeTargets(diagram) {
|
|
if (this.isDfdDiagramModel(diagram.diagram)) {
|
|
return (diagram.diagram.domains?.length ?? 0) > 0 ? ["dfd", "domain"] : ["dfd"];
|
|
}
|
|
return [];
|
|
}
|
|
getImpactColorSchemeTargets(baseTargets, impactSummary) {
|
|
return impactSummary ? [...baseTargets, "weave_map"] : baseTargets;
|
|
}
|
|
isDfdDiagramModel(diagram) {
|
|
return diagram.schema === "dfd_diagram";
|
|
}
|
|
createCollectionDiagramLowerPaneSlots(container) {
|
|
const review = container.createDiv({
|
|
cls: "model-weave-lower-pane-slot model-weave-lower-pane-review-slot"
|
|
});
|
|
const diagnostics = container.createDiv({
|
|
cls: "model-weave-lower-pane-slot model-weave-lower-pane-diagnostics-slot"
|
|
});
|
|
const impact = container.createDiv({
|
|
cls: "model-weave-lower-pane-slot model-weave-lower-pane-impact-slot"
|
|
});
|
|
const sourceLinks = container.createDiv({
|
|
cls: "model-weave-lower-pane-slot model-weave-lower-pane-source-links-slot"
|
|
});
|
|
const details = container.createDiv({
|
|
cls: "model-weave-lower-pane-slot model-weave-lower-pane-details-slot"
|
|
});
|
|
const source = container.createDiv({
|
|
cls: "model-weave-lower-pane-slot model-weave-lower-pane-source-slot"
|
|
});
|
|
return { review, diagnostics, impact, sourceLinks, details, source };
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
appendViewerToolbarControls(container, viewOnlyTarget = container) {
|
|
this.appendViewOnlyControl(container, viewOnlyTarget);
|
|
}
|
|
appendViewerFocusToolbar() {
|
|
const toolbar = this.contentEl.createDiv({
|
|
cls: "model-weave-viewer-toolbar"
|
|
});
|
|
const currentFilePath = this.getCurrentFilePath();
|
|
if (currentFilePath) {
|
|
const openGroup = toolbar.createDiv({ cls: "model-weave-preview-pane-actions" });
|
|
if (this.paneActions.onOpenPreviewInMainPane) {
|
|
const mainPaneButton = openGroup.createEl("button", {
|
|
text: this.t("preview.openInMainPane.short"),
|
|
cls: "model-weave-secondary-button model-weave-preview-pane-button"
|
|
});
|
|
mainPaneButton.type = "button";
|
|
mainPaneButton.title = this.t("preview.openInMainPane");
|
|
mainPaneButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
this.paneActions.onOpenPreviewInMainPane?.(currentFilePath);
|
|
});
|
|
}
|
|
if (this.paneActions.onOpenPreviewInNewPane) {
|
|
const newPaneButton = openGroup.createEl("button", {
|
|
text: this.t("preview.openInNewPane.short"),
|
|
cls: "model-weave-secondary-button model-weave-preview-pane-button"
|
|
});
|
|
newPaneButton.type = "button";
|
|
newPaneButton.title = this.t("preview.openInNewPane");
|
|
newPaneButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
this.paneActions.onOpenPreviewInNewPane?.(currentFilePath);
|
|
});
|
|
}
|
|
}
|
|
const button = toolbar.createEl("button", {
|
|
cls: "model-weave-secondary-button model-weave-focus-mode-button"
|
|
});
|
|
button.type = "button";
|
|
this.updateFocusModeButton(button);
|
|
button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
this.setFocusMode(!this.focusModeEnabled);
|
|
});
|
|
}
|
|
appendViewOnlyControl(container, viewOnlyTarget) {
|
|
const toolbar = container.querySelector(".mdspec-zoom-toolbar");
|
|
if (!toolbar) {
|
|
return;
|
|
}
|
|
const controls = toolbar.querySelector(".model-weave-zoom-toolbar-controls");
|
|
if (!controls || controls.querySelector(".model-weave-view-only-button")) {
|
|
return;
|
|
}
|
|
const button = container.ownerDocument.createElement("button");
|
|
button.type = "button";
|
|
button.addClass("model-weave-zoom-toolbar-button");
|
|
button.addClass("model-weave-view-only-button");
|
|
this.updateViewOnlyButton(button, viewOnlyTarget);
|
|
button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
this.setViewOnlyMode(
|
|
!(this.viewOnlyEnabled && this.viewOnlyTarget === viewOnlyTarget),
|
|
{ target: viewOnlyTarget }
|
|
);
|
|
});
|
|
const exportButton = controls.querySelector(
|
|
".model-weave-zoom-toolbar-export-png"
|
|
);
|
|
controls.insertBefore(button, exportButton ?? null);
|
|
}
|
|
updateFocusModeButton(button) {
|
|
const label = this.focusModeEnabled ? this.t("graph.focusModeExit") : this.t("graph.focusModeEnter");
|
|
button.textContent = this.focusModeEnabled ? "Exit" : "Focus";
|
|
button.setAttribute("aria-label", label);
|
|
button.title = label;
|
|
button.toggleClass("is-active", this.focusModeEnabled);
|
|
}
|
|
updateViewOnlyButton(button, target) {
|
|
const isActive = this.viewOnlyEnabled && Boolean(this.viewOnlyTarget?.contains(button));
|
|
const label = isActive ? this.t("graph.viewOnlyExit") : this.t("graph.viewOnlyEnter");
|
|
button.textContent = isActive ? "Exit View" : "View";
|
|
button.setAttribute("aria-label", label);
|
|
button.title = label;
|
|
button.toggleClass("is-active", isActive);
|
|
}
|
|
setFocusMode(enabled, options) {
|
|
if (this.focusModeEnabled === enabled) {
|
|
return;
|
|
}
|
|
this.focusModeEnabled = enabled;
|
|
if (enabled) {
|
|
this.attachFocusModeOverlay();
|
|
} else {
|
|
this.detachFocusModeOverlay();
|
|
}
|
|
this.contentEl.classList.toggle("model-weave-viewer-focus-mode", enabled);
|
|
this.contentEl.querySelectorAll(".model-weave-focus-mode-button").forEach((button) => this.updateFocusModeButton(button));
|
|
if (!options?.skipFit) {
|
|
this.scheduleActiveGraphFit();
|
|
}
|
|
}
|
|
setViewOnlyMode(enabled, options) {
|
|
const target = enabled ? options?.target ?? this.viewOnlyTarget : null;
|
|
if (enabled && !target) {
|
|
return;
|
|
}
|
|
this.detachViewOnlyTarget();
|
|
this.viewOnlyEnabled = enabled;
|
|
this.viewOnlyTarget = target;
|
|
if (enabled && target && !this.attachViewOnlyTarget(target)) {
|
|
this.viewOnlyEnabled = false;
|
|
this.viewOnlyTarget = null;
|
|
}
|
|
this.contentEl.classList.toggle(
|
|
"model-weave-viewer-view-only",
|
|
this.viewOnlyEnabled
|
|
);
|
|
this.contentEl.querySelectorAll(".model-weave-view-only-button").forEach((button) => this.updateViewOnlyButton(button, button));
|
|
if (!options?.skipFit) {
|
|
this.scheduleActiveGraphFit();
|
|
}
|
|
}
|
|
attachViewOnlyTarget(target) {
|
|
const parent = target.parentNode;
|
|
if (!parent) {
|
|
return false;
|
|
}
|
|
const stage = this.ensureViewOnlyStage();
|
|
const placeholder = target.ownerDocument.createComment(
|
|
"model-weave-view-only-placeholder"
|
|
);
|
|
parent.insertBefore(placeholder, target);
|
|
target.addClass("model-weave-view-only-target");
|
|
stage.appendChild(target);
|
|
this.viewOnlyPlaceholder = placeholder;
|
|
return true;
|
|
}
|
|
detachViewOnlyTarget() {
|
|
const target = this.viewOnlyTarget;
|
|
const placeholder = this.viewOnlyPlaceholder;
|
|
target?.removeClass("model-weave-view-only-target");
|
|
if (target && placeholder?.parentNode) {
|
|
placeholder.parentNode.insertBefore(target, placeholder);
|
|
placeholder.remove();
|
|
}
|
|
this.viewOnlyPlaceholder = null;
|
|
}
|
|
ensureViewOnlyStage() {
|
|
if (this.viewOnlyStage && this.viewOnlyStage.parentElement === this.contentEl) {
|
|
return this.viewOnlyStage;
|
|
}
|
|
const stage = this.contentEl.createDiv({
|
|
cls: "model-weave-view-only-stage"
|
|
});
|
|
this.viewOnlyStage = stage;
|
|
return stage;
|
|
}
|
|
attachFocusModeOverlay() {
|
|
if (this.focusModePlaceholder) {
|
|
return;
|
|
}
|
|
const parent = this.contentEl.parentNode;
|
|
if (!parent) {
|
|
return;
|
|
}
|
|
const doc = this.contentEl.ownerDocument;
|
|
const placeholder = doc.createComment("model-weave-focus-mode-placeholder");
|
|
parent.insertBefore(placeholder, this.contentEl);
|
|
this.contentEl.setCssProps({
|
|
"--mw-focus-overlay-top": this.getFocusOverlayTopOffset()
|
|
});
|
|
doc.body.appendChild(this.contentEl);
|
|
doc.body.classList.add("model-weave-focus-mode-active");
|
|
this.focusModePlaceholder = placeholder;
|
|
}
|
|
getFocusOverlayTopOffset() {
|
|
const titlebar = this.contentEl.ownerDocument.querySelector(
|
|
".titlebar"
|
|
);
|
|
const rect = titlebar?.getBoundingClientRect();
|
|
if (!rect || rect.height <= 0) {
|
|
return "0px";
|
|
}
|
|
return Math.max(0, Math.ceil(rect.bottom)).toString() + "px";
|
|
}
|
|
detachFocusModeOverlay() {
|
|
const placeholder = this.focusModePlaceholder;
|
|
const doc = this.contentEl.ownerDocument;
|
|
doc.body.classList.remove("model-weave-focus-mode-active");
|
|
this.contentEl.setCssProps({
|
|
"--mw-focus-overlay-top": "0px"
|
|
});
|
|
if (placeholder?.parentNode) {
|
|
placeholder.parentNode.insertBefore(this.contentEl, placeholder);
|
|
placeholder.remove();
|
|
}
|
|
this.focusModePlaceholder = null;
|
|
}
|
|
scheduleActiveGraphFit() {
|
|
const view = this.contentEl.ownerDocument.defaultView;
|
|
if (!view) {
|
|
return;
|
|
}
|
|
view.requestAnimationFrame(() => {
|
|
view.requestAnimationFrame(() => {
|
|
this.contentEl.querySelectorAll(".model-weave-zoom-toolbar-fit").forEach((button) => button.click());
|
|
});
|
|
});
|
|
}
|
|
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 doc = container.ownerDocument;
|
|
const wrapper = doc.createElement("div");
|
|
wrapper.className = "mdspec-renderer-select-group model-weave-render-mode-row";
|
|
const title = doc.createElement("span");
|
|
title.addClass("model-weave-render-mode-label");
|
|
title.textContent = "Renderer";
|
|
const meta = doc.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 = doc.createElement("select");
|
|
select.addClass("model-weave-render-mode-select");
|
|
select.title = meta.textContent;
|
|
for (const mode of selection.supportedModes) {
|
|
const option = doc.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);
|
|
const rightGroup = toolbar.querySelector(".model-weave-zoom-toolbar-right") ?? toolbar;
|
|
rightGroup.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, t) {
|
|
return {
|
|
title: state.title,
|
|
sourcePath: state.filePath,
|
|
blocks: localizeScreenPreviewBlocks(state.layoutBlocks ?? [], t),
|
|
transitions: state.screenPreviewTransitions ?? []
|
|
};
|
|
}
|
|
function localizeScreenPreviewBlocks(blocks, t) {
|
|
if (!t) {
|
|
return blocks;
|
|
}
|
|
return blocks.map((block) => ({
|
|
...block,
|
|
label: block.label === "Unassigned" || block.label === LEGACY_SCREEN_UNASSIGNED_LABEL ? t("screen.preview.unassigned") : block.label,
|
|
subtitle: block.subtitle === "Layout is missing or undefined" || block.subtitle === LEGACY_SCREEN_LAYOUT_MISSING_SUBTITLE ? t("screen.preview.layoutMissing") : block.subtitle
|
|
}));
|
|
}
|
|
var LEGACY_SCREEN_UNASSIGNED_LABEL = "\u672A\u5206\u985E [unassigned]";
|
|
var LEGACY_SCREEN_LAYOUT_MISSING_SUBTITLE = "layout \u672A\u6307\u5B9A\u307E\u305F\u306F\u672A\u5B9A\u7FA9";
|
|
function createScreenPreviewDiagram(data, options) {
|
|
const root = activeDocument.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 = activeDocument.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("Ctrl/Meta + wheel: zoom / Drag background: pan");
|
|
if (toolbar) {
|
|
root.appendChild(toolbar.root);
|
|
}
|
|
const viewport = activeDocument.createElement("div");
|
|
viewport.className = "mdspec-screen-viewport";
|
|
viewport.addClass("model-weave-screen-preview-viewport");
|
|
const surface = activeDocument.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: "Unassigned", items: [] }];
|
|
const mainBoxHeight = SCREEN_BOX_HEADER_HEIGHT + blocks.reduce((sum, block) => sum + measureScreenPreviewBlockHeight(block), 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 measureScreenPreviewBlockHeight(block) {
|
|
const visibleRows = Math.max(1, block.items.length);
|
|
return SCREEN_SECTION_HEADER_HEIGHT + SCREEN_SECTION_PADDING * 2 + visibleRows * SCREEN_FIELD_ROW_HEIGHT;
|
|
}
|
|
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 = activeDocument.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 = activeDocument.createElement("header");
|
|
header.addClass("model-weave-screen-preview-header");
|
|
header.addClass("model-weave-screen-card-header");
|
|
const kind = activeDocument.createElement("div");
|
|
kind.addClass("model-weave-screen-preview-muted");
|
|
kind.textContent = "Screen";
|
|
const title = activeDocument.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 = activeDocument.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: "Unassigned", items: [] }];
|
|
blocks.forEach((block, index) => {
|
|
const section = activeDocument.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 = activeDocument.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 = activeDocument.createElement("div");
|
|
empty.addClass("model-weave-screen-preview-empty");
|
|
empty.textContent = "None";
|
|
section.appendChild(empty);
|
|
} else {
|
|
const list = activeDocument.createElement("ul");
|
|
list.addClass("model-weave-screen-preview-list");
|
|
for (const item of block.items) {
|
|
const entry = activeDocument.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}`;
|
|
if (options.app) {
|
|
attachGraphElementHoverPreview({
|
|
app: options.app,
|
|
targetEl: box,
|
|
target: {
|
|
mermaidId: `current:${data.sourcePath}`,
|
|
linktext: data.sourcePath,
|
|
sourcePath: data.sourcePath,
|
|
label: data.title,
|
|
kind: "screen-current",
|
|
targetType: "screen",
|
|
filePath: data.sourcePath
|
|
},
|
|
source: "model-weave",
|
|
hoverParent: (targetEl, fallback) => targetEl.closest(
|
|
".model-weave-view-only-stage, .model-weave-screen-preview, .mdspec-diagram"
|
|
) ?? fallback
|
|
});
|
|
}
|
|
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 = activeDocument.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 = activeDocument.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
const marker = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.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 = activeDocument.createElement("div");
|
|
kind.addClass("model-weave-screen-preview-target-kind");
|
|
kind.textContent = target.target.unresolved ? "unresolved screen" : "screen";
|
|
const title = activeDocument.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 = activeDocument.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;
|
|
if (options.app && target.target.targetLinktext) {
|
|
attachGraphElementHoverPreview({
|
|
app: options.app,
|
|
targetEl: box,
|
|
target: {
|
|
mermaidId: target.target.key,
|
|
linktext: target.target.targetLinktext,
|
|
sourcePath: dataSourcePathFromTransition(target.target),
|
|
label: target.target.targetLabel,
|
|
kind: "screen-transition",
|
|
targetType: "screen",
|
|
filePath: target.target.targetPath
|
|
},
|
|
source: "model-weave",
|
|
hoverParent: (targetEl, fallback) => targetEl.closest(
|
|
".model-weave-view-only-stage, .model-weave-screen-preview, .mdspec-diagram"
|
|
) ?? fallback
|
|
});
|
|
}
|
|
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 dataSourcePathFromTransition(target) {
|
|
return target.sourcePath ?? target.targetPath ?? "";
|
|
}
|
|
function createScreenPreviewActionPill(pill, _onNavigateToLocation) {
|
|
const element = activeDocument.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, language) {
|
|
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;
|
|
}
|
|
const t = createModelWeaveTranslator(toModelWeaveUiLanguage(language));
|
|
renderDiagnosticsPanelSummary(container, errors.length, warnings.length, notes.length, t);
|
|
if (errors.length > 0) {
|
|
renderDiagnosticSection(
|
|
container,
|
|
"errors",
|
|
t("diagnostics.errors"),
|
|
errors,
|
|
onOpenDiagnostic,
|
|
"model-weave-diagnostics-summary-error",
|
|
getOpenState,
|
|
setOpenState,
|
|
language
|
|
);
|
|
}
|
|
if (warnings.length > 0) {
|
|
renderDiagnosticSection(
|
|
container,
|
|
"warnings",
|
|
t("diagnostics.warnings"),
|
|
warnings,
|
|
onOpenDiagnostic,
|
|
"model-weave-diagnostics-summary-warning",
|
|
getOpenState,
|
|
setOpenState,
|
|
language
|
|
);
|
|
}
|
|
if (notes.length > 0) {
|
|
renderDiagnosticSection(
|
|
container,
|
|
"notes",
|
|
t("diagnostics.notes"),
|
|
notes,
|
|
onOpenDiagnostic,
|
|
"model-weave-diagnostics-summary-note",
|
|
getOpenState,
|
|
setOpenState,
|
|
language
|
|
);
|
|
}
|
|
}
|
|
function renderDiagnosticsPanelSummary(container, errorCount, warningCount, noteCount, t) {
|
|
const summary = container.createDiv({ cls: "model-weave-diagnostics-panel-summary" });
|
|
summary.createSpan({ text: t("diagnostics.summary"), cls: "model-weave-diagnostics-panel-title" });
|
|
renderDiagnosticCountChip(summary, t("diagnostics.errors"), errorCount, "error");
|
|
renderDiagnosticCountChip(summary, t("diagnostics.warnings"), warningCount, "warning");
|
|
renderDiagnosticCountChip(summary, t("diagnostics.notes"), noteCount, "info");
|
|
}
|
|
function renderDiagnosticCountChip(container, label, count, severity) {
|
|
const chip = container.createSpan({
|
|
text: label + " " + String(count),
|
|
cls: "model-weave-diagnostics-count-chip model-weave-diagnostics-count-" + severity
|
|
});
|
|
chip.setAttribute("aria-label", label + ": " + String(count));
|
|
}
|
|
function renderDiagnosticSection(container, key, title, diagnostics, onOpenDiagnostic, summaryModifierClass, getOpenState, setOpenState, language) {
|
|
const t = createModelWeaveTranslator(toModelWeaveUiLanguage(language));
|
|
const details = container.createEl("details");
|
|
details.className = "mdspec-diagnostic-section";
|
|
details.addClass("model-weave-preview-section");
|
|
details.open = getOpenState ? getOpenState(key, key !== "notes") : key !== "notes";
|
|
if (setOpenState) {
|
|
details.addEventListener("toggle", () => {
|
|
setOpenState(key, details.open);
|
|
});
|
|
}
|
|
details.addClass("model-weave-diagnostics-details");
|
|
const summary = details.createEl("summary", {
|
|
text: title + " (" + String(diagnostics.length) + ")"
|
|
});
|
|
summary.addClass("model-weave-diagnostics-summary");
|
|
summary.addClass("model-weave-preview-section-title");
|
|
summary.addClass(summaryModifierClass);
|
|
const list = details.createDiv({ cls: "model-weave-diagnostics-card-list" });
|
|
for (const diagnostic of diagnostics) {
|
|
renderDiagnosticCard(list, diagnostic, onOpenDiagnostic, t, language);
|
|
}
|
|
}
|
|
function renderDiagnosticCard(container, diagnostic, onOpenDiagnostic, t, language) {
|
|
const card = container.createDiv({
|
|
cls: "model-weave-diagnostic-card model-weave-diagnostic-card-" + diagnostic.severity
|
|
});
|
|
const header = card.createDiv({ cls: "model-weave-diagnostic-card-header" });
|
|
header.createSpan({
|
|
text: getDiagnosticSeverityLabel(diagnostic.severity, t),
|
|
cls: "model-weave-diagnostic-severity model-weave-diagnostic-severity-" + diagnostic.severity
|
|
});
|
|
header.createSpan({ text: diagnostic.code, cls: "model-weave-diagnostic-code" });
|
|
const message = localizeDiagnosticMessage(diagnostic.message, language);
|
|
card.createDiv({ text: message, cls: "model-weave-diagnostic-message" });
|
|
const metadata = getDiagnosticMetadata(diagnostic, t);
|
|
if (metadata.length > 0) {
|
|
const metaList = card.createDiv({ cls: "model-weave-diagnostic-meta-list" });
|
|
for (const entry of metadata) {
|
|
const item = metaList.createDiv({ cls: "model-weave-diagnostic-meta" });
|
|
item.createSpan({ text: entry.label, cls: "model-weave-diagnostic-meta-label" });
|
|
item.createSpan({ text: entry.value, cls: "model-weave-diagnostic-meta-value" });
|
|
}
|
|
}
|
|
const details = getDiagnosticDetailEntries(diagnostic, t);
|
|
if (details.length > 0) {
|
|
const detailBox = card.createDiv({ cls: "model-weave-diagnostic-detail-box" });
|
|
detailBox.createDiv({
|
|
text: t("diagnostics.details.title"),
|
|
cls: "model-weave-diagnostic-detail-title"
|
|
});
|
|
const detailList = detailBox.createDiv({ cls: "model-weave-diagnostic-meta-list" });
|
|
for (const entry of details) {
|
|
const item = detailList.createDiv({ cls: "model-weave-diagnostic-meta" });
|
|
item.createSpan({ text: entry.label, cls: "model-weave-diagnostic-meta-label" });
|
|
item.createSpan({ text: entry.value, cls: "model-weave-diagnostic-meta-value" });
|
|
}
|
|
}
|
|
const actions = card.createDiv({ cls: "model-weave-diagnostic-actions" });
|
|
if (onOpenDiagnostic) {
|
|
const openButton = actions.createEl("button", {
|
|
text: t("diagnostics.openLocation"),
|
|
cls: "model-weave-secondary-button model-weave-diagnostic-action"
|
|
});
|
|
openButton.type = "button";
|
|
openButton.title = t("diagnostics.openLocationTooltip");
|
|
openButton.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
onOpenDiagnostic(diagnostic);
|
|
});
|
|
}
|
|
renderDiagnosticCopyButton(
|
|
actions,
|
|
t("diagnostics.copyMessage"),
|
|
message,
|
|
"model-weave-diagnostic-action"
|
|
);
|
|
renderDiagnosticCopyButton(
|
|
actions,
|
|
t("diagnostics.copyMarkdown"),
|
|
formatDiagnosticAsMarkdown(diagnostic, message, t),
|
|
"model-weave-diagnostic-action"
|
|
);
|
|
const reference = getDiagnosticReferenceValue(diagnostic);
|
|
if (reference) {
|
|
renderDiagnosticCopyButton(
|
|
actions,
|
|
t("diagnostics.copyReference"),
|
|
reference,
|
|
"model-weave-diagnostic-action"
|
|
);
|
|
}
|
|
const expectedHeader = getExpectedHeaderForDiagnostic(diagnostic);
|
|
if (expectedHeader) {
|
|
renderDiagnosticCopyButton(
|
|
actions,
|
|
t("diagnostics.copyExpectedHeader"),
|
|
expectedHeader,
|
|
"model-weave-diagnostic-action"
|
|
);
|
|
}
|
|
const frontmatterExample = getFrontmatterExampleForDiagnostic(diagnostic);
|
|
if (frontmatterExample) {
|
|
renderDiagnosticCopyButton(
|
|
actions,
|
|
t("diagnostics.copyFrontmatterExample"),
|
|
frontmatterExample,
|
|
"model-weave-diagnostic-action"
|
|
);
|
|
}
|
|
}
|
|
function renderDiagnosticCopyButton(container, label, value, className) {
|
|
const button = container.createEl("button", {
|
|
text: label,
|
|
cls: "model-weave-secondary-button " + className
|
|
});
|
|
button.type = "button";
|
|
button.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
void navigator.clipboard?.writeText(value);
|
|
});
|
|
}
|
|
function getDiagnosticSeverityLabel(severity, t) {
|
|
if (severity === "error") {
|
|
return t("diagnostics.severity.error");
|
|
}
|
|
if (severity === "warning") {
|
|
return t("diagnostics.severity.warning");
|
|
}
|
|
return t("diagnostics.severity.note");
|
|
}
|
|
function getDiagnosticDetailEntries(diagnostic, t) {
|
|
const details = [];
|
|
const expectedHeader = getExpectedHeaderForDiagnostic(diagnostic);
|
|
if (expectedHeader) {
|
|
details.push({ label: t("diagnostics.details.expectedHeader"), value: expectedHeader });
|
|
}
|
|
const actualHeader = findDiagnosticContextValue(diagnostic.context, [
|
|
"actualHeader",
|
|
"actualHeaders",
|
|
"header",
|
|
"headers"
|
|
]);
|
|
if (actualHeader) {
|
|
details.push({ label: t("diagnostics.details.actualHeader"), value: actualHeader });
|
|
}
|
|
const missingColumns = findDiagnosticContextValue(diagnostic.context, ["missingColumns", "missing"]);
|
|
if (missingColumns) {
|
|
details.push({ label: t("diagnostics.details.missingColumns"), value: missingColumns });
|
|
}
|
|
const extraColumns = findDiagnosticContextValue(diagnostic.context, ["extraColumns", "extra"]);
|
|
if (extraColumns) {
|
|
details.push({ label: t("diagnostics.details.extraColumns"), value: extraColumns });
|
|
}
|
|
const reference = getDiagnosticReferenceValue(diagnostic);
|
|
if (reference) {
|
|
const referenceLabel = diagnostic.code === "duplicate-mapping-target-member" ? t("diagnostics.details.duplicateTarget") : diagnostic.code === "unresolved-reference" ? t("diagnostics.details.unresolvedReference") : t("diagnostics.meta.reference");
|
|
details.push({ label: referenceLabel, value: reference });
|
|
}
|
|
const duplicateMappingRow = getDuplicateMappingRowValue(diagnostic);
|
|
if (duplicateMappingRow) {
|
|
details.push({ label: t("diagnostics.details.mappingRow"), value: duplicateMappingRow });
|
|
}
|
|
const missingFrontmatterKey = getMissingFrontmatterKey(diagnostic);
|
|
if (missingFrontmatterKey) {
|
|
details.push({ label: t("diagnostics.details.frontmatterKey"), value: missingFrontmatterKey });
|
|
}
|
|
const frontmatterExample = getFrontmatterExampleForDiagnostic(diagnostic);
|
|
if (frontmatterExample) {
|
|
details.push({ label: t("diagnostics.details.frontmatterExample"), value: frontmatterExample });
|
|
}
|
|
const requestedRenderMode = getRequestedRenderMode(diagnostic);
|
|
if (requestedRenderMode) {
|
|
details.push({ label: t("diagnostics.details.requestedRenderMode"), value: requestedRenderMode });
|
|
details.push({
|
|
label: t("diagnostics.details.effectiveRenderMode"),
|
|
value: t("diagnostics.details.formatDefault")
|
|
});
|
|
}
|
|
if (diagnostic.code === "class-relation-target-not-diagram-compatible") {
|
|
details.push({
|
|
label: t("diagnostics.details.diagramCompatibility"),
|
|
value: t("diagnostics.details.notClassDiagramCompatible")
|
|
});
|
|
const targetType = findDiagnosticContextValue(diagnostic.context, [
|
|
"targetType",
|
|
"resolvedType",
|
|
"fileType",
|
|
"modelType"
|
|
]);
|
|
if (targetType) {
|
|
details.push({ label: t("diagnostics.details.targetType"), value: targetType });
|
|
}
|
|
}
|
|
const section = getDiagnosticSectionName(diagnostic);
|
|
if (section && /row|reference|mapping|frontmatter|render_mode|class relation/i.test(diagnostic.message)) {
|
|
details.push({ label: t("diagnostics.meta.section"), value: section });
|
|
}
|
|
const row = getDiagnosticStringValue(diagnostic.context?.rowIndex);
|
|
if (row && /row|reference|mapping/i.test(diagnostic.message)) {
|
|
details.push({ label: t("diagnostics.meta.row"), value: row });
|
|
}
|
|
const field = getDiagnosticStringValue(diagnostic.context?.field) ?? diagnostic.field;
|
|
if (field && /reference|field/i.test(diagnostic.message)) {
|
|
details.push({ label: t("diagnostics.meta.field"), value: field });
|
|
}
|
|
return dedupeDiagnosticDetailEntries(details);
|
|
}
|
|
function dedupeDiagnosticDetailEntries(entries) {
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const result = [];
|
|
for (const entry of entries) {
|
|
const key = entry.label + "\0" + entry.value;
|
|
if (seen.has(key)) {
|
|
continue;
|
|
}
|
|
seen.add(key);
|
|
result.push(entry);
|
|
}
|
|
return result;
|
|
}
|
|
function getExpectedHeaderForDiagnostic(diagnostic) {
|
|
const contextHeader = findDiagnosticContextValue(diagnostic.context, [
|
|
"expectedHeader",
|
|
"expectedHeaders",
|
|
"expected"
|
|
]);
|
|
if (contextHeader) {
|
|
return contextHeader;
|
|
}
|
|
if (!/table columns in section/i.test(diagnostic.message)) {
|
|
return null;
|
|
}
|
|
const message = diagnostic.message;
|
|
if (/screen field headers/i.test(message)) {
|
|
return "id | label | kind | layout | rule | ref | default | required | notes";
|
|
}
|
|
if (/legacy headers/i.test(message) && /Transitions/i.test(message)) {
|
|
return "id | event | to | condition | notes";
|
|
}
|
|
if (/supported DFD object headers/i.test(message)) {
|
|
return "id | label | kind | ref | domain | notes";
|
|
}
|
|
if (/supported class relation headers/i.test(message)) {
|
|
return "id | from | to | kind | label | notes";
|
|
}
|
|
if (/Domain Sources headers/i.test(message)) {
|
|
return "ref | notes";
|
|
}
|
|
if (/app_process step headers/i.test(message)) {
|
|
return "id | domain | label | kind | input | output | rule | invoke | screen | notes";
|
|
}
|
|
const section = getDiagnosticSectionName(diagnostic);
|
|
return section ? getGenericExpectedHeaderForSection(section) : null;
|
|
}
|
|
function getGenericExpectedHeaderForSection(section) {
|
|
const normalized = section.trim().toLowerCase();
|
|
if (normalized === "mappings") {
|
|
return "source_ref | target_ref | transform | rule | required | notes";
|
|
}
|
|
if (normalized === "layout") {
|
|
return "id | label | kind | parent | order | notes";
|
|
}
|
|
if (normalized === "actions") {
|
|
return "id | target | event | kind | invoke | transition | rule | condition | notes";
|
|
}
|
|
if (normalized === "messages") {
|
|
return "id | timing | severity | audience | text | notes";
|
|
}
|
|
if (normalized === "objects") {
|
|
return "id | label | kind | ref | domain | notes";
|
|
}
|
|
if (normalized === "flows") {
|
|
return "id | from | to | label | kind | notes";
|
|
}
|
|
if (normalized === "domain sources") {
|
|
return "ref | notes";
|
|
}
|
|
return null;
|
|
}
|
|
function getDiagnosticSectionName(diagnostic) {
|
|
const contextSection = getDiagnosticStringValue(diagnostic.context?.section);
|
|
if (contextSection) {
|
|
return contextSection;
|
|
}
|
|
const quoted = diagnostic.message.match(/section "([^"]+)"/i)?.[1];
|
|
return quoted ?? diagnostic.section ?? null;
|
|
}
|
|
function getDuplicateMappingRowValue(diagnostic) {
|
|
if (!/duplicate mapping row/i.test(diagnostic.message)) {
|
|
return null;
|
|
}
|
|
return getFirstQuotedDiagnosticValue(diagnostic.message);
|
|
}
|
|
function getMissingFrontmatterKey(diagnostic) {
|
|
if (!/required frontmatter/i.test(diagnostic.message)) {
|
|
return null;
|
|
}
|
|
return getFirstQuotedDiagnosticValue(diagnostic.message);
|
|
}
|
|
function getFrontmatterExampleForDiagnostic(diagnostic) {
|
|
const missingKey = getMissingFrontmatterKey(diagnostic);
|
|
if (!missingKey) {
|
|
return null;
|
|
}
|
|
const modelType = findDiagnosticContextValue(diagnostic.context, ["type", "modelType", "fileType"]) ?? "model_type";
|
|
const modelId = missingKey === "id" ? "MODEL-ID" : findDiagnosticContextValue(diagnostic.context, ["id", "modelId"]) ?? "MODEL-ID";
|
|
return ["---", "type: " + modelType, "id: " + modelId, "---"].join("\n");
|
|
}
|
|
function getRequestedRenderMode(diagnostic) {
|
|
if (!/render_mode/i.test(diagnostic.message)) {
|
|
return null;
|
|
}
|
|
return getFirstQuotedDiagnosticValue(diagnostic.message);
|
|
}
|
|
function getDiagnosticMetadata(diagnostic, t) {
|
|
const metadata = [];
|
|
const file = diagnostic.filePath ?? diagnostic.path;
|
|
if (file) {
|
|
metadata.push({ label: t("diagnostics.meta.file"), value: file });
|
|
}
|
|
const section = getDiagnosticStringValue(diagnostic.context?.section) ?? diagnostic.section ?? diagnostic.field;
|
|
if (section) {
|
|
metadata.push({ label: t("diagnostics.meta.section"), value: section });
|
|
}
|
|
const line = diagnostic.line ?? diagnostic.fromLine;
|
|
if (typeof line === "number") {
|
|
metadata.push({ label: t("diagnostics.meta.line"), value: String(line) });
|
|
}
|
|
const row = getDiagnosticStringValue(diagnostic.context?.rowIndex);
|
|
if (row) {
|
|
metadata.push({ label: t("diagnostics.meta.row"), value: row });
|
|
}
|
|
const reference = getDiagnosticReferenceValue(diagnostic);
|
|
if (reference) {
|
|
metadata.push({ label: t("diagnostics.meta.reference"), value: reference });
|
|
}
|
|
return metadata;
|
|
}
|
|
function getDiagnosticReferenceValue(diagnostic) {
|
|
const contextReference = findDiagnosticContextValue(diagnostic.context, [
|
|
"reference",
|
|
"ref",
|
|
"target",
|
|
"targetRef",
|
|
"sourceRef",
|
|
"value"
|
|
]);
|
|
if (contextReference) {
|
|
return contextReference;
|
|
}
|
|
if (diagnostic.code !== "unresolved-reference" && !/reference/i.test(diagnostic.message)) {
|
|
return null;
|
|
}
|
|
return getFirstQuotedDiagnosticValue(diagnostic.message);
|
|
}
|
|
function findDiagnosticContextValue(context, keys) {
|
|
if (!context) {
|
|
return null;
|
|
}
|
|
for (const key of keys) {
|
|
const value = getDiagnosticStringValue(context[key]);
|
|
if (value) {
|
|
return value;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function getDiagnosticStringValue(value) {
|
|
if (typeof value === "string") {
|
|
const trimmed = value.trim();
|
|
return trimmed || null;
|
|
}
|
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
return String(value);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
const parts = value.map((entry) => typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean" ? String(entry).trim() : "").filter((entry) => entry.length > 0);
|
|
return parts.length > 0 ? parts.join(" | ") : null;
|
|
}
|
|
return null;
|
|
}
|
|
function getFirstQuotedDiagnosticValue(message) {
|
|
const match = message.match(/"([^"]+)"/);
|
|
return match?.[1] ?? null;
|
|
}
|
|
function formatDiagnosticAsMarkdown(diagnostic, localizedMessage, t) {
|
|
const lines = [
|
|
"- " + t("diagnostics.meta.severity") + ": " + getDiagnosticSeverityLabel(diagnostic.severity, t),
|
|
"- " + t("diagnostics.meta.code") + ": " + diagnostic.code,
|
|
"- " + t("diagnostics.meta.message") + ": " + localizedMessage
|
|
];
|
|
for (const entry of getDiagnosticMetadata(diagnostic, t)) {
|
|
lines.push("- " + entry.label + ": " + entry.value);
|
|
}
|
|
for (const entry of getDiagnosticDetailEntries(diagnostic, t)) {
|
|
lines.push("- " + entry.label + ": " + entry.value);
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
function toModelWeaveUiLanguage(language) {
|
|
if (language === "en" || language === "ja" || language === "auto") {
|
|
return language;
|
|
}
|
|
return "auto";
|
|
}
|
|
function asModelRecord(value) {
|
|
return typeof value === "object" && value !== null ? value : null;
|
|
}
|
|
function getStringField(value, key) {
|
|
const record = asModelRecord(value);
|
|
const field = record?.[key];
|
|
return typeof field === "string" && field.trim().length > 0 ? field.trim() : void 0;
|
|
}
|
|
function getFrontmatterString2(value, key) {
|
|
const frontmatter = asModelRecord(asModelRecord(value)?.frontmatter);
|
|
const field = frontmatter?.[key];
|
|
return typeof field === "string" && field.trim().length > 0 ? field.trim() : void 0;
|
|
}
|
|
function getModelDisplayName(value) {
|
|
return getStringField(value, "name") ?? getStringField(value, "title") ?? getStringField(value, "logicalName") ?? getStringField(value, "physicalName") ?? getFrontmatterString2(value, "name") ?? getFrontmatterString2(value, "title") ?? getModelId3(value);
|
|
}
|
|
function getModelId3(value) {
|
|
return getStringField(value, "id") ?? getFrontmatterString2(value, "id");
|
|
}
|
|
function getModelType(value) {
|
|
return getStringField(value, "fileType") ?? getFrontmatterString2(value, "type") ?? getStringField(value, "schema") ?? getFrontmatterString2(value, "schema");
|
|
}
|
|
function buildGraphIdentityTitle(value, fallbackName, fallbackType) {
|
|
const modelId = getModelId3(value);
|
|
const modelType = getModelType(value) ?? fallbackType;
|
|
const displayName = getModelDisplayName(value) ?? modelId ?? fallbackName ?? "Model";
|
|
const suffixParts = [
|
|
modelType,
|
|
modelId && modelId !== displayName ? modelId : void 0
|
|
].filter((part) => Boolean(part));
|
|
return suffixParts.length > 0 ? displayName + " (" + suffixParts.join(" / ") + ")" : displayName;
|
|
}
|
|
function findSummaryMetadataValue(state, keys) {
|
|
const normalizedKeys = new Set(keys.map((key) => key.toLowerCase()));
|
|
for (const entry of state.metadata) {
|
|
if (normalizedKeys.has(entry.label.toLowerCase())) {
|
|
return entry.value;
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
function buildSummaryGraphTitle(state) {
|
|
const summaryType = state.summaryKind === "screen" ? "screen" : state.businessFlow ? "app_process" : void 0;
|
|
return buildGraphIdentityTitle(
|
|
{
|
|
title: state.title,
|
|
fileType: summaryType,
|
|
path: state.filePath,
|
|
id: findSummaryMetadataValue(state, ["id", "model id", "model_id"])
|
|
},
|
|
state.title,
|
|
summaryType
|
|
);
|
|
}
|
|
function buildWeaveMapGraphTitle(t, summary) {
|
|
const target = summary.modelId || summary.modelLabel || summary.modelPath;
|
|
return t("relationship.weaveMap.title") + " \u2014 " + target;
|
|
}
|
|
function ensureGraphIdentityTitle(root, title) {
|
|
const existingTitle = root.querySelector(
|
|
".model-weave-mermaid-title, .model-weave-graph-identity-title"
|
|
);
|
|
const titleElement = existingTitle ?? root.ownerDocument.createElement("h2");
|
|
titleElement.textContent = title;
|
|
titleElement.title = title;
|
|
titleElement.addClass("model-weave-graph-identity-title");
|
|
if (!existingTitle) {
|
|
root.insertBefore(titleElement, root.firstChild);
|
|
}
|
|
}
|
|
|
|
// 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: ${SUPPORTED_MODEL_WEAVE_FORMAT_LIST}`,
|
|
`\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: ${SUPPORTED_MODEL_WEAVE_FORMAT_LIST}`
|
|
);
|
|
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"
|
|
];
|
|
var DOMAIN_VIEW_MODE_OPTIONS = [
|
|
"mindmap",
|
|
"area",
|
|
"tree"
|
|
];
|
|
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 isDomainViewModeOption(value) {
|
|
return DOMAIN_VIEW_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);
|
|
}
|
|
function getFrontmatterValue(frontmatter, key) {
|
|
if (typeof frontmatter !== "object" || frontmatter === null) {
|
|
return void 0;
|
|
}
|
|
return frontmatter[key];
|
|
}
|
|
var ModelWeavePlugin = class extends import_obsidian8.Plugin {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.index = null;
|
|
this.previewLeaf = null;
|
|
this.rendererOverridesByLeaf = /* @__PURE__ */ new WeakMap();
|
|
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(), {
|
|
onOpenPreviewInMainPane: (filePath) => {
|
|
void this.openPreviewForPathInPane(filePath, "main");
|
|
},
|
|
onOpenPreviewInNewPane: (filePath) => {
|
|
void this.openPreviewForPathInPane(filePath, "new");
|
|
},
|
|
onOpenModelFile: (filePath) => {
|
|
void this.openReferencedFile(filePath);
|
|
}
|
|
})
|
|
);
|
|
this.registerHoverLinkSource("model-weave", {
|
|
display: "Model Weave",
|
|
defaultMod: false
|
|
});
|
|
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: "open-modeling-preview-in-main-pane",
|
|
name: "Open modeling preview in main pane",
|
|
callback: async () => {
|
|
await this.openPreviewForCurrentFileInPane("main");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "open-modeling-preview-in-new-pane",
|
|
name: "Open modeling preview in new pane",
|
|
callback: async () => {
|
|
await this.openPreviewForCurrentFileInPane("new");
|
|
}
|
|
});
|
|
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-domains-template",
|
|
name: "Insert domains template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("domains");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-domain-diagram-template",
|
|
name: "Insert domain diagram template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("domainDiagram");
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-color-scheme-template",
|
|
name: "Insert color scheme template",
|
|
callback: async () => {
|
|
await this.insertTemplateIntoActiveFile("colorScheme");
|
|
}
|
|
});
|
|
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 ensureStandaloneDomainsValidationReady() {
|
|
if (!this.index) {
|
|
return;
|
|
}
|
|
await this.ensureFullParsedFiles(
|
|
(candidate) => candidate.fileType === "domains" || candidate.fileType === "dfd-diagram"
|
|
);
|
|
ensureVaultValidation(this.index);
|
|
}
|
|
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",
|
|
"color-scheme"
|
|
].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);
|
|
const colorScheme = resolveDefaultColorScheme(
|
|
this.index,
|
|
this.settings.defaultColorSchemeRef
|
|
).colorScheme;
|
|
const weaveMapMermaidSource = this.buildWeaveMapMermaidSource(
|
|
impactSummary,
|
|
colorScheme
|
|
);
|
|
return {
|
|
impactSummary,
|
|
weaveMapMermaidSource,
|
|
colorScheme,
|
|
onCopyImpactSummary: () => {
|
|
void this.copyImpactSummary(impactSummary);
|
|
},
|
|
onOpenImpactModel: (filePath, navigation) => {
|
|
void this.openReferencedFile(filePath, Boolean(navigation?.openInNewLeaf));
|
|
}
|
|
};
|
|
}
|
|
buildWeaveMapMermaidSource(summary, colorScheme) {
|
|
try {
|
|
return buildWeaveMapMermaidSource(buildWeaveMapModel(summary), {
|
|
colorScheme
|
|
});
|
|
} catch {
|
|
return void 0;
|
|
}
|
|
}
|
|
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,
|
|
defaultDomainsViewMode: this.settings.defaultDomainsViewMode,
|
|
defaultDomainDiagramViewMode: this.settings.defaultDomainDiagramViewMode,
|
|
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(modelWeaveText("No active Markdown file.", "\u30A2\u30AF\u30C6\u30A3\u30D6\u306A Markdown \u30D5\u30A1\u30A4\u30EB\u304C\u3042\u308A\u307E\u305B\u3093\u3002"));
|
|
return;
|
|
}
|
|
await this.showPreviewForFile(file, void 0, true, "external-file-open");
|
|
}
|
|
async openPreviewForCurrentFileInPane(target) {
|
|
if (!this.index) {
|
|
await this.rebuildIndex();
|
|
}
|
|
const file = this.getCurrentPreviewCommandFile();
|
|
if (!file) {
|
|
new import_obsidian8.Notice(modelWeaveText("No active Model Weave file.", "\u30A2\u30AF\u30C6\u30A3\u30D6\u306A Model Weave \u30D5\u30A1\u30A4\u30EB\u304C\u3042\u308A\u307E\u305B\u3093\u3002"));
|
|
return;
|
|
}
|
|
await this.openPreviewForFileInPane(file, target);
|
|
}
|
|
async openPreviewForPathInPane(filePath, target) {
|
|
if (!this.index) {
|
|
await this.rebuildIndex();
|
|
}
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(filePath);
|
|
if (!(abstractFile instanceof import_obsidian8.TFile)) {
|
|
new import_obsidian8.Notice(modelWeaveText("Preview source file was not found.", "Preview \u306E\u5143\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002"));
|
|
return;
|
|
}
|
|
await this.openPreviewForFileInPane(abstractFile, target);
|
|
}
|
|
async openPreviewForFileInPane(file, target) {
|
|
const leaf = target === "new" ? this.app.workspace.getLeaf(true) : this.app.workspace.getLeaf(false);
|
|
await this.showPreviewForFile(file, leaf, true, "initial-open", {
|
|
managePreviewLeaf: false
|
|
});
|
|
this.app.workspace.setActiveLeaf(leaf, { focus: true });
|
|
}
|
|
getCurrentPreviewCommandFile() {
|
|
const activePreviewPath = this.getActivePreviewFilePath();
|
|
if (activePreviewPath) {
|
|
const previewFile = this.app.vault.getAbstractFileByPath(activePreviewPath);
|
|
if (previewFile instanceof import_obsidian8.TFile) {
|
|
return previewFile;
|
|
}
|
|
}
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (activeFile) {
|
|
return activeFile;
|
|
}
|
|
const managedPreviewPath = this.getManagedPreviewFilePath();
|
|
if (managedPreviewPath) {
|
|
const previewFile = this.app.vault.getAbstractFileByPath(managedPreviewPath);
|
|
if (previewFile instanceof import_obsidian8.TFile) {
|
|
return previewFile;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
getActivePreviewFilePath() {
|
|
const mostRecentLeaf = this.app.workspace.getMostRecentLeaf();
|
|
if (!mostRecentLeaf || !this.isPreviewLeaf(mostRecentLeaf)) {
|
|
return null;
|
|
}
|
|
const view = mostRecentLeaf.view;
|
|
return view instanceof ModelingPreviewView ? view.getCurrentFilePath() : null;
|
|
}
|
|
getManagedPreviewFilePath() {
|
|
if (!this.previewLeaf || !this.isPreviewLeaf(this.previewLeaf)) {
|
|
return null;
|
|
}
|
|
const view = this.previewLeaf.view;
|
|
return view instanceof ModelingPreviewView ? view.getCurrentFilePath() : null;
|
|
}
|
|
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 = getFrontmatterValue(
|
|
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();
|
|
}
|
|
const model = this.index?.modelsByFilePath[file.path];
|
|
const fileType = model ? detectFileType(model.frontmatter) : "markdown";
|
|
const isSupported = isModelWeavePreviewSupportedFileType(fileType);
|
|
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", options) {
|
|
if (!this.index) {
|
|
await this.rebuildIndex();
|
|
}
|
|
if (!this.index) {
|
|
return;
|
|
}
|
|
const model = await this.ensureFullModelForFile(file);
|
|
const leaf = await this.ensurePreviewLeaf(
|
|
preferredLeaf,
|
|
activate,
|
|
options?.managePreviewLeaf ?? true
|
|
);
|
|
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 leafRenderModeOverride = this.getRendererOverrideForLeaf(leaf, file.path);
|
|
const renderMode = this.resolveFileRenderMode(
|
|
file.path,
|
|
fileType,
|
|
model.frontmatter,
|
|
"kind" in model && typeof model.kind === "string" ? model.kind : null,
|
|
leafRenderModeOverride
|
|
);
|
|
const renderModeWarnings = renderMode.diagnostics;
|
|
const rendererSelection = this.buildRendererSelectionState(
|
|
file,
|
|
leaf,
|
|
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" || candidate.fileType === "color-scheme" || candidate.fileType === "domains"
|
|
);
|
|
}
|
|
const colorSchemeResult = resolveDefaultColorScheme(
|
|
this.index,
|
|
this.settings.defaultColorSchemeRef
|
|
);
|
|
const resolved = model.fileType === "dfd-diagram" && this.index ? resolveDiagramRelations(model, this.index) : null;
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings,
|
|
...colorSchemeResult.warnings,
|
|
...resolved?.warnings ?? []
|
|
];
|
|
const diagnostics = resolved ? buildCurrentDiagramDiagnostics(resolved, warnings) : warnings;
|
|
view.updateContent(
|
|
resolved ? {
|
|
mode: "diagram",
|
|
diagram: resolved,
|
|
...impactPreviewProps,
|
|
warnings: diagnostics,
|
|
colorScheme: colorSchemeResult.colorScheme,
|
|
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();
|
|
await this.ensureFullParsedFiles(
|
|
(candidate) => candidate.fileType === "color-scheme" || candidate.fileType === "domains"
|
|
);
|
|
const colorSchemeResult = resolveDefaultColorScheme(
|
|
this.index,
|
|
this.settings.defaultColorSchemeRef
|
|
);
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings,
|
|
...colorSchemeResult.warnings
|
|
];
|
|
if (model.fileType === "app-process") {
|
|
const domainPlacement = resolveAppProcessDomainPlacement(
|
|
model,
|
|
this.index
|
|
);
|
|
const diagnostics = buildCurrentObjectDiagnostics(
|
|
model,
|
|
this.index,
|
|
null,
|
|
[
|
|
...warnings,
|
|
...this.buildAppProcessBusinessFlowWarnings(model),
|
|
...domainPlacement.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: "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 }] : [],
|
|
...model.domains.length > 0 ? [{ label: "Domains", value: model.domains.length }] : [],
|
|
...model.domainSources.length > 0 ? [{ label: "Domain Sources", value: model.domainSources.length }] : []
|
|
],
|
|
textSections: this.buildAppProcessTextSections(model),
|
|
tables: this.buildAppProcessSummaryTables(model, file.path),
|
|
appProcessDomainPlacement: domainPlacement,
|
|
businessFlow: (model.steps?.length ?? 0) > 0 ? {
|
|
title: model.name || model.id,
|
|
steps: model.steps ?? [],
|
|
flows: model.flows ?? [],
|
|
hasExplicitFlows: Boolean(model.hasExplicitFlows),
|
|
domains: domainPlacement.domains.length > 0 ? domainPlacement.domains : model.domains
|
|
} : void 0,
|
|
colorScheme: colorSchemeResult.colorScheme,
|
|
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 "color-scheme": {
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings
|
|
];
|
|
if (model.fileType === "color-scheme") {
|
|
view.updateContent({
|
|
mode: "color-scheme",
|
|
model,
|
|
warnings,
|
|
rendererSelection,
|
|
onOpenDiagnostic: (diagnostic) => {
|
|
void this.openDiagnosticLocation(file.path, diagnostic);
|
|
}
|
|
}, reason);
|
|
} else {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
}, reason);
|
|
}
|
|
return;
|
|
}
|
|
case "domains": {
|
|
await this.ensureStandaloneDomainsValidationReady();
|
|
await this.ensureFullParsedFiles((candidate) => candidate.fileType === "color-scheme");
|
|
const colorSchemeResult = resolveDefaultColorScheme(
|
|
this.index,
|
|
this.settings.defaultColorSchemeRef
|
|
);
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings,
|
|
...colorSchemeResult.warnings
|
|
];
|
|
if (model.fileType === "domains") {
|
|
const diagnostics = buildCurrentObjectDiagnostics(
|
|
model,
|
|
this.index,
|
|
null,
|
|
warnings
|
|
);
|
|
view.updateContent({
|
|
mode: "domains",
|
|
model,
|
|
relationships: buildDomainRelationshipSummaries(model, this.index),
|
|
warnings: diagnostics,
|
|
colorScheme: colorSchemeResult.colorScheme,
|
|
rendererSelection,
|
|
onOpenDiagnostic: (diagnostic) => {
|
|
void this.openDiagnosticLocation(file.path, diagnostic);
|
|
}
|
|
}, reason);
|
|
} else {
|
|
view.updateContent({
|
|
mode: "empty",
|
|
message: UNSUPPORTED_MESSAGE,
|
|
warnings: []
|
|
}, reason);
|
|
}
|
|
return;
|
|
}
|
|
case "domain-diagram": {
|
|
await this.ensureStandaloneDomainsValidationReady();
|
|
await this.ensureFullParsedFiles((candidate) => candidate.fileType === "color-scheme");
|
|
const colorSchemeResult = resolveDefaultColorScheme(
|
|
this.index,
|
|
this.settings.defaultColorSchemeRef
|
|
);
|
|
const warnings = [
|
|
...this.index.warningsByFilePath[file.path] ?? [],
|
|
...renderModeWarnings,
|
|
...colorSchemeResult.warnings
|
|
];
|
|
if (model.fileType === "domain-diagram") {
|
|
const resolved = resolveDomainDiagram(model, this.index);
|
|
const diagnostics = [
|
|
...warnings,
|
|
...resolved.warnings
|
|
];
|
|
const mergedDomainsModel = {
|
|
...model,
|
|
fileType: "domains",
|
|
schema: "domains",
|
|
domains: resolved.domains,
|
|
description: void 0
|
|
};
|
|
view.updateContent({
|
|
mode: "domain-diagram",
|
|
resolved,
|
|
relationships: buildDomainRelationshipSummaries(
|
|
mergedDomainsModel,
|
|
this.index
|
|
),
|
|
warnings: diagnostics,
|
|
colorScheme: colorSchemeResult.colorScheme,
|
|
rendererSelection,
|
|
onOpenDiagnostic: (diagnostic) => {
|
|
void this.openDiagnosticLocation(file.path, diagnostic);
|
|
}
|
|
}, 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",
|
|
"Domains",
|
|
"Domain Sources",
|
|
"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 === "Domains") {
|
|
sections.push({
|
|
label: `Domains: ${model.sections.Domains?.length ?? 0} lines`,
|
|
line,
|
|
ch: 0
|
|
});
|
|
} else if (key === "Domain Sources") {
|
|
sections.push({
|
|
label: `Domain Sources: ${model.sections["Domain Sources"]?.length ?? 0} lines`,
|
|
line,
|
|
ch: 0
|
|
});
|
|
} else 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, toolbarOverride = null) {
|
|
return resolveRenderMode({
|
|
filePath,
|
|
formatType: fileType,
|
|
modelKind: modelKind ?? (typeof frontmatter.kind === "string" ? frontmatter.kind : null),
|
|
toolbarOverride,
|
|
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;
|
|
case "domains":
|
|
return this.settings.defaultDomainsViewMode;
|
|
case "domain-diagram":
|
|
return this.settings.defaultDomainDiagramViewMode;
|
|
default:
|
|
return "custom";
|
|
}
|
|
}
|
|
getRendererOverrideForLeaf(leaf, filePath) {
|
|
const override = this.rendererOverridesByLeaf.get(leaf);
|
|
if (!override) {
|
|
return null;
|
|
}
|
|
if (override.filePath !== filePath) {
|
|
this.rendererOverridesByLeaf.delete(leaf);
|
|
return null;
|
|
}
|
|
return override.mode;
|
|
}
|
|
buildRendererSelectionState(file, leaf, 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.rendererOverridesByLeaf.set(leaf, { filePath: file.path, mode });
|
|
void this.showPreviewForFile(file, leaf, false, "renderer-switch", {
|
|
managePreviewLeaf: false
|
|
});
|
|
}
|
|
};
|
|
}
|
|
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 transitionDisplay = this.formatReferenceDisplay(transition);
|
|
const currentScreenId = model.id?.trim();
|
|
const currentScreenName = model.name?.trim();
|
|
const currentScreenBasename = this.getPathBasename(model.path);
|
|
const isSelfTransition = !resolvedModel && (transition.trim() === model.path || Boolean(currentScreenId && transitionDisplay === currentScreenId) || transitionDisplay === currentScreenBasename);
|
|
const targetPath = resolvedModel?.path ?? (isSelfTransition ? model.path : void 0);
|
|
const targetLabel = resolvedModel?.name?.trim() || resolvedModel?.id?.trim() || (isSelfTransition ? currentScreenName || currentScreenId || currentScreenBasename : transitionDisplay) || transition;
|
|
const targetTitle = targetPath ? `${targetLabel}
|
|
${targetPath}` : `${targetLabel}
|
|
${transition}`;
|
|
const targetLinktext = targetPath ? resolvedModel?.id?.trim() || (isSelfTransition ? currentScreenId || model.path : transition) : void 0;
|
|
const key = targetPath ? `path:${targetPath}` : `raw:${transition}`;
|
|
const group = groups.get(key) ?? {
|
|
key,
|
|
targetLabel,
|
|
targetTitle,
|
|
targetPath,
|
|
targetLinktext,
|
|
sourcePath: model.path,
|
|
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: "Unassigned",
|
|
subtitle: "Layout is missing or undefined",
|
|
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 domainRows = this.readTableRows(filePath, "Domains");
|
|
const domainSourceRows = this.readTableRows(filePath, "Domain Sources");
|
|
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", "domain", "lane", "label", "kind", "input", "output", "rule", "invoke", "screen", "notes"],
|
|
rows: stepRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
row.record.domain ?? "",
|
|
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
|
|
}))
|
|
});
|
|
}
|
|
if (domainSourceRows.length > 0) {
|
|
tables.push({
|
|
title: "Domain Sources Summary",
|
|
columns: ["ref", "notes"],
|
|
rows: domainSourceRows.map((row) => ({
|
|
cells: [
|
|
this.formatReferenceDisplay(row.record.ref),
|
|
row.record.notes ?? ""
|
|
],
|
|
line: row.line,
|
|
ch: row.ch
|
|
}))
|
|
});
|
|
}
|
|
if (domainRows.length > 0) {
|
|
tables.push({
|
|
title: "Domains Summary",
|
|
columns: ["id", "name", "kind", "parent", "description"],
|
|
rows: domainRows.map((row) => ({
|
|
cells: [
|
|
row.record.id ?? "",
|
|
row.record.name ?? "",
|
|
row.record.kind ?? "",
|
|
row.record.parent ?? "",
|
|
row.record.description ?? ""
|
|
],
|
|
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, managePreviewLeaf = true) {
|
|
const leaf = preferredLeaf ?? await this.findOrCreatePreviewLeaf();
|
|
await leaf.setViewState({
|
|
type: MODELING_PREVIEW_VIEW_TYPE,
|
|
active: activate
|
|
});
|
|
if (managePreviewLeaf) {
|
|
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) {
|
|
const rowIndex = getDiagnosticRowIndex(diagnostic);
|
|
if (typeof rowIndex === "number") {
|
|
const rowLine = findDiagnosticTableRowLine(lines, sectionLine, rowIndex);
|
|
if (rowLine >= 0) {
|
|
return rowLine;
|
|
}
|
|
}
|
|
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"
|
|
};
|
|
if (fieldToSection[field]) {
|
|
return fieldToSection[field];
|
|
}
|
|
const fieldSection = field.split(".")[0]?.trim();
|
|
return fieldSection || null;
|
|
}
|
|
function getDiagnosticRowIndex(diagnostic) {
|
|
const rawRowIndex = diagnostic.context?.rowIndex;
|
|
if (typeof rawRowIndex === "number" && Number.isFinite(rawRowIndex)) {
|
|
return rawRowIndex;
|
|
}
|
|
if (typeof rawRowIndex === "string") {
|
|
const parsed = Number.parseInt(rawRowIndex, 10);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
return null;
|
|
}
|
|
function findDiagnosticTableRowLine(lines, sectionLine, rowIndex) {
|
|
if (rowIndex < 1) {
|
|
return -1;
|
|
}
|
|
let tableRow = 0;
|
|
let tableStarted = false;
|
|
for (let index = sectionLine + 1; index < lines.length; index += 1) {
|
|
const trimmed = lines[index].trim();
|
|
if (trimmed.startsWith("## ")) {
|
|
return -1;
|
|
}
|
|
if (!trimmed.startsWith("|")) {
|
|
continue;
|
|
}
|
|
tableStarted = true;
|
|
if (/^\|?\s*:?-{3,}:?/.test(trimmed)) {
|
|
continue;
|
|
}
|
|
tableRow += 1;
|
|
if (tableRow === rowIndex + 1) {
|
|
return index;
|
|
}
|
|
}
|
|
return tableStarted ? sectionLine : -1;
|
|
}
|
|
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();
|
|
const t = createModelWeaveTranslator(settings.uiLanguage);
|
|
containerEl.empty();
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.uiLanguage.name")).setDesc(t("settings.uiLanguage.desc")).addDropdown((dropdown) => {
|
|
dropdown.addOption("auto", t("settings.option.auto")).addOption("en", t("settings.option.english")).addOption("ja", t("settings.option.japanese")).setValue(settings.uiLanguage).onChange(async (value) => {
|
|
if (!isUiLanguageOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
uiLanguage: value
|
|
});
|
|
this.display();
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.section.viewer")).setHeading();
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.defaultClassRenderMode.name")).setDesc(t("settings.defaultClassRenderMode.desc")).addDropdown((dropdown) => {
|
|
dropdown.addOption("custom", t("settings.option.custom")).addOption("mermaid", t("settings.option.mermaid")).addOption("mermaid-detail", t("settings.option.mermaidDetail")).setValue(settings.defaultClassRenderMode).onChange(async (value) => {
|
|
if (!isClassRenderModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultClassRenderMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.defaultErRenderMode.name")).setDesc(t("settings.defaultErRenderMode.desc")).addDropdown((dropdown) => {
|
|
dropdown.addOption("custom", t("settings.option.custom")).addOption("mermaid", t("settings.option.mermaid")).addOption("mermaid-detail", t("settings.option.mermaidDetail")).setValue(settings.defaultErRenderMode).onChange(async (value) => {
|
|
if (!isErRenderModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultErRenderMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.defaultDfdRenderMode.name")).setDesc(t("settings.defaultDfdRenderMode.desc")).addDropdown((dropdown) => {
|
|
dropdown.addOption("mermaid", t("settings.option.mermaid")).setValue(settings.defaultDfdRenderMode).onChange(async (value) => {
|
|
if (!isDfdRenderModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultDfdRenderMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.defaultProcessRenderMode.name")).setDesc(t("settings.defaultProcessRenderMode.desc")).addDropdown((dropdown) => {
|
|
dropdown.addOption("custom", t("settings.option.custom")).setValue(settings.defaultProcessRenderMode).onChange(async (value) => {
|
|
if (!isProcessRenderModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultProcessRenderMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.defaultScreenRenderMode.name")).setDesc(t("settings.defaultScreenRenderMode.desc")).addDropdown((dropdown) => {
|
|
dropdown.addOption("custom", t("settings.option.custom")).setValue(settings.defaultScreenRenderMode).onChange(async (value) => {
|
|
if (!isScreenRenderModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultScreenRenderMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.defaultDomainsViewMode.name")).setDesc(t("settings.defaultDomainsViewMode.desc")).addDropdown((dropdown) => {
|
|
for (const option of DOMAIN_VIEW_MODE_SETTING_OPTIONS) {
|
|
dropdown.addOption(option.value, option.label);
|
|
}
|
|
dropdown.setValue(settings.defaultDomainsViewMode).onChange(async (value) => {
|
|
if (!isDomainViewModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultDomainsViewMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.defaultDomainDiagramViewMode.name")).setDesc(t("settings.defaultDomainDiagramViewMode.desc")).addDropdown((dropdown) => {
|
|
for (const option of DOMAIN_VIEW_MODE_SETTING_OPTIONS) {
|
|
dropdown.addOption(option.value, option.label);
|
|
}
|
|
dropdown.setValue(settings.defaultDomainDiagramViewMode).onChange(async (value) => {
|
|
if (!isDomainViewModeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
defaultDomainDiagramViewMode: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.defaultZoom.name")).setDesc(t("settings.defaultZoom.desc")).addDropdown((dropdown) => {
|
|
dropdown.addOption("fit", t("settings.option.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(t("settings.fontSize.name")).setDesc(t("settings.fontSize.desc")).addDropdown((dropdown) => {
|
|
dropdown.addOption("small", t("settings.option.small")).addOption("normal", t("settings.option.normal")).addOption("large", t("settings.option.large")).setValue(settings.fontSize).onChange(async (value) => {
|
|
if (!isFontSizeOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
fontSize: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.nodeDensity.name")).setDesc(t("settings.nodeDensity.desc")).addDropdown((dropdown) => {
|
|
dropdown.addOption("compact", t("settings.option.compact")).addOption("normal", t("settings.option.normal")).addOption("relaxed", t("settings.option.relaxed")).setValue(settings.nodeDensity).onChange(async (value) => {
|
|
if (!isNodeDensityOption(value)) {
|
|
return;
|
|
}
|
|
await this.plugin.updateSettings({
|
|
nodeDensity: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.relationshipView.name")).setDesc(t("settings.relationshipView.desc")).addToggle((toggle) => {
|
|
toggle.setValue(settings.enableRelationshipView).onChange(async (value) => {
|
|
await this.plugin.updateSettings({
|
|
enableRelationshipView: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.showMermaidRenderDebug.name")).setDesc(t("settings.showMermaidRenderDebug.desc")).addToggle((toggle) => {
|
|
toggle.setValue(settings.showMermaidRenderDebug).onChange(async (value) => {
|
|
await this.plugin.updateSettings({
|
|
showMermaidRenderDebug: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.localSourceRoot.name")).setDesc(t("settings.localSourceRoot.desc")).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(t("settings.defaultColorScheme.name")).setDesc(t("settings.defaultColorScheme.desc")).addText((text) => {
|
|
text.setPlaceholder("[[color-scheme-default]]").setValue(settings.defaultColorSchemeRef ?? "").onChange(async (value) => {
|
|
await this.plugin.updateSettings({
|
|
defaultColorSchemeRef: value
|
|
});
|
|
});
|
|
});
|
|
new import_obsidian8.Setting(containerEl).setName(t("settings.refreshOpenViews.name")).setDesc(t("settings.refreshOpenViews.desc")).addButton((button) => {
|
|
button.setButtonText(t("settings.refreshOpenViews.button")).onClick(async () => {
|
|
await this.plugin.refreshOpenModelWeaveViews();
|
|
new import_obsidian8.Notice(t("settings.refreshOpenViews.notice"));
|
|
});
|
|
});
|
|
}
|
|
};
|