@holmdigital/engine 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,11 +23,11 @@ For a comprehensive guide on CLI flags, CI/CD integration, and configuration fil
23
23
 
24
24
  - **Regulatory Mapping**: Maps technical failures to EU laws (EN 301 549, EAA).
25
25
  - **HTML Structure Validation**: Built-in `html-validate` checks to prevent false positives/negatives.
26
- - **Internationalization (i18n)**: Supports English (`en`), Swedish (`sv`), Norwegian (`no`), Finnish (`fi`), Danish (`da`), German (`de`), French (`fr`), Spanish (`es`), and Dutch (`nl`).
27
- - **Premium V2 Accessibility Statement**: Generates modern, glassmorphism-styled statements compliant with Digg & EU templates.
26
+ - **Internationalization (i18n)**: Comprehensive support for 9 languages: English (`en`), Swedish (`sv`), Norwegian (`no`), Finnish (`fi`), Danish (`da`), German (`de`), French (`fr`), Spanish (`es`), and Dutch (`nl`).
27
+ - **Template-Driven Accessibility Statements**: Generates modern, glassmorphism-styled statements using externalized JSON templates for each language, allowing for professional legal phrasing and deep customization.
28
28
  - **Multi-Company Metadata**: Easily customize statements via CLI flags or `.a11yrc` for scalable client generation.
29
+ - **Enriched JUnit XML**: Professional CI/CD reports including scan duration, page title, engine metadata, and **detailed failing node** snippets (Target + HTML).
29
30
  - **Configurable Severity Threshold**: Fail CI only on critical/high issues (configurable).
30
- - **Rich Metadata**: Includes scan duration, page title, language, and version info.
31
31
  - **Pseudo-Automation**: Automatically generates Playwright/Puppeteer test scripts for manual verification steps.
32
32
  - **PDF Reporting**: Generates beautiful, compliant PDF reports with severity-sorted violations, HTML error counts, and `@HolmDigital/engine` branding.
33
33
  - **TypeScript**: Written in TypeScript with full type definitions included.
