Files
automaker/libs/spec-parser/src/xml-utils.ts
Stefan de Vogelaere c4652190eb feat: add three viewing modes for app specification (#566)
* feat: add three viewing modes for app specification

Introduces View, Edit, and Source modes for the spec page:

- View: Clean read-only display with cards, badges, and accordions
- Edit: Structured form-based editor for all spec fields
- Source: Raw XML editor for advanced users

Also adds @automaker/spec-parser shared package for XML parsing
between server and client.

* fix: address PR review feedback

- Replace array index keys with stable UUIDs in array-field-editor,
  features-section, and roadmap-section components
- Replace regex-based XML parsing with fast-xml-parser for robustness
- Simplify renderContent logic in spec-view by removing dead code paths

* fix: convert git+ssh URLs to https in package-lock.json

* fix: address PR review feedback for spec visualiser

- Remove unused RefreshCw import from spec-view.tsx
- Add explicit parsedSpec check in renderContent for robustness
- Hide save button in view mode since it's read-only
- Remove GripVertical drag handles since drag-and-drop is not implemented
- Rename Map imports to MapIcon to avoid shadowing global Map
- Escape tagName in xml-utils.ts RegExp functions for safety
- Add aria-label attributes for accessibility on mode tabs

* fix: address additional PR review feedback

- Fix Textarea controlled/uncontrolled warning with default value
- Preserve IDs in useEffect sync to avoid unnecessary remounts
- Consolidate lucide-react imports
- Add JSDoc note about tag attributes limitation in xml-utils.ts
- Remove redundant disabled prop from SpecModeTabs
2026-01-18 23:45:43 +01:00

80 lines
2.3 KiB
TypeScript

/**
* XML utility functions for escaping, unescaping, and extracting XML content.
* These are pure functions with no dependencies for maximum reusability.
*/
/**
* Escape special XML characters.
* Handles undefined/null values by converting them to empty strings.
*/
export function escapeXml(str: string | undefined | null): string {
if (str == null) {
return '';
}
return str
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
/**
* Unescape XML entities back to regular characters.
*/
export function unescapeXml(str: string): string {
return str
.replace(/&apos;/g, "'")
.replace(/&quot;/g, '"')
.replace(/&gt;/g, '>')
.replace(/&lt;/g, '<')
.replace(/&amp;/g, '&');
}
/**
* Escape special RegExp characters in a string.
*/
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Extract the content of a specific XML section.
*
* Note: This function only matches bare tags without attributes.
* Tags with attributes (e.g., `<tag id="1">`) are not supported.
*
* @param xmlContent - The full XML content
* @param tagName - The tag name to extract (e.g., 'implemented_features')
* @returns The content between the tags, or null if not found
*/
export function extractXmlSection(xmlContent: string, tagName: string): string | null {
const safeTag = escapeRegExp(tagName);
const regex = new RegExp(`<${safeTag}>([\\s\\S]*?)<\\/${safeTag}>`, 'i');
const match = xmlContent.match(regex);
return match ? match[1] : null;
}
/**
* Extract all values from repeated XML elements.
*
* Note: This function only matches bare tags without attributes.
* Tags with attributes (e.g., `<tag id="1">`) are not supported.
*
* @param xmlContent - The XML content to search
* @param tagName - The tag name to extract values from
* @returns Array of extracted values (unescaped and trimmed)
*/
export function extractXmlElements(xmlContent: string, tagName: string): string[] {
const values: string[] = [];
const safeTag = escapeRegExp(tagName);
const regex = new RegExp(`<${safeTag}>([\\s\\S]*?)<\\/${safeTag}>`, 'g');
const matches = xmlContent.matchAll(regex);
for (const match of matches) {
values.push(unescapeXml(match[1].trim()));
}
return values;
}