diff options
| author | Ruben Beltran del Rio <jj@r.bdr.sh> | 2025-12-12 01:02:45 +0100 |
|---|---|---|
| committer | Ruben Beltran del Rio <jj@r.bdr.sh> | 2025-12-12 12:30:14 +0100 |
| commit | 68a999ed21907ef8f14c1aa0bf760d3c9b4b4c8c (patch) | |
| tree | a9cb57d999005c2b0fc7c94f45b89aacea23a95b /src/index.js | |
Add parsing
Diffstat (limited to 'src/index.js')
| -rw-r--r-- | src/index.js | 354 |
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, + }; +} |