@@ -298,6 +298,7 @@ var RegulatoryScanner = class {
298
298
  holmdigitalInsight: {
299
299
  diggRisk: "medium",
300
300
  eaaImpact: "medium",
301
+ reasoning: violation.help,
301
302
  swedishInterpretation: violation.help,
302
303
  priorityRationale: "Detta fel uppt\xE4cktes av scannern men saknar specifik mappning i HolmDigital-databasen."
303
304
  },
@@ -473,9 +474,25 @@ function generateBadgeMarkdown(score) {
473
474
  import React from "react";
474
475
  import { renderToStaticMarkup } from "react-dom/server";
475
476
  import { AccessibilityStatement } from "@holmdigital/components";
477
+ import { ENFORCEMENT_BODIES } from "@holmdigital/standards";
476
478
  import fs from "fs/promises";
477
479
  import path from "path";
478
480
  async function generateStatementContent(result, lang = "en", format = "html", metadata) {
481
+ let template;
482
+ try {
483
+ const templatePath = path.join(__dirname, "templates", `${lang}.json`);
484
+ const templateData = await fs.readFile(templatePath, "utf-8");
485
+ template = JSON.parse(templateData);
486
+ } catch (e) {
487
+ try {
488
+ const fallbackPath = path.join(__dirname, "templates", "en.json");
489
+ const templateData = await fs.readFile(fallbackPath, "utf-8");
490
+ template = JSON.parse(templateData);
491
+ } catch (err) {
492
+ console.error("Could not load accessibility statement templates", err);
493
+ throw new Error("Accessibility statement templates missing");
494
+ }
495
+ }
479
496
  let complianceLevel = "full";
480
497
  if (result.stats.critical > 0 || result.legalSummary && result.legalSummary.eaaDeadlineViolations > 0) {
481
498
  complianceLevel = "non-compliant";
@@ -560,41 +577,116 @@ async function generateStatementContent(result, lang = "en", format = "html", me
560
577
  } else {
561
578
  const dateStr = props.lastReviewDate.toISOString().split("T")[0];
562
579
  const statusMap = {
563
- "full": "Full",
564
- "partial": "Partial",
565
- "non-compliant": "Non-Compliant"
580
+ "full": lang === "sv" ? "Fullt ut f\xF6renlig" : lang === "no" || lang === "nb" ? "Helt i samsvar" : lang === "da" ? "Fuldt ud i overensstemmelse" : "Fully compliant",
581
+ "partial": lang === "sv" ? "Delvis f\xF6renlig" : lang === "no" || lang === "nb" ? "Delvis i samsvar" : lang === "da" ? "Delvist i overensstemmelse" : "Partially compliant",
582
+ "non-compliant": lang === "sv" ? "Inte f\xF6renlig" : lang === "no" || lang === "nb" ? "Ikke i samsvar" : lang === "da" ? "Ikke i overensstemmelse" : "Non-compliant"
566
583
  };
567
- content = `# Accessibility Statement for ${props.organizationName}
568
-
569
- This accessibility statement applies to [${props.websiteUrl}](${props.websiteUrl}).
570
-
571
- ## Compliance Status
572
-
573
- **Status:** ${statusMap[complianceLevel] || complianceLevel}
574
-
575
- This website is ${complianceLevel} compliant with ${sector === "public" ? "EN 301 549 (WAD)" : "EAA"}.
576
-
577
- ## Non-accessible Content
578
-
579
- The following content is non-accessible for the following reasons:
580
-
581
- ${nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `- ${item}`).join("\n") : "_No known issues._"}
582
-
583
- ## Preparation of this statement
584
-
585
- This statement was prepared on ${dateStr}.
586
- Method used: **Automated Scan** via @holmdigital/engine.
587
-
588
- ## Feedback and contact information
584
+ const substitutions = {
585
+ statusString: statusMap[complianceLevel],
586
+ "{<webbplats>}": props.organizationName,
587
+ "{<website>}": props.organizationName,
588
+ "{<nettsted>}": props.organizationName,
589
+ "{<organisation>}": props.organizationName,
590
+ "{<organisasjon>}": props.organizationName,
591
+ "{<e-postadress>}": props.contactEmail || "",
592
+ "{<e-mailosoite>}": props.contactEmail || "",
593
+ "{<e-mailadresse>}": props.contactEmail || "",
594
+ "{<e-mail address>}": props.contactEmail || "",
595
+ "{<email address>}": props.contactEmail || "",
596
+ "{<telefonnummer>}": props.phoneNumber || "",
597
+ "{<puhelinnumero>}": props.phoneNumber || "",
598
+ "{<telefoonnummer>}": props.phoneNumber || "",
599
+ "{<telephone number>}": props.phoneNumber || "",
600
+ "{<svarstid>}": props.responseTime || "",
601
+ "{<svartid>}": props.responseTime || "",
602
+ "{<response time>}": props.responseTime || "",
603
+ "{<bed\xF6mningsdatum>}": dateStr,
604
+ "{<vurderingsdato>}": dateStr,
605
+ "{<arviointip\xE4iv\xE4>}": dateStr,
606
+ "{<beoordelingsdatum>}": dateStr,
607
+ "{<bewertungsdatum>}": dateStr,
608
+ "{<date_evaluation>}": dateStr,
609
+ "{<fecha_evaluacion>}": dateStr,
610
+ "{<assessment date>}": dateStr,
611
+ "{<uppdateringsdatum>}": dateStr,
612
+ "{<oppdateringsdato>}": dateStr,
613
+ "{<p\xE4ivitysp\xE4iv\xE4>}": dateStr,
614
+ "{<updatedatum>}": dateStr,
615
+ "{<aktualisierungsdatum>}": dateStr,
616
+ "{<date_mise_a_jour>}": dateStr,
617
+ "{<fecha_actualizacion>}": dateStr,
618
+ "{<update date>}": dateStr,
619
+ "{<publiceringsdatum>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
620
+ "{<publiseringsdato>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
621
+ "{<julkaisup\xE4iv\xE4>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
622
+ "{<publicatiedatum>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
623
+ "{<ver\xF6ffentlichungsdatum>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
624
+ "{<date_publication>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
625
+ "{<fecha_publicacion>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
626
+ "{<publish date>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
627
+ "{<metod>}": props.evaluationMethod || "Automated Scan",
628
+ "{<metodi>}": props.evaluationMethod || "Automated Scan",
629
+ "{<methode>}": props.evaluationMethod || "Automated Scan",
630
+ "{<m\xE9todo>}": props.evaluationMethod || "Automated Scan",
631
+ "{<method>}": props.evaluationMethod || "Automated Scan",
632
+ "{<extern akt\xF6r>}": props.generatorTool?.name || "HolmDigital Engine",
633
+ "{<ekstern aktor>}": props.generatorTool?.name || "HolmDigital Engine",
634
+ "{<ulkoinen taho>}": props.generatorTool?.name || "HolmDigital Engine",
635
+ "{<externe partij>}": props.generatorTool?.name || "HolmDigital Engine",
636
+ "{<externer Dritter>}": props.generatorTool?.name || "HolmDigital Engine",
637
+ "{<tiers externe>}": props.generatorTool?.name || "HolmDigital Engine",
638
+ "{<tercero externo>}": props.generatorTool?.name || "HolmDigital Engine",
639
+ "{<third party>}": props.generatorTool?.name || "HolmDigital Engine",
640
+ "{<enforcement_body>}": ENFORCEMENT_BODIES[country] || ENFORCEMENT_BODIES.EU,
641
+ "{<brister>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "sv" ? "Inga k\xE4nda brister." : "No known issues.",
642
+ "{<puutteet>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "fi" ? "Ei tiedossa olevia puutteita." : "No known issues.",
643
+ "{<gebreken>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "nl" ? "Geen bekende gebreken." : "No known issues.",
644
+ "{<m\xE4ngel>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "de" ? "Keine bekannten M\xE4ngel." : "No known issues.",
645
+ "{<d\xE9fauts>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "fr" ? "Aucun d\xE9faut connu." : "No known issues.",
646
+ "{<deficiencias>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "es" ? "No hay deficiencias conocidas." : "No known issues.",
647
+ "{<mangler>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "no" || lang === "da" ? "Ingen kendte mangler." : "No known issues.",
648
+ "{<issues>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : "No known issues."
649
+ };
650
+ const processText = (text) => {
651
+ let processed = text;
652
+ processed = processed.replace(/\[([\s\S]*?)\]/g, (_match, content2) => {
653
+ if (content2.includes("{<svarstid>}") || content2.includes("{<svartid>}") || content2.includes("{<response time>}")) {
654
+ return props.responseTime ? content2 : "";
655
+ }
656
+ if (content2.includes("{<telefonnummer>}") || content2.includes("{<telephone number>}")) {
657
+ return props.phoneNumber ? content2 : "";
658
+ }
659
+ if (content2.includes("{<brister>}") || content2.includes("{<mangler>}") || content2.includes("{<issues>}")) {
660
+ return complianceLevel !== "full" ? content2 : "";
661
+ }
662
+ return content2;
663
+ });
664
+ processed = processed.replace(/\{([^{}]*?)\}/g, (_match, content2) => {
665
+ const parts = content2.split("/");
666
+ if (parts.length >= 2) {
667
+ let idx = 0;
668
+ if (complianceLevel === "partial") idx = 1;
669
+ if (complianceLevel === "non-compliant") idx = parts.length > 2 ? 2 : 1;
670
+ return parts[idx].trim();
671
+ }
672
+ const key = `{${content2}}`;
673
+ return substitutions[key] !== void 0 ? substitutions[key] : _match;
674
+ });
675
+ return processed;
676
+ };
677
+ const title = processText(template.title);
678
+ const intro = processText(template.intro);
679
+ const sections = template.sections.map((s) => `## ${s.title}
589
680
 
590
- If you need information on this website in a different format like accessible PDF, large print, easy-to-read, audio recording or braille:
681
+ ${processText(s.content)}`).join("\n\n");
682
+ content = `# ${title}
591
683
 
592
- - email: [${props.contactEmail}](mailto:${props.contactEmail})
684
+ ${intro}
593
685
 
594
- ## Enforcement procedure
686
+ ${sections}
595
687
 
596
- The enforcement body for ${country} is responsible for enforcing these regulations.
597
- `;
688
+ ---
689
+ Generated using [${props.generatorTool?.name || "HolmDigital Engine"}](${props.generatorTool?.url || "https://holmdigital.se"})`;
598
690
  }
599
691
  return content;
600
692
  }
package/dist/cli/index.js CHANGED
@@ -550,29 +550,62 @@ var junit_generator_exports = {};
550
550
  __export(junit_generator_exports, {
551
551
  generateJUnitXML: () => generateJUnitXML
552
552
  });
553
- function generateJUnitXML(reports, url, duration) {
554
- const failures = reports.length;
555
- const testSuites = [
553
+ function generateJUnitXML(result) {
554
+ const { url, reports, stats, metadata } = result;
555
+ const duration = metadata.scanDuration;
556
+ const timestamp = result.timestamp;
557
+ const totalTests = stats.passed + stats.total;
558
+ const failures = stats.total;
559
+ const xmlLines = [
556
560
  `<?xml version="1.0" encoding="UTF-8"?>`,
557
- `<testsuites name="HolmDigital Accessibility Scan" time="${duration / 1e3}">`,
558
- ` <testsuite name="${url}" tests="${failures}" failures="${failures}" errors="0" time="${duration / 1e3}">`
561
+ `<testsuites name="HolmDigital Accessibility Scan" time="${duration / 1e3}" tests="${totalTests}" failures="${failures}">`,
562
+ ` <testsuite name="${url}" tests="${totalTests}" failures="${failures}" errors="0" time="${duration / 1e3}" timestamp="${timestamp}">`,
563
+ ` <properties>`,
564
+ ` <property name="engineVersion" value="${metadata.engineVersion}"/>`,
565
+ ` <property name="axeCoreVersion" value="${metadata.axeCoreVersion}"/>`,
566
+ ` <property name="standardsVersion" value="${metadata.standardsVersion}"/>`,
567
+ ` <property name="pageTitle" value="${escapeXML(metadata.pageTitle || "N/A")}"/>`,
568
+ ` <property name="pageLanguage" value="${escapeXML(metadata.pageLanguage || "N/A")}"/>`,
569
+ ` <property name="score" value="${result.score}"/>`,
570
+ ` <property name="complianceStatus" value="${result.complianceStatus}"/>`,
571
+ ` </properties>`
559
572
  ];
573
+ for (let i = 0; i < stats.passed; i++) {
574
+ xmlLines.push(` <testcase name="Passed Accessibility Rule ${i + 1}" classname="Accessibility.Success" time="0" />`);
575
+ }
560
576
  reports.forEach((report) => {
561
577
  const severity = report.holmdigitalInsight.diggRisk;
562
578
  const message = escapeXML(report.holmdigitalInsight.reasoning);
563
- const criteria = escapeXML(`WCAG ${report.wcagCriteria} | EN 301 549 ${report.en301549Criteria}`);
564
- const help = escapeXML(`Ref: ${report.dosLagenReference}. Remediation: ${report.remediation.component || "Manual"}`);
565
- testSuites.push(` <testcase name="[${severity.toUpperCase()}] ${report.ruleId}" classname="${report.wcagCriteria}" time="0">`);
566
- testSuites.push(` <failure message="${message}" type="${severity}">${criteria}
567
- ${help}</failure>`);
568
- testSuites.push(` </testcase>`);
579
+ const criteria = `WCAG ${report.wcagCriteria} | EN 301 549 ${report.en301549Criteria}`;
580
+ const help = `Ref: ${report.dosLagenReference}. Remediation: ${report.remediation.component || "Manual"}`;
581
+ xmlLines.push(` <testcase name="[${severity.toUpperCase()}] ${report.ruleId}" classname="${report.wcagCriteria}" time="0">`);
582
+ xmlLines.push(` <failure message="${message}" type="${severity}">${escapeXML(criteria)}
583
+ ${escapeXML(help)}</failure>`);
584
+ if (report.failingNodes && report.failingNodes.length > 0) {
585
+ let nodeInfo = "Failing Nodes:\n";
586
+ report.failingNodes.forEach((node, idx) => {
587
+ nodeInfo += `
588
+ [Node ${idx + 1}]
589
+ `;
590
+ nodeInfo += `Target: ${node.target}
591
+ `;
592
+ nodeInfo += `HTML: ${node.html}
593
+ `;
594
+ nodeInfo += `Fix: ${node.failureSummary}
595
+ `;
596
+ });
597
+ xmlLines.push(` <system-out>${escapeXML(nodeInfo)}</system-out>`);
598
+ }
599
+ xmlLines.push(` </testcase>`);
569
600
  });
570
- testSuites.push(` </testsuite>`);
571
- testSuites.push(`</testsuites>`);
572
- return testSuites.join("\n");
601
+ xmlLines.push(` </testsuite>`);
602
+ xmlLines.push(`</testsuites>`);
603
+ return xmlLines.join("\n");
573
604
  }
574
605
  function escapeXML(unsafe) {
575
- return unsafe.replace(/[<>&'"]/g, (c) => {
606
+ if (unsafe === void 0 || unsafe === null) return "";
607
+ const str = String(unsafe);
608
+ return str.replace(/[<>&'"]/g, (c) => {
576
609
  switch (c) {
577
610
  case "<":
578
611
  return "&lt;";
@@ -900,6 +933,7 @@ var RegulatoryScanner = class {
900
933
  holmdigitalInsight: {
901
934
  diggRisk: "medium",
902
935
  eaaImpact: "medium",
936
+ reasoning: violation.help,
903
937
  swedishInterpretation: violation.help,
904
938
  priorityRationale: "Detta fel uppt\xE4cktes av scannern men saknar specifik mappning i HolmDigital-databasen."
905
939
  },
@@ -1391,9 +1425,25 @@ async function generatePDF(htmlContent, outputPath) {
1391
1425
  var import_react = __toESM(require("react"));
1392
1426
  var import_server = require("react-dom/server");
1393
1427
  var import_components = require("@holmdigital/components");
1428
+ var import_standards = require("@holmdigital/standards");
1394
1429
  var import_promises = __toESM(require("fs/promises"));
1395
1430
  var import_path = __toESM(require("path"));
1396
1431
  async function generateStatementContent(result, lang = "en", format = "html", metadata) {
1432
+ let template;
1433
+ try {
1434
+ const templatePath = import_path.default.join(__dirname, "templates", `${lang}.json`);
1435
+ const templateData = await import_promises.default.readFile(templatePath, "utf-8");
1436
+ template = JSON.parse(templateData);
1437
+ } catch (e) {
1438
+ try {
1439
+ const fallbackPath = import_path.default.join(__dirname, "templates", "en.json");
1440
+ const templateData = await import_promises.default.readFile(fallbackPath, "utf-8");
1441
+ template = JSON.parse(templateData);
1442
+ } catch (err) {
1443
+ console.error("Could not load accessibility statement templates", err);
1444
+ throw new Error("Accessibility statement templates missing");
1445
+ }
1446
+ }
1397
1447
  let complianceLevel = "full";
1398
1448
  if (result.stats.critical > 0 || result.legalSummary && result.legalSummary.eaaDeadlineViolations > 0) {
1399
1449
  complianceLevel = "non-compliant";
@@ -1478,41 +1528,116 @@ async function generateStatementContent(result, lang = "en", format = "html", me
1478
1528
  } else {
1479
1529
  const dateStr = props.lastReviewDate.toISOString().split("T")[0];
1480
1530
  const statusMap = {
1481
- "full": "Full",
1482
- "partial": "Partial",
1483
- "non-compliant": "Non-Compliant"
1531
+ "full": lang === "sv" ? "Fullt ut f\xF6renlig" : lang === "no" || lang === "nb" ? "Helt i samsvar" : lang === "da" ? "Fuldt ud i overensstemmelse" : "Fully compliant",
1532
+ "partial": lang === "sv" ? "Delvis f\xF6renlig" : lang === "no" || lang === "nb" ? "Delvis i samsvar" : lang === "da" ? "Delvist i overensstemmelse" : "Partially compliant",
1533
+ "non-compliant": lang === "sv" ? "Inte f\xF6renlig" : lang === "no" || lang === "nb" ? "Ikke i samsvar" : lang === "da" ? "Ikke i overensstemmelse" : "Non-compliant"
1484
1534
  };
1485
- content = `# Accessibility Statement for ${props.organizationName}
1486
-
1487
- This accessibility statement applies to [${props.websiteUrl}](${props.websiteUrl}).
1488
-
1489
- ## Compliance Status
1490
-
1491
- **Status:** ${statusMap[complianceLevel] || complianceLevel}
1492
-
1493
- This website is ${complianceLevel} compliant with ${sector === "public" ? "EN 301 549 (WAD)" : "EAA"}.
1494
-
1495
- ## Non-accessible Content
1496
-
1497
- The following content is non-accessible for the following reasons:
1498
-
1499
- ${nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `- ${item}`).join("\n") : "_No known issues._"}
1500
-
1501
- ## Preparation of this statement
1502
-
1503
- This statement was prepared on ${dateStr}.
1504
- Method used: **Automated Scan** via @holmdigital/engine.
1505
-
1506
- ## Feedback and contact information
1535
+ const substitutions = {
1536
+ statusString: statusMap[complianceLevel],
1537
+ "{<webbplats>}": props.organizationName,
1538
+ "{<website>}": props.organizationName,
1539
+ "{<nettsted>}": props.organizationName,
1540
+ "{<organisation>}": props.organizationName,
1541
+ "{<organisasjon>}": props.organizationName,
1542
+ "{<e-postadress>}": props.contactEmail || "",
1543
+ "{<e-mailosoite>}": props.contactEmail || "",
1544
+ "{<e-mailadresse>}": props.contactEmail || "",
1545
+ "{<e-mail address>}": props.contactEmail || "",
1546
+ "{<email address>}": props.contactEmail || "",
1547
+ "{<telefonnummer>}": props.phoneNumber || "",
1548
+ "{<puhelinnumero>}": props.phoneNumber || "",
1549
+ "{<telefoonnummer>}": props.phoneNumber || "",
1550
+ "{<telephone number>}": props.phoneNumber || "",
1551
+ "{<svarstid>}": props.responseTime || "",
1552
+ "{<svartid>}": props.responseTime || "",
1553
+ "{<response time>}": props.responseTime || "",
1554
+ "{<bed\xF6mningsdatum>}": dateStr,
1555
+ "{<vurderingsdato>}": dateStr,
1556
+ "{<arviointip\xE4iv\xE4>}": dateStr,
1557
+ "{<beoordelingsdatum>}": dateStr,
1558
+ "{<bewertungsdatum>}": dateStr,
1559
+ "{<date_evaluation>}": dateStr,
1560
+ "{<fecha_evaluacion>}": dateStr,
1561
+ "{<assessment date>}": dateStr,
1562
+ "{<uppdateringsdatum>}": dateStr,
1563
+ "{<oppdateringsdato>}": dateStr,
1564
+ "{<p\xE4ivitysp\xE4iv\xE4>}": dateStr,
1565
+ "{<updatedatum>}": dateStr,
1566
+ "{<aktualisierungsdatum>}": dateStr,
1567
+ "{<date_mise_a_jour>}": dateStr,
1568
+ "{<fecha_actualizacion>}": dateStr,
1569
+ "{<update date>}": dateStr,
1570
+ "{<publiceringsdatum>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1571
+ "{<publiseringsdato>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1572
+ "{<julkaisup\xE4iv\xE4>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1573
+ "{<publicatiedatum>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1574
+ "{<ver\xF6ffentlichungsdatum>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1575
+ "{<date_publication>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1576
+ "{<fecha_publicacion>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1577
+ "{<publish date>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1578
+ "{<metod>}": props.evaluationMethod || "Automated Scan",
1579
+ "{<metodi>}": props.evaluationMethod || "Automated Scan",
1580
+ "{<methode>}": props.evaluationMethod || "Automated Scan",
1581
+ "{<m\xE9todo>}": props.evaluationMethod || "Automated Scan",
1582
+ "{<method>}": props.evaluationMethod || "Automated Scan",
1583
+ "{<extern akt\xF6r>}": props.generatorTool?.name || "HolmDigital Engine",
1584
+ "{<ekstern aktor>}": props.generatorTool?.name || "HolmDigital Engine",
1585
+ "{<ulkoinen taho>}": props.generatorTool?.name || "HolmDigital Engine",
1586
+ "{<externe partij>}": props.generatorTool?.name || "HolmDigital Engine",
1587
+ "{<externer Dritter>}": props.generatorTool?.name || "HolmDigital Engine",
1588
+ "{<tiers externe>}": props.generatorTool?.name || "HolmDigital Engine",
1589
+ "{<tercero externo>}": props.generatorTool?.name || "HolmDigital Engine",
1590
+ "{<third party>}": props.generatorTool?.name || "HolmDigital Engine",
1591
+ "{<enforcement_body>}": import_standards.ENFORCEMENT_BODIES[country] || import_standards.ENFORCEMENT_BODIES.EU,
1592
+ "{<brister>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "sv" ? "Inga k\xE4nda brister." : "No known issues.",
1593
+ "{<puutteet>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "fi" ? "Ei tiedossa olevia puutteita." : "No known issues.",
1594
+ "{<gebreken>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "nl" ? "Geen bekende gebreken." : "No known issues.",
1595
+ "{<m\xE4ngel>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "de" ? "Keine bekannten M\xE4ngel." : "No known issues.",
1596
+ "{<d\xE9fauts>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "fr" ? "Aucun d\xE9faut connu." : "No known issues.",
1597
+ "{<deficiencias>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "es" ? "No hay deficiencias conocidas." : "No known issues.",
1598
+ "{<mangler>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "no" || lang === "da" ? "Ingen kendte mangler." : "No known issues.",
1599
+ "{<issues>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : "No known issues."
1600
+ };
1601
+ const processText = (text) => {
1602
+ let processed = text;
1603
+ processed = processed.replace(/\[([\s\S]*?)\]/g, (_match, content2) => {
1604
+ if (content2.includes("{<svarstid>}") || content2.includes("{<svartid>}") || content2.includes("{<response time>}")) {
1605
+ return props.responseTime ? content2 : "";
1606
+ }
1607
+ if (content2.includes("{<telefonnummer>}") || content2.includes("{<telephone number>}")) {
1608
+ return props.phoneNumber ? content2 : "";
1609
+ }
1610
+ if (content2.includes("{<brister>}") || content2.includes("{<mangler>}") || content2.includes("{<issues>}")) {
1611
+ return complianceLevel !== "full" ? content2 : "";
1612
+ }
1613
+ return content2;
1614
+ });
1615
+ processed = processed.replace(/\{([^{}]*?)\}/g, (_match, content2) => {
1616
+ const parts = content2.split("/");
1617
+ if (parts.length >= 2) {
1618
+ let idx = 0;
1619
+ if (complianceLevel === "partial") idx = 1;
1620
+ if (complianceLevel === "non-compliant") idx = parts.length > 2 ? 2 : 1;
1621
+ return parts[idx].trim();
1622
+ }
1623
+ const key = `{${content2}}`;
1624
+ return substitutions[key] !== void 0 ? substitutions[key] : _match;
1625
+ });
1626
+ return processed;
1627
+ };
1628
+ const title = processText(template.title);
1629
+ const intro = processText(template.intro);
1630
+ const sections = template.sections.map((s) => `## ${s.title}
1507
1631
 
1508
- If you need information on this website in a different format like accessible PDF, large print, easy-to-read, audio recording or braille:
1632
+ ${processText(s.content)}`).join("\n\n");
1633
+ content = `# ${title}
1509
1634
 
1510
- - email: [${props.contactEmail}](mailto:${props.contactEmail})
1635
+ ${intro}
1511
1636
 
1512
- ## Enforcement procedure
1637
+ ${sections}
1513
1638
 
1514
- The enforcement body for ${country} is responsible for enforcing these regulations.
1515
- `;
1639
+ ---
1640
+ Generated using [${props.generatorTool?.name || "HolmDigital Engine"}](${props.generatorTool?.url || "https://holmdigital.se"})`;
1516
1641
  }
1517
1642
  return content;
1518
1643
  }
@@ -1816,7 +1941,7 @@ program.argument("<url>", "URL to scan").option("--lang <code>", "Language code
1816
1941
  if (options.junit) {
1817
1942
  const { generateJUnitXML: generateJUnitXML2 } = await Promise.resolve().then(() => (init_junit_generator(), junit_generator_exports));
1818
1943
  const fs2 = await import("fs/promises");
1819
- const xml = generateJUnitXML2(result.reports, url, result.metadata.scanDuration);
1944
+ const xml = generateJUnitXML2(result);
1820
1945
  await fs2.writeFile(options.junit, xml);
1821
1946
  if (spinner) spinner.succeed(t("cli.junit_saved", { path: options.junit }));
1822
1947
  else if (!options.json) console.log(import_chalk.default.green(`\u2713 JUnit report saved to ${options.junit}`));
@@ -6,7 +6,7 @@ import {
6
6
  generateBadgeUrl,
7
7
  generateStatement,
8
8
  generateStatementContent
9
- } from "../chunk-ZO2XNHHT.mjs";
9
+ } from "../chunk-F6MWN7DX.mjs";
10
10
  import {
11
11
  getCurrentLang,
12
12
  setLanguage,
@@ -621,9 +621,9 @@ program.argument("<url>", "URL to scan").option("--lang <code>", "Language code
621
621
  generateGitHubActionsAnnotations(result.reports);
622
622
  }
623
623
  if (options.junit) {
624
- const { generateJUnitXML } = await import("../junit-generator-FK3GE5E4.mjs");
624
+ const { generateJUnitXML } = await import("../junit-generator-6ITTV4W5.mjs");
625
625
  const fs = await import("fs/promises");
626
- const xml = generateJUnitXML(result.reports, url, result.metadata.scanDuration);
626
+ const xml = generateJUnitXML(result);
627
627
  await fs.writeFile(options.junit, xml);
628
628
  if (spinner) spinner.succeed(t("cli.junit_saved", { path: options.junit }));
629
629
  else if (!options.json) console.log(chalk.green(`\u2713 JUnit report saved to ${options.junit}`));
package/dist/index.js CHANGED
@@ -828,6 +828,7 @@ var RegulatoryScanner = class {
828
828
  holmdigitalInsight: {
829
829
  diggRisk: "medium",
830
830
  eaaImpact: "medium",
831
+ reasoning: violation.help,
831
832
  swedishInterpretation: violation.help,
832
833
  priorityRationale: "Detta fel uppt\xE4cktes av scannern men saknar specifik mappning i HolmDigital-databasen."
833
834
  },
@@ -989,6 +990,7 @@ init_i18n();
989
990
  var import_react = __toESM(require("react"));
990
991
  var import_server = require("react-dom/server");
991
992
  var import_components = require("@holmdigital/components");
993
+ var import_standards = require("@holmdigital/standards");
992
994
  var import_promises = __toESM(require("fs/promises"));
993
995
  var import_path = __toESM(require("path"));
994
996
 
@@ -1004,6 +1006,21 @@ function generateBadgeUrl(score) {
1004
1006
 
1005
1007
  // src/reporting/statement-generator.ts
1006
1008
  async function generateStatementContent(result, lang = "en", format = "html", metadata) {
1009
+ let template;
1010
+ try {
1011
+ const templatePath = import_path.default.join(__dirname, "templates", `${lang}.json`);
1012
+ const templateData = await import_promises.default.readFile(templatePath, "utf-8");
1013
+ template = JSON.parse(templateData);
1014
+ } catch (e) {
1015
+ try {
1016
+ const fallbackPath = import_path.default.join(__dirname, "templates", "en.json");
1017
+ const templateData = await import_promises.default.readFile(fallbackPath, "utf-8");
1018
+ template = JSON.parse(templateData);
1019
+ } catch (err) {
1020
+ console.error("Could not load accessibility statement templates", err);
1021
+ throw new Error("Accessibility statement templates missing");
1022
+ }
1023
+ }
1007
1024
  let complianceLevel = "full";
1008
1025
  if (result.stats.critical > 0 || result.legalSummary && result.legalSummary.eaaDeadlineViolations > 0) {
1009
1026
  complianceLevel = "non-compliant";
@@ -1088,41 +1105,116 @@ async function generateStatementContent(result, lang = "en", format = "html", me
1088
1105
  } else {
1089
1106
  const dateStr = props.lastReviewDate.toISOString().split("T")[0];
1090
1107
  const statusMap = {
1091
- "full": "Full",
1092
- "partial": "Partial",
1093
- "non-compliant": "Non-Compliant"
1108
+ "full": lang === "sv" ? "Fullt ut f\xF6renlig" : lang === "no" || lang === "nb" ? "Helt i samsvar" : lang === "da" ? "Fuldt ud i overensstemmelse" : "Fully compliant",
1109
+ "partial": lang === "sv" ? "Delvis f\xF6renlig" : lang === "no" || lang === "nb" ? "Delvis i samsvar" : lang === "da" ? "Delvist i overensstemmelse" : "Partially compliant",
1110
+ "non-compliant": lang === "sv" ? "Inte f\xF6renlig" : lang === "no" || lang === "nb" ? "Ikke i samsvar" : lang === "da" ? "Ikke i overensstemmelse" : "Non-compliant"
1094
1111
  };
1095
- content = `# Accessibility Statement for ${props.organizationName}
1096
-
1097
- This accessibility statement applies to [${props.websiteUrl}](${props.websiteUrl}).
1098
-
1099
- ## Compliance Status
1100
-
1101
- **Status:** ${statusMap[complianceLevel] || complianceLevel}
1102
-
1103
- This website is ${complianceLevel} compliant with ${sector === "public" ? "EN 301 549 (WAD)" : "EAA"}.
1104
-
1105
- ## Non-accessible Content
1106
-
1107
- The following content is non-accessible for the following reasons:
1108
-
1109
- ${nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `- ${item}`).join("\n") : "_No known issues._"}
1110
-
1111
- ## Preparation of this statement
1112
-
1113
- This statement was prepared on ${dateStr}.
1114
- Method used: **Automated Scan** via @holmdigital/engine.
1115
-
1116
- ## Feedback and contact information
1112
+ const substitutions = {
1113
+ statusString: statusMap[complianceLevel],
1114
+ "{<webbplats>}": props.organizationName,
1115
+ "{<website>}": props.organizationName,
1116
+ "{<nettsted>}": props.organizationName,
1117
+ "{<organisation>}": props.organizationName,
1118
+ "{<organisasjon>}": props.organizationName,
1119
+ "{<e-postadress>}": props.contactEmail || "",
1120
+ "{<e-mailosoite>}": props.contactEmail || "",
1121
+ "{<e-mailadresse>}": props.contactEmail || "",
1122
+ "{<e-mail address>}": props.contactEmail || "",
1123
+ "{<email address>}": props.contactEmail || "",
1124
+ "{<telefonnummer>}": props.phoneNumber || "",
1125
+ "{<puhelinnumero>}": props.phoneNumber || "",
1126
+ "{<telefoonnummer>}": props.phoneNumber || "",
1127
+ "{<telephone number>}": props.phoneNumber || "",
1128
+ "{<svarstid>}": props.responseTime || "",
1129
+ "{<svartid>}": props.responseTime || "",
1130
+ "{<response time>}": props.responseTime || "",
1131
+ "{<bed\xF6mningsdatum>}": dateStr,
1132
+ "{<vurderingsdato>}": dateStr,
1133
+ "{<arviointip\xE4iv\xE4>}": dateStr,
1134
+ "{<beoordelingsdatum>}": dateStr,
1135
+ "{<bewertungsdatum>}": dateStr,
1136
+ "{<date_evaluation>}": dateStr,
1137
+ "{<fecha_evaluacion>}": dateStr,
1138
+ "{<assessment date>}": dateStr,
1139
+ "{<uppdateringsdatum>}": dateStr,
1140
+ "{<oppdateringsdato>}": dateStr,
1141
+ "{<p\xE4ivitysp\xE4iv\xE4>}": dateStr,
1142
+ "{<updatedatum>}": dateStr,
1143
+ "{<aktualisierungsdatum>}": dateStr,
1144
+ "{<date_mise_a_jour>}": dateStr,
1145
+ "{<fecha_actualizacion>}": dateStr,
1146
+ "{<update date>}": dateStr,
1147
+ "{<publiceringsdatum>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1148
+ "{<publiseringsdato>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1149
+ "{<julkaisup\xE4iv\xE4>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1150
+ "{<publicatiedatum>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1151
+ "{<ver\xF6ffentlichungsdatum>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1152
+ "{<date_publication>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1153
+ "{<fecha_publicacion>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1154
+ "{<publish date>}": props.publishDate?.toISOString().split("T")[0] || "2024-01-01",
1155
+ "{<metod>}": props.evaluationMethod || "Automated Scan",
1156
+ "{<metodi>}": props.evaluationMethod || "Automated Scan",
1157
+ "{<methode>}": props.evaluationMethod || "Automated Scan",
1158
+ "{<m\xE9todo>}": props.evaluationMethod || "Automated Scan",
1159
+ "{<method>}": props.evaluationMethod || "Automated Scan",
1160
+ "{<extern akt\xF6r>}": props.generatorTool?.name || "HolmDigital Engine",
1161
+ "{<ekstern aktor>}": props.generatorTool?.name || "HolmDigital Engine",
1162
+ "{<ulkoinen taho>}": props.generatorTool?.name || "HolmDigital Engine",
1163
+ "{<externe partij>}": props.generatorTool?.name || "HolmDigital Engine",
1164
+ "{<externer Dritter>}": props.generatorTool?.name || "HolmDigital Engine",
1165
+ "{<tiers externe>}": props.generatorTool?.name || "HolmDigital Engine",
1166
+ "{<tercero externo>}": props.generatorTool?.name || "HolmDigital Engine",
1167
+ "{<third party>}": props.generatorTool?.name || "HolmDigital Engine",
1168
+ "{<enforcement_body>}": import_standards.ENFORCEMENT_BODIES[country] || import_standards.ENFORCEMENT_BODIES.EU,
1169
+ "{<brister>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "sv" ? "Inga k\xE4nda brister." : "No known issues.",
1170
+ "{<puutteet>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "fi" ? "Ei tiedossa olevia puutteita." : "No known issues.",
1171
+ "{<gebreken>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "nl" ? "Geen bekende gebreken." : "No known issues.",
1172
+ "{<m\xE4ngel>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "de" ? "Keine bekannten M\xE4ngel." : "No known issues.",
1173
+ "{<d\xE9fauts>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "fr" ? "Aucun d\xE9faut connu." : "No known issues.",
1174
+ "{<deficiencias>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "es" ? "No hay deficiencias conocidas." : "No known issues.",
1175
+ "{<mangler>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : lang === "no" || lang === "da" ? "Ingen kendte mangler." : "No known issues.",
1176
+ "{<issues>}": nonComplianceItems.length > 0 ? nonComplianceItems.map((item) => `* ${item}`).join("\n") : "No known issues."
1177
+ };
1178
+ const processText = (text) => {
1179
+ let processed = text;
1180
+ processed = processed.replace(/\[([\s\S]*?)\]/g, (_match, content2) => {
1181
+ if (content2.includes("{<svarstid>}") || content2.includes("{<svartid>}") || content2.includes("{<response time>}")) {
1182
+ return props.responseTime ? content2 : "";
1183
+ }
1184
+ if (content2.includes("{<telefonnummer>}") || content2.includes("{<telephone number>}")) {
1185
+ return props.phoneNumber ? content2 : "";
1186
+ }
1187
+ if (content2.includes("{<brister>}") || content2.includes("{<mangler>}") || content2.includes("{<issues>}")) {
1188
+ return complianceLevel !== "full" ? content2 : "";
1189
+ }
1190
+ return content2;
1191
+ });
1192
+ processed = processed.replace(/\{([^{}]*?)\}/g, (_match, content2) => {
1193
+ const parts = content2.split("/");
1194
+ if (parts.length >= 2) {
1195
+ let idx = 0;
1196
+ if (complianceLevel === "partial") idx = 1;
1197
+ if (complianceLevel === "non-compliant") idx = parts.length > 2 ? 2 : 1;
1198
+ return parts[idx].trim();
1199
+ }
1200
+ const key = `{${content2}}`;
1201
+ return substitutions[key] !== void 0 ? substitutions[key] : _match;
1202
+ });
1203
+ return processed;
1204
+ };
1205
+ const title = processText(template.title);
1206
+ const intro = processText(template.intro);
1207
+ const sections = template.sections.map((s) => `## ${s.title}
1117
1208
 
1118
- If you need information on this website in a different format like accessible PDF, large print, easy-to-read, audio recording or braille:
1209
+ ${processText(s.content)}`).join("\n\n");
1210
+ content = `# ${title}
1119
1211
 
1120
- - email: [${props.contactEmail}](mailto:${props.contactEmail})
1212
+ ${intro}
1121
1213
 
1122
- ## Enforcement procedure
1214
+ ${sections}
1123
1215
 
1124
- The enforcement body for ${country} is responsible for enforcing these regulations.
1125
- `;
1216
+ ---
1217
+ Generated using [${props.generatorTool?.name || "HolmDigital Engine"}](${props.generatorTool?.url || "https://holmdigital.se"})`;
1126
1218
  }
1127
1219
  return content;
1128
1220
  }
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  VirtualDOMBuilder,
5
5
  generateStatement,
6
6
  generateStatementContent
7
- } from "./chunk-ZO2XNHHT.mjs";
7
+ } from "./chunk-F6MWN7DX.mjs";
8
8
  import {
9
9
  getCurrentLang,
10
10
  setLanguage,
@@ -0,0 +1,76 @@
1
+ // src/reporting/junit-generator.ts
2
+ function generateJUnitXML(result) {
3
+ const { url, reports, stats, metadata } = result;
4
+ const duration = metadata.scanDuration;
5
+ const timestamp = result.timestamp;
6
+ const totalTests = stats.passed + stats.total;
7
+ const failures = stats.total;
8
+ const xmlLines = [
9
+ `<?xml version="1.0" encoding="UTF-8"?>`,
10
+ `<testsuites name="HolmDigital Accessibility Scan" time="${duration / 1e3}" tests="${totalTests}" failures="${failures}">`,
11
+ ` <testsuite name="${url}" tests="${totalTests}" failures="${failures}" errors="0" time="${duration / 1e3}" timestamp="${timestamp}">`,
12
+ ` <properties>`,
13
+ ` <property name="engineVersion" value="${metadata.engineVersion}"/>`,
14
+ ` <property name="axeCoreVersion" value="${metadata.axeCoreVersion}"/>`,
15
+ ` <property name="standardsVersion" value="${metadata.standardsVersion}"/>`,
16
+ ` <property name="pageTitle" value="${escapeXML(metadata.pageTitle || "N/A")}"/>`,
17
+ ` <property name="pageLanguage" value="${escapeXML(metadata.pageLanguage || "N/A")}"/>`,
18
+ ` <property name="score" value="${result.score}"/>`,
19
+ ` <property name="complianceStatus" value="${result.complianceStatus}"/>`,
20
+ ` </properties>`
21
+ ];
22
+ for (let i = 0; i < stats.passed; i++) {
23
+ xmlLines.push(` <testcase name="Passed Accessibility Rule ${i + 1}" classname="Accessibility.Success" time="0" />`);
24
+ }
25
+ reports.forEach((report) => {
26
+ const severity = report.holmdigitalInsight.diggRisk;
27
+ const message = escapeXML(report.holmdigitalInsight.reasoning);
28
+ const criteria = `WCAG ${report.wcagCriteria} | EN 301 549 ${report.en301549Criteria}`;
29
+ const help = `Ref: ${report.dosLagenReference}. Remediation: ${report.remediation.component || "Manual"}`;
30
+ xmlLines.push(` <testcase name="[${severity.toUpperCase()}] ${report.ruleId}" classname="${report.wcagCriteria}" time="0">`);
31
+ xmlLines.push(` <failure message="${message}" type="${severity}">${escapeXML(criteria)}
32
+ ${escapeXML(help)}</failure>`);
33
+ if (report.failingNodes && report.failingNodes.length > 0) {
34
+ let nodeInfo = "Failing Nodes:\n";
35
+ report.failingNodes.forEach((node, idx) => {
36
+ nodeInfo += `
37
+ [Node ${idx + 1}]
38
+ `;
39
+ nodeInfo += `Target: ${node.target}
40
+ `;
41
+ nodeInfo += `HTML: ${node.html}
42
+ `;
43
+ nodeInfo += `Fix: ${node.failureSummary}
44
+ `;
45
+ });
46
+ xmlLines.push(` <system-out>${escapeXML(nodeInfo)}</system-out>`);
47
+ }
48
+ xmlLines.push(` </testcase>`);
49
+ });
50
+ xmlLines.push(` </testsuite>`);
51
+ xmlLines.push(`</testsuites>`);
52
+ return xmlLines.join("\n");
53
+ }
54
+ function escapeXML(unsafe) {
55
+ if (unsafe === void 0 || unsafe === null) return "";
56
+ const str = String(unsafe);
57
+ return str.replace(/[<>&'"]/g, (c) => {
58
+ switch (c) {
59
+ case "<":
60
+ return "&lt;";
61
+ case ">":
62
+ return "&gt;";
63
+ case "&":
64
+ return "&amp;";
65
+ case "'":
66
+ return "&apos;";
67
+ case '"':
68
+ return "&quot;";
69
+ default:
70
+ return c;
71
+ }
72
+ });
73
+ }
74
+ export {
75
+ generateJUnitXML
76
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holmdigital/engine",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -60,7 +60,7 @@
60
60
  "author": "Holm Digital AB",
61
61
  "license": "MIT",
62
62
  "dependencies": {
63
- "@holmdigital/components": "^2.0.0",
63
+ "@holmdigital/components": "^2.1.0",
64
64
  "@holmdigital/standards": "*",
65
65
  "axe-core": "4.10.2",
66
66
  "chalk": "^5.3.0",
@@ -76,9 +76,11 @@
76
76
  "devDependencies": {
77
77
  "@types/cosmiconfig": "^5.0.3",
78
78
  "@types/node": "^22.10.2",
79
+ "@types/react": "^18.3.1",
80
+ "@types/react-dom": "^18.3.1",
79
81
  "@types/ws": "^8.5.13",
80
82
  "tsup": "^8.3.5",
81
83
  "typescript": "^5.7.2",
82
84
  "vitest": "^4.0.16"
83
85
  }
84
- }
86
+ }
@@ -1,43 +0,0 @@
1
- // src/reporting/junit-generator.ts
2
- function generateJUnitXML(reports, url, duration) {
3
- const failures = reports.length;
4
- const testSuites = [
5
- `<?xml version="1.0" encoding="UTF-8"?>`,
6
- `<testsuites name="HolmDigital Accessibility Scan" time="${duration / 1e3}">`,
7
- ` <testsuite name="${url}" tests="${failures}" failures="${failures}" errors="0" time="${duration / 1e3}">`
8
- ];
9
- reports.forEach((report) => {
10
- const severity = report.holmdigitalInsight.diggRisk;
11
- const message = escapeXML(report.holmdigitalInsight.reasoning);
12
- const criteria = escapeXML(`WCAG ${report.wcagCriteria} | EN 301 549 ${report.en301549Criteria}`);
13
- const help = escapeXML(`Ref: ${report.dosLagenReference}. Remediation: ${report.remediation.component || "Manual"}`);
14
- testSuites.push(` <testcase name="[${severity.toUpperCase()}] ${report.ruleId}" classname="${report.wcagCriteria}" time="0">`);
15
- testSuites.push(` <failure message="${message}" type="${severity}">${criteria}
16
- ${help}</failure>`);
17
- testSuites.push(` </testcase>`);
18
- });
19
- testSuites.push(` </testsuite>`);
20
- testSuites.push(`</testsuites>`);
21
- return testSuites.join("\n");
22
- }
23
- function escapeXML(unsafe) {
24
- return unsafe.replace(/[<>&'"]/g, (c) => {
25
- switch (c) {
26
- case "<":
27
- return "&lt;";
28
- case ">":
29
- return "&gt;";
30
- case "&":
31
- return "&amp;";
32
- case "'":
33
- return "&apos;";
34
- case '"':
35
- return "&quot;";
36
- default:
37
- return c;
38
- }
39
- });
40
- }
41
- export {
42
- generateJUnitXML
43
- };