diff --git a/dist/datamodel/node/container/ProjectSectionNode.d.ts.map b/dist/datamodel/node/container/ProjectSectionNode.d.ts.map
index e0bc9af..1cc8394 100644
--- a/dist/datamodel/node/container/ProjectSectionNode.d.ts.map
+++ b/dist/datamodel/node/container/ProjectSectionNode.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"ProjectSectionNode.d.ts","sourceRoot":"","sources":["../../../../src/datamodel/node/container/ProjectSectionNode.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEjG,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,aAAa;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;gBAEH,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAMhF,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,iBAAiB,IAAI,iBAAiB,CAEzC;IAED,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,QAAQ;IAWzC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAUtC,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,QAAQ;IAS5C,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,GAAG,QAAQ;CAQrD"}
\ No newline at end of file
+{"version":3,"file":"ProjectSectionNode.d.ts","sourceRoot":"","sources":["../../../../src/datamodel/node/container/ProjectSectionNode.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjG,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,aAAa;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;gBAEH,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAMhF,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,iBAAiB,IAAI,iBAAiB,CAEzC;IAED,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,QAAQ;IAWzC,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAmBtC,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,QAAQ;IAS5C,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,GAAG,QAAQ;CAQrD"}
\ No newline at end of file
diff --git a/dist/datamodel/node/container/ProjectSectionNode.js b/dist/datamodel/node/container/ProjectSectionNode.js
index 03aae4e..9d72b30 100644
--- a/dist/datamodel/node/container/ProjectSectionNode.js
+++ b/dist/datamodel/node/container/ProjectSectionNode.js
@@ -1,6 +1,8 @@
// Copyright 2026 The MathWorks, Inc.
import ContainerNode from '../ContainerNode.js';
import ProjectItemNode from '../data/ProjectItemNode.js';
+/** What to call the project root in a list of path folders. See addPathEntry. */
+const ROOT_FOLDER_NAME = '(project root)';
export default class ProjectSectionNode extends ContainerNode {
constructor(name, parent, label, iconId) {
super(name, parent);
@@ -27,7 +29,15 @@ export default class ProjectSectionNode extends ContainerNode {
return node;
}
addPathEntry(folder) {
- const name = folder.split(/[/\\]/).filter((p) => p.length > 0).pop() || folder;
+ // The project ROOT is on the path, and the store spells it as the EMPTY
+ // string — so unlike every other entry it has no segment to be named by, and
+ // the usual "show the path itself" fallback leaves a blank row. It is
+ // normally the first folder MATLAB adds, so this is the common first row and
+ // not an edge case. Only '' is the root: a degenerate path like '/' is still
+ // shown verbatim, since that one at least says something.
+ const name = folder === ''
+ ? ROOT_FOLDER_NAME
+ : folder.split(/[/\\]/).filter((p) => p.length > 0).pop() || folder;
const node = new ProjectItemNode(name, this, {
itemType: 'Path Folder',
location: folder,
diff --git a/dist/datamodel/node/container/ProjectSectionNode.js.map b/dist/datamodel/node/container/ProjectSectionNode.js.map
index 7c4e2e2..de0d440 100644
--- a/dist/datamodel/node/container/ProjectSectionNode.js.map
+++ b/dist/datamodel/node/container/ProjectSectionNode.js.map
@@ -1 +1 @@
-{"version":3,"file":"ProjectSectionNode.js","sourceRoot":"","sources":["../../../../src/datamodel/node/container/ProjectSectionNode.ts"],"names":[],"mappings":"AAAA,qCAAqC;AAErC,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAEhD,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAIzD,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,aAAa;IAIzD,YAAY,IAAY,EAAE,MAAuB,EAAE,KAAa,EAAE,MAAc;QAC5E,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAED,IAAI,IAAI;QACJ,OAAO,IAAI,CAAC,MAAM,CAAC;IACvB,CAAC;IAED,IAAI,WAAW;QACX,OAAO,IAAI,CAAC,KAAK,CAAC;IACtB,CAAC;IAED,IAAI,iBAAiB;QACjB,OAAO,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;IAC/D,CAAC;IAED,YAAY,CAAC,IAAiB;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC;QACzD,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE;YACzC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM;YAC3C,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;SACtB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,YAAY,CAAC,MAAc;QACvB,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC;QAC/E,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE;YACzC,QAAQ,EAAE,aAAa;YACvB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,aAAa,CAAC,KAAmB;QAC7B,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE;YAC/C,QAAQ,EAAE,OAAO;YACjB,QAAQ,EAAE,KAAK,CAAC,QAAQ;SAC3B,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,iBAAiB,CAAC,GAAqB;QACnC,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE;YACvD,QAAQ,EAAE,WAAW;YACrB,QAAQ,EAAE,GAAG,CAAC,EAAE;SACnB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IAChB,CAAC;CACJ"}
\ No newline at end of file
+{"version":3,"file":"ProjectSectionNode.js","sourceRoot":"","sources":["../../../../src/datamodel/node/container/ProjectSectionNode.ts"],"names":[],"mappings":"AAAA,qCAAqC;AAErC,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAEhD,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAIzD,iFAAiF;AACjF,MAAM,gBAAgB,GAAG,gBAAgB,CAAC;AAE1C,MAAM,CAAC,OAAO,OAAO,kBAAmB,SAAQ,aAAa;IAIzD,YAAY,IAAY,EAAE,MAAuB,EAAE,KAAa,EAAE,MAAc;QAC5E,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAED,IAAI,IAAI;QACJ,OAAO,IAAI,CAAC,MAAM,CAAC;IACvB,CAAC;IAED,IAAI,WAAW;QACX,OAAO,IAAI,CAAC,KAAK,CAAC;IACtB,CAAC;IAED,IAAI,iBAAiB;QACjB,OAAO,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;IAC/D,CAAC;IAED,YAAY,CAAC,IAAiB;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC;QACzD,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE;YACzC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM;YAC3C,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;SACtB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,YAAY,CAAC,MAAc;QACvB,wEAAwE;QACxE,6EAA6E;QAC7E,sEAAsE;QACtE,6EAA6E;QAC7E,6EAA6E;QAC7E,0DAA0D;QAC1D,MAAM,IAAI,GACN,MAAM,KAAK,EAAE;YACT,CAAC,CAAC,gBAAgB;YAClB,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC;QAC5E,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE;YACzC,QAAQ,EAAE,aAAa;YACvB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,aAAa,CAAC,KAAmB;QAC7B,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE;YAC/C,QAAQ,EAAE,OAAO;YACjB,QAAQ,EAAE,KAAK,CAAC,QAAQ;SAC3B,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,iBAAiB,CAAC,GAAqB;QACnC,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE;YACvD,QAAQ,EAAE,WAAW;YACrB,QAAQ,EAAE,GAAG,CAAC,EAAE;SACnB,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IAChB,CAAC;CACJ"}
\ No newline at end of file
diff --git a/dist/datamodel/parser/ProjectPage.d.ts b/dist/datamodel/parser/ProjectPage.d.ts
new file mode 100644
index 0000000..6ffbfa2
--- /dev/null
+++ b/dist/datamodel/parser/ProjectPage.d.ts
@@ -0,0 +1,79 @@
+import type { ParsedProject } from './ProjectParser.js';
+import type { ParseWarning } from './ParseWarning.js';
+/** One file MATLAB runs for a hook. Position in the list IS the run order. */
+export interface ProjectPageRun {
+ name: string;
+ /** Project-root-relative path. */
+ file: string;
+}
+/** One entry in the Shortcuts gallery. */
+export interface ProjectPageShortcut {
+ name: string;
+ /** Project-root-relative path. May name a FOLDER. */
+ file: string;
+ /** The group's display name, '' when ungrouped. */
+ group: string;
+}
+/** One label, with how much of the project carries it. */
+export interface ProjectPageLabel {
+ id: string;
+ name: string;
+ /** Members carrying this label. 0 is meaningful — a defined but unused label. */
+ count: number;
+ /** Defined by this project rather than shipped by MATLAB. */
+ custom: boolean;
+}
+export interface ProjectPageCategory {
+ name: string;
+ labels: ProjectPageLabel[];
+}
+/** A folder — or file — the project designates for a purpose. */
+export interface ProjectPageLocation {
+ /** The store's own key, e.g. 'SimulinkCacheFolder'. */
+ key: string;
+ /** English for `key`, falling back to `key` itself when unrecognized. */
+ label: string;
+ /** Project-root-relative path. */
+ ref: string;
+}
+export interface ProjectPageReference {
+ name: string;
+ /**
+ * Where the referenced project lives, relative to this project's root — so
+ * typically `../OtherProject/other.prj`. '' when the store recorded no path, which
+ * is a reference a host cannot open and should not offer a link for.
+ */
+ path: string;
+}
+export interface ProjectPage {
+ name: string;
+ /** The raw `MetadataType`, for a host that wants to report it exactly. */
+ format: string;
+ /** English for `format` — what a MATLAB user sees in Project Settings. */
+ formatLabel: string;
+ /** Every member, folders included: what the project "contains". */
+ memberCount: number;
+ /** Members carrying at least one label — the numerator of label coverage. */
+ labelledCount: number;
+ /** In run order. */
+ startup: ProjectPageRun[];
+ /** In run order. */
+ shutdown: ProjectPageRun[];
+ shortcuts: ProjectPageShortcut[];
+ /** Project-root-relative; '' is the project root itself. */
+ pathFolders: string[];
+ locations: ProjectPageLocation[];
+ categories: ProjectPageCategory[];
+ references: ProjectPageReference[];
+ warnings: ParseWarning[];
+}
+/**
+ * Project a parsed project into its page.
+ *
+ * Pure and total: every field is derived from `parsed`, and a project that holds
+ * nothing yields a page of empty sections rather than a missing one — the sections
+ * are what tell a user "this project defines no shortcuts", which is a fact worth
+ * showing and not the same as a page that failed to render.
+ */
+export declare function buildProjectPage(parsed: ParsedProject): ProjectPage;
+//# sourceMappingURL=ProjectPage.d.ts.map
\ No newline at end of file
diff --git a/dist/datamodel/parser/ProjectPage.d.ts.map b/dist/datamodel/parser/ProjectPage.d.ts.map
new file mode 100644
index 0000000..2787795
--- /dev/null
+++ b/dist/datamodel/parser/ProjectPage.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"ProjectPage.d.ts","sourceRoot":"","sources":["../../../src/datamodel/parser/ProjectPage.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,aAAa,EAAqB,MAAM,oBAAoB,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0CAA0C;AAC1C,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,0DAA0D;AAC1D,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC5B;AAED,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,WAAW,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB;IACpB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,oBAAoB;IACpB,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,SAAS,EAAE,mBAAmB,EAAE,CAAC;IACjC,4DAA4D;IAC5D,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,mBAAmB,EAAE,CAAC;IACjC,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAClC,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACnC,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAmCD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAgCnE"}
\ No newline at end of file
diff --git a/dist/datamodel/parser/ProjectPage.js b/dist/datamodel/parser/ProjectPage.js
new file mode 100644
index 0000000..2af1805
--- /dev/null
+++ b/dist/datamodel/parser/ProjectPage.js
@@ -0,0 +1,170 @@
+// Copyright 2026 The MathWorks, Inc.
+//
+// The view model behind a project's main page.
+//
+// A `.prj` is not a table. MATLAB itself opens NO document tab for one — its
+// Project panel *is* the file panel — and the folder tree a table would show is
+// already in VS Code's Explorer, so repeating it costs a tab and says nothing new.
+// What is NOT anywhere else is everything around the file list: which files run on
+// open and close and IN WHICH ORDER, the shortcuts and their groups, which folders
+// are on the MATLAB path, where the cache and generated code go, which labels this
+// project defines and how much of it they cover. That is what a page shows.
+//
+// WHY THIS LIVES BESIDE THE PARSER. It is a projection of `ParsedProject` and needs
+// its types; `datamodel/display/` is pinned by `moduleBoundaries.test.ts` to have
+// ZERO outbound edges, so it cannot be there. It is also not presentation: the
+// grouping, the run-order split, the label usage counts and the coverage figure are
+// all facts about the store, derived once here so that every host computes them the
+// same way and so they are testable without a webview.
+/**
+ * English for a working-folder key.
+ *
+ * Deliberately not exhaustive, and the fallback is the raw key: a newer release can
+ * designate a purpose this version has no word for, and showing `key` is honest
+ * where dropping the row or printing 'Unknown' is not.
+ */
+const LOCATION_LABEL = {
+ SimulinkCacheFolder: 'Simulation cache',
+ SimulinkCodeGenFolder: 'Code generation',
+ ProjectStartupFolder: 'Startup folder',
+ DependencyCacheFile: 'Dependency cache',
+};
+/**
+ * English for a `MetadataType`.
+ *
+ * These are the words MATLAB's own Project Settings uses, not the store's spelling:
+ * `fixedPathV2` and `distributed` are two encodings of the same USER choice ("save
+ * as multiple XML files"), and a page that printed the internal name would invite a
+ * user to look for a setting that is not spelled that way anywhere in MATLAB.
+ */
+const FORMAT_LABEL = {
+ fixedPathV2: 'multiple XML files',
+ distributed: 'multiple XML files',
+ monolithic: 'single XML file',
+ toml: 'matlab.toml',
+};
+/** The entry-point kinds MATLAB runs, as opposed to the ones a user clicks. */
+const STARTUP = 'StartUp';
+const SHUTDOWN = 'Shutdown';
+/**
+ * Project a parsed project into its page.
+ *
+ * Pure and total: every field is derived from `parsed`, and a project that holds
+ * nothing yields a page of empty sections rather than a missing one — the sections
+ * are what tell a user "this project defines no shortcuts", which is a fact worth
+ * showing and not the same as a page that failed to render.
+ */
+export function buildProjectPage(parsed) {
+ const runsOf = (kind) => parsed.entryPoints
+ .filter((e) => e.kind === kind)
+ // `parsed.entryPoints` is already in run order and `filter` preserves it, so
+ // there is deliberately no sort here — see ParsedProject.entryPoints.
+ .map((e) => ({ name: e.name, file: e.file }));
+ const groupNames = new Map(parsed.entryPointGroups.map((g) => [g.id, g.name]));
+ return {
+ name: parsed.name,
+ format: parsed.format,
+ formatLabel: FORMAT_LABEL[parsed.format] ?? parsed.format,
+ memberCount: parsed.files.length,
+ labelledCount: parsed.files.filter((f) => f.labels.length > 0).length,
+ startup: runsOf(STARTUP),
+ shutdown: runsOf(SHUTDOWN),
+ shortcuts: shortcutsOf(parsed.entryPoints, groupNames),
+ pathFolders: parsed.pathFolders,
+ locations: parsed.workingFolders.map((w) => ({
+ key: w.key,
+ label: LOCATION_LABEL[w.key] ?? w.key,
+ ref: w.ref,
+ })),
+ categories: categoriesOf(parsed),
+ // The id is deliberately NOT the fallback target: it is a UUID, and a page that
+ // linked it would offer to open a path that cannot exist. A reference the store
+ // gave no path for is shown by name alone.
+ references: parsed.references.map((r) => ({ name: r.name ?? r.id, path: r.path ?? '' })),
+ warnings: parsed.warnings,
+ };
+}
+/**
+ * The Shortcuts gallery: every entry point that is not a hook.
+ *
+ * Selected by EXCLUDING the two run kinds rather than by asking for 'Basic'. A
+ * newer release can add a shortcut kind, and an unknown kind is far better shown in
+ * the gallery — where it is at worst grouped oddly — than silently absent from a
+ * page whose whole job is to list what this project put there.
+ */
+function shortcutsOf(entryPoints, groupNames) {
+ const out = [];
+ for (const e of entryPoints) {
+ if (e.kind === STARTUP || e.kind === SHUTDOWN) {
+ continue;
+ }
+ // `visible` is MATLAB's own answer to "does this belong in the gallery", so a
+ // hidden entry of any other kind stays out of it.
+ if (!e.visible) {
+ continue;
+ }
+ out.push({
+ name: e.name,
+ file: e.file,
+ // A groupId naming no group falls back to ungrouped rather than to the raw
+ // UUID: a store can outlive the group it points at, and a gallery heading
+ // reading `a0ea675a-85ba-…` is worse than no heading.
+ group: groupNames.get(e.groupId) ?? '',
+ });
+ }
+ // Grouped shortcuts first, so a host rendering group headings emits each heading
+ // once. Order WITHIN a group, and among the ungrouped, stays the store's.
+ return [...out.filter((s) => s.group), ...out.filter((s) => !s.group)];
+}
+/**
+ * The label catalog with usage counts, grouped by category.
+ *
+ * Counted from the members' own assignments rather than from the catalog, because
+ * the two can disagree: a file may carry a label id the catalog does not define
+ * (written by a release that knew it, or by a hand-edited store). Such an id is
+ * counted under a category named for it, so the coverage figure stays consistent
+ * with `labelledCount` instead of quietly excluding files it includes.
+ */
+function categoriesOf(parsed) {
+ const counts = new Map();
+ for (const file of parsed.files) {
+ // Per file, not per assignment: `parsed.files` already de-duplicates a file's
+ // labels, so each hit here is one member carrying one label.
+ for (const id of file.labels) {
+ counts.set(id, (counts.get(id) ?? 0) + 1);
+ }
+ }
+ const byCategory = new Map();
+ const push = (category, label) => {
+ const list = byCategory.get(category);
+ if (list) {
+ list.push(label);
+ }
+ else {
+ byCategory.set(category, [label]);
+ }
+ };
+ const defined = new Set();
+ for (const l of parsed.labels) {
+ defined.add(l.id);
+ push(l.category, {
+ id: l.id,
+ name: l.name,
+ count: counts.get(l.id) ?? 0,
+ custom: !l.readOnly,
+ });
+ }
+ for (const [id, count] of counts) {
+ if (!defined.has(id)) {
+ push('', { id, name: id, count, custom: true });
+ }
+ }
+ // Used labels first, then by name: the seven built-ins ship with every project
+ // and most stay at zero, so catalog order buries the two or three that say
+ // something about THIS project under five that say nothing about any.
+ for (const labels of byCategory.values()) {
+ labels.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
+ }
+ return [...byCategory.entries()].map(([name, labels]) => ({ name, labels }));
+}
+//# sourceMappingURL=ProjectPage.js.map
\ No newline at end of file
diff --git a/dist/datamodel/parser/ProjectPage.js.map b/dist/datamodel/parser/ProjectPage.js.map
new file mode 100644
index 0000000..4d71002
--- /dev/null
+++ b/dist/datamodel/parser/ProjectPage.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"ProjectPage.js","sourceRoot":"","sources":["../../../src/datamodel/parser/ProjectPage.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/dist/datamodel/parser/ProjectParser.d.ts b/dist/datamodel/parser/ProjectParser.d.ts
index 463044c..1ce648a 100644
--- a/dist/datamodel/parser/ProjectParser.d.ts
+++ b/dist/datamodel/parser/ProjectParser.d.ts
@@ -1,6 +1,7 @@
import { type ParseWarning } from './ParseWarning.js';
/** A member file (or folder) of the project. */
export interface ProjectFile {
+ /** POSIX path relative to the project ROOT, not to the parent entity. */
path: string;
isFolder: boolean;
/** Label UUIDs assigned to this file (e.g. 'design'). */
@@ -12,19 +13,98 @@ export interface ProjectLabel {
id: string;
category: string;
name: string;
+ /**
+ * Whether MATLAB owns this label. The seven Classification labels ship with
+ * every project and are marked read-only in the store; a label the project
+ * itself added is not, which is the only thing distinguishing the two.
+ */
+ readOnly: boolean;
}
/** A project-to-project reference. */
export interface ProjectReference {
id: string;
name: string | null;
+ /**
+ * Where the referenced project lives, as the store spells it — relative to THIS
+ * project's root, so typically `../OtherProject/other.prj`. Null when the store
+ * recorded no path, which leaves `id` (a UUID) as all that is known about it.
+ *
+ * Kept alongside `name` rather than folded into it: `name` is the basename, which
+ * is what a user reads, and this is what a host needs to actually open the thing.
+ * Deriving one from the other only works in that direction.
+ */
+ path: string | null;
+}
+/**
+ * A registered entry point: a shortcut, or a file MATLAB runs on open/close.
+ *
+ * One store collection covers all three, separated only by `kind`, which is why
+ * they are one type here. `prev` is deliberately NOT exposed: the store encodes run
+ * order as a linked list over these entries, and `entryPoints` is already in that
+ * order (see `orderEntryPoints`), so a consumer never has to know the mechanism.
+ */
+export interface ProjectEntryPoint {
+ /** The entry point's own UUID (its pointer `location`). */
+ id: string;
+ name: string;
+ /** Project-root-relative path. May name a FOLDER — a shortcut can open one. */
+ file: string;
+ /** 'Basic' (a shortcut), 'StartUp', 'Shutdown', or whatever a newer release writes. */
+ kind: string;
+ /** Whether MATLAB shows this in its Shortcuts gallery. Startup/shutdown files are hidden. */
+ visible: boolean;
+ /** The EntryPointGroup id; '' when ungrouped (the store spells that 'default'). */
+ groupId: string;
+}
+/** A named group in the Shortcuts gallery. */
+export interface ProjectEntryPointGroup {
+ id: string;
+ name: string;
+}
+/**
+ * A folder (or file) the project designates for a particular purpose — simulation
+ * cache, code generation, startup folder, dependency cache.
+ *
+ * `key` is the store's own name for the purpose (`SimulinkCacheFolder`,
+ * `SimulinkCodeGenFolder`, `ProjectStartupFolder`, `DependencyCacheFile`) and is
+ * NOT translated here: a newer release may designate something this version has no
+ * label for, and passing the raw key on lets a host show it rather than drop it.
+ * Note the last of those four names a FILE, so these are not all folders.
+ */
+export interface ProjectWorkingFolder {
+ key: string;
+ /** Project-root-relative path. */
+ ref: string;
}
export interface ParsedProject {
name: string;
+ /**
+ * The store's declared `MetadataType` — how the project's metadata is laid out on
+ * disk. '' when the store declares none and the layout could not be inferred.
+ * Two are read: `fixedPathV2` and `distributed`. Anything else is reported through
+ * `warnings` rather than read, because guessing at a layout produces a project
+ * that looks complete and is not.
+ */
+ format: string;
files: ProjectFile[];
+ /**
+ * Folders added to the MATLAB path, project-root-relative. The project ROOT
+ * itself is spelled '' — the store records it as `Ref=""`, and it is a real entry
+ * (usually the first one MATLAB adds), so dropping it would under-report the path.
+ */
pathFolders: string[];
/** The catalog of labels defined in the project. */
labels: ProjectLabel[];
references: ProjectReference[];
+ /**
+ * Shortcuts and startup/shutdown files, in this order: startup files in RUN
+ * order, then shutdown files in run order, then everything else (shortcuts) in
+ * store order. Run order is the store's, which is meaningful — MATLAB runs the
+ * files top-down — and is not recoverable from any other field.
+ */
+ entryPoints: ProjectEntryPoint[];
+ entryPointGroups: ProjectEntryPointGroup[];
+ workingFolders: ProjectWorkingFolder[];
/**
* What could not be read, empty when everything could. ALWAYS an array: this
* reader has the channel, so a caller may read `.length` without guarding, and
diff --git a/dist/datamodel/parser/ProjectParser.d.ts.map b/dist/datamodel/parser/ProjectParser.d.ts.map
index f2be42d..ca6b49a 100644
--- a/dist/datamodel/parser/ProjectParser.d.ts.map
+++ b/dist/datamodel/parser/ProjectParser.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"ProjectParser.d.ts","sourceRoot":"","sources":["../../../src/datamodel/parser/ProjectParser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEhE,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,yDAAyD;IACzD,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,sCAAsC;AACtC,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,oDAAoD;IACpD,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAyCD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,aAAa,CA2H9F"}
\ No newline at end of file
+{"version":3,"file":"ProjectParser.d.ts","sourceRoot":"","sources":["../../../src/datamodel/parser/ProjectParser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEhE,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,yDAAyD;IACzD,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,sCAAsC;AACtC,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB;;;;;;;;OAQG;IACH,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,OAAO,EAAE,OAAO,CAAC;IACjB,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,8CAA8C;AAC9C,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB;;;;OAIG;IACH,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,oDAAoD;IACpD,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,UAAU,EAAE,gBAAgB,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,gBAAgB,EAAE,sBAAsB,EAAE,CAAC;IAC3C,cAAc,EAAE,oBAAoB,EAAE,CAAC;IACvC;;;;OAIG;IACH,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AA8GD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,aAAa,CAqJ9F"}
\ No newline at end of file
diff --git a/dist/datamodel/parser/ProjectParser.js b/dist/datamodel/parser/ProjectParser.js
index 0b0c44d..81a6755 100644
--- a/dist/datamodel/parser/ProjectParser.js
+++ b/dist/datamodel/parser/ProjectParser.js
@@ -2,12 +2,52 @@
import { readProjectXml } from './XmlReader.js';
import { reasonOf } from './ParseWarning.js';
const PROJECT_PREFIX = 'resources/project/';
+/** The store manifest, which declares the layout the rest of the store is in. */
+const MANIFEST = 'Project.xml';
+/** The layouts this reader walks. See `ParsedProject.format`. */
+const FIXED_PATH_V2 = 'fixedPathV2';
+const DISTRIBUTED = 'distributed';
+/**
+ * Collection types whose `type="Reference"` children are NOT project references.
+ *
+ * A project->project reference and a path folder and a working folder are all
+ * spelled `type="Reference"`, distinguished only by which collection they sit in.
+ * So this is a DENY-list rather than an allow-list of reference collections, and
+ * deliberately: the collection holding real references is spelled differently
+ * across releases, and an allow-list missing a spelling silently drops references,
+ * whereas this list — the collections whose meaning we positively model — cannot.
+ * Before it existed, every WorkingFolders entry was reported as a project
+ * reference, which INVENTED references a project does not have.
+ */
+const NOT_REFERENCE_COLLECTIONS = new Set([
+ 'ProjectPath',
+ 'WorkingFolders',
+ 'Categories',
+ 'Files',
+ 'EntryPoints',
+ 'EntryPointGroups',
+ 'Info',
+]);
function toArray(v) {
if (v === undefined || v === null) {
return [];
}
return Array.isArray(v) ? v : [v];
}
+function emptyResult(name, warnings) {
+ return {
+ name,
+ format: '',
+ files: [],
+ pathFolders: [],
+ labels: [],
+ references: [],
+ entryPoints: [],
+ entryPointGroups: [],
+ workingFolders: [],
+ warnings,
+ };
+}
/**
* Parse a MATLAB/Simulink Project content store.
*
@@ -19,14 +59,7 @@ function toArray(v) {
*/
export function parseProject(files, projectName) {
const warnings = [];
- const result = {
- name: projectName,
- files: [],
- pathFolders: [],
- labels: [],
- references: [],
- warnings,
- };
+ const result = emptyResult(projectName, warnings);
try {
// Index every parseable Info doc by its relpath (project-relative).
const index = new Map();
@@ -47,64 +80,99 @@ export function parseProject(files, projectName) {
// Every collection below reads out of this index, so an empty one means the
// whole result is empty — and a `.prj` always has a store, so reaching here
// is either the wrong kind of file or a store that did not survive its trip.
+ //
+ // Except for one case worth naming separately: a store MATLAB wrote in its
+ // `matlab.toml` format holds no XML at all, so it lands here looking exactly
+ // like damage. It is not damaged, it is a format this reader does not read,
+ // and a user told "nothing readable" would go looking for a corrupt file.
+ const toml = Object.keys(files).find((k) => k.startsWith(PROJECT_PREFIX) && k.slice(PROJECT_PREFIX.length).endsWith('matlab.toml'));
+ warnings.push(toml
+ ? {
+ // 'source-empty' rather than a code of its own: the code is the kind of
+ // loss (the source opened and held nothing this reader recognizes) and
+ // the message is which, per ParseWarningCode's note that the codes are
+ // about containers and parts and not about any one format.
+ code: 'source-empty',
+ message: 'This project stores its metadata as matlab.toml, which this viewer cannot read yet, ' +
+ 'so this project reads as empty. In MATLAB, Project Settings can save it as XML instead.',
+ part: toml,
+ }
+ : {
+ code: 'source-empty',
+ message: 'No readable project entries were found under resources/project/, ' +
+ 'so this project reads as empty.',
+ });
+ return result;
+ }
+ // The manifest declares which layout the rest of the store is in. A store that
+ // declares one we cannot walk is reported and NOT guessed at: the collection
+ // readers below would find nothing in it and return a project that looks
+ // complete and empty, which is the one outcome a user cannot tell from a fact.
+ const declared = index.get(MANIFEST)?.['@_MetadataType'] ?? '';
+ const layoutName = declared || inferLayout(index);
+ if (layoutName !== FIXED_PATH_V2 && layoutName !== DISTRIBUTED) {
+ // The name is still worth salvaging — it is what a host titles the view with,
+ // and it reads out of the index without knowing the layout.
+ result.name = salvageName(index) ?? projectName;
warnings.push({
code: 'source-empty',
- message: 'No readable project entries were found under resources/project/, ' +
- 'so this project reads as empty.',
+ message: declared
+ ? `This project's metadata is stored as "${declared}", which this viewer cannot read yet, ` +
+ 'so this project reads as empty. In MATLAB, Project Settings can save it as multiple XML files instead.'
+ : 'The layout of this project store was not recognized, so this project reads as empty.',
+ part: PROJECT_PREFIX + MANIFEST,
});
return result;
}
- // The `root/` directory holds the top-level entry pointers.
- const rootEntities = readDir(index, 'root');
- // Project name: the def whose pointer has location="ProjectData" type="Info".
+ result.format = layoutName;
+ const layout = layoutName === FIXED_PATH_V2 ? fixedPathV2Layout(index) : distributedLayout(index);
+ const rootEntities = layout.roots();
+ result.name = resolveNameFrom(rootEntities) ?? projectName;
+ // Collections are matched on type AND location. Two collections of one type can
+ // coexist — a real project carries both `location="Root" type="Files"` (its
+ // members) and `location="ALM" type="Files"` (artifact tracking) — so matching
+ // on type alone assigned the field twice and let store iteration order pick the
+ // winner. `Root` is the project's own; anything else belongs to a feature we do
+ // not model, and reading it as the member list is simply wrong.
for (const ent of rootEntities) {
- if (ent.pointer?.['@_location'] === 'ProjectData' && ent.pointer?.['@_type'] === 'Info') {
- const name = ent.def?.['@_Name'];
- if (name) {
- result.name = name;
- }
+ if (ent.location !== 'Root') {
+ continue;
}
- }
- // Some stores also carry the name on a bare def whose
- // pointer has no matching location; take it if we still have the fallback.
- if (result.name === projectName) {
- for (const ent of rootEntities) {
- const name = ent.def?.['@_Name'];
- if (name && !ent.pointer?.['@_type']) {
- result.name = name;
- break;
- }
+ if (ent.type === 'Files') {
+ result.files = readFiles(layout, ent);
}
- }
- for (const ent of rootEntities) {
- const type = ent.pointer?.['@_type'];
- if (type === 'Files') {
- result.files = readFiles(index, ent.hash);
+ else if (ent.type === 'ProjectPath') {
+ result.pathFolders = readPathFolders(layout, ent);
+ }
+ else if (ent.type === 'Categories') {
+ result.labels = readCategories(layout, ent);
+ }
+ else if (ent.type === 'EntryPoints') {
+ result.entryPoints = orderEntryPoints(readEntryPoints(layout, ent));
}
- else if (type === 'ProjectPath') {
- result.pathFolders = readPathFolders(index, ent.hash);
+ else if (ent.type === 'EntryPointGroups') {
+ result.entryPointGroups = readEntryPointGroups(layout, ent);
}
- else if (type === 'Categories') {
- result.labels = readCategories(index, ent.hash);
+ else if (ent.type === 'WorkingFolders') {
+ result.workingFolders = readWorkingFolders(layout, ent);
}
- else if (type === 'Reference') {
- // A genuine project->project reference living directly in root.
+ }
+ for (const ent of rootEntities) {
+ // A genuine project->project reference living directly in root.
+ if (ent.type === 'Reference') {
const ref = resolveReference(ent);
if (ref) {
result.references.push(ref);
}
+ continue;
}
- }
- // References may also live in their own top-level collection outside the
- // ProjectPath collection. Scan any root entry whose dir contains
- // type="Reference" children (but skip the ProjectPath collection itself).
- for (const ent of rootEntities) {
- if (ent.pointer?.['@_type'] === 'ProjectPath') {
+ // …or in its own collection. See NOT_REFERENCE_COLLECTIONS for why the set of
+ // collections to skip is spelled out rather than the set to read.
+ if (NOT_REFERENCE_COLLECTIONS.has(ent.type)) {
continue;
}
- const children = readDir(index, ent.hash);
- for (const child of children) {
- if (child.pointer?.['@_type'] === 'Reference') {
+ for (const child of layout.children(ent)) {
+ if (child.type === 'Reference') {
const ref = resolveReference(child);
if (ref) {
result.references.push(ref);
@@ -126,15 +194,69 @@ export function parseProject(files, projectName) {
code: 'source-unreadable',
message: `The project store could not be read (${reasonOf(err)}), so this project reads as empty.`,
});
- return {
- name: projectName,
- files: [],
- pathFolders: [],
- labels: [],
- references: [],
- warnings,
- };
+ return emptyResult(projectName, warnings);
+ }
+}
+/**
+ * Which layout a store with no manifest is in.
+ *
+ * Every store this reader has seen carries `Project.xml`, so this is the fallback
+ * for one that lost it: `root/` is the entry directory `fixedPathV2` and nothing
+ * else uses, and a `.type.` in a top-level name is `distributed`'s own spelling.
+ */
+function inferLayout(index) {
+ for (const key of index.keys()) {
+ if (key.startsWith('root/')) {
+ return FIXED_PATH_V2;
+ }
+ }
+ for (const key of index.keys()) {
+ if (!key.includes('/') && key.includes('.type.')) {
+ return DISTRIBUTED;
+ }
+ }
+ return '';
+}
+/**
+ * The project name, for a store whose layout could not be walked.
+ *
+ * Kept separate from `resolveNameFrom` because it reads the index directly rather
+ * than the entity tree — there is no tree in this case — and every layout seen so
+ * far spells the document holding the name recognizably enough to find it without
+ * knowing which layout this is.
+ */
+function salvageName(index) {
+ for (const [key, info] of index.entries()) {
+ if (key === MANIFEST) {
+ continue;
+ }
+ // `distributed` names it ProjectData.type.Info.xml; `fixedPathV2` puts it in a
+ // hash-named def whose pointer says location="ProjectData".
+ if (key.includes('ProjectData') && info['@_Name']) {
+ return info['@_Name'];
+ }
+ }
+ return null;
+}
+/** The project name from the root entities, or null to keep the filename fallback. */
+function resolveNameFrom(rootEntities) {
+ for (const ent of rootEntities) {
+ if (ent.location === 'ProjectData' && ent.type === 'Info') {
+ const name = ent.def?.['@_Name'];
+ if (name) {
+ return name;
+ }
+ }
}
+ // Some stores also carry the name on a bare def whose
+ // pointer has no type at all.
+ for (const ent of rootEntities) {
+ const name = ent.def?.['@_Name'];
+ if (name && !ent.type) {
+ return name;
+ }
+ }
+ return null;
}
/**
* One `` document, or null when there is nothing here to index.
@@ -181,49 +303,141 @@ function parseInfo(content, relPath, warnings) {
}
return info;
}
+// ---------------------------------------------------------------------------------
+// The two layouts.
/**
- * Read all pointer/def entities in a directory (relative to resources/project/).
- * Groups files by hash/stem, treating `p`/`_sp` as pointers and `d`/`_sd` as defs.
+ * `fixedPathV2`: every entity is a POINTER/def pair of documents, and every child
+ * directory sits at the TOP of the store named by an opaque hash — flat, however
+ * deep the logical nesting goes, which is what "fixed path" means. The top-level
+ * entities live in `root/`.
*/
-function readDir(index, dir) {
- if (!dir) {
- return [];
- }
- const prefix = dir + '/';
- const byHash = new Map();
- for (const [relPath, info] of index.entries()) {
- if (!relPath.startsWith(prefix)) {
- continue;
- }
- const rest = relPath.slice(prefix.length);
- // Only immediate children (no further nesting).
- if (rest.includes('/')) {
- continue;
- }
- const parsed = parseChildName(rest);
- if (!parsed) {
- continue;
+function fixedPathV2Layout(index) {
+ const readDir = (dir) => {
+ if (!dir) {
+ return [];
}
- const { hash, isPointer } = parsed;
- let ent = byHash.get(hash);
- if (!ent) {
- ent = { hash, pointer: null, def: null };
- byHash.set(hash, ent);
+ const prefix = dir + '/';
+ const byHash = new Map();
+ for (const [relPath, info] of index.entries()) {
+ if (!relPath.startsWith(prefix)) {
+ continue;
+ }
+ const rest = relPath.slice(prefix.length);
+ // Only immediate children (no further nesting).
+ if (rest.includes('/')) {
+ continue;
+ }
+ const parsed = parseChildName(rest);
+ if (!parsed) {
+ continue;
+ }
+ const { hash, isPointer } = parsed;
+ let pair = byHash.get(hash);
+ if (!pair) {
+ pair = { pointer: null, def: null };
+ byHash.set(hash, pair);
+ }
+ if (isPointer) {
+ pair.pointer = info;
+ }
+ else {
+ pair.def = info;
+ }
}
- if (isPointer) {
- ent.pointer = info;
+ // Store order (Map preserves insertion), NOT sorted by hash: the hashes are
+ // opaque, so sorting on them would impose an order that means nothing while
+ // discarding the one the store was written in — which is the fallback order for
+ // entry points whose run-order chain is absent.
+ return [...byHash.entries()].map(([hash, pair]) => ({
+ dir: hash,
+ location: pair.pointer?.['@_location'] ?? '',
+ type: pair.pointer?.['@_type'] ?? '',
+ def: pair.def,
+ }));
+ };
+ return { roots: () => readDir('root'), children: (e) => readDir(e.dir) };
+}
+/**
+ * `distributed`: no pointer documents at all. An entity's location and type are its
+ * FILENAME — `.type..xml` for the def, `.type./`
+ * for the child directory — and directories nest the way the entities do. The
+ * top-level entities sit directly in the store root.
+ *
+ * A collection may exist as a directory with no def document beside it
+ * (`Root.type.Files/` without `Root.type.Files.xml`), so entities are discovered
+ * from directory names as well as from documents, and a missing def is normal here
+ * rather than a loss.
+ */
+function distributedLayout(index) {
+ const readDir = (dir) => {
+ const prefix = dir ? dir + '/' : '';
+ const byStem = new Map();
+ for (const [relPath, info] of index.entries()) {
+ if (!relPath.startsWith(prefix)) {
+ continue;
+ }
+ const rest = relPath.slice(prefix.length);
+ if (!rest) {
+ continue;
+ }
+ const slash = rest.indexOf('/');
+ if (slash === -1) {
+ // An immediate document: the entity's def.
+ const stem = rest.slice(0, -'.xml'.length);
+ if (!byStem.has(stem)) {
+ byStem.set(stem, info);
+ }
+ else if (byStem.get(stem) === null) {
+ byStem.set(stem, info);
+ }
+ }
+ else {
+ // Something deeper: the first segment is an entity's child DIRECTORY, and
+ // is how a collection with no def document of its own is found at all.
+ const stem = rest.slice(0, slash);
+ if (!byStem.has(stem)) {
+ byStem.set(stem, null);
+ }
+ }
}
- else {
- ent.def = info;
+ const out = [];
+ for (const [stem, def] of byStem) {
+ const split = splitTypedName(stem);
+ if (!split) {
+ continue;
+ }
+ out.push({ dir: prefix + stem, location: split.location, type: split.type, def });
}
+ return out;
+ };
+ return { roots: () => readDir(''), children: (e) => readDir(e.dir) };
+}
+/**
+ * Split a `distributed` stem into its location and type.
+ *
+ * The LAST `.type.` is the separator, not the first: a location is a filename and
+ * may contain the marker itself (a project file called `foo.type.File` yields the
+ * stem `foo.type.File.type.File`), and splitting on the first would truncate it.
+ * Null for a stem carrying no marker — `Project.xml` and the store's own uuid
+ * document are both of that shape, and neither is an entity.
+ */
+function splitTypedName(stem) {
+ const marker = '.type.';
+ const at = stem.lastIndexOf(marker);
+ if (at <= 0) {
+ return null;
}
- return [...byHash.values()];
+ const type = stem.slice(at + marker.length);
+ if (!type || type.includes('.')) {
+ return null;
+ }
+ return { location: stem.slice(0, at), type };
}
/**
- * Given a child filename like `8AEH..._sp.xml` or `qaw0...p.xml`, return the
- * hash (stem before the suffix) and whether it is a pointer. Null when the stem
- * carries none of the four recognized suffixes — a file in the store that is not
- * half of a pointer/def pair, which readDir skips.
+ * Given a `fixedPathV2` child filename like `8AEH..._sp.xml` or `qaw0...p.xml`,
+ * return the hash (stem before the suffix) and whether it is a pointer. Null when
+ * the stem carries none of the four recognized suffixes — a file in the store that
+ * is not half of a pointer/def pair, which readDir skips.
*
* PRECONDITION: `name` ends in `.xml`. parseProject's index only admits `.xml`
* paths, and readDir only asks about entries of that index, so re-checking here
@@ -245,43 +459,48 @@ function parseChildName(name) {
}
return null;
}
+// ---------------------------------------------------------------------------------
+// The collections.
/**
* Read the Files collection. Members are File entities; each File entity's own
- * dir holds its children as pointer/def pairs: a DIR_SIGNIFIER marks a folder,
- * and any nested type="File" children are themselves project files (recurse).
+ * dir holds its children as entities: a DIR_SIGNIFIER marks a folder, and any
+ * nested type="File" children are themselves project files (recurse).
*/
-function readFiles(index, filesHash) {
+function readFiles(layout, collection) {
const out = [];
const seen = new Set();
- const members = readDir(index, filesHash);
- for (const member of members) {
- collectFile(index, member, out, seen);
+ for (const member of layout.children(collection)) {
+ collectFile(layout, member, '', out, seen);
}
return out;
}
-function collectFile(index, entity, out, seen) {
- if (entity.pointer?.['@_type'] !== 'File') {
+function collectFile(layout, entity, parentPath, out, seen) {
+ if (entity.type !== 'File') {
return;
}
- const path = entity.pointer['@_location'];
- if (!path) {
+ // A file entity's location is relative to its PARENT, not to the project root —
+ // the store nests them the way the filesystem does. Read verbatim, every file
+ // inside a folder reported the bare name that a sibling at the root would, so
+ // `utils/helper.m` and a root-level `helper.m` were indistinguishable.
+ const name = entity.location;
+ if (!name) {
return;
}
+ const path = parentPath ? `${parentPath}/${name}` : name;
// Guard against cycles / repeated hashes.
- if (seen.has(entity.hash)) {
+ if (seen.has(entity.dir)) {
return;
}
- seen.add(entity.hash);
- const children = readDir(index, entity.hash);
+ seen.add(entity.dir);
+ const children = layout.children(entity);
let isFolder = false;
const labels = [];
const fileChildren = [];
for (const child of children) {
- const ctype = child.pointer?.['@_type'];
- if (ctype === 'DIR_SIGNIFIER') {
+ if (child.type === 'DIR_SIGNIFIER') {
isFolder = true;
}
- else if (ctype === 'File') {
+ else if (child.type === 'File') {
fileChildren.push(child);
}
}
@@ -292,7 +511,7 @@ function collectFile(index, entity, out, seen) {
out.push({ path, isFolder, labels: dedupe(labels) });
// Recurse into nested File children (folder contents).
for (const child of fileChildren) {
- collectFile(index, child, out, seen);
+ collectFile(layout, child, path, out, seen);
}
}
/** Collect all Label UUIDs from an Info def's nested nodes. */
@@ -310,18 +529,21 @@ function dedupe(arr) {
return [...new Set(arr)];
}
/**
- * Read the ProjectPath collection. Each entry is a type="Reference" pointer
- * whose def carries `Ref=""`; that Ref is the path folder name.
+ * Read the ProjectPath collection. Each entry is a type="Reference" whose def
+ * carries `Ref=""`; that Ref is the path folder, relative to the project
+ * root, and `Ref=""` is the project root itself.
*/
-function readPathFolders(index, hash) {
+function readPathFolders(layout, collection) {
const out = [];
- const entries = readDir(index, hash);
- for (const ent of entries) {
- if (ent.pointer?.['@_type'] !== 'Reference') {
+ for (const ent of layout.children(collection)) {
+ if (ent.type !== 'Reference') {
continue;
}
const ref = ent.def?.['@_Ref'];
- if (ref) {
+ // `!== undefined`, not truthiness: the project ROOT is on the path as `Ref=""`,
+ // and it is usually the first folder MATLAB adds, so testing for a non-empty
+ // string dropped a real entry from every project that has one.
+ if (ref !== undefined) {
out.push(ref);
}
}
@@ -331,37 +553,176 @@ function readPathFolders(index, hash) {
* Read the Categories collection into a flat label catalog. Each Category dir
* holds type="Label" entries; the label display name is the def's Name.
*/
-function readCategories(index, hash) {
+function readCategories(layout, collection) {
const out = [];
- const categories = readDir(index, hash);
- for (const cat of categories) {
- if (cat.pointer?.['@_type'] !== 'Category') {
+ for (const cat of layout.children(collection)) {
+ if (cat.type !== 'Category') {
continue;
}
- // Category display name: def Name, else the pointer location (the id).
- const categoryName = cat.def?.['@_Name'] || cat.pointer?.['@_location'] || '';
- const labelEntries = readDir(index, cat.hash);
- for (const labelEnt of labelEntries) {
- if (labelEnt.pointer?.['@_type'] !== 'Label') {
+ // Category display name: def Name, else the location (the id).
+ const categoryName = cat.def?.['@_Name'] || cat.location || '';
+ for (const labelEnt of layout.children(cat)) {
+ if (labelEnt.type !== 'Label') {
continue;
}
- const id = labelEnt.pointer?.['@_location'] || '';
+ const id = labelEnt.location || '';
const name = labelEnt.def?.['@_Name'] || id || '';
if (name) {
- out.push({ id, category: categoryName, name });
+ // The attribute spells itself 'READ_ONLY' on a label and '1' on a category,
+ // so this tests for PRESENCE rather than for a value — both mean MATLAB owns
+ // it, and a project's own label carries no such marking.
+ const ro = labelEnt.def?.['@_ReadOnly'];
+ out.push({ id, category: categoryName, name, readOnly: ro !== undefined && ro !== '0' });
}
}
}
return out;
}
+/** Read the EntryPoints collection (shortcuts + startup/shutdown files). */
+function readEntryPoints(layout, collection) {
+ const out = [];
+ for (const ent of layout.children(collection)) {
+ if (ent.type !== 'EntryPoint') {
+ continue;
+ }
+ const def = ent.def;
+ const file = def?.['@_File'] ?? '';
+ const name = def?.['@_Name'] ?? '';
+ if (!file && !name) {
+ continue;
+ }
+ const group = def?.['@_GroupUUID'] ?? '';
+ out.push({
+ id: ent.location,
+ name: name || file,
+ file,
+ kind: def?.['@_Type'] ?? '',
+ // Attribute values are never coerced by the reader, so this is the string
+ // '1' or '0' (see XmlReader's contract) — not a number and not a boolean.
+ visible: def?.['@_Visible'] !== '0',
+ // 'default' is the store's spelling for "no group"; a caller grouping by this
+ // field would otherwise render a group literally called default.
+ groupId: group === 'default' ? '' : group,
+ prev: readPrev(def),
+ });
+ }
+ return out;
+}
+/**
+ * The predecessor link from an entry point's def.
+ *
+ * It arrives as a nested ``. `toArray` is required, not defensive: the XML reader's
+ * `isArray` is configured per-dictionary and does not cover this element, so a def
+ * with one Extension yields an object and a def with two yields an array.
+ */
+function readPrev(def) {
+ for (const ext of toArray(def?.Extension)) {
+ const name = ext['@_Name'];
+ if (name === 'StartUpPrev' || name === 'ShutdownPrev') {
+ return ext['@_Value'] ?? '';
+ }
+ }
+ return '';
+}
+/** Read the EntryPointGroups collection (the named groups of the Shortcuts gallery). */
+function readEntryPointGroups(layout, collection) {
+ const out = [];
+ for (const ent of layout.children(collection)) {
+ if (ent.type !== 'EntryPointGroup') {
+ continue;
+ }
+ const name = ent.def?.['@_Name'];
+ if (name) {
+ out.push({ id: ent.location, name });
+ }
+ }
+ return out;
+}
+/** Read the WorkingFolders collection (cache, code generation, startup folder, …). */
+function readWorkingFolders(layout, collection) {
+ const out = [];
+ for (const ent of layout.children(collection)) {
+ if (ent.type !== 'Reference') {
+ continue;
+ }
+ const ref = ent.def?.['@_Ref'];
+ if (!ent.location || ref === undefined) {
+ continue;
+ }
+ out.push({ key: ent.location, ref });
+ }
+ return out;
+}
+/**
+ * Put the startup and shutdown files in RUN order.
+ *
+ * The store encodes the order as a linked list, and only as one: each entry names
+ * its PREDECESSOR in a nested ``, with `HEAD` marking the first. MATLAB runs the files top-down,
+ * so this order is meaningful and no other field recovers it — sorting these by
+ * name would silently reorder a shutdown sequence.
+ *
+ * Everything unchained keeps its store order and follows: a project with one
+ * startup file writes no Extension at all, and a chain that is broken (a missing
+ * predecessor, a cycle) must still yield every entry exactly once.
+ */
+function orderEntryPoints(entries) {
+ const out = [];
+ const taken = new Set();
+ for (const kind of ['StartUp', 'Shutdown']) {
+ const hook = entries.filter((e) => e.kind === kind);
+ // Index by predecessor so the walk is a lookup per step rather than a scan, and
+ // so a store with two entries claiming the same predecessor (which nothing
+ // prevents) still yields both instead of looping on one.
+ const byPrev = new Map();
+ for (const e of hook) {
+ const list = byPrev.get(e.prev);
+ if (list) {
+ list.push(e);
+ }
+ else {
+ byPrev.set(e.prev, [e]);
+ }
+ }
+ // Follow the chain from HEAD. `taken` is what makes this terminate on a cycle:
+ // a store whose links form a loop would otherwise spin here forever.
+ let cursor = 'HEAD';
+ for (;;) {
+ const step = (byPrev.get(cursor) ?? []).find((e) => !taken.has(e));
+ if (!step) {
+ break;
+ }
+ taken.add(step);
+ out.push(step);
+ cursor = step.id;
+ }
+ // Whatever the chain did not reach, in store order. A project with a single
+ // startup file writes no Extension at all and lands here — which is why the
+ // fallback is not an error path but the common one.
+ for (const e of hook) {
+ if (!taken.has(e)) {
+ taken.add(e);
+ out.push(e);
+ }
+ }
+ }
+ // Then the shortcuts, which have no order to preserve beyond the store's.
+ for (const e of entries) {
+ if (!taken.has(e)) {
+ out.push(e);
+ }
+ }
+ // Drop `prev` on the way out: see ChainedEntryPoint.
+ return out.map(({ prev: _prev, ...rest }) => rest);
+}
/**
* Resolve a genuine project->project reference. Name is the basename of the
* Ref path when present, else the UUID location.
*/
function resolveReference(ent) {
const ref = ent.def?.['@_Ref'];
- const location = ent.pointer?.['@_location'] || '';
- const id = location || ref || '';
+ const id = ent.location || ref || '';
if (!id) {
return null;
}
@@ -370,6 +731,6 @@ function resolveReference(ent) {
const parts = ref.split(/[/\\]/).filter((p) => p.length > 0);
name = parts.length > 0 ? parts[parts.length - 1] : ref;
}
- return { id, name };
+ return { id, name, path: ref ?? null };
}
//# sourceMappingURL=ProjectParser.js.map
\ No newline at end of file
diff --git a/dist/datamodel/parser/ProjectParser.js.map b/dist/datamodel/parser/ProjectParser.js.map
index b90078f..a282b47 100644
--- a/dist/datamodel/parser/ProjectParser.js.map
+++ b/dist/datamodel/parser/ProjectParser.js.map
@@ -1 +1 @@
-{"version":3,"file":"ProjectParser.js","sourceRoot":"","sources":["../../../src/datamodel/parser/ProjectParser.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"ProjectParser.js","sourceRoot":"","sources":["../../../src/datamodel/parser/ProjectParser.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 1fa89fd..3a174f4 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -25,7 +25,9 @@ export { parseMdl } from './datamodel/parser/MdlParser.js';
export { parseModel } from './datamodel/parser/ModelParser.js';
export { parseMat } from './datamodel/parser/MatParser.js';
export { parseProject } from './datamodel/parser/ProjectParser.js';
-export type { ParsedProject, ProjectFile, ProjectLabel, ProjectReference } from './datamodel/parser/ProjectParser.js';
+export type { ParsedProject, ProjectFile, ProjectLabel, ProjectReference, ProjectEntryPoint, ProjectEntryPointGroup, ProjectWorkingFolder, } from './datamodel/parser/ProjectParser.js';
+export { buildProjectPage } from './datamodel/parser/ProjectPage.js';
+export type { ProjectPage, ProjectPageRun, ProjectPageShortcut, ProjectPageLabel, ProjectPageCategory, ProjectPageLocation, ProjectPageReference, } from './datamodel/parser/ProjectPage.js';
export type { ParsedSlx, BlockParamUsage } from './datamodel/parser/SlxParser.js';
export type { ParsedMdl } from './datamodel/parser/MdlParser.js';
export type { ParsedMat, MatVariable } from './datamodel/parser/MatParser.js';
diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map
index c1e7a5b..63f28f4 100644
--- a/dist/index.d.ts.map
+++ b/dist/index.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/dist/index.js b/dist/index.js
index 2934fcd..c6a9e14 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -56,6 +56,11 @@ export { parseMdl } from './datamodel/parser/MdlParser.js';
export { parseModel } from './datamodel/parser/ModelParser.js';
export { parseMat } from './datamodel/parser/MatParser.js';
export { parseProject } from './datamodel/parser/ProjectParser.js';
+// The view model behind a project's MAIN PAGE, as opposed to a table of its files: a
+// `.prj` has no table worth showing (see ProjectPage.ts), and everything a page shows
+// around the file list — run order, shortcut groups, label coverage — is derived from
+// the parse rather than read from it, so it is derived once here for every host.
+export { buildProjectPage } from './datamodel/parser/ProjectPage.js';
// Universal ingest (sniff + dispatch) — superset entry over addXSource.
export { ingest } from './core/ingest.js';
// Which KIND of file a name refers to. Public because every consumer decides this too —
diff --git a/dist/index.js.map b/dist/index.js.map
index 71f2746..30af86d 100644
--- a/dist/index.js.map
+++ b/dist/index.js.map
@@ -1 +1 @@
-{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qCAAqC;AACrC,6CAA6C;AAC7C,8EAA8E;AAC9E,4EAA4E;AAE5E,gFAAgF;AAChF,OAAO,uCAAuC,CAAC;AAE/C,2BAA2B;AAC3B,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,QAAQ,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,WAAW,MAAM,uBAAuB,CAAC;AAErD,iEAAiE;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAgBpD,uFAAuF;AACvF,qFAAqF;AACrF,wFAAwF;AACxF,sFAAsF;AACtF,wFAAwF;AACxF,8CAA8C;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,0FAA0F;AAC1F,uFAAuF;AACvF,oFAAoF;AACpF,uFAAuF;AACvF,sFAAsF;AACtF,wFAAwF;AACxF,2CAA2C;AAC3C,2FAA2F;AAC3F,4FAA4F;AAC5F,wFAAwF;AACxF,8DAA8D;AAC9D,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AACtG,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAG1D,sFAAsF;AACtF,oFAAoF;AACpF,sFAAsF;AACtF,sFAAsF;AACtF,uFAAuF;AACvF,iEAAiE;AACjE,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAGzF,oCAAoC;AACpC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAC;AAC/F,qFAAqF;AACrF,kFAAkF;AAClF,kFAAkF;AAClF,mFAAmF;AACnF,gFAAgF;AAChF,oFAAoF;AACpF,oEAAoE;AACpE,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,4CAA4C,CAAC;AACtG,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAC3D,iFAAiF;AACjF,8EAA8E;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAUnE,qFAAqF;AACrF,sFAAsF;AACtF,sFAAsF;AACtF,iFAAiF;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AA4BrE,wEAAwE;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG1C,wFAAwF;AACxF,0FAA0F;AAC1F,0FAA0F;AAC1F,yFAAyF;AACzF,0FAA0F;AAC1F,kDAAkD;AAClD,uFAAuF;AACvF,uFAAuF;AACvF,sFAAsF;AACtF,yFAAyF;AACzF,0FAA0F;AAC1F,mEAAmE;AACnE,4FAA4F;AAC5F,wFAAwF;AACxF,uFAAuF;AACvF,+EAA+E;AAC/E,OAAO,EACL,KAAK,EACL,UAAU,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACX,aAAa,EACb,WAAW,EACX,UAAU,EACV,SAAS,EACT,aAAa,GACd,MAAM,0BAA0B,CAAC;AAElC,uFAAuF;AACvF,yFAAyF;AACzF,0FAA0F;AAC1F,qFAAqF;AACrF,6FAA6F;AAC7F,4FAA4F;AAC5F,qFAAqF;AACrF,sEAAsE;AACtE,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAE1H,iFAAiF;AACjF,wFAAwF;AACxF,uFAAuF;AACvF,uFAAuF;AACvF,oFAAoF;AACpF,wFAAwF;AACxF,sFAAsF;AACtF,OAAO,EAAE,QAAQ,EAAuB,MAAM,gCAAgC,CAAC;AAE/E,2FAA2F;AAC3F,2FAA2F;AAC3F,0FAA0F;AAC1F,wFAAwF;AACxF,sFAAsF;AACtF,2FAA2F;AAC3F,2FAA2F;AAC3F,2CAA2C;AAC3C,OAAO,EAAE,OAAO,EAAsB,MAAM,+BAA+B,CAAC;AAE5E,wFAAwF;AACxF,yFAAyF;AACzF,yFAAyF;AACzF,sFAAsF;AACtF,2FAA2F;AAC3F,2FAA2F;AAC3F,yFAAyF;AACzF,yCAAyC;AACzC,OAAO,EAAE,kBAAkB,EAAuB,MAAM,0CAA0C,CAAC;AAEnG,wFAAwF;AACxF,0FAA0F;AAC1F,0FAA0F;AAC1F,0FAA0F;AAC1F,wFAAwF;AACxF,0FAA0F;AAC1F,oFAAoF;AACpF,sFAAsF;AACtF,2EAA2E;AAC3E,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpH,uFAAuF;AACvF,wFAAwF;AACxF,uFAAuF;AACvF,yFAAyF;AACzF,oFAAoF;AACpF,sFAAsF;AACtF,wFAAwF;AACxF,yFAAyF;AACzF,kFAAkF;AAClF,OAAO,EACL,OAAO,EACP,WAAW,EACX,yBAAyB,EACzB,YAAY,EACZ,sBAAsB,EACtB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,SAAS,GACV,MAAM,iCAAiC,CAAC;AAGzC,qFAAqF;AACrF,wFAAwF;AACxF,wFAAwF;AACxF,0FAA0F;AAC1F,4FAA4F;AAC5F,yFAAyF;AACzF,cAAc;AACd,4FAA4F;AAC5F,yFAAyF;AACzF,0FAA0F;AAC1F,uFAAuF;AACvF,0FAA0F;AAC1F,2FAA2F;AAC3F,kFAAkF;AAClF,4FAA4F;AAC5F,2FAA2F;AAC3F,4FAA4F;AAC5F,wFAAwF;AACxF,0FAA0F;AAC1F,uEAAuE;AACvE,OAAO,EACL,eAAe,EACf,4BAA4B,EAC5B,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,WAAW,GACZ,MAAM,iCAAiC,CAAC;AAUzC,2FAA2F;AAC3F,uFAAuF;AACvF,uFAAuF;AACvF,oCAAoC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD,kFAAkF;AAClF,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGtC,qDAAqD;AACrD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yCAAyC,CAAC;AACpF,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,wCAAwC,CAAC;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAErE,oFAAoF;AACpF,0FAA0F;AAC1F,8EAA8E;AAC9E,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAEzE,wFAAwF;AACxF,sFAAsF;AACtF,yFAAyF;AACzF,uFAAuF;AACvF,4FAA4F;AAC5F,uFAAuF;AACvF,6FAA6F;AAC7F,qFAAqF;AACrF,8EAA8E;AAC9E,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGzE,uFAAuF;AACvF,0FAA0F;AAC1F,yFAAyF;AACzF,sFAAsF;AACtF,qFAAqF;AACrF,0FAA0F;AAC1F,uFAAuF;AACvF,oEAAoE;AACpE,EAAE;AACF,mFAAmF;AACnF,wFAAwF;AACxF,0FAA0F;AAC1F,wFAAwF;AACxF,0FAA0F;AAC1F,EAAE;AACF,uFAAuF;AACvF,wFAAwF;AACxF,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AAE1H,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAGvF,2FAA2F;AAC3F,4FAA4F;AAC5F,sFAAsF;AACtF,4FAA4F;AAC5F,4FAA4F;AAC5F,uEAAuE;AACvE,EAAE;AACF,0FAA0F;AAC1F,4FAA4F;AAC5F,wFAAwF;AACxF,0FAA0F;AAC1F,6CAA6C;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE7D,2FAA2F;AAC3F,0FAA0F;AAC1F,0FAA0F;AAC1F,0FAA0F;AAC1F,0FAA0F;AAC1F,kDAAkD;AAClD,EAAE;AACF,yFAAyF;AACzF,2FAA2F;AAC3F,qFAAqF;AACrF,2FAA2F;AAC3F,sFAAsF;AACtF,kFAAkF;AAClF,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAGtD,0FAA0F;AAC1F,6FAA6F;AAC7F,2FAA2F;AAC3F,iFAAiF;AACjF,4FAA4F;AAC5F,4FAA4F;AAC5F,wEAAwE;AACxE,EAAE;AACF,2FAA2F;AAC3F,0FAA0F;AAC1F,sFAAsF;AACtF,0FAA0F;AAC1F,4FAA4F;AAC5F,0FAA0F;AAC1F,oDAAoD;AACpD,EAAE;AACF,6FAA6F;AAC7F,6FAA6F;AAC7F,+EAA+E;AAC/E,OAAO,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC"}
\ No newline at end of file
diff --git a/src/datamodel/node/container/ProjectSectionNode.ts b/src/datamodel/node/container/ProjectSectionNode.ts
index 21f753d..7e8ce88 100644
--- a/src/datamodel/node/container/ProjectSectionNode.ts
+++ b/src/datamodel/node/container/ProjectSectionNode.ts
@@ -6,6 +6,9 @@ import ProjectItemNode from '../data/ProjectItemNode.js';
import type BaseNode from '../BaseNode.js';
import type { ProjectFile, ProjectLabel, ProjectReference } from '../../parser/ProjectParser.js';
+/** What to call the project root in a list of path folders. See addPathEntry. */
+const ROOT_FOLDER_NAME = '(project root)';
+
export default class ProjectSectionNode extends ContainerNode {
label: string;
iconId: string;
@@ -40,7 +43,16 @@ export default class ProjectSectionNode extends ContainerNode {
}
addPathEntry(folder: string): BaseNode {
- const name = folder.split(/[/\\]/).filter((p) => p.length > 0).pop() || folder;
+ // The project ROOT is on the path, and the store spells it as the EMPTY
+ // string — so unlike every other entry it has no segment to be named by, and
+ // the usual "show the path itself" fallback leaves a blank row. It is
+ // normally the first folder MATLAB adds, so this is the common first row and
+ // not an edge case. Only '' is the root: a degenerate path like '/' is still
+ // shown verbatim, since that one at least says something.
+ const name =
+ folder === ''
+ ? ROOT_FOLDER_NAME
+ : folder.split(/[/\\]/).filter((p) => p.length > 0).pop() || folder;
const node = new ProjectItemNode(name, this, {
itemType: 'Path Folder',
location: folder,
diff --git a/src/datamodel/parser/ProjectPage.ts b/src/datamodel/parser/ProjectPage.ts
new file mode 100644
index 0000000..86752ee
--- /dev/null
+++ b/src/datamodel/parser/ProjectPage.ts
@@ -0,0 +1,260 @@
+// Copyright 2026 The MathWorks, Inc.
+//
+// The view model behind a project's main page.
+//
+// A `.prj` is not a table. MATLAB itself opens NO document tab for one — its
+// Project panel *is* the file panel — and the folder tree a table would show is
+// already in VS Code's Explorer, so repeating it costs a tab and says nothing new.
+// What is NOT anywhere else is everything around the file list: which files run on
+// open and close and IN WHICH ORDER, the shortcuts and their groups, which folders
+// are on the MATLAB path, where the cache and generated code go, which labels this
+// project defines and how much of it they cover. That is what a page shows.
+//
+// WHY THIS LIVES BESIDE THE PARSER. It is a projection of `ParsedProject` and needs
+// its types; `datamodel/display/` is pinned by `moduleBoundaries.test.ts` to have
+// ZERO outbound edges, so it cannot be there. It is also not presentation: the
+// grouping, the run-order split, the label usage counts and the coverage figure are
+// all facts about the store, derived once here so that every host computes them the
+// same way and so they are testable without a webview.
+
+import type { ParsedProject, ProjectEntryPoint } from './ProjectParser.js';
+import type { ParseWarning } from './ParseWarning.js';
+
+/** One file MATLAB runs for a hook. Position in the list IS the run order. */
+export interface ProjectPageRun {
+ name: string;
+ /** Project-root-relative path. */
+ file: string;
+}
+
+/** One entry in the Shortcuts gallery. */
+export interface ProjectPageShortcut {
+ name: string;
+ /** Project-root-relative path. May name a FOLDER. */
+ file: string;
+ /** The group's display name, '' when ungrouped. */
+ group: string;
+}
+
+/** One label, with how much of the project carries it. */
+export interface ProjectPageLabel {
+ id: string;
+ name: string;
+ /** Members carrying this label. 0 is meaningful — a defined but unused label. */
+ count: number;
+ /** Defined by this project rather than shipped by MATLAB. */
+ custom: boolean;
+}
+
+export interface ProjectPageCategory {
+ name: string;
+ labels: ProjectPageLabel[];
+}
+
+/** A folder — or file — the project designates for a purpose. */
+export interface ProjectPageLocation {
+ /** The store's own key, e.g. 'SimulinkCacheFolder'. */
+ key: string;
+ /** English for `key`, falling back to `key` itself when unrecognized. */
+ label: string;
+ /** Project-root-relative path. */
+ ref: string;
+}
+
+export interface ProjectPageReference {
+ name: string;
+ /**
+ * Where the referenced project lives, relative to this project's root — so
+ * typically `../OtherProject/other.prj`. '' when the store recorded no path, which
+ * is a reference a host cannot open and should not offer a link for.
+ */
+ path: string;
+}
+
+export interface ProjectPage {
+ name: string;
+ /** The raw `MetadataType`, for a host that wants to report it exactly. */
+ format: string;
+ /** English for `format` — what a MATLAB user sees in Project Settings. */
+ formatLabel: string;
+ /** Every member, folders included: what the project "contains". */
+ memberCount: number;
+ /** Members carrying at least one label — the numerator of label coverage. */
+ labelledCount: number;
+ /** In run order. */
+ startup: ProjectPageRun[];
+ /** In run order. */
+ shutdown: ProjectPageRun[];
+ shortcuts: ProjectPageShortcut[];
+ /** Project-root-relative; '' is the project root itself. */
+ pathFolders: string[];
+ locations: ProjectPageLocation[];
+ categories: ProjectPageCategory[];
+ references: ProjectPageReference[];
+ warnings: ParseWarning[];
+}
+
+/**
+ * English for a working-folder key.
+ *
+ * Deliberately not exhaustive, and the fallback is the raw key: a newer release can
+ * designate a purpose this version has no word for, and showing `key` is honest
+ * where dropping the row or printing 'Unknown' is not.
+ */
+const LOCATION_LABEL: Record = {
+ SimulinkCacheFolder: 'Simulation cache',
+ SimulinkCodeGenFolder: 'Code generation',
+ ProjectStartupFolder: 'Startup folder',
+ DependencyCacheFile: 'Dependency cache',
+};
+
+/**
+ * English for a `MetadataType`.
+ *
+ * These are the words MATLAB's own Project Settings uses, not the store's spelling:
+ * `fixedPathV2` and `distributed` are two encodings of the same USER choice ("save
+ * as multiple XML files"), and a page that printed the internal name would invite a
+ * user to look for a setting that is not spelled that way anywhere in MATLAB.
+ */
+const FORMAT_LABEL: Record = {
+ fixedPathV2: 'multiple XML files',
+ distributed: 'multiple XML files',
+ monolithic: 'single XML file',
+ toml: 'matlab.toml',
+};
+
+/** The entry-point kinds MATLAB runs, as opposed to the ones a user clicks. */
+const STARTUP = 'StartUp';
+const SHUTDOWN = 'Shutdown';
+
+/**
+ * Project a parsed project into its page.
+ *
+ * Pure and total: every field is derived from `parsed`, and a project that holds
+ * nothing yields a page of empty sections rather than a missing one — the sections
+ * are what tell a user "this project defines no shortcuts", which is a fact worth
+ * showing and not the same as a page that failed to render.
+ */
+export function buildProjectPage(parsed: ParsedProject): ProjectPage {
+ const runsOf = (kind: string): ProjectPageRun[] =>
+ parsed.entryPoints
+ .filter((e) => e.kind === kind)
+ // `parsed.entryPoints` is already in run order and `filter` preserves it, so
+ // there is deliberately no sort here — see ParsedProject.entryPoints.
+ .map((e) => ({ name: e.name, file: e.file }));
+
+ const groupNames = new Map(parsed.entryPointGroups.map((g) => [g.id, g.name]));
+
+ return {
+ name: parsed.name,
+ format: parsed.format,
+ formatLabel: FORMAT_LABEL[parsed.format] ?? parsed.format,
+ memberCount: parsed.files.length,
+ labelledCount: parsed.files.filter((f) => f.labels.length > 0).length,
+ startup: runsOf(STARTUP),
+ shutdown: runsOf(SHUTDOWN),
+ shortcuts: shortcutsOf(parsed.entryPoints, groupNames),
+ pathFolders: parsed.pathFolders,
+ locations: parsed.workingFolders.map((w) => ({
+ key: w.key,
+ label: LOCATION_LABEL[w.key] ?? w.key,
+ ref: w.ref,
+ })),
+ categories: categoriesOf(parsed),
+ // The id is deliberately NOT the fallback target: it is a UUID, and a page that
+ // linked it would offer to open a path that cannot exist. A reference the store
+ // gave no path for is shown by name alone.
+ references: parsed.references.map((r) => ({ name: r.name ?? r.id, path: r.path ?? '' })),
+ warnings: parsed.warnings,
+ };
+}
+
+/**
+ * The Shortcuts gallery: every entry point that is not a hook.
+ *
+ * Selected by EXCLUDING the two run kinds rather than by asking for 'Basic'. A
+ * newer release can add a shortcut kind, and an unknown kind is far better shown in
+ * the gallery — where it is at worst grouped oddly — than silently absent from a
+ * page whose whole job is to list what this project put there.
+ */
+function shortcutsOf(
+ entryPoints: ProjectEntryPoint[],
+ groupNames: Map,
+): ProjectPageShortcut[] {
+ const out: ProjectPageShortcut[] = [];
+ for (const e of entryPoints) {
+ if (e.kind === STARTUP || e.kind === SHUTDOWN) {
+ continue;
+ }
+ // `visible` is MATLAB's own answer to "does this belong in the gallery", so a
+ // hidden entry of any other kind stays out of it.
+ if (!e.visible) {
+ continue;
+ }
+ out.push({
+ name: e.name,
+ file: e.file,
+ // A groupId naming no group falls back to ungrouped rather than to the raw
+ // UUID: a store can outlive the group it points at, and a gallery heading
+ // reading `a0ea675a-85ba-…` is worse than no heading.
+ group: groupNames.get(e.groupId) ?? '',
+ });
+ }
+ // Grouped shortcuts first, so a host rendering group headings emits each heading
+ // once. Order WITHIN a group, and among the ungrouped, stays the store's.
+ return [...out.filter((s) => s.group), ...out.filter((s) => !s.group)];
+}
+
+/**
+ * The label catalog with usage counts, grouped by category.
+ *
+ * Counted from the members' own assignments rather than from the catalog, because
+ * the two can disagree: a file may carry a label id the catalog does not define
+ * (written by a release that knew it, or by a hand-edited store). Such an id is
+ * counted under a category named for it, so the coverage figure stays consistent
+ * with `labelledCount` instead of quietly excluding files it includes.
+ */
+function categoriesOf(parsed: ParsedProject): ProjectPageCategory[] {
+ const counts = new Map();
+ for (const file of parsed.files) {
+ // Per file, not per assignment: `parsed.files` already de-duplicates a file's
+ // labels, so each hit here is one member carrying one label.
+ for (const id of file.labels) {
+ counts.set(id, (counts.get(id) ?? 0) + 1);
+ }
+ }
+
+ const byCategory = new Map();
+ const push = (category: string, label: ProjectPageLabel): void => {
+ const list = byCategory.get(category);
+ if (list) {
+ list.push(label);
+ } else {
+ byCategory.set(category, [label]);
+ }
+ };
+
+ const defined = new Set();
+ for (const l of parsed.labels) {
+ defined.add(l.id);
+ push(l.category, {
+ id: l.id,
+ name: l.name,
+ count: counts.get(l.id) ?? 0,
+ custom: !l.readOnly,
+ });
+ }
+ for (const [id, count] of counts) {
+ if (!defined.has(id)) {
+ push('', { id, name: id, count, custom: true });
+ }
+ }
+
+ // Used labels first, then by name: the seven built-ins ship with every project
+ // and most stay at zero, so catalog order buries the two or three that say
+ // something about THIS project under five that say nothing about any.
+ for (const labels of byCategory.values()) {
+ labels.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
+ }
+ return [...byCategory.entries()].map(([name, labels]) => ({ name, labels }));
+}
diff --git a/src/datamodel/parser/ProjectParser.ts b/src/datamodel/parser/ProjectParser.ts
index 0276046..755af78 100644
--- a/src/datamodel/parser/ProjectParser.ts
+++ b/src/datamodel/parser/ProjectParser.ts
@@ -5,6 +5,7 @@ import { reasonOf, type ParseWarning } from './ParseWarning.js';
/** A member file (or folder) of the project. */
export interface ProjectFile {
+ /** POSIX path relative to the project ROOT, not to the parent entity. */
path: string;
isFolder: boolean;
/** Label UUIDs assigned to this file (e.g. 'design'). */
@@ -17,21 +18,103 @@ export interface ProjectLabel {
id: string;
category: string;
name: string;
+ /**
+ * Whether MATLAB owns this label. The seven Classification labels ship with
+ * every project and are marked read-only in the store; a label the project
+ * itself added is not, which is the only thing distinguishing the two.
+ */
+ readOnly: boolean;
}
/** A project-to-project reference. */
export interface ProjectReference {
id: string;
name: string | null;
+ /**
+ * Where the referenced project lives, as the store spells it — relative to THIS
+ * project's root, so typically `../OtherProject/other.prj`. Null when the store
+ * recorded no path, which leaves `id` (a UUID) as all that is known about it.
+ *
+ * Kept alongside `name` rather than folded into it: `name` is the basename, which
+ * is what a user reads, and this is what a host needs to actually open the thing.
+ * Deriving one from the other only works in that direction.
+ */
+ path: string | null;
+}
+
+/**
+ * A registered entry point: a shortcut, or a file MATLAB runs on open/close.
+ *
+ * One store collection covers all three, separated only by `kind`, which is why
+ * they are one type here. `prev` is deliberately NOT exposed: the store encodes run
+ * order as a linked list over these entries, and `entryPoints` is already in that
+ * order (see `orderEntryPoints`), so a consumer never has to know the mechanism.
+ */
+export interface ProjectEntryPoint {
+ /** The entry point's own UUID (its pointer `location`). */
+ id: string;
+ name: string;
+ /** Project-root-relative path. May name a FOLDER — a shortcut can open one. */
+ file: string;
+ /** 'Basic' (a shortcut), 'StartUp', 'Shutdown', or whatever a newer release writes. */
+ kind: string;
+ /** Whether MATLAB shows this in its Shortcuts gallery. Startup/shutdown files are hidden. */
+ visible: boolean;
+ /** The EntryPointGroup id; '' when ungrouped (the store spells that 'default'). */
+ groupId: string;
+}
+
+/** A named group in the Shortcuts gallery. */
+export interface ProjectEntryPointGroup {
+ id: string;
+ name: string;
+}
+
+/**
+ * A folder (or file) the project designates for a particular purpose — simulation
+ * cache, code generation, startup folder, dependency cache.
+ *
+ * `key` is the store's own name for the purpose (`SimulinkCacheFolder`,
+ * `SimulinkCodeGenFolder`, `ProjectStartupFolder`, `DependencyCacheFile`) and is
+ * NOT translated here: a newer release may designate something this version has no
+ * label for, and passing the raw key on lets a host show it rather than drop it.
+ * Note the last of those four names a FILE, so these are not all folders.
+ */
+export interface ProjectWorkingFolder {
+ key: string;
+ /** Project-root-relative path. */
+ ref: string;
}
export interface ParsedProject {
name: string;
+ /**
+ * The store's declared `MetadataType` — how the project's metadata is laid out on
+ * disk. '' when the store declares none and the layout could not be inferred.
+ * Two are read: `fixedPathV2` and `distributed`. Anything else is reported through
+ * `warnings` rather than read, because guessing at a layout produces a project
+ * that looks complete and is not.
+ */
+ format: string;
files: ProjectFile[];
+ /**
+ * Folders added to the MATLAB path, project-root-relative. The project ROOT
+ * itself is spelled '' — the store records it as `Ref=""`, and it is a real entry
+ * (usually the first one MATLAB adds), so dropping it would under-report the path.
+ */
pathFolders: string[];
/** The catalog of labels defined in the project. */
labels: ProjectLabel[];
references: ProjectReference[];
+ /**
+ * Shortcuts and startup/shutdown files, in this order: startup files in RUN
+ * order, then shutdown files in run order, then everything else (shortcuts) in
+ * store order. Run order is the store's, which is meaningful — MATLAB runs the
+ * files top-down — and is not recoverable from any other field.
+ */
+ entryPoints: ProjectEntryPoint[];
+ entryPointGroups: ProjectEntryPointGroup[];
+ workingFolders: ProjectWorkingFolder[];
/**
* What could not be read, empty when everything could. ALWAYS an array: this
* reader has the channel, so a caller may read `.length` without guarding, and
@@ -42,6 +125,40 @@ export interface ParsedProject {
const PROJECT_PREFIX = 'resources/project/';
+/** The store manifest, which declares the layout the rest of the store is in. */
+const MANIFEST = 'Project.xml';
+
+/** The layouts this reader walks. See `ParsedProject.format`. */
+const FIXED_PATH_V2 = 'fixedPathV2';
+const DISTRIBUTED = 'distributed';
+
+/**
+ * Collection types whose `type="Reference"` children are NOT project references.
+ *
+ * A project->project reference and a path folder and a working folder are all
+ * spelled `type="Reference"`, distinguished only by which collection they sit in.
+ * So this is a DENY-list rather than an allow-list of reference collections, and
+ * deliberately: the collection holding real references is spelled differently
+ * across releases, and an allow-list missing a spelling silently drops references,
+ * whereas this list — the collections whose meaning we positively model — cannot.
+ * Before it existed, every WorkingFolders entry was reported as a project
+ * reference, which INVENTED references a project does not have.
+ */
+const NOT_REFERENCE_COLLECTIONS = new Set([
+ 'ProjectPath',
+ 'WorkingFolders',
+ 'Categories',
+ 'Files',
+ 'EntryPoints',
+ 'EntryPointGroups',
+ 'Info',
+]);
+
+interface XmlExtension {
+ '@_Name'?: string;
+ '@_Value'?: string;
+}
+
interface XmlInfo {
'@_Name'?: string;
'@_location'?: string;
@@ -49,8 +166,14 @@ interface XmlInfo {
'@_Ref'?: string;
'@_Type'?: string;
'@_DataType'?: string;
+ '@_ReadOnly'?: string;
'@_UUID'?: string;
+ '@_MetadataType'?: string;
+ '@_File'?: string;
+ '@_Visible'?: string;
+ '@_GroupUUID'?: string;
Category?: XmlCategory | XmlCategory[];
+ Extension?: XmlExtension | XmlExtension[];
}
interface XmlCategory {
@@ -62,16 +185,30 @@ interface XmlLabel {
'@_UUID'?: string;
}
-/** A resolved pointer/def pair within a directory. */
+/**
+ * One entity in the store, normalized across layouts.
+ *
+ * The two layouts differ ONLY in how these three fields are found. `fixedPathV2`
+ * puts `location`/`type` in a pointer document beside the def and names the child
+ * directory by an opaque hash; `distributed` encodes all three in the filename
+ * (`.type.`) and nests the child directory under its parent. Once
+ * both are read into this shape, every collection reader below is layout-blind.
+ */
interface Entity {
- /** The hash/stem: names this entity's own child directory. */
- hash: string;
- /** The pointer (location + type). */
- pointer: XmlInfo | null;
- /** The definition (attributes / nested Category). */
+ /** Store-relative path of this entity's OWN child directory. Also its identity. */
+ dir: string;
+ location: string;
+ type: string;
+ /** The definition `` (attributes / nested Category). */
def: XmlInfo | null;
}
+/** Reads the children of an entity, and the top-level entities, for one layout. */
+interface Layout {
+ roots(): Entity[];
+ children(entity: Entity): Entity[];
+}
+
function toArray(v: T | T[] | undefined): T[] {
if (v === undefined || v === null) {
return [];
@@ -79,6 +216,21 @@ function toArray(v: T | T[] | undefined): T[] {
return Array.isArray(v) ? v : [v];
}
+function emptyResult(name: string, warnings: ParseWarning[]): ParsedProject {
+ return {
+ name,
+ format: '',
+ files: [],
+ pathFolders: [],
+ labels: [],
+ references: [],
+ entryPoints: [],
+ entryPointGroups: [],
+ workingFolders: [],
+ warnings,
+ };
+}
+
/**
* Parse a MATLAB/Simulink Project content store.
*
@@ -90,14 +242,7 @@ function toArray(v: T | T[] | undefined): T[] {
*/
export function parseProject(files: Record, projectName: string): ParsedProject {
const warnings: ParseWarning[] = [];
- const result: ParsedProject = {
- name: projectName,
- files: [],
- pathFolders: [],
- labels: [],
- references: [],
- warnings,
- };
+ const result = emptyResult(projectName, warnings);
try {
// Index every parseable Info doc by its relpath (project-relative).
@@ -120,66 +265,106 @@ export function parseProject(files: Record, projectName: string)
// Every collection below reads out of this index, so an empty one means the
// whole result is empty — and a `.prj` always has a store, so reaching here
// is either the wrong kind of file or a store that did not survive its trip.
+ //
+ // Except for one case worth naming separately: a store MATLAB wrote in its
+ // `matlab.toml` format holds no XML at all, so it lands here looking exactly
+ // like damage. It is not damaged, it is a format this reader does not read,
+ // and a user told "nothing readable" would go looking for a corrupt file.
+ const toml = Object.keys(files).find(
+ (k) => k.startsWith(PROJECT_PREFIX) && k.slice(PROJECT_PREFIX.length).endsWith('matlab.toml'),
+ );
+ warnings.push(
+ toml
+ ? {
+ // 'source-empty' rather than a code of its own: the code is the kind of
+ // loss (the source opened and held nothing this reader recognizes) and
+ // the message is which, per ParseWarningCode's note that the codes are
+ // about containers and parts and not about any one format.
+ code: 'source-empty',
+ message:
+ 'This project stores its metadata as matlab.toml, which this viewer cannot read yet, ' +
+ 'so this project reads as empty. In MATLAB, Project Settings can save it as XML instead.',
+ part: toml,
+ }
+ : {
+ code: 'source-empty',
+ message:
+ 'No readable project entries were found under resources/project/, ' +
+ 'so this project reads as empty.',
+ },
+ );
+ return result;
+ }
+
+ // The manifest declares which layout the rest of the store is in. A store that
+ // declares one we cannot walk is reported and NOT guessed at: the collection
+ // readers below would find nothing in it and return a project that looks
+ // complete and empty, which is the one outcome a user cannot tell from a fact.
+ const declared = index.get(MANIFEST)?.['@_MetadataType'] ?? '';
+ const layoutName = declared || inferLayout(index);
+ if (layoutName !== FIXED_PATH_V2 && layoutName !== DISTRIBUTED) {
+ // The name is still worth salvaging — it is what a host titles the view with,
+ // and it reads out of the index without knowing the layout.
+ result.name = salvageName(index) ?? projectName;
warnings.push({
code: 'source-empty',
- message:
- 'No readable project entries were found under resources/project/, ' +
- 'so this project reads as empty.',
+ message: declared
+ ? `This project's metadata is stored as "${declared}", which this viewer cannot read yet, ` +
+ 'so this project reads as empty. In MATLAB, Project Settings can save it as multiple XML files instead.'
+ : 'The layout of this project store was not recognized, so this project reads as empty.',
+ part: PROJECT_PREFIX + MANIFEST,
});
return result;
}
+ result.format = layoutName;
+
+ const layout =
+ layoutName === FIXED_PATH_V2 ? fixedPathV2Layout(index) : distributedLayout(index);
+ const rootEntities = layout.roots();
- // The `root/` directory holds the top-level entry pointers.
- const rootEntities = readDir(index, 'root');
+ result.name = resolveNameFrom(rootEntities) ?? projectName;
- // Project name: the def whose pointer has location="ProjectData" type="Info".
+ // Collections are matched on type AND location. Two collections of one type can
+ // coexist — a real project carries both `location="Root" type="Files"` (its
+ // members) and `location="ALM" type="Files"` (artifact tracking) — so matching
+ // on type alone assigned the field twice and let store iteration order pick the
+ // winner. `Root` is the project's own; anything else belongs to a feature we do
+ // not model, and reading it as the member list is simply wrong.
for (const ent of rootEntities) {
- if (ent.pointer?.['@_location'] === 'ProjectData' && ent.pointer?.['@_type'] === 'Info') {
- const name = ent.def?.['@_Name'];
- if (name) {
- result.name = name;
- }
+ if (ent.location !== 'Root') {
+ continue;
}
- }
- // Some stores also carry the name on a bare def whose
- // pointer has no matching location; take it if we still have the fallback.
- if (result.name === projectName) {
- for (const ent of rootEntities) {
- const name = ent.def?.['@_Name'];
- if (name && !ent.pointer?.['@_type']) {
- result.name = name;
- break;
- }
+ if (ent.type === 'Files') {
+ result.files = readFiles(layout, ent);
+ } else if (ent.type === 'ProjectPath') {
+ result.pathFolders = readPathFolders(layout, ent);
+ } else if (ent.type === 'Categories') {
+ result.labels = readCategories(layout, ent);
+ } else if (ent.type === 'EntryPoints') {
+ result.entryPoints = orderEntryPoints(readEntryPoints(layout, ent));
+ } else if (ent.type === 'EntryPointGroups') {
+ result.entryPointGroups = readEntryPointGroups(layout, ent);
+ } else if (ent.type === 'WorkingFolders') {
+ result.workingFolders = readWorkingFolders(layout, ent);
}
}
for (const ent of rootEntities) {
- const type = ent.pointer?.['@_type'];
- if (type === 'Files') {
- result.files = readFiles(index, ent.hash);
- } else if (type === 'ProjectPath') {
- result.pathFolders = readPathFolders(index, ent.hash);
- } else if (type === 'Categories') {
- result.labels = readCategories(index, ent.hash);
- } else if (type === 'Reference') {
- // A genuine project->project reference living directly in root.
+ // A genuine project->project reference living directly in root.
+ if (ent.type === 'Reference') {
const ref = resolveReference(ent);
if (ref) {
result.references.push(ref);
}
+ continue;
}
- }
-
- // References may also live in their own top-level collection outside the
- // ProjectPath collection. Scan any root entry whose dir contains
- // type="Reference" children (but skip the ProjectPath collection itself).
- for (const ent of rootEntities) {
- if (ent.pointer?.['@_type'] === 'ProjectPath') {
+ // …or in its own collection. See NOT_REFERENCE_COLLECTIONS for why the set of
+ // collections to skip is spelled out rather than the set to read.
+ if (NOT_REFERENCE_COLLECTIONS.has(ent.type)) {
continue;
}
- const children = readDir(index, ent.hash);
- for (const child of children) {
- if (child.pointer?.['@_type'] === 'Reference') {
+ for (const child of layout.children(ent)) {
+ if (child.type === 'Reference') {
const ref = resolveReference(child);
if (ref) {
result.references.push(ref);
@@ -202,15 +387,72 @@ export function parseProject(files: Record, projectName: string)
code: 'source-unreadable',
message: `The project store could not be read (${reasonOf(err)}), so this project reads as empty.`,
});
- return {
- name: projectName,
- files: [],
- pathFolders: [],
- labels: [],
- references: [],
- warnings,
- };
+ return emptyResult(projectName, warnings);
+ }
+}
+
+/**
+ * Which layout a store with no manifest is in.
+ *
+ * Every store this reader has seen carries `Project.xml`, so this is the fallback
+ * for one that lost it: `root/` is the entry directory `fixedPathV2` and nothing
+ * else uses, and a `.type.` in a top-level name is `distributed`'s own spelling.
+ */
+function inferLayout(index: Map): string {
+ for (const key of index.keys()) {
+ if (key.startsWith('root/')) {
+ return FIXED_PATH_V2;
+ }
}
+ for (const key of index.keys()) {
+ if (!key.includes('/') && key.includes('.type.')) {
+ return DISTRIBUTED;
+ }
+ }
+ return '';
+}
+
+/**
+ * The project name, for a store whose layout could not be walked.
+ *
+ * Kept separate from `resolveNameFrom` because it reads the index directly rather
+ * than the entity tree — there is no tree in this case — and every layout seen so
+ * far spells the document holding the name recognizably enough to find it without
+ * knowing which layout this is.
+ */
+function salvageName(index: Map): string | null {
+ for (const [key, info] of index.entries()) {
+ if (key === MANIFEST) {
+ continue;
+ }
+ // `distributed` names it ProjectData.type.Info.xml; `fixedPathV2` puts it in a
+ // hash-named def whose pointer says location="ProjectData".
+ if (key.includes('ProjectData') && info['@_Name']) {
+ return info['@_Name'];
+ }
+ }
+ return null;
+}
+
+/** The project name from the root entities, or null to keep the filename fallback. */
+function resolveNameFrom(rootEntities: Entity[]): string | null {
+ for (const ent of rootEntities) {
+ if (ent.location === 'ProjectData' && ent.type === 'Info') {
+ const name = ent.def?.['@_Name'];
+ if (name) {
+ return name;
+ }
+ }
+ }
+ // Some stores also carry the name on a bare def whose
+ // pointer has no type at all.
+ for (const ent of rootEntities) {
+ const name = ent.def?.['@_Name'];
+ if (name && !ent.type) {
+ return name;
+ }
+ }
+ return null;
}
/**
@@ -260,51 +502,148 @@ function parseInfo(content: string, relPath: string, warnings: ParseWarning[]):
return info as XmlInfo;
}
+// ---------------------------------------------------------------------------------
+// The two layouts.
+
/**
- * Read all pointer/def entities in a directory (relative to resources/project/).
- * Groups files by hash/stem, treating `p`/`_sp` as pointers and `d`/`_sd` as defs.
+ * `fixedPathV2`: every entity is a POINTER/def pair of documents, and every child
+ * directory sits at the TOP of the store named by an opaque hash — flat, however
+ * deep the logical nesting goes, which is what "fixed path" means. The top-level
+ * entities live in `root/`.
*/
-function readDir(index: Map, dir: string): Entity[] {
- if (!dir) {
- return [];
- }
- const prefix = dir + '/';
- const byHash = new Map();
-
- for (const [relPath, info] of index.entries()) {
- if (!relPath.startsWith(prefix)) {
- continue;
+function fixedPathV2Layout(index: Map): Layout {
+ const readDir = (dir: string): Entity[] => {
+ if (!dir) {
+ return [];
}
- const rest = relPath.slice(prefix.length);
- // Only immediate children (no further nesting).
- if (rest.includes('/')) {
- continue;
- }
- const parsed = parseChildName(rest);
- if (!parsed) {
- continue;
+ const prefix = dir + '/';
+ const byHash = new Map();
+
+ for (const [relPath, info] of index.entries()) {
+ if (!relPath.startsWith(prefix)) {
+ continue;
+ }
+ const rest = relPath.slice(prefix.length);
+ // Only immediate children (no further nesting).
+ if (rest.includes('/')) {
+ continue;
+ }
+ const parsed = parseChildName(rest);
+ if (!parsed) {
+ continue;
+ }
+ const { hash, isPointer } = parsed;
+ let pair = byHash.get(hash);
+ if (!pair) {
+ pair = { pointer: null, def: null };
+ byHash.set(hash, pair);
+ }
+ if (isPointer) {
+ pair.pointer = info;
+ } else {
+ pair.def = info;
+ }
}
- const { hash, isPointer } = parsed;
- let ent = byHash.get(hash);
- if (!ent) {
- ent = { hash, pointer: null, def: null };
- byHash.set(hash, ent);
+
+ // Store order (Map preserves insertion), NOT sorted by hash: the hashes are
+ // opaque, so sorting on them would impose an order that means nothing while
+ // discarding the one the store was written in — which is the fallback order for
+ // entry points whose run-order chain is absent.
+ return [...byHash.entries()].map(([hash, pair]) => ({
+ dir: hash,
+ location: pair.pointer?.['@_location'] ?? '',
+ type: pair.pointer?.['@_type'] ?? '',
+ def: pair.def,
+ }));
+ };
+
+ return { roots: () => readDir('root'), children: (e) => readDir(e.dir) };
+}
+
+/**
+ * `distributed`: no pointer documents at all. An entity's location and type are its
+ * FILENAME — `.type..xml` for the def, `.type./`
+ * for the child directory — and directories nest the way the entities do. The
+ * top-level entities sit directly in the store root.
+ *
+ * A collection may exist as a directory with no def document beside it
+ * (`Root.type.Files/` without `Root.type.Files.xml`), so entities are discovered
+ * from directory names as well as from documents, and a missing def is normal here
+ * rather than a loss.
+ */
+function distributedLayout(index: Map): Layout {
+ const readDir = (dir: string): Entity[] => {
+ const prefix = dir ? dir + '/' : '';
+ const byStem = new Map();
+
+ for (const [relPath, info] of index.entries()) {
+ if (!relPath.startsWith(prefix)) {
+ continue;
+ }
+ const rest = relPath.slice(prefix.length);
+ if (!rest) {
+ continue;
+ }
+ const slash = rest.indexOf('/');
+ if (slash === -1) {
+ // An immediate document: the entity's def.
+ const stem = rest.slice(0, -'.xml'.length);
+ if (!byStem.has(stem)) {
+ byStem.set(stem, info);
+ } else if (byStem.get(stem) === null) {
+ byStem.set(stem, info);
+ }
+ } else {
+ // Something deeper: the first segment is an entity's child DIRECTORY, and
+ // is how a collection with no def document of its own is found at all.
+ const stem = rest.slice(0, slash);
+ if (!byStem.has(stem)) {
+ byStem.set(stem, null);
+ }
+ }
}
- if (isPointer) {
- ent.pointer = info;
- } else {
- ent.def = info;
+
+ const out: Entity[] = [];
+ for (const [stem, def] of byStem) {
+ const split = splitTypedName(stem);
+ if (!split) {
+ continue;
+ }
+ out.push({ dir: prefix + stem, location: split.location, type: split.type, def });
}
- }
+ return out;
+ };
+
+ return { roots: () => readDir(''), children: (e) => readDir(e.dir) };
+}
- return [...byHash.values()];
+/**
+ * Split a `distributed` stem into its location and type.
+ *
+ * The LAST `.type.` is the separator, not the first: a location is a filename and
+ * may contain the marker itself (a project file called `foo.type.File` yields the
+ * stem `foo.type.File.type.File`), and splitting on the first would truncate it.
+ * Null for a stem carrying no marker — `Project.xml` and the store's own uuid
+ * document are both of that shape, and neither is an entity.
+ */
+function splitTypedName(stem: string): { location: string; type: string } | null {
+ const marker = '.type.';
+ const at = stem.lastIndexOf(marker);
+ if (at <= 0) {
+ return null;
+ }
+ const type = stem.slice(at + marker.length);
+ if (!type || type.includes('.')) {
+ return null;
+ }
+ return { location: stem.slice(0, at), type };
}
/**
- * Given a child filename like `8AEH..._sp.xml` or `qaw0...p.xml`, return the
- * hash (stem before the suffix) and whether it is a pointer. Null when the stem
- * carries none of the four recognized suffixes — a file in the store that is not
- * half of a pointer/def pair, which readDir skips.
+ * Given a `fixedPathV2` child filename like `8AEH..._sp.xml` or `qaw0...p.xml`,
+ * return the hash (stem before the suffix) and whether it is a pointer. Null when
+ * the stem carries none of the four recognized suffixes — a file in the store that
+ * is not half of a pointer/def pair, which readDir skips.
*
* PRECONDITION: `name` ends in `.xml`. parseProject's index only admits `.xml`
* paths, and readDir only asks about entries of that index, so re-checking here
@@ -327,49 +666,56 @@ function parseChildName(name: string): { hash: string; isPointer: boolean } | nu
return null;
}
+// ---------------------------------------------------------------------------------
+// The collections.
+
/**
* Read the Files collection. Members are File entities; each File entity's own
- * dir holds its children as pointer/def pairs: a DIR_SIGNIFIER marks a folder,
- * and any nested type="File" children are themselves project files (recurse).
+ * dir holds its children as entities: a DIR_SIGNIFIER marks a folder, and any
+ * nested type="File" children are themselves project files (recurse).
*/
-function readFiles(index: Map, filesHash: string): ProjectFile[] {
+function readFiles(layout: Layout, collection: Entity): ProjectFile[] {
const out: ProjectFile[] = [];
const seen = new Set();
- const members = readDir(index, filesHash);
- for (const member of members) {
- collectFile(index, member, out, seen);
+ for (const member of layout.children(collection)) {
+ collectFile(layout, member, '', out, seen);
}
return out;
}
function collectFile(
- index: Map,
+ layout: Layout,
entity: Entity,
+ parentPath: string,
out: ProjectFile[],
seen: Set,
): void {
- if (entity.pointer?.['@_type'] !== 'File') {
+ if (entity.type !== 'File') {
return;
}
- const path = entity.pointer['@_location'];
- if (!path) {
+ // A file entity's location is relative to its PARENT, not to the project root —
+ // the store nests them the way the filesystem does. Read verbatim, every file
+ // inside a folder reported the bare name that a sibling at the root would, so
+ // `utils/helper.m` and a root-level `helper.m` were indistinguishable.
+ const name = entity.location;
+ if (!name) {
return;
}
+ const path = parentPath ? `${parentPath}/${name}` : name;
// Guard against cycles / repeated hashes.
- if (seen.has(entity.hash)) {
+ if (seen.has(entity.dir)) {
return;
}
- seen.add(entity.hash);
+ seen.add(entity.dir);
- const children = readDir(index, entity.hash);
+ const children = layout.children(entity);
let isFolder = false;
const labels: string[] = [];
const fileChildren: Entity[] = [];
for (const child of children) {
- const ctype = child.pointer?.['@_type'];
- if (ctype === 'DIR_SIGNIFIER') {
+ if (child.type === 'DIR_SIGNIFIER') {
isFolder = true;
- } else if (ctype === 'File') {
+ } else if (child.type === 'File') {
fileChildren.push(child);
}
}
@@ -381,7 +727,7 @@ function collectFile(
// Recurse into nested File children (folder contents).
for (const child of fileChildren) {
- collectFile(index, child, out, seen);
+ collectFile(layout, child, path, out, seen);
}
}
@@ -402,18 +748,21 @@ function dedupe(arr: string[]): string[] {
}
/**
- * Read the ProjectPath collection. Each entry is a type="Reference" pointer
- * whose def carries `Ref=""`; that Ref is the path folder name.
+ * Read the ProjectPath collection. Each entry is a type="Reference" whose def
+ * carries `Ref=""`; that Ref is the path folder, relative to the project
+ * root, and `Ref=""` is the project root itself.
*/
-function readPathFolders(index: Map, hash: string): string[] {
+function readPathFolders(layout: Layout, collection: Entity): string[] {
const out: string[] = [];
- const entries = readDir(index, hash);
- for (const ent of entries) {
- if (ent.pointer?.['@_type'] !== 'Reference') {
+ for (const ent of layout.children(collection)) {
+ if (ent.type !== 'Reference') {
continue;
}
const ref = ent.def?.['@_Ref'];
- if (ref) {
+ // `!== undefined`, not truthiness: the project ROOT is on the path as `Ref=""`,
+ // and it is usually the first folder MATLAB adds, so testing for a non-empty
+ // string dropped a real entry from every project that has one.
+ if (ref !== undefined) {
out.push(ref);
}
}
@@ -424,38 +773,200 @@ function readPathFolders(index: Map, hash: string): string[] {
* Read the Categories collection into a flat label catalog. Each Category dir
* holds type="Label" entries; the label display name is the def's Name.
*/
-function readCategories(index: Map, hash: string): ProjectLabel[] {
+function readCategories(layout: Layout, collection: Entity): ProjectLabel[] {
const out: ProjectLabel[] = [];
- const categories = readDir(index, hash);
- for (const cat of categories) {
- if (cat.pointer?.['@_type'] !== 'Category') {
+ for (const cat of layout.children(collection)) {
+ if (cat.type !== 'Category') {
continue;
}
- // Category display name: def Name, else the pointer location (the id).
- const categoryName = cat.def?.['@_Name'] || cat.pointer?.['@_location'] || '';
- const labelEntries = readDir(index, cat.hash);
- for (const labelEnt of labelEntries) {
- if (labelEnt.pointer?.['@_type'] !== 'Label') {
+ // Category display name: def Name, else the location (the id).
+ const categoryName = cat.def?.['@_Name'] || cat.location || '';
+ for (const labelEnt of layout.children(cat)) {
+ if (labelEnt.type !== 'Label') {
continue;
}
- const id = labelEnt.pointer?.['@_location'] || '';
+ const id = labelEnt.location || '';
const name = labelEnt.def?.['@_Name'] || id || '';
if (name) {
- out.push({ id, category: categoryName, name });
+ // The attribute spells itself 'READ_ONLY' on a label and '1' on a category,
+ // so this tests for PRESENCE rather than for a value — both mean MATLAB owns
+ // it, and a project's own label carries no such marking.
+ const ro = labelEnt.def?.['@_ReadOnly'];
+ out.push({ id, category: categoryName, name, readOnly: ro !== undefined && ro !== '0' });
}
}
}
return out;
}
+/**
+ * An entry point plus the store's link to its predecessor.
+ *
+ * `prev` stays internal to this module: it is the MECHANISM by which the store
+ * records run order, and `orderEntryPoints` resolves it into the order itself
+ * before anything outside sees an entry point, so no consumer has to walk a linked
+ * list to answer "which startup file runs first".
+ */
+interface ChainedEntryPoint extends ProjectEntryPoint {
+ /** The id of the entry that runs BEFORE this one; 'HEAD' when first, '' when unchained. */
+ prev: string;
+}
+
+/** Read the EntryPoints collection (shortcuts + startup/shutdown files). */
+function readEntryPoints(layout: Layout, collection: Entity): ChainedEntryPoint[] {
+ const out: ChainedEntryPoint[] = [];
+ for (const ent of layout.children(collection)) {
+ if (ent.type !== 'EntryPoint') {
+ continue;
+ }
+ const def = ent.def;
+ const file = def?.['@_File'] ?? '';
+ const name = def?.['@_Name'] ?? '';
+ if (!file && !name) {
+ continue;
+ }
+ const group = def?.['@_GroupUUID'] ?? '';
+ out.push({
+ id: ent.location,
+ name: name || file,
+ file,
+ kind: def?.['@_Type'] ?? '',
+ // Attribute values are never coerced by the reader, so this is the string
+ // '1' or '0' (see XmlReader's contract) — not a number and not a boolean.
+ visible: def?.['@_Visible'] !== '0',
+ // 'default' is the store's spelling for "no group"; a caller grouping by this
+ // field would otherwise render a group literally called default.
+ groupId: group === 'default' ? '' : group,
+ prev: readPrev(def),
+ });
+ }
+ return out;
+}
+
+/**
+ * The predecessor link from an entry point's def.
+ *
+ * It arrives as a nested ``. `toArray` is required, not defensive: the XML reader's
+ * `isArray` is configured per-dictionary and does not cover this element, so a def
+ * with one Extension yields an object and a def with two yields an array.
+ */
+function readPrev(def: XmlInfo | null | undefined): string {
+ for (const ext of toArray(def?.Extension)) {
+ const name = ext['@_Name'];
+ if (name === 'StartUpPrev' || name === 'ShutdownPrev') {
+ return ext['@_Value'] ?? '';
+ }
+ }
+ return '';
+}
+
+/** Read the EntryPointGroups collection (the named groups of the Shortcuts gallery). */
+function readEntryPointGroups(layout: Layout, collection: Entity): ProjectEntryPointGroup[] {
+ const out: ProjectEntryPointGroup[] = [];
+ for (const ent of layout.children(collection)) {
+ if (ent.type !== 'EntryPointGroup') {
+ continue;
+ }
+ const name = ent.def?.['@_Name'];
+ if (name) {
+ out.push({ id: ent.location, name });
+ }
+ }
+ return out;
+}
+
+/** Read the WorkingFolders collection (cache, code generation, startup folder, …). */
+function readWorkingFolders(layout: Layout, collection: Entity): ProjectWorkingFolder[] {
+ const out: ProjectWorkingFolder[] = [];
+ for (const ent of layout.children(collection)) {
+ if (ent.type !== 'Reference') {
+ continue;
+ }
+ const ref = ent.def?.['@_Ref'];
+ if (!ent.location || ref === undefined) {
+ continue;
+ }
+ out.push({ key: ent.location, ref });
+ }
+ return out;
+}
+
+/**
+ * Put the startup and shutdown files in RUN order.
+ *
+ * The store encodes the order as a linked list, and only as one: each entry names
+ * its PREDECESSOR in a nested ``, with `HEAD` marking the first. MATLAB runs the files top-down,
+ * so this order is meaningful and no other field recovers it — sorting these by
+ * name would silently reorder a shutdown sequence.
+ *
+ * Everything unchained keeps its store order and follows: a project with one
+ * startup file writes no Extension at all, and a chain that is broken (a missing
+ * predecessor, a cycle) must still yield every entry exactly once.
+ */
+function orderEntryPoints(entries: ChainedEntryPoint[]): ProjectEntryPoint[] {
+ const out: ChainedEntryPoint[] = [];
+ const taken = new Set();
+
+ for (const kind of ['StartUp', 'Shutdown']) {
+ const hook = entries.filter((e) => e.kind === kind);
+
+ // Index by predecessor so the walk is a lookup per step rather than a scan, and
+ // so a store with two entries claiming the same predecessor (which nothing
+ // prevents) still yields both instead of looping on one.
+ const byPrev = new Map();
+ for (const e of hook) {
+ const list = byPrev.get(e.prev);
+ if (list) {
+ list.push(e);
+ } else {
+ byPrev.set(e.prev, [e]);
+ }
+ }
+
+ // Follow the chain from HEAD. `taken` is what makes this terminate on a cycle:
+ // a store whose links form a loop would otherwise spin here forever.
+ let cursor = 'HEAD';
+ for (;;) {
+ const step = (byPrev.get(cursor) ?? []).find((e) => !taken.has(e));
+ if (!step) {
+ break;
+ }
+ taken.add(step);
+ out.push(step);
+ cursor = step.id;
+ }
+
+ // Whatever the chain did not reach, in store order. A project with a single
+ // startup file writes no Extension at all and lands here — which is why the
+ // fallback is not an error path but the common one.
+ for (const e of hook) {
+ if (!taken.has(e)) {
+ taken.add(e);
+ out.push(e);
+ }
+ }
+ }
+
+ // Then the shortcuts, which have no order to preserve beyond the store's.
+ for (const e of entries) {
+ if (!taken.has(e)) {
+ out.push(e);
+ }
+ }
+
+ // Drop `prev` on the way out: see ChainedEntryPoint.
+ return out.map(({ prev: _prev, ...rest }) => rest);
+}
+
/**
* Resolve a genuine project->project reference. Name is the basename of the
* Ref path when present, else the UUID location.
*/
function resolveReference(ent: Entity): ProjectReference | null {
const ref = ent.def?.['@_Ref'];
- const location = ent.pointer?.['@_location'] || '';
- const id = location || ref || '';
+ const id = ent.location || ref || '';
if (!id) {
return null;
}
@@ -464,5 +975,5 @@ function resolveReference(ent: Entity): ProjectReference | null {
const parts = ref.split(/[/\\]/).filter((p) => p.length > 0);
name = parts.length > 0 ? parts[parts.length - 1] : ref;
}
- return { id, name };
+ return { id, name, path: ref ?? null };
}
diff --git a/src/index.ts b/src/index.ts
index 2b6bc28..3b6af54 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -81,7 +81,29 @@ export { parseMdl } from './datamodel/parser/MdlParser.js';
export { parseModel } from './datamodel/parser/ModelParser.js';
export { parseMat } from './datamodel/parser/MatParser.js';
export { parseProject } from './datamodel/parser/ProjectParser.js';
-export type { ParsedProject, ProjectFile, ProjectLabel, ProjectReference } from './datamodel/parser/ProjectParser.js';
+export type {
+ ParsedProject,
+ ProjectFile,
+ ProjectLabel,
+ ProjectReference,
+ ProjectEntryPoint,
+ ProjectEntryPointGroup,
+ ProjectWorkingFolder,
+} from './datamodel/parser/ProjectParser.js';
+// The view model behind a project's MAIN PAGE, as opposed to a table of its files: a
+// `.prj` has no table worth showing (see ProjectPage.ts), and everything a page shows
+// around the file list — run order, shortcut groups, label coverage — is derived from
+// the parse rather than read from it, so it is derived once here for every host.
+export { buildProjectPage } from './datamodel/parser/ProjectPage.js';
+export type {
+ ProjectPage,
+ ProjectPageRun,
+ ProjectPageShortcut,
+ ProjectPageLabel,
+ ProjectPageCategory,
+ ProjectPageLocation,
+ ProjectPageReference,
+} from './datamodel/parser/ProjectPage.js';
// What the model and MAT readers return. A consumer has to be able to NAME a parse
// result to hold one in a field, annotate a variable, or write a function that takes
diff --git a/test/projectNode.test.ts b/test/projectNode.test.ts
index 161223d..f6ac8e0 100644
--- a/test/projectNode.test.ts
+++ b/test/projectNode.test.ts
@@ -110,6 +110,19 @@ describe('ProjectNode.fromParsed', () => {
expect(named(node.getSection('path')!.children[0] as any)).toEqual(['/', '/']);
});
+ it('names the project root, which the store records as the empty path', () => {
+ // The root IS on the MATLAB path — usually first — and the store spells it ''.
+ // Alone among path entries it has no segment to be named by, so the "show the
+ // path itself" fallback above would leave a blank, unidentifiable row.
+ const parsed = makeParsed();
+ parsed.pathFolders = ['', 'utils'];
+ const rows = ProjectNode.fromParsed(parsed, 'p.prj').getSection('path')!.children;
+ expect(rows.map((c) => [c.name, (c as any).location])).toEqual([
+ ['(project root)', ''],
+ ['utils', 'utils'],
+ ]);
+ });
+
it('falls back to the reference id when it has no name', () => {
const parsed = makeParsed();
parsed.references = [{ id: 'ref-uuid-2' }];
diff --git a/test/projectPage.test.ts b/test/projectPage.test.ts
new file mode 100644
index 0000000..6ae4160
--- /dev/null
+++ b/test/projectPage.test.ts
@@ -0,0 +1,276 @@
+// Copyright 2026 The MathWorks, Inc.
+//
+// The page is the whole reason a `.prj` opens at all: it is the one surface that
+// shows what a project IS rather than which files it happens to contain (the
+// Explorer already shows those, and MATLAB opens no document tab for a project).
+//
+// So the cases that matter here are the ones where the page would otherwise state
+// something false about a real project: a shutdown sequence in the wrong order, a
+// shortcut filed under a group that no longer exists, a label coverage figure that
+// counts a file the catalog cannot name, an unrecognized entry-point kind vanishing
+// from a gallery whose job is to list what the project put there.
+import { describe, it, expect } from 'vitest';
+import { buildProjectPage } from '../src/datamodel/parser/ProjectPage.js';
+import type { ParsedProject } from '../src/datamodel/parser/ProjectParser.js';
+
+/** An empty parse, for a test to fill in only the field it is about. */
+function parsed(over: Partial = {}): ParsedProject {
+ return {
+ name: 'Proj',
+ format: 'fixedPathV2',
+ files: [],
+ pathFolders: [],
+ labels: [],
+ references: [],
+ entryPoints: [],
+ entryPointGroups: [],
+ workingFolders: [],
+ warnings: [],
+ ...over,
+ };
+}
+
+const ep = (over: Partial) => ({
+ id: 'id',
+ name: 'name',
+ file: 'file.m',
+ kind: 'Basic',
+ visible: true,
+ groupId: '',
+ ...over,
+});
+
+describe('buildProjectPage — the hooks', () => {
+ it('splits the hooks out and keeps the order the parser put them in', () => {
+ // The parser hands these over in RUN order (from the store's *Prev chain), and
+ // nothing here may reorder them: MATLAB runs the files top-down, so a page that
+ // sorted them by name would misreport the sequence a project shuts down in.
+ const page = buildProjectPage(
+ parsed({
+ entryPoints: [
+ ep({ id: 'u', name: 'up', file: 'a.m', kind: 'StartUp', visible: false }),
+ ep({ id: 'd1', name: 'zzz-first', file: 'z.m', kind: 'Shutdown', visible: false }),
+ ep({ id: 'd2', name: 'aaa-second', file: 'a2.m', kind: 'Shutdown', visible: false }),
+ ep({ id: 's', name: 'shortcut', file: 's.m' }),
+ ],
+ }),
+ );
+ expect(page.startup).toEqual([{ name: 'up', file: 'a.m' }]);
+ expect(page.shutdown).toEqual([
+ { name: 'zzz-first', file: 'z.m' },
+ { name: 'aaa-second', file: 'a2.m' },
+ ]);
+ expect(page.shortcuts.map((s) => s.name)).toEqual(['shortcut']);
+ });
+
+ it('leaves both hooks empty for a project that registers none', () => {
+ const page = buildProjectPage(parsed());
+ expect(page.startup).toEqual([]);
+ expect(page.shutdown).toEqual([]);
+ });
+});
+
+describe('buildProjectPage — the shortcuts gallery', () => {
+ it('resolves a group id to its name and puts grouped shortcuts first', () => {
+ const page = buildProjectPage(
+ parsed({
+ entryPointGroups: [{ id: 'g1', name: 'Utility' }],
+ entryPoints: [
+ ep({ id: 'a', name: 'loose', file: 'a.m' }),
+ ep({ id: 'b', name: 'grouped', file: 'b.m', groupId: 'g1' }),
+ ],
+ }),
+ );
+ expect(page.shortcuts).toEqual([
+ { name: 'grouped', file: 'b.m', group: 'Utility' },
+ { name: 'loose', file: 'a.m', group: '' },
+ ]);
+ });
+
+ it('treats a group id that names no group as ungrouped', () => {
+ // A store can outlive the group it points at, and a gallery heading reading a
+ // bare UUID is worse than no heading at all.
+ const page = buildProjectPage(
+ parsed({ entryPoints: [ep({ name: 'orphan', groupId: 'gone' })] }),
+ );
+ expect(page.shortcuts).toEqual([{ name: 'orphan', file: 'file.m', group: '' }]);
+ });
+
+ it('keeps a shortcut of a kind it does not recognize', () => {
+ // Selected by excluding the two hook kinds, not by asking for 'Basic': a newer
+ // release can add a kind, and an unknown one belongs in the gallery — at worst
+ // grouped oddly — rather than absent from a page that exists to list these.
+ const page = buildProjectPage(
+ parsed({ entryPoints: [ep({ name: 'future', kind: 'SomethingNew' })] }),
+ );
+ expect(page.shortcuts.map((s) => s.name)).toEqual(['future']);
+ });
+
+ it('leaves a hidden entry out of the gallery', () => {
+ // `visible` is MATLAB's own answer to whether this belongs in the gallery.
+ const page = buildProjectPage(
+ parsed({ entryPoints: [ep({ name: 'hidden', visible: false })] }),
+ );
+ expect(page.shortcuts).toEqual([]);
+ });
+
+ it('keeps a shortcut that targets a folder', () => {
+ const page = buildProjectPage(parsed({ entryPoints: [ep({ name: 'utils', file: 'utilities' })] }));
+ expect(page.shortcuts[0].file).toBe('utilities');
+ });
+});
+
+describe('buildProjectPage — labels and coverage', () => {
+ const catalog = [
+ { id: 'design', category: 'Classification', name: 'Design', readOnly: true },
+ { id: 'test', category: 'Classification', name: 'Test', readOnly: true },
+ { id: 'unused', category: 'Classification', name: 'Unused', readOnly: true },
+ { id: 'mine', category: 'Classification', name: 'ForUser', readOnly: false },
+ ];
+
+ it('counts members per label and reports coverage', () => {
+ const page = buildProjectPage(
+ parsed({
+ labels: catalog,
+ files: [
+ { path: 'a.m', isFolder: false, labels: ['design'] },
+ { path: 'b.m', isFolder: false, labels: ['design'] },
+ { path: 'c.m', isFolder: false, labels: ['mine'] },
+ { path: 'd.m', isFolder: false, labels: [] },
+ { path: 'folder', isFolder: true, labels: [] },
+ ],
+ }),
+ );
+ // memberCount counts folders too: they are members of the project, and the
+ // Explorer shows them as such.
+ expect(page.memberCount).toBe(5);
+ expect(page.labelledCount).toBe(3);
+ const byName = new Map(page.categories[0].labels.map((l) => [l.name, l]));
+ expect(byName.get('Design')?.count).toBe(2);
+ expect(byName.get('Unused')?.count).toBe(0);
+ expect(byName.get('ForUser')?.custom).toBe(true);
+ expect(byName.get('Design')?.custom).toBe(false);
+ });
+
+ it('orders used labels before unused ones', () => {
+ // The seven built-ins ship with every project and most stay at zero, so catalog
+ // order buries the two or three that say something about THIS project.
+ const page = buildProjectPage(
+ parsed({
+ labels: catalog,
+ files: [
+ { path: 'a.m', isFolder: false, labels: ['test'] },
+ { path: 'b.m', isFolder: false, labels: ['test'] },
+ { path: 'c.m', isFolder: false, labels: ['design'] },
+ ],
+ }),
+ );
+ expect(page.categories[0].labels.map((l) => l.name)).toEqual([
+ 'Test',
+ 'Design',
+ 'ForUser',
+ 'Unused',
+ ]);
+ });
+
+ it('counts a label a member carries but the catalog does not define', () => {
+ // The two can disagree — a store written by a release that knew the label, or
+ // hand-edited. Dropping it would make the coverage figure disagree with the
+ // chips beneath it, which is the one thing a page must not do.
+ const page = buildProjectPage(
+ parsed({
+ labels: [catalog[0]],
+ files: [
+ { path: 'a.m', isFolder: false, labels: ['design'] },
+ { path: 'b.m', isFolder: false, labels: ['ghost'] },
+ ],
+ }),
+ );
+ expect(page.labelledCount).toBe(2);
+ const all = page.categories.flatMap((c) => c.labels);
+ expect(all.find((l) => l.id === 'ghost')).toEqual({
+ id: 'ghost',
+ name: 'ghost',
+ count: 1,
+ custom: true,
+ });
+ });
+
+ it('counts a member once per label, not once per category it appears in', () => {
+ const page = buildProjectPage(
+ parsed({
+ labels: [
+ { id: 'design', category: 'Classification', name: 'Design', readOnly: true },
+ { id: 'rev', category: 'Review', name: 'Reviewed', readOnly: false },
+ ],
+ files: [{ path: 'a.m', isFolder: false, labels: ['design', 'rev'] }],
+ }),
+ );
+ expect(page.labelledCount).toBe(1);
+ expect(page.categories.map((c) => c.name)).toEqual(['Classification', 'Review']);
+ expect(page.categories.every((c) => c.labels[0].count === 1)).toBe(true);
+ });
+
+ it('has no categories at all for a project that defines no labels', () => {
+ expect(buildProjectPage(parsed()).categories).toEqual([]);
+ });
+});
+
+describe('buildProjectPage — the remaining sections', () => {
+ it('labels a known designated location and passes an unknown key through', () => {
+ const page = buildProjectPage(
+ parsed({
+ workingFolders: [
+ { key: 'SimulinkCacheFolder', ref: 'Cache' },
+ { key: 'NewInSomeFutureRelease', ref: 'Elsewhere' },
+ ],
+ }),
+ );
+ expect(page.locations).toEqual([
+ { key: 'SimulinkCacheFolder', label: 'Simulation cache', ref: 'Cache' },
+ // Showing the raw key is honest; dropping the row or printing 'Unknown' is not.
+ { key: 'NewInSomeFutureRelease', label: 'NewInSomeFutureRelease', ref: 'Elsewhere' },
+ ]);
+ });
+
+ it("uses MATLAB's words for the metadata format, not the store's", () => {
+ // `fixedPathV2` and `distributed` are two encodings of one USER choice. Printing
+ // the internal name invites a user to look for a setting spelled that way.
+ expect(buildProjectPage(parsed({ format: 'fixedPathV2' })).formatLabel).toBe('multiple XML files');
+ expect(buildProjectPage(parsed({ format: 'distributed' })).formatLabel).toBe('multiple XML files');
+ expect(buildProjectPage(parsed({ format: 'toml' })).formatLabel).toBe('matlab.toml');
+ // An unrecognized one still says something rather than nothing.
+ expect(buildProjectPage(parsed({ format: 'v99' })).formatLabel).toBe('v99');
+ });
+
+ it('keeps the project root among the path folders', () => {
+ const page = buildProjectPage(parsed({ pathFolders: ['', 'utils'] }));
+ expect(page.pathFolders).toEqual(['', 'utils']);
+ });
+
+ it('shows a reference by its path, and by name alone when there is none', () => {
+ // The path is what a host resolves to open the referenced project. A reference
+ // the store gave no path for leaves only its UUID, and linking THAT would offer
+ // to open a path that cannot exist — so it is empty, which is how the page knows
+ // not to make it a link.
+ const page = buildProjectPage(
+ parsed({
+ references: [
+ { id: 'uuid-1', name: 'Lib.prj', path: '../Lib/Lib.prj' },
+ { id: 'uuid-2', name: null, path: null },
+ ],
+ }),
+ );
+ expect(page.references).toEqual([
+ { name: 'Lib.prj', path: '../Lib/Lib.prj' },
+ { name: 'uuid-2', path: '' },
+ ]);
+ });
+
+ it('carries the parse warnings through to the page', () => {
+ // The page is where a user finds out their project did not read whole; a warning
+ // that stops at the parse boundary is a warning nobody sees.
+ const warnings = [{ code: 'part-unreadable' as const, message: 'lost one', part: 'x.xml' }];
+ expect(buildProjectPage(parsed({ warnings })).warnings).toEqual(warnings);
+ });
+});
diff --git a/test/projectParser.test.ts b/test/projectParser.test.ts
index f1be446..8e1080f 100644
--- a/test/projectParser.test.ts
+++ b/test/projectParser.test.ts
@@ -132,14 +132,16 @@ describe('parseProject', () => {
const byPath = new Map(parsed.files.map((f) => [f.path, f]));
- // helper.m: a File with the 'design' label.
- const helper = byPath.get('helper.m');
+ // helper.m: a File with the 'design' label. Its path is qualified by the folder
+ // it sits in — a File entity's `location` is relative to its PARENT entity, not
+ // to the project root, so the bare name the store records is only half of it.
+ const helper = byPath.get('utils/helper.m');
expect(helper).toBeDefined();
expect(helper?.isFolder).toBe(false);
expect(helper?.labels).toContain('design');
- // projmodel.slx: a File.
- const model = byPath.get('projmodel.slx');
+ // projmodel.slx: a File, likewise under the folder that holds it.
+ const model = byPath.get('models/projmodel.slx');
expect(model).toBeDefined();
expect(model?.isFolder).toBe(false);
@@ -182,7 +184,7 @@ describe('parseProject', () => {
expect(parsed.name).toBe('MyProj');
// The other file (projmodel.slx) and folders still parse.
const paths = parsed.files.map((f) => f.path);
- expect(paths).toContain('projmodel.slx');
+ expect(paths).toContain('models/projmodel.slx');
expect(paths).toContain('models');
// Label catalog is unaffected.
expect(parsed.labels.map((l) => l.name)).toContain('Design');
@@ -204,10 +206,10 @@ describe('parseProject', () => {
expect(parsed.labels).toEqual([]);
// Other sections still parse.
expect(parsed.name).toBe('MyProj');
- expect(parsed.files.map((f) => f.path)).toContain('helper.m');
+ expect(parsed.files.map((f) => f.path)).toContain('utils/helper.m');
expect(parsed.pathFolders).toContain('utils');
// Per-file label assignments (UUIDs) are still surfaced.
- const helper = parsed.files.find((f) => f.path === 'helper.m');
+ const helper = parsed.files.find((f) => f.path === 'utils/helper.m');
expect(helper?.labels).toContain('design');
});
@@ -542,7 +544,32 @@ describe('parseProject — the label catalog', () => {
}),
'fallback',
);
- expect(parsed.labels).toEqual([{ id: 'design', category: 'Classification', name: 'Design' }]);
+ expect(parsed.labels).toEqual([
+ { id: 'design', category: 'Classification', name: 'Design', readOnly: false },
+ ]);
+ });
+
+ it('marks the labels MATLAB owns as read-only, and the project\'s own as not', () => {
+ // The seven Classification labels ship with every project and are marked
+ // ReadOnly in the store; a label the project ADDED is not. That marking is the
+ // only thing separating the two, and it is what lets a page distinguish "this
+ // project defines its own vocabulary" from "this project uses the defaults".
+ // Note the attribute spells itself 'READ_ONLY' on a label and '1' on a category,
+ // so presence is what is read, not any one value.
+ const parsed = parseProject(
+ store({
+ root: { cc: [''] },
+ cc: { cat: ['', ''] },
+ cat: {
+ builtin: ['', ''],
+ mine: ['', ''],
+ },
+ }),
+ 'fallback',
+ );
+ const byId = new Map(parsed.labels.map((l) => [l.id, l]));
+ expect(byId.get('design')?.readOnly).toBe(true);
+ expect(byId.get('mine')?.readOnly).toBe(false);
});
it('names a category by its pointer location when its def has no Name', () => {
@@ -556,7 +583,9 @@ describe('parseProject — the label catalog', () => {
}),
'fallback',
);
- expect(parsed.labels).toEqual([{ id: 'design', category: 'FileClassCategory', name: 'Design' }]);
+ expect(parsed.labels).toEqual([
+ { id: 'design', category: 'FileClassCategory', name: 'Design', readOnly: false },
+ ]);
});
it('names a label by its id when its def has no Name, and drops one with neither', () => {
@@ -575,8 +604,8 @@ describe('parseProject — the label catalog', () => {
'fallback',
);
expect(parsed.labels).toEqual([
- { id: 'lab1', category: 'Cat', name: 'Label One' },
- { id: 'lab2', category: 'Cat', name: 'lab2' },
+ { id: 'lab1', category: 'Cat', name: 'Label One', readOnly: false },
+ { id: 'lab2', category: 'Cat', name: 'lab2', readOnly: false },
]);
});
});
@@ -589,7 +618,12 @@ describe('parseProject — project references', () => {
store({ root: { r: ['', ''] } }),
'fallback',
);
- expect(parsed.references).toEqual([{ id: 'uuid-root-ref', name: 'Lib.prj' }]);
+ expect(parsed.references).toEqual([
+ // The PATH is kept as the store spelled it, not just its basename: a host that
+ // offers to open the referenced project needs somewhere to resolve, and the id
+ // is a UUID.
+ { id: 'uuid-root-ref', name: 'Lib.prj', path: '../Lib/Lib.prj' },
+ ]);
});
it('falls back to the Ref as the id when the pointer has no location', () => {
@@ -597,7 +631,9 @@ describe('parseProject — project references', () => {
store({ root: { r: ['', ''] } }),
'fallback',
);
- expect(parsed.references).toEqual([{ id: 'Sibling/Sibling.prj', name: 'Sibling.prj' }]);
+ expect(parsed.references).toEqual([
+ { id: 'Sibling/Sibling.prj', name: 'Sibling.prj', path: 'Sibling/Sibling.prj' },
+ ]);
});
it('reports a null name for a reference with no Ref path', () => {
@@ -607,7 +643,7 @@ describe('parseProject — project references', () => {
store({ root: { r: ['', ''] } }),
'fallback',
);
- expect(parsed.references).toEqual([{ id: 'uuid-only', name: null }]);
+ expect(parsed.references).toEqual([{ id: 'uuid-only', name: null, path: null }]);
});
it('drops a reference with neither a location nor a Ref', () => {
@@ -625,7 +661,7 @@ describe('parseProject — project references', () => {
store({ root: { r: ['', ''] } }),
'fallback',
);
- expect(parsed.references).toEqual([{ id: 'u', name: '///' }]);
+ expect(parsed.references).toEqual([{ id: 'u', name: '///', path: '///' }]);
});
it('resolves a Windows-style backslash Ref to its basename', () => {
@@ -633,6 +669,382 @@ describe('parseProject — project references', () => {
store({ root: { r: ['', ''] } }),
'fallback',
);
- expect(parsed.references).toEqual([{ id: 'u', name: 'Lib.prj' }]);
+ expect(parsed.references).toEqual([
+ { id: 'u', name: 'Lib.prj', path: '..\\Lib\\Lib.prj' },
+ ]);
+ });
+});
+
+describe('parseProject — nested member paths', () => {
+ it('qualifies a nested file by the folders above it', () => {
+ // A File entity's `location` is relative to its PARENT entity, not to the
+ // project root: `utils/helper.m` is stored as a File named `helper.m` inside a
+ // File named `utils`. Read verbatim, that file and a root-level `helper.m`
+ // produce the same row, so a host cannot open either one reliably.
+ const parsed = parseProject(
+ store({
+ root: { f: [''] },
+ f: { a: ['', ''] },
+ a: { b: ['', ''] },
+ b: { c: ['', ''] },
+ }),
+ 'fallback',
+ );
+ expect(parsed.files.map((f) => f.path)).toEqual(['top', 'top/mid', 'top/mid/leaf.m']);
+ });
+});
+
+describe('parseProject — two collections of one type', () => {
+ it('reads the Root Files collection and ignores another one beside it', () => {
+ // A real project carries BOTH `location="Root" type="Files"` (its members) and
+ // `location="ALM" type="Files"` (artifact tracking). Matching on type alone
+ // assigned `files` twice and let store order pick the winner — and the ALM
+ // collection holds no member files at all, only a DIR_SIGNIFIER, so losing that
+ // race produced a project with zero members.
+ const parsed = parseProject(
+ store({
+ root: {
+ rootFiles: [''],
+ almFiles: [''],
+ },
+ rootFiles: { m: ['', ''] },
+ almFiles: { d: ['', ''] },
+ }),
+ 'fallback',
+ );
+ expect(parsed.files.map((f) => f.path)).toEqual(['real.m']);
+ });
+});
+
+describe('parseProject — the project root on the path', () => {
+ it('keeps the empty Ref that names the project root itself', () => {
+ // The root is on the MATLAB path as `Ref=""` and is usually the FIRST folder
+ // MATLAB adds, so testing the Ref for truthiness dropped a real entry from
+ // almost every project. Empty-string is the root's name here; a host renders it.
+ const parsed = parseProject(
+ store({
+ root: { pp: [''] },
+ pp: {
+ a: ['', ''],
+ b: ['', ''],
+ },
+ }),
+ 'fallback',
+ );
+ expect(parsed.pathFolders).toEqual(['', 'utils']);
+ });
+});
+
+describe('parseProject — working folders', () => {
+ const workingFolderStore = () =>
+ store({
+ root: { wf: [''] },
+ wf: {
+ a: ['', ''],
+ b: ['', ''],
+ c: ['', ''],
+ },
+ });
+
+ it('reads each designated location, keeping the store key verbatim', () => {
+ // The key is NOT translated to a display name here: a newer release can
+ // designate a purpose this version has no label for, and passing the raw key
+ // through lets a host show it rather than silently drop the row.
+ const parsed = parseProject(workingFolderStore(), 'fallback');
+ expect(parsed.workingFolders).toEqual([
+ { key: 'SimulinkCacheFolder', ref: 'Cache' },
+ { key: 'DependencyCacheFile', ref: 'Dep/cache.graphml' },
+ { key: 'NewInSomeFutureRelease', ref: 'Somewhere' },
+ ]);
+ });
+
+ it('does not report them as project references', () => {
+ // These are spelled `type="Reference"`, exactly like a project->project
+ // reference and like a path folder — the collection they sit in is the ONLY
+ // thing separating them. A scan that looked at every collection's Reference
+ // children therefore INVENTED references: this project has none, and the
+ // monophonic_syntethizer project was reported as referencing three.
+ const parsed = parseProject(workingFolderStore(), 'fallback');
+ expect(parsed.references).toEqual([]);
+ });
+
+ it('still finds a reference in a collection it does not model', () => {
+ // The flip side of that fix: the collection holding real references is spelled
+ // differently across releases, so the parser skips the collections it KNOWS are
+ // not references rather than allow-listing the ones it thinks are.
+ const parsed = parseProject(
+ store({
+ root: { c: [''] },
+ c: { r: ['', ''] },
+ }),
+ 'fallback',
+ );
+ expect(parsed.references).toEqual([
+ { id: 'uuid-1', name: 'Lib.prj', path: '../Lib/Lib.prj' },
+ ]);
+ });
+});
+
+describe('parseProject — entry points', () => {
+ /** A store with a two-file shutdown chain, written in the REVERSE of run order. */
+ const chainStore = () =>
+ store({
+ root: { ep: [''] },
+ ep: {
+ second: [
+ '',
+ '' +
+ '',
+ ],
+ first: [
+ '',
+ '' +
+ '',
+ ],
+ },
+ });
+
+ it('puts a hook in run order, not store order', () => {
+ // MATLAB runs these top-down, and the order lives ONLY in the *Prev chain —
+ // each entry naming its predecessor, HEAD marking the first. Nothing else in
+ // the store recovers it, so presenting these in store order (or sorted by name)
+ // silently misreports the sequence a project shuts down in.
+ const parsed = parseProject(chainStore(), 'fallback');
+ expect(parsed.entryPoints.map((e) => e.name)).toEqual(['first', 'second']);
+ });
+
+ it('yields every entry exactly once when the chain is a cycle', () => {
+ // Nothing in the store prevents it, and a chain walk that trusts the links
+ // would never terminate. Order is then undefined, but completeness is not.
+ const parsed = parseProject(
+ store({
+ root: { ep: [''] },
+ ep: {
+ a: [
+ '',
+ '',
+ ],
+ b: [
+ '',
+ '',
+ ],
+ },
+ }),
+ 'fallback',
+ );
+ expect(parsed.entryPoints.map((e) => e.name).sort()).toEqual(['a', 'b']);
+ });
+
+ it('keeps an unchained hook file, which is what a single-file hook looks like', () => {
+ // A project with one startup file writes no Extension at all, so the absent
+ // chain is the COMMON case here and not a damaged store.
+ const parsed = parseProject(
+ store({
+ root: { ep: [''] },
+ ep: { a: ['', ''] },
+ }),
+ 'fallback',
+ );
+ expect(parsed.entryPoints).toEqual([
+ { id: 'id-a', name: 's', file: 's.m', kind: 'StartUp', visible: true, groupId: '' },
+ ]);
+ });
+
+ it('orders startup before shutdown, then the shortcuts', () => {
+ const parsed = parseProject(
+ store({
+ root: { ep: [''] },
+ ep: {
+ s: ['', ''],
+ d: ['', ''],
+ u: ['', ''],
+ },
+ }),
+ 'fallback',
+ );
+ expect(parsed.entryPoints.map((e) => e.name)).toEqual(['up', 'down', 'shortcut']);
+ });
+
+ it("reads Visible as the string it is, and normalizes the 'default' group", () => {
+ // Attributes are not coerced by the reader, so Visible arrives as '1'/'0' —
+ // comparing it to a number or to `false` would make every entry visible.
+ // 'default' is the store's spelling for ungrouped; passed through, a host would
+ // render a shortcuts group literally named default.
+ const parsed = parseProject(
+ store({
+ root: { ep: [''] },
+ ep: {
+ a: [
+ '',
+ '',
+ ],
+ b: [
+ '',
+ '',
+ ],
+ },
+ }),
+ 'fallback',
+ );
+ const byName = new Map(parsed.entryPoints.map((e) => [e.name, e]));
+ expect(byName.get('hidden')?.visible).toBe(false);
+ expect(byName.get('hidden')?.groupId).toBe('');
+ expect(byName.get('shown')?.visible).toBe(true);
+ expect(byName.get('shown')?.groupId).toBe('grp-1');
+ });
+
+ it('keeps a shortcut that targets a folder', () => {
+ // An entry point is not always a file: a real project ships shortcuts whose
+ // File attribute names a DIRECTORY, which a host must open differently.
+ const parsed = parseProject(
+ store({
+ root: { ep: [''] },
+ ep: { a: ['', ''] },
+ }),
+ 'fallback',
+ );
+ expect(parsed.entryPoints[0].file).toBe('utilities');
+ });
+
+ it('skips an entry with neither a name nor a file, and a non-EntryPoint member', () => {
+ const parsed = parseProject(
+ store({
+ root: { ep: [''] },
+ ep: {
+ empty: ['', ''],
+ other: ['', ''],
+ good: ['', ''],
+ },
+ }),
+ 'fallback',
+ );
+ expect(parsed.entryPoints.map((e) => e.name)).toEqual(['g']);
+ });
+
+ it('reads the named shortcut groups', () => {
+ const parsed = parseProject(
+ store({
+ root: { g: [''] },
+ g: {
+ a: ['', ''],
+ unnamed: ['', ''],
+ other: ['', ''],
+ },
+ }),
+ 'fallback',
+ );
+ expect(parsed.entryPointGroups).toEqual([{ id: 'grp-1', name: 'Utility' }]);
+ });
+});
+
+describe('parseProject — the distributed layout', () => {
+ /**
+ * A store in the `distributed` layout: no pointer documents at all. An entity's
+ * location and type are its FILENAME (`.type..xml` for the def,
+ * `.type./` for the child directory), directories nest the way
+ * the entities do, and the top-level entities sit directly in the store root.
+ */
+ function distStore(files: Record): Record {
+ const out: Record = {
+ [at('Project.xml')]: info(''),
+ };
+ for (const [rel, body] of Object.entries(files)) {
+ out[at(rel)] = info(body);
+ }
+ return out;
+ }
+
+ it('reads a whole project out of filenames', () => {
+ const parsed = parseProject(
+ distStore({
+ 'ProjectData.type.Info.xml': '',
+ 'Root.type.Files/top.m.type.File.xml': '',
+ 'Root.type.Files/utils.type.File/1.type.DIR_SIGNIFIER.xml': '',
+ 'Root.type.Files/utils.type.File/helper.m.type.File.xml': '',
+ 'Root.type.ProjectPath/u1.type.Reference.xml': '',
+ 'Root.type.ProjectPath/u2.type.Reference.xml': '',
+ 'Root.type.EntryPoints/id-a.type.EntryPoint.xml': '',
+ }),
+ 'fallback',
+ );
+ expect(parsed.name).toBe('DistProj');
+ expect(parsed.format).toBe('distributed');
+ expect(parsed.warnings).toEqual([]);
+ expect(parsed.files.map((f) => f.path)).toEqual(['top.m', 'utils', 'utils/helper.m']);
+ expect(parsed.files.find((f) => f.path === 'utils')?.isFolder).toBe(true);
+ expect(parsed.files.find((f) => f.path === 'top.m')?.labels).toEqual(['design']);
+ expect(parsed.pathFolders).toEqual(['', 'utils']);
+ expect(parsed.entryPoints.map((e) => e.file)).toEqual(['utils']);
+ });
+
+ it('finds a collection that has no def document of its own', () => {
+ // `Root.type.Files/` exists as a directory with no `Root.type.Files.xml` beside
+ // it — normal in this layout, not a loss — so entities must be discovered from
+ // directory names and not only from documents.
+ const parsed = parseProject(
+ distStore({ 'Root.type.Files/only.m.type.File.xml': '' }),
+ 'fallback',
+ );
+ expect(parsed.files.map((f) => f.path)).toEqual(['only.m']);
+ });
+
+ it('splits a location that contains the type marker itself', () => {
+ // The separator is the LAST `.type.`: a project file may be NAMED `a.type.File`,
+ // whose def is then `a.type.File.type.File.xml`. Splitting on the first
+ // occurrence truncates the name to `a`.
+ const parsed = parseProject(
+ distStore({ 'Root.type.Files/a.type.File.type.File.xml': '' }),
+ 'fallback',
+ );
+ expect(parsed.files.map((f) => f.path)).toEqual(['a.type.File']);
+ });
+
+ it('infers the layout when the manifest is missing', () => {
+ const noManifest = {
+ [at('ProjectData.type.Info.xml')]: info(''),
+ [at('Root.type.Files/f.m.type.File.xml')]: info(''),
+ };
+ const parsed = parseProject(noManifest, 'fallback');
+ expect(parsed.format).toBe('distributed');
+ expect(parsed.name).toBe('Inferred');
+ expect(parsed.files.map((f) => f.path)).toEqual(['f.m']);
+ });
+});
+
+describe('parseProject — the declared metadata format', () => {
+ it('reports the format it read', () => {
+ expect(parseProject(myProjStore(), 'fallback').format).toBe('fixedPathV2');
+ });
+
+ it('warns and reads nothing for a layout it cannot walk, keeping the name', () => {
+ // Guessing is the one thing not to do here: the collection readers would find
+ // nothing in an unknown layout and return a project that looks complete and
+ // empty — the single outcome a user cannot tell apart from a fact about their
+ // project. The name is still salvaged, since it titles the view.
+ const parsed = parseProject(
+ {
+ [at('Project.xml')]: info(''),
+ [at('ProjectData.type.Info.xml')]: info(''),
+ },
+ 'fallback',
+ );
+ expect(parsed.name).toBe('Mono');
+ expect(parsed.format).toBe('');
+ expect(parsed.files).toEqual([]);
+ expect(parsed.warnings).toHaveLength(1);
+ expect(parsed.warnings[0].code).toBe('source-empty');
+ expect(parsed.warnings[0].message).toContain('monolithic');
+ });
+
+ it('names matlab.toml rather than reporting an empty store', () => {
+ // A toml store holds no XML at all, so it lands in the same place as a store
+ // that did not survive its trip. It is not damaged, and a user told "nothing
+ // readable" would go looking for a corrupt file that does not exist.
+ const parsed = parseProject({ 'resources/project/matlab.toml': '[project]\nname = "T"\n' }, 'TomlProj');
+ expect(parsed.name).toBe('TomlProj');
+ expect(parsed.warnings).toHaveLength(1);
+ expect(parsed.warnings[0].code).toBe('source-empty');
+ expect(parsed.warnings[0].message).toContain('matlab.toml');
+ expect(parsed.warnings[0].part).toBe('resources/project/matlab.toml');
});
});