mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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
This commit is contained in:
committed by
GitHub
parent
af95dae73a
commit
c4652190eb
39
libs/spec-parser/package.json
Normal file
39
libs/spec-parser/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@automaker/spec-parser",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "XML spec parser for AutoMaker - parses and generates app_spec.txt XML",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"keywords": [
|
||||
"automaker",
|
||||
"spec-parser",
|
||||
"xml"
|
||||
],
|
||||
"author": "AutoMaker Team",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"engines": {
|
||||
"node": ">=22.0.0 <23.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automaker/types": "1.0.0",
|
||||
"fast-xml-parser": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.19.3",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.16"
|
||||
}
|
||||
}
|
||||
26
libs/spec-parser/src/index.ts
Normal file
26
libs/spec-parser/src/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @automaker/spec-parser
|
||||
*
|
||||
* XML spec parser for AutoMaker - parses and generates app_spec.txt XML.
|
||||
* This package provides utilities for:
|
||||
* - Parsing XML spec content into SpecOutput objects
|
||||
* - Converting SpecOutput objects back to XML
|
||||
* - Validating spec data
|
||||
*/
|
||||
|
||||
// Re-export types from @automaker/types for convenience
|
||||
export type { SpecOutput } from '@automaker/types';
|
||||
|
||||
// XML utilities
|
||||
export { escapeXml, unescapeXml, extractXmlSection, extractXmlElements } from './xml-utils.js';
|
||||
|
||||
// XML to Spec parsing
|
||||
export { xmlToSpec } from './xml-to-spec.js';
|
||||
export type { ParseResult } from './xml-to-spec.js';
|
||||
|
||||
// Spec to XML conversion
|
||||
export { specToXml } from './spec-to-xml.js';
|
||||
|
||||
// Validation
|
||||
export { validateSpec, isValidSpecXml } from './validate.js';
|
||||
export type { ValidationResult } from './validate.js';
|
||||
88
libs/spec-parser/src/spec-to-xml.ts
Normal file
88
libs/spec-parser/src/spec-to-xml.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* SpecOutput to XML converter.
|
||||
* Converts a structured SpecOutput object back to XML format.
|
||||
*/
|
||||
|
||||
import type { SpecOutput } from '@automaker/types';
|
||||
import { escapeXml } from './xml-utils.js';
|
||||
|
||||
/**
|
||||
* Convert structured spec output to XML format.
|
||||
*
|
||||
* @param spec - The SpecOutput object to convert
|
||||
* @returns XML string formatted for app_spec.txt
|
||||
*/
|
||||
export function specToXml(spec: SpecOutput): string {
|
||||
const indent = ' ';
|
||||
|
||||
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project_specification>
|
||||
${indent}<project_name>${escapeXml(spec.project_name)}</project_name>
|
||||
|
||||
${indent}<overview>
|
||||
${indent}${indent}${escapeXml(spec.overview)}
|
||||
${indent}</overview>
|
||||
|
||||
${indent}<technology_stack>
|
||||
${spec.technology_stack.map((t) => `${indent}${indent}<technology>${escapeXml(t)}</technology>`).join('\n')}
|
||||
${indent}</technology_stack>
|
||||
|
||||
${indent}<core_capabilities>
|
||||
${spec.core_capabilities.map((c) => `${indent}${indent}<capability>${escapeXml(c)}</capability>`).join('\n')}
|
||||
${indent}</core_capabilities>
|
||||
|
||||
${indent}<implemented_features>
|
||||
${spec.implemented_features
|
||||
.map(
|
||||
(f) => `${indent}${indent}<feature>
|
||||
${indent}${indent}${indent}<name>${escapeXml(f.name)}</name>
|
||||
${indent}${indent}${indent}<description>${escapeXml(f.description)}</description>${
|
||||
f.file_locations && f.file_locations.length > 0
|
||||
? `\n${indent}${indent}${indent}<file_locations>
|
||||
${f.file_locations.map((loc) => `${indent}${indent}${indent}${indent}<location>${escapeXml(loc)}</location>`).join('\n')}
|
||||
${indent}${indent}${indent}</file_locations>`
|
||||
: ''
|
||||
}
|
||||
${indent}${indent}</feature>`
|
||||
)
|
||||
.join('\n')}
|
||||
${indent}</implemented_features>`;
|
||||
|
||||
// Optional sections
|
||||
if (spec.additional_requirements && spec.additional_requirements.length > 0) {
|
||||
xml += `
|
||||
|
||||
${indent}<additional_requirements>
|
||||
${spec.additional_requirements.map((r) => `${indent}${indent}<requirement>${escapeXml(r)}</requirement>`).join('\n')}
|
||||
${indent}</additional_requirements>`;
|
||||
}
|
||||
|
||||
if (spec.development_guidelines && spec.development_guidelines.length > 0) {
|
||||
xml += `
|
||||
|
||||
${indent}<development_guidelines>
|
||||
${spec.development_guidelines.map((g) => `${indent}${indent}<guideline>${escapeXml(g)}</guideline>`).join('\n')}
|
||||
${indent}</development_guidelines>`;
|
||||
}
|
||||
|
||||
if (spec.implementation_roadmap && spec.implementation_roadmap.length > 0) {
|
||||
xml += `
|
||||
|
||||
${indent}<implementation_roadmap>
|
||||
${spec.implementation_roadmap
|
||||
.map(
|
||||
(r) => `${indent}${indent}<phase>
|
||||
${indent}${indent}${indent}<name>${escapeXml(r.phase)}</name>
|
||||
${indent}${indent}${indent}<status>${escapeXml(r.status)}</status>
|
||||
${indent}${indent}${indent}<description>${escapeXml(r.description)}</description>
|
||||
${indent}${indent}</phase>`
|
||||
)
|
||||
.join('\n')}
|
||||
${indent}</implementation_roadmap>`;
|
||||
}
|
||||
|
||||
xml += `
|
||||
</project_specification>`;
|
||||
|
||||
return xml;
|
||||
}
|
||||
143
libs/spec-parser/src/validate.ts
Normal file
143
libs/spec-parser/src/validate.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Validation utilities for SpecOutput objects.
|
||||
*/
|
||||
|
||||
import type { SpecOutput } from '@automaker/types';
|
||||
|
||||
/**
|
||||
* Validation result containing errors if any.
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a SpecOutput object for required fields and data integrity.
|
||||
*
|
||||
* @param spec - The SpecOutput object to validate
|
||||
* @returns ValidationResult with errors if validation fails
|
||||
*/
|
||||
export function validateSpec(spec: SpecOutput | null | undefined): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!spec) {
|
||||
return { valid: false, errors: ['Spec is null or undefined'] };
|
||||
}
|
||||
|
||||
// Required string fields
|
||||
if (!spec.project_name || typeof spec.project_name !== 'string') {
|
||||
errors.push('project_name is required and must be a string');
|
||||
} else if (spec.project_name.trim().length === 0) {
|
||||
errors.push('project_name cannot be empty');
|
||||
}
|
||||
|
||||
if (!spec.overview || typeof spec.overview !== 'string') {
|
||||
errors.push('overview is required and must be a string');
|
||||
} else if (spec.overview.trim().length === 0) {
|
||||
errors.push('overview cannot be empty');
|
||||
}
|
||||
|
||||
// Required array fields
|
||||
if (!Array.isArray(spec.technology_stack)) {
|
||||
errors.push('technology_stack is required and must be an array');
|
||||
} else if (spec.technology_stack.length === 0) {
|
||||
errors.push('technology_stack must have at least one item');
|
||||
} else if (spec.technology_stack.some((t) => typeof t !== 'string' || t.trim() === '')) {
|
||||
errors.push('technology_stack items must be non-empty strings');
|
||||
}
|
||||
|
||||
if (!Array.isArray(spec.core_capabilities)) {
|
||||
errors.push('core_capabilities is required and must be an array');
|
||||
} else if (spec.core_capabilities.length === 0) {
|
||||
errors.push('core_capabilities must have at least one item');
|
||||
} else if (spec.core_capabilities.some((c) => typeof c !== 'string' || c.trim() === '')) {
|
||||
errors.push('core_capabilities items must be non-empty strings');
|
||||
}
|
||||
|
||||
// Implemented features
|
||||
if (!Array.isArray(spec.implemented_features)) {
|
||||
errors.push('implemented_features is required and must be an array');
|
||||
} else {
|
||||
spec.implemented_features.forEach((f, i) => {
|
||||
if (!f.name || typeof f.name !== 'string' || f.name.trim() === '') {
|
||||
errors.push(`implemented_features[${i}].name is required and must be a non-empty string`);
|
||||
}
|
||||
if (!f.description || typeof f.description !== 'string') {
|
||||
errors.push(`implemented_features[${i}].description is required and must be a string`);
|
||||
}
|
||||
if (f.file_locations !== undefined) {
|
||||
if (!Array.isArray(f.file_locations)) {
|
||||
errors.push(`implemented_features[${i}].file_locations must be an array if provided`);
|
||||
} else if (f.file_locations.some((loc) => typeof loc !== 'string' || loc.trim() === '')) {
|
||||
errors.push(`implemented_features[${i}].file_locations items must be non-empty strings`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Optional array fields
|
||||
if (spec.additional_requirements !== undefined) {
|
||||
if (!Array.isArray(spec.additional_requirements)) {
|
||||
errors.push('additional_requirements must be an array if provided');
|
||||
} else if (spec.additional_requirements.some((r) => typeof r !== 'string' || r.trim() === '')) {
|
||||
errors.push('additional_requirements items must be non-empty strings');
|
||||
}
|
||||
}
|
||||
|
||||
if (spec.development_guidelines !== undefined) {
|
||||
if (!Array.isArray(spec.development_guidelines)) {
|
||||
errors.push('development_guidelines must be an array if provided');
|
||||
} else if (spec.development_guidelines.some((g) => typeof g !== 'string' || g.trim() === '')) {
|
||||
errors.push('development_guidelines items must be non-empty strings');
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation roadmap
|
||||
if (spec.implementation_roadmap !== undefined) {
|
||||
if (!Array.isArray(spec.implementation_roadmap)) {
|
||||
errors.push('implementation_roadmap must be an array if provided');
|
||||
} else {
|
||||
const validStatuses = ['completed', 'in_progress', 'pending'];
|
||||
spec.implementation_roadmap.forEach((r, i) => {
|
||||
if (!r.phase || typeof r.phase !== 'string' || r.phase.trim() === '') {
|
||||
errors.push(
|
||||
`implementation_roadmap[${i}].phase is required and must be a non-empty string`
|
||||
);
|
||||
}
|
||||
if (!r.status || !validStatuses.includes(r.status)) {
|
||||
errors.push(
|
||||
`implementation_roadmap[${i}].status must be one of: ${validStatuses.join(', ')}`
|
||||
);
|
||||
}
|
||||
if (!r.description || typeof r.description !== 'string') {
|
||||
errors.push(`implementation_roadmap[${i}].description is required and must be a string`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if XML content appears to be a valid spec XML (basic structure check).
|
||||
* This is a quick check, not a full validation.
|
||||
*
|
||||
* @param xmlContent - The XML content to check
|
||||
* @returns true if the content appears to be valid spec XML
|
||||
*/
|
||||
export function isValidSpecXml(xmlContent: string): boolean {
|
||||
if (!xmlContent || typeof xmlContent !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for essential elements
|
||||
const hasRoot = xmlContent.includes('<project_specification>');
|
||||
const hasProjectName = /<project_name>[\s\S]*?<\/project_name>/.test(xmlContent);
|
||||
const hasOverview = /<overview>[\s\S]*?<\/overview>/.test(xmlContent);
|
||||
const hasTechStack = /<technology_stack>[\s\S]*?<\/technology_stack>/.test(xmlContent);
|
||||
const hasCapabilities = /<core_capabilities>[\s\S]*?<\/core_capabilities>/.test(xmlContent);
|
||||
|
||||
return hasRoot && hasProjectName && hasOverview && hasTechStack && hasCapabilities;
|
||||
}
|
||||
232
libs/spec-parser/src/xml-to-spec.ts
Normal file
232
libs/spec-parser/src/xml-to-spec.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* XML to SpecOutput parser.
|
||||
* Parses app_spec.txt XML content into a structured SpecOutput object.
|
||||
* Uses fast-xml-parser for robust XML parsing.
|
||||
*/
|
||||
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
import type { SpecOutput } from '@automaker/types';
|
||||
|
||||
/**
|
||||
* Result of parsing XML content.
|
||||
*/
|
||||
export interface ParseResult {
|
||||
success: boolean;
|
||||
spec: SpecOutput | null;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
// Configure the XML parser
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: true,
|
||||
trimValues: true,
|
||||
// Preserve arrays for elements that can have multiple values
|
||||
isArray: (name) => {
|
||||
return [
|
||||
'technology',
|
||||
'capability',
|
||||
'feature',
|
||||
'location',
|
||||
'requirement',
|
||||
'guideline',
|
||||
'phase',
|
||||
].includes(name);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Safely get a string value from parsed XML, handling various input types.
|
||||
*/
|
||||
function getString(value: unknown): string {
|
||||
if (typeof value === 'string') return value.trim();
|
||||
if (typeof value === 'number') return String(value);
|
||||
if (value === null || value === undefined) return '';
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely get an array of strings from parsed XML.
|
||||
*/
|
||||
function getStringArray(value: unknown): string[] {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => getString(item)).filter((s) => s.length > 0);
|
||||
}
|
||||
const str = getString(value);
|
||||
return str ? [str] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse implemented features from the parsed XML object.
|
||||
*/
|
||||
function parseImplementedFeatures(featuresSection: unknown): SpecOutput['implemented_features'] {
|
||||
const features: SpecOutput['implemented_features'] = [];
|
||||
|
||||
if (!featuresSection || typeof featuresSection !== 'object') {
|
||||
return features;
|
||||
}
|
||||
|
||||
const section = featuresSection as Record<string, unknown>;
|
||||
const featureList = section.feature;
|
||||
|
||||
if (!featureList) return features;
|
||||
|
||||
const featureArray = Array.isArray(featureList) ? featureList : [featureList];
|
||||
|
||||
for (const feature of featureArray) {
|
||||
if (typeof feature !== 'object' || feature === null) continue;
|
||||
|
||||
const f = feature as Record<string, unknown>;
|
||||
const name = getString(f.name);
|
||||
const description = getString(f.description);
|
||||
|
||||
if (!name) continue;
|
||||
|
||||
const locationsSection = f.file_locations as Record<string, unknown> | undefined;
|
||||
const file_locations = locationsSection ? getStringArray(locationsSection.location) : undefined;
|
||||
|
||||
features.push({
|
||||
name,
|
||||
description,
|
||||
...(file_locations && file_locations.length > 0 ? { file_locations } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return features;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse implementation roadmap phases from the parsed XML object.
|
||||
*/
|
||||
function parseImplementationRoadmap(roadmapSection: unknown): SpecOutput['implementation_roadmap'] {
|
||||
if (!roadmapSection || typeof roadmapSection !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const section = roadmapSection as Record<string, unknown>;
|
||||
const phaseList = section.phase;
|
||||
|
||||
if (!phaseList) return undefined;
|
||||
|
||||
const phaseArray = Array.isArray(phaseList) ? phaseList : [phaseList];
|
||||
const roadmap: NonNullable<SpecOutput['implementation_roadmap']> = [];
|
||||
|
||||
for (const phase of phaseArray) {
|
||||
if (typeof phase !== 'object' || phase === null) continue;
|
||||
|
||||
const p = phase as Record<string, unknown>;
|
||||
const phaseName = getString(p.name);
|
||||
const statusRaw = getString(p.status);
|
||||
const description = getString(p.description);
|
||||
|
||||
if (!phaseName) continue;
|
||||
|
||||
const status = (
|
||||
['completed', 'in_progress', 'pending'].includes(statusRaw) ? statusRaw : 'pending'
|
||||
) as 'completed' | 'in_progress' | 'pending';
|
||||
|
||||
roadmap.push({ phase: phaseName, status, description });
|
||||
}
|
||||
|
||||
return roadmap.length > 0 ? roadmap : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse XML content into a SpecOutput object.
|
||||
*
|
||||
* @param xmlContent - The raw XML content from app_spec.txt
|
||||
* @returns ParseResult with the parsed spec or errors
|
||||
*/
|
||||
export function xmlToSpec(xmlContent: string): ParseResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Check for root element before parsing
|
||||
if (!xmlContent.includes('<project_specification>')) {
|
||||
return {
|
||||
success: false,
|
||||
spec: null,
|
||||
errors: ['Missing <project_specification> root element'],
|
||||
};
|
||||
}
|
||||
|
||||
// Parse the XML
|
||||
let parsed: Record<string, unknown>;
|
||||
try {
|
||||
parsed = parser.parse(xmlContent) as Record<string, unknown>;
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
spec: null,
|
||||
errors: [`XML parsing error: ${e instanceof Error ? e.message : 'Unknown error'}`],
|
||||
};
|
||||
}
|
||||
|
||||
const root = parsed.project_specification as Record<string, unknown> | undefined;
|
||||
|
||||
if (!root) {
|
||||
return {
|
||||
success: false,
|
||||
spec: null,
|
||||
errors: ['Missing <project_specification> root element'],
|
||||
};
|
||||
}
|
||||
|
||||
// Extract required fields
|
||||
const project_name = getString(root.project_name);
|
||||
if (!project_name) {
|
||||
errors.push('Missing or empty <project_name>');
|
||||
}
|
||||
|
||||
const overview = getString(root.overview);
|
||||
if (!overview) {
|
||||
errors.push('Missing or empty <overview>');
|
||||
}
|
||||
|
||||
// Extract technology stack
|
||||
const techSection = root.technology_stack as Record<string, unknown> | undefined;
|
||||
const technology_stack = techSection ? getStringArray(techSection.technology) : [];
|
||||
if (technology_stack.length === 0) {
|
||||
errors.push('Missing or empty <technology_stack>');
|
||||
}
|
||||
|
||||
// Extract core capabilities
|
||||
const capSection = root.core_capabilities as Record<string, unknown> | undefined;
|
||||
const core_capabilities = capSection ? getStringArray(capSection.capability) : [];
|
||||
if (core_capabilities.length === 0) {
|
||||
errors.push('Missing or empty <core_capabilities>');
|
||||
}
|
||||
|
||||
// Extract implemented features
|
||||
const implemented_features = parseImplementedFeatures(root.implemented_features);
|
||||
|
||||
// Extract optional sections
|
||||
const reqSection = root.additional_requirements as Record<string, unknown> | undefined;
|
||||
const additional_requirements = reqSection ? getStringArray(reqSection.requirement) : undefined;
|
||||
|
||||
const guideSection = root.development_guidelines as Record<string, unknown> | undefined;
|
||||
const development_guidelines = guideSection ? getStringArray(guideSection.guideline) : undefined;
|
||||
|
||||
const implementation_roadmap = parseImplementationRoadmap(root.implementation_roadmap);
|
||||
|
||||
// Build spec object
|
||||
const spec: SpecOutput = {
|
||||
project_name,
|
||||
overview,
|
||||
technology_stack,
|
||||
core_capabilities,
|
||||
implemented_features,
|
||||
...(additional_requirements && additional_requirements.length > 0
|
||||
? { additional_requirements }
|
||||
: {}),
|
||||
...(development_guidelines && development_guidelines.length > 0
|
||||
? { development_guidelines }
|
||||
: {}),
|
||||
...(implementation_roadmap ? { implementation_roadmap } : {}),
|
||||
};
|
||||
|
||||
return {
|
||||
success: errors.length === 0,
|
||||
spec,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
79
libs/spec-parser/src/xml-utils.ts
Normal file
79
libs/spec-parser/src/xml-utils.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescape XML entities back to regular characters.
|
||||
*/
|
||||
export function unescapeXml(str: string): string {
|
||||
return str
|
||||
.replace(/'/g, "'")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<')
|
||||
.replace(/&/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;
|
||||
}
|
||||
9
libs/spec-parser/tsconfig.json
Normal file
9
libs/spec-parser/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user