aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRuben Beltran del Rio <jj@r.bdr.sh>2025-12-12 01:02:45 +0100
committerRuben Beltran del Rio <jj@r.bdr.sh>2025-12-12 12:30:14 +0100
commit68a999ed21907ef8f14c1aa0bf760d3c9b4b4c8c (patch)
treea9cb57d999005c2b0fc7c94f45b89aacea23a95b /src
Add parsing
Diffstat (limited to 'src')
-rw-r--r--src/index.js354
1 files changed, 354 insertions, 0 deletions
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..653de18
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,354 @@
+/**
+ * A parsed wardley map.
+ * @typedef {Object} Map
+ * @property {Entity[]} entities - The list of parsed entities
+ */
+
+/**
+ * Any of the potential entities in the map.
+ * @typedef {Component|Dependency|Note|Stage|Group|Inertia|Evolution} Entity
+ */
+
+/**
+ * A component in the map.
+ * @typedef {Object} Component
+ * @property {'component'} type - Entity type
+ * @property {string} label - Component label
+ * @property {[number, number]} coordinates - X and Y coordinates
+ * @property {string|null} shape - Shape type (x, square, triangle, circle) or null
+ */
+
+/**
+ * A dependency between two components.
+ * @typedef {Object} Dependency
+ * @property {'dependency'} type - Entity type
+ * @property {string} from - Source component label
+ * @property {string} to - Target component label
+ * @property {boolean} isDirected - Whether the dependency is directed (->) or undirected (--)
+ */
+
+/**
+ * A note.
+ * @typedef {Object} Note
+ * @property {'note'} type - Entity type
+ * @property {[number, number]} coordinates - X and Y coordinates
+ * @property {string} text - Note text content
+ */
+
+/**
+ * An override for the width of an evolution stage.
+ * @typedef {Object} Stage
+ * @property {'stage'} type - Entity type
+ * @property {string} number - Stage number (i, ii, iii, iv)
+ * @property {number} value - Stage value
+ */
+
+/**
+ * A group of components.
+ * @typedef {Object} Group
+ * @property {'group'} type - Entity type
+ * @property {string[]} vertices - Array of component labels in the group
+ */
+
+/**
+ * Inertia associated with a component.
+ * @typedef {Object} Inertia
+ * @property {'inertia'} type - Entity type
+ * @property {string} vertex - Component label with inertia
+ */
+
+/**
+ * Evolution associated with a component.
+ * @typedef {Object} Evolution
+ * @property {'evolution'} type - Entity type
+ * @property {string} vertex - Component label
+ * @property {boolean} isPositive - Whether evolution is positive (+) or negative (-)
+ * @property {number} value - Evolution value
+ */
+
+/**
+ * Helper: Check if character is whitespace
+ */
+function isWhitespace(character) {
+ return character === " " || character === "\t";
+}
+
+/**
+ * Helper: Check if character is a digit or dot
+ */
+function isDigitOrDot(character) {
+ return (character >= "0" && character <= "9") || character === ".";
+}
+
+/**
+ * wmap format Wardley Map parser.
+ * @param {string} source - The wmap source code to parse
+ * @returns {Map} - Parsed map with entities
+ */
+export function parse(source) {
+ const entities = [];
+ const lines = source.split(/\r\n|\r|\n/);
+
+ for (const rawLine of lines) {
+ const line = rawLine.trim();
+ if (!line) continue;
+
+ const entity = parseLine(line);
+ if (entity) {
+ entities.push(entity);
+ }
+ }
+
+ return { entities };
+}
+
+/**
+ * Parse a line
+ */
+function parseLine(line) {
+ const length = line.length;
+ let i = 0;
+
+ // Skip leading whitespace
+ while (i < length && isWhitespace(line[i])) i++;
+ if (i >= length) return null;
+
+ if (line[i] === "[") {
+ return parseKeywordEntity(line, i, length);
+ }
+
+ return parseComponentOrDependency(line, length);
+}
+
+/**
+ * Parse entities that start with keywords in brackets: [Stage], [Note], [Group], [Inertia], [Evolution]
+ */
+function parseKeywordEntity(line, start, length) {
+ let i = start + 1; // skip '['
+ const keywordStart = i;
+
+ // Read until ']'
+ while (i < length && line[i] !== "]") i++;
+ if (i >= length) return null;
+
+ const keyword = line.substring(keywordStart, i).trim().toLowerCase();
+ i++; // skip ']'
+
+ // Skip whitespace after ']'
+ while (i < length && isWhitespace(line[i])) i++;
+
+ // Stage: [I|II|III|IV] number
+ if (
+ keyword === "i" ||
+ keyword === "ii" ||
+ keyword === "iii" ||
+ keyword === "iv"
+ ) {
+ const numStart = i;
+ while (i < length && isDigitOrDot(line[i])) i++;
+ if (i > numStart) {
+ return {
+ type: "stage",
+ number: keyword,
+ value: parseFloat(line.substring(numStart, i)),
+ };
+ }
+ return null;
+ }
+
+ // Note: [Note] (x, y) text
+ if (keyword === "note") {
+ // Expect '('
+ if (i >= length || line[i] !== "(") return null;
+ i++;
+
+ // Read x
+ while (i < length && isWhitespace(line[i])) i++;
+ const xStart = i;
+ while (i < length && isDigitOrDot(line[i])) i++;
+ if (i === xStart) return null;
+ const x = parseFloat(line.substring(xStart, i));
+
+ // Expect ','
+ while (i < length && isWhitespace(line[i])) i++;
+ if (i >= length || line[i] !== ",") return null;
+ i++;
+
+ // Read y
+ while (i < length && isWhitespace(line[i])) i++;
+ const yStart = i;
+ while (i < length && isDigitOrDot(line[i])) i++;
+ if (i === yStart) return null;
+ const y = parseFloat(line.substring(yStart, i));
+
+ // Expect ')'
+ while (i < length && isWhitespace(line[i])) i++;
+ if (i >= length || line[i] !== ")") return null;
+ i++;
+
+ // Read text
+ while (i < length && isWhitespace(line[i])) i++;
+ const text = line.substring(i).trim();
+
+ return {
+ type: "note",
+ coordinates: [x, y],
+ text,
+ };
+ }
+
+ // Group: [Group] label1, label2, ...
+ if (keyword === "group") {
+ const vertices = line
+ .substring(i)
+ .split(",")
+ .map(v => v.trim())
+ .filter(v => v);
+ return vertices.length > 0 ? { type: "group", vertices } : null;
+ }
+
+ // Inertia: [Inertia] label
+ if (keyword === "inertia") {
+ const vertex = line.substring(i).trim();
+ return vertex ? { type: "inertia", vertex } : null;
+ }
+
+ // Evolution: [Evolution] label +/- number
+ if (keyword === "evolution") {
+ let indexOfSign = -1;
+ let signCharacter = null;
+
+ // Find the sign
+ for (let j = i; j < length; j++) {
+ if (line[j] === "+" || line[j] === "-") {
+ // Make sure it's not part of a dependency arrow
+ if (j + 1 < length && (line[j + 1] === ">" || line[j + 1] === "-"))
+ continue;
+ indexOfSign = j;
+ signCharacter = line[j];
+ break;
+ }
+ }
+
+ if (indexOfSign === -1 || indexOfSign === i) return null;
+
+ const vertex = line.substring(i, indexOfSign).trim();
+ if (!vertex) return null;
+
+ // Read number after sign
+ let indexOfNumber = indexOfSign + 1;
+ while (indexOfNumber < length && isWhitespace(line[indexOfNumber])) indexOfNumber++;
+ const numStart = indexOfNumber;
+ while (indexOfNumber < length && isDigitOrDot(line[indexOfNumber])) indexOfNumber++;
+ if (indexOfNumber === numStart) return null;
+
+ return {
+ type: "evolution",
+ vertex,
+ isPositive: signCharacter === "+",
+ value: parseFloat(line.substring(numStart, indexOfNumber)),
+ };
+ }
+
+ return null;
+}
+
+/**
+ * Parse component or dependency
+ */
+function parseComponentOrDependency(line, length) {
+ // Use indexOf to quickly find potential dependency arrows and parentheses
+ const indexOfArrow = line.indexOf("->");
+ const indexOfDash = line.indexOf("--");
+ const indexOfParenthesis = line.indexOf("(");
+
+ // Check for directed dependency (->)
+ if (indexOfArrow !== -1 && (indexOfParenthesis === -1 || indexOfArrow < indexOfParenthesis)) {
+ const from = line.substring(0, indexOfArrow).trim();
+ const to = line.substring(indexOfArrow + 2).trim();
+ if (from && to) {
+ return {
+ type: "dependency",
+ from,
+ to,
+ isDirected: true,
+ };
+ }
+ return null;
+ }
+
+ // Check for undirected dependency (--)
+ if (indexOfDash !== -1 && (indexOfParenthesis === -1 || indexOfDash < indexOfParenthesis)) {
+ const from = line.substring(0, indexOfDash).trim();
+ const to = line.substring(indexOfDash + 2).trim();
+ if (from && to) {
+ return {
+ type: "dependency",
+ from,
+ to,
+ isDirected: false,
+ };
+ }
+ return null;
+ }
+
+ // Check for component (opening parenthesis)
+ if (indexOfParenthesis > 0) {
+ return parseComponent(line, indexOfParenthesis, length);
+ }
+
+ return null;
+}
+
+/**
+ * Parse component starting from the opening parenthesis
+ */
+function parseComponent(line, indexOfParenthesis, length) {
+ const label = line.substring(0, indexOfParenthesis).trim();
+ if (!label) return null;
+
+ let i = indexOfParenthesis + 1;
+
+ // Read x
+ while (i < length && isWhitespace(line[i])) i++;
+ const xStart = i;
+ while (i < length && isDigitOrDot(line[i])) i++;
+ if (i === xStart) return null;
+ const x = parseFloat(line.substring(xStart, i));
+
+ // Expect ','
+ while (i < length && isWhitespace(line[i])) i++;
+ if (i >= length || line[i] !== ",") return null;
+ i++;
+
+ // Read y
+ while (i < length && isWhitespace(line[i])) i++;
+ const yStart = i;
+ while (i < length && isDigitOrDot(line[i])) i++;
+ if (i === yStart) return null;
+ const y = parseFloat(line.substring(yStart, i));
+
+ // Expect ')'
+ while (i < length && isWhitespace(line[i])) i++;
+ if (i >= length || line[i] !== ")") return null;
+ i++;
+
+ // Optional shape: [shape]
+ let shape = null;
+ while (i < length && isWhitespace(line[i])) i++;
+ if (i < length && line[i] === "[") {
+ i++;
+ const shapeStart = i;
+ while (i < length && line[i] !== "]") i++;
+ if (i > shapeStart && i < length) {
+ shape = line.substring(shapeStart, i).trim().toLowerCase();
+ }
+ }
+
+ return {
+ type: "component",
+ label,
+ coordinates: [x, y],
+ shape,
+ };
+}