add lint script
This commit is contained in:
		
							parent
							
								
									e9516556e2
								
							
						
					
					
						commit
						b6e6d3e23b
					
				
					 3 changed files with 1481 additions and 12 deletions
				
			
		
							
								
								
									
										572
									
								
								lint_intro.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										572
									
								
								lint_intro.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,572 @@
 | 
			
		|||
#!/usr/bin/env node
 | 
			
		||||
import * as fs from "fs";
 | 
			
		||||
import * as path from "path";
 | 
			
		||||
import { Command } from "@commander-js/extra-typings";
 | 
			
		||||
import { DOMParser, Document, Element } from "@xmldom/xmldom";
 | 
			
		||||
 | 
			
		||||
interface ElementInfo {
 | 
			
		||||
  type: string;
 | 
			
		||||
  element: Element;
 | 
			
		||||
  properties: Record<string, string>;
 | 
			
		||||
  descriptions: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LintWarning {
 | 
			
		||||
  code: string;
 | 
			
		||||
  message: string;
 | 
			
		||||
  element?: ElementInfo;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LintContext {
 | 
			
		||||
  filePath: string;
 | 
			
		||||
  fileDir: string;
 | 
			
		||||
  svgDoc: Document;
 | 
			
		||||
  elements: ElementInfo[];
 | 
			
		||||
  warnings: LintWarning[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Setup the command line interface
 | 
			
		||||
const program = new Command()
 | 
			
		||||
  .name("svg-linter")
 | 
			
		||||
  .description("Lints SVG files according to our constraints")
 | 
			
		||||
  .version("0.1.0")
 | 
			
		||||
  .argument("<file>", "Path to the SVG file to lint")
 | 
			
		||||
  .option(
 | 
			
		||||
    "--inspect",
 | 
			
		||||
    "Inspect mode - outputs all SVG elements and their properties"
 | 
			
		||||
  )
 | 
			
		||||
  .parse();
 | 
			
		||||
 | 
			
		||||
const options = program.opts();
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Reads and parses an SVG file
 | 
			
		||||
 * @param filePath Path to the SVG file
 | 
			
		||||
 * @returns The parsed SVG document
 | 
			
		||||
 */
 | 
			
		||||
function parseSvgFile(filePath: string): Document {
 | 
			
		||||
  try {
 | 
			
		||||
    const resolvedPath = path.resolve(filePath);
 | 
			
		||||
    const svgContent = fs.readFileSync(resolvedPath, "utf-8");
 | 
			
		||||
    const parser = new DOMParser();
 | 
			
		||||
    return parser.parseFromString(svgContent, "image/svg+xml");
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error(
 | 
			
		||||
      `Error reading or parsing SVG file: ${
 | 
			
		||||
        error instanceof Error ? error.message : String(error)
 | 
			
		||||
      }`
 | 
			
		||||
    );
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Extracts properties from an SVG element
 | 
			
		||||
 * @param element The SVG element to extract properties from
 | 
			
		||||
 * @returns Record of property name to value
 | 
			
		||||
 */
 | 
			
		||||
function extractElementProperties(element: Element): Record<string, string> {
 | 
			
		||||
  const properties: Record<string, string> = {};
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < element.attributes.length; i++) {
 | 
			
		||||
    const attr = element.attributes[i];
 | 
			
		||||
    properties[attr.name] = attr.value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return properties;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Extracts all elements of a specific type from an SVG document
 | 
			
		||||
 * @param doc The SVG document
 | 
			
		||||
 * @param elementType The element type to extract
 | 
			
		||||
 * @returns Array of elements with their properties
 | 
			
		||||
 */
 | 
			
		||||
/**
 | 
			
		||||
 * Extract desc elements from a parent element
 | 
			
		||||
 * @param element The parent element
 | 
			
		||||
 * @returns Array of text content from desc elements
 | 
			
		||||
 */
 | 
			
		||||
function extractDescElements(element: Element): string[] {
 | 
			
		||||
  const descriptions: string[] = [];
 | 
			
		||||
  const descElements = element.getElementsByTagName("desc");
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < descElements.length; i++) {
 | 
			
		||||
    const descElement = descElements[i];
 | 
			
		||||
    const textContent = descElement.textContent;
 | 
			
		||||
    if (textContent) {
 | 
			
		||||
      descriptions.push(textContent.trim());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return descriptions;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function extractElements(doc: Document, elementType: string): ElementInfo[] {
 | 
			
		||||
  const elements = doc.getElementsByTagName(elementType);
 | 
			
		||||
  const result: ElementInfo[] = [];
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < elements.length; i++) {
 | 
			
		||||
    const element = elements[i];
 | 
			
		||||
    result.push({
 | 
			
		||||
      type: elementType,
 | 
			
		||||
      element: element,
 | 
			
		||||
      properties: extractElementProperties(element),
 | 
			
		||||
      descriptions: extractDescElements(element),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Linting functions
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// Lint: There should be exactly one element with an id "start", and it should be a rect
 | 
			
		||||
function lintStartRect(context: LintContext): void {
 | 
			
		||||
  const startElements = context.elements.filter(
 | 
			
		||||
    (el) => el.properties.id === "start"
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (startElements.length === 0) {
 | 
			
		||||
    context.warnings.push({
 | 
			
		||||
      code: "START_MISSING",
 | 
			
		||||
      message:
 | 
			
		||||
        "No element with id 'start' found. One rect with id='start' is required.",
 | 
			
		||||
    });
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (startElements.length > 1) {
 | 
			
		||||
    context.warnings.push({
 | 
			
		||||
      code: "START_DUPLICATE",
 | 
			
		||||
      message: `Found ${startElements.length} elements with id 'start'. Only one is allowed.`,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const nonRectStarts = startElements.filter((el) => el.type !== "rect");
 | 
			
		||||
  if (nonRectStarts.length > 0) {
 | 
			
		||||
    context.warnings.push({
 | 
			
		||||
      code: "START_NOT_RECT",
 | 
			
		||||
      message: `Element with id 'start' must be a rect, but found: ${nonRectStarts
 | 
			
		||||
        .map((el) => el.type)
 | 
			
		||||
        .join(", ")}.`,
 | 
			
		||||
      element: nonRectStarts[0],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: For circles with a description element, the description should point to a relative path to an existing file
 | 
			
		||||
function lintCircleDescriptions(context: LintContext): void {
 | 
			
		||||
  const circlesWithDesc = context.elements.filter(
 | 
			
		||||
    (el) => el.type === "circle" && el.descriptions.length > 0
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  circlesWithDesc.forEach((circle) => {
 | 
			
		||||
    circle.descriptions.forEach((desc) => {
 | 
			
		||||
      // Check if description appears to be a file path
 | 
			
		||||
      if (desc.includes("/") || desc.includes(".")) {
 | 
			
		||||
        // Try to resolve the path relative to the SVG file's directory
 | 
			
		||||
        const descPath = path.resolve(context.fileDir, desc);
 | 
			
		||||
        if (!fs.existsSync(descPath)) {
 | 
			
		||||
          context.warnings.push({
 | 
			
		||||
            code: "CIRCLE_DESC_FILE_MISSING",
 | 
			
		||||
            message: `Circle (id=${circle.properties.id ||
 | 
			
		||||
              "no-id"}) has description pointing to non-existent file: ${desc}`,
 | 
			
		||||
            element: circle,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: If there's an ellipse with any description, emit a warning
 | 
			
		||||
function lintEllipseDescriptions(context: LintContext): void {
 | 
			
		||||
  const ellipsesWithDesc = context.elements.filter(
 | 
			
		||||
    (el) => el.type === "ellipse" && el.descriptions.length > 0
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  ellipsesWithDesc.forEach((ellipse) => {
 | 
			
		||||
    // Calculate average radius
 | 
			
		||||
    const rx = parseFloat(ellipse.properties.rx || "0");
 | 
			
		||||
    const ry = parseFloat(ellipse.properties.ry || "0");
 | 
			
		||||
    const avgRadius = (rx + ry) / 2;
 | 
			
		||||
 | 
			
		||||
    context.warnings.push({
 | 
			
		||||
      code: "ELLIPSE_WITH_DESC",
 | 
			
		||||
      message: `Ellipse (id=${ellipse.properties.id ||
 | 
			
		||||
        "no-id"}) has a description. It will be processed as a circle with radius ${avgRadius} (average of rx=${rx} and ry=${ry}).`,
 | 
			
		||||
      element: ellipse,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: Check that hyperlinks starting with "anchor" point to existing anchors and all anchors are referenced
 | 
			
		||||
function lintHyperlinkAnchors(context: LintContext): void {
 | 
			
		||||
  // Find all anchor elements (rect elements with IDs starting with "anchor")
 | 
			
		||||
  const anchorElements = context.elements.filter(
 | 
			
		||||
    (el) => el.type === "rect" && el.properties.id?.startsWith("anchor")
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // Find all hyperlinks (a elements)
 | 
			
		||||
  const aElements = Array.from(context.svgDoc.getElementsByTagName("a"));
 | 
			
		||||
 | 
			
		||||
  // Track anchors that have been referenced
 | 
			
		||||
  const referencedAnchors = new Set<string>();
 | 
			
		||||
 | 
			
		||||
  // Check all hyperlinks for valid anchor references
 | 
			
		||||
  aElements.forEach((aElement) => {
 | 
			
		||||
    const href = aElement.getAttribute("xlink:href");
 | 
			
		||||
    if (!href) return;
 | 
			
		||||
 | 
			
		||||
    if (href.startsWith("anchor")) {
 | 
			
		||||
      // This is an anchor reference - check if the anchor exists
 | 
			
		||||
      const anchorId = href.startsWith("#") ? href.substring(1) : href;
 | 
			
		||||
      const targetAnchor = anchorElements.find(
 | 
			
		||||
        (el) => el.properties.id === anchorId
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (!targetAnchor) {
 | 
			
		||||
        // Hyperlink points to a non-existent anchor
 | 
			
		||||
        context.warnings.push({
 | 
			
		||||
          code: "BROKEN_ANCHOR_LINK",
 | 
			
		||||
          message: `Hyperlink points to non-existent anchor: ${href}`,
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        // Mark this anchor as referenced
 | 
			
		||||
        referencedAnchors.add(anchorId);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Check for unreferenced anchors
 | 
			
		||||
  anchorElements.forEach((anchor) => {
 | 
			
		||||
    if (!referencedAnchors.has(anchor.properties.id)) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "UNREFERENCED_ANCHOR",
 | 
			
		||||
        message: `Anchor element with id '${anchor.properties.id}' is not referenced by any hyperlink.`,
 | 
			
		||||
        element: anchor,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: Validate that image elements with descriptions follow the proper format for video scrolls
 | 
			
		||||
function lintVideoScrollPaths(context: LintContext): void {
 | 
			
		||||
  const imagesWithDesc = context.elements.filter(
 | 
			
		||||
    (el) => el.type === "image" && el.descriptions.length > 0
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  imagesWithDesc.forEach((image) => {
 | 
			
		||||
    const desc = image.descriptions[0];
 | 
			
		||||
    const descLines = desc.split("\n").filter((line) => line.trim().length > 0);
 | 
			
		||||
 | 
			
		||||
    // Check that the description has at least 2 lines
 | 
			
		||||
    if (descLines.length < 2) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "INVALID_SCROLL_DESC_FORMAT",
 | 
			
		||||
        message: `Image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}) has a description that doesn't follow the required format: first line for direction, second line for file path.`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // First line should be a valid direction
 | 
			
		||||
    const directionLine = descLines[0];
 | 
			
		||||
    const validDirections = [
 | 
			
		||||
      "up",
 | 
			
		||||
      "down",
 | 
			
		||||
      "left",
 | 
			
		||||
      "right",
 | 
			
		||||
      "up left",
 | 
			
		||||
      "up right",
 | 
			
		||||
      "down left",
 | 
			
		||||
      "down right",
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    // Check if any valid direction is in the direction line
 | 
			
		||||
    const hasValidDirection = validDirections.some((dir) =>
 | 
			
		||||
      directionLine.toLowerCase().includes(dir)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!hasValidDirection) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "INVALID_SCROLL_DIRECTION",
 | 
			
		||||
        message: `Image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}) has a description with invalid scroll direction: "${directionLine}". Valid directions: ${validDirections.join(
 | 
			
		||||
          ", "
 | 
			
		||||
        )}.`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Second line should point to an existing file
 | 
			
		||||
    const filePath = descLines[1].replace(/^\//, "");
 | 
			
		||||
    try {
 | 
			
		||||
      const fullPath = path.resolve(context.fileDir, filePath);
 | 
			
		||||
      if (!fs.existsSync(fullPath)) {
 | 
			
		||||
        context.warnings.push({
 | 
			
		||||
          code: "SCROLL_FILE_MISSING",
 | 
			
		||||
          message: `Image (id=${image.properties.id ||
 | 
			
		||||
            "no-id"}) references a non-existent file in description: ${filePath}`,
 | 
			
		||||
          element: image,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Path resolution error
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "SCROLL_PATH_ERROR",
 | 
			
		||||
        message: `Error checking path for image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}): ${error instanceof Error ? error.message : String(error)}`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Also check if xlink:href exists and points to a valid directory
 | 
			
		||||
    const xlinkHref = image.properties["xlink:href"];
 | 
			
		||||
    if (!xlinkHref) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "SCROLL_MISSING_HREF",
 | 
			
		||||
        message: `Image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}) with scroll description is missing xlink:href attribute`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
    } else if (!xlinkHref.includes("/")) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "INVALID_SCROLL_PATH",
 | 
			
		||||
        message: `Image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}) has an xlink:href that doesn't point to a directory: ${xlinkHref}`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: Check that all element IDs are unique across the SVG
 | 
			
		||||
function lintIdUniqueness(context: LintContext): void {
 | 
			
		||||
  // Create a map to track IDs and their elements
 | 
			
		||||
  const idMap = new Map<string, ElementInfo[]>();
 | 
			
		||||
 | 
			
		||||
  // Collect all elements with IDs
 | 
			
		||||
  context.elements.forEach((element) => {
 | 
			
		||||
    if (element.properties.id) {
 | 
			
		||||
      const elementsWithId = idMap.get(element.properties.id) || [];
 | 
			
		||||
      elementsWithId.push(element);
 | 
			
		||||
      idMap.set(element.properties.id, elementsWithId);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Check for duplicates
 | 
			
		||||
  idMap.forEach((elements, id) => {
 | 
			
		||||
    if (elements.length > 1) {
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "DUPLICATE_ID",
 | 
			
		||||
        message: `ID '${id}' is used by ${
 | 
			
		||||
          elements.length
 | 
			
		||||
        } elements: ${elements.map((e) => e.type).join(", ")}.`,
 | 
			
		||||
        element: elements[0],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Lint: Check that files referenced in videoscroll file lists exist
 | 
			
		||||
function lintVideoScrollFileContents(context: LintContext): void {
 | 
			
		||||
  // Find all image elements with descriptions that follow the videoscroll format
 | 
			
		||||
  const imagesWithDesc = context.elements.filter(
 | 
			
		||||
    (el) => el.type === "image" && el.descriptions.length > 0
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // Process only images with valid descriptions (at least 2 lines)
 | 
			
		||||
  imagesWithDesc.forEach((image) => {
 | 
			
		||||
    const desc = image.descriptions[0];
 | 
			
		||||
    const descLines = desc.split("\n").filter((line) => line.trim().length > 0);
 | 
			
		||||
 | 
			
		||||
    // Skip if the description doesn't have at least 2 lines
 | 
			
		||||
    if (descLines.length < 2) return;
 | 
			
		||||
 | 
			
		||||
    // Get the file list path from the second line
 | 
			
		||||
    const fileListPath = descLines[1].replace(/^\//, "");
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      // Resolve the full path to the file list
 | 
			
		||||
      const fullFileListPath = path.resolve(context.fileDir, fileListPath);
 | 
			
		||||
 | 
			
		||||
      // Skip if the file list doesn't exist (already checked in lintVideoScrollPaths)
 | 
			
		||||
      if (!fs.existsSync(fullFileListPath)) return;
 | 
			
		||||
 | 
			
		||||
      // Read the file list contents
 | 
			
		||||
      const fileListContent = fs.readFileSync(fullFileListPath, "utf-8");
 | 
			
		||||
      const referencedFiles = fileListContent
 | 
			
		||||
        .split("\n")
 | 
			
		||||
        .map((line) => line.trim())
 | 
			
		||||
        .filter((line) => line.length > 0);
 | 
			
		||||
 | 
			
		||||
      // Get the directory containing the file list
 | 
			
		||||
      const fileListDir = path.dirname(fullFileListPath);
 | 
			
		||||
 | 
			
		||||
      // Check if each referenced file exists
 | 
			
		||||
      const missingFiles: string[] = [];
 | 
			
		||||
 | 
			
		||||
      referencedFiles.forEach((referencedFile) => {
 | 
			
		||||
        const fullFilePath = path.resolve(fileListDir, referencedFile);
 | 
			
		||||
        if (!fs.existsSync(fullFilePath)) {
 | 
			
		||||
          missingFiles.push(referencedFile);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // If there are missing files, emit a warning
 | 
			
		||||
      if (missingFiles.length > 0) {
 | 
			
		||||
        context.warnings.push({
 | 
			
		||||
          code: "SCROLL_MISSING_FILES",
 | 
			
		||||
          message: `Image (id=${image.properties.id ||
 | 
			
		||||
            "no-id"}) references a file list (${fileListPath}) that contains ${
 | 
			
		||||
            missingFiles.length
 | 
			
		||||
          } missing files: ${missingFiles.join(", ")}.`,
 | 
			
		||||
          element: image,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Error reading or parsing the file list
 | 
			
		||||
      context.warnings.push({
 | 
			
		||||
        code: "SCROLL_FILE_LIST_ERROR",
 | 
			
		||||
        message: `Error reading file list for image (id=${image.properties.id ||
 | 
			
		||||
          "no-id"}): ${error instanceof Error ? error.message : String(error)}`,
 | 
			
		||||
        element: image,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Run all lint checks
 | 
			
		||||
function runAllLints(context: LintContext): void {
 | 
			
		||||
  lintStartRect(context);
 | 
			
		||||
  lintCircleDescriptions(context);
 | 
			
		||||
  lintEllipseDescriptions(context);
 | 
			
		||||
  lintHyperlinkAnchors(context);
 | 
			
		||||
  lintVideoScrollPaths(context);
 | 
			
		||||
  lintIdUniqueness(context);
 | 
			
		||||
  lintVideoScrollFileContents(context);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Output warnings
 | 
			
		||||
function outputWarnings(context: LintContext): void {
 | 
			
		||||
  if (context.warnings.length === 0) {
 | 
			
		||||
    console.log("✅ SVG file passes all lints.");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.log("\n⚠️  Lint Warnings:");
 | 
			
		||||
  console.log("=================\n");
 | 
			
		||||
 | 
			
		||||
  context.warnings.forEach((warning, index) => {
 | 
			
		||||
    console.log(`${index + 1}. [${warning.code}] ${warning.message}`);
 | 
			
		||||
    if (warning.element) {
 | 
			
		||||
      console.log(
 | 
			
		||||
        `   Element: ${warning.element.type}${
 | 
			
		||||
          warning.element.properties.id
 | 
			
		||||
            ? ` (id=${warning.element.properties.id})`
 | 
			
		||||
            : ""
 | 
			
		||||
        }`
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    console.log("");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  console.log(`Total warnings: ${context.warnings.length}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Output inspect details
 | 
			
		||||
function outputInspectDetails(elements: ElementInfo[]): void {
 | 
			
		||||
  console.log("\nElements found in the SVG:");
 | 
			
		||||
  console.log("========================\n");
 | 
			
		||||
 | 
			
		||||
  if (elements.length === 0) {
 | 
			
		||||
    console.log(
 | 
			
		||||
      "No matching elements (image, rect, ellipse, circle) found in the SVG."
 | 
			
		||||
    );
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  elements.forEach((element, index) => {
 | 
			
		||||
    console.log(`${index + 1}. Type: ${element.type}`);
 | 
			
		||||
    console.log("   Properties:");
 | 
			
		||||
    Object.entries(element.properties).forEach(([name, value]) => {
 | 
			
		||||
      if (value.length > 128) {
 | 
			
		||||
        console.log(`     - ${name}: ${value.substring(0, 128)}...`);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log(`     - ${name}: ${value}`);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Output descriptions if any exist
 | 
			
		||||
    if (element.descriptions.length > 0) {
 | 
			
		||||
      console.log("   Descriptions:");
 | 
			
		||||
      element.descriptions.forEach((desc, descIndex) => {
 | 
			
		||||
        if (desc.length > 128) {
 | 
			
		||||
          console.log(`     - ${descIndex + 1}: ${desc.substring(0, 128)}...`);
 | 
			
		||||
        } else {
 | 
			
		||||
          console.log(`     - ${descIndex + 1}: ${desc}`);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log("");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  console.log(`Total elements found: ${elements.length}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Main function to process the SVG file
 | 
			
		||||
 */
 | 
			
		||||
function main() {
 | 
			
		||||
  const options = program.opts();
 | 
			
		||||
  const file = program.args[0];
 | 
			
		||||
  console.log(`Processing SVG file: ${file}`);
 | 
			
		||||
 | 
			
		||||
  const svgDoc = parseSvgFile(file);
 | 
			
		||||
  const fileDir = path.dirname(path.resolve(file));
 | 
			
		||||
 | 
			
		||||
  // Extract elements of interest
 | 
			
		||||
  const elementTypes = ["image", "rect", "ellipse", "circle"];
 | 
			
		||||
  const allElements: ElementInfo[] = [];
 | 
			
		||||
 | 
			
		||||
  elementTypes.forEach((type) => {
 | 
			
		||||
    const elements = extractElements(svgDoc, type);
 | 
			
		||||
    allElements.push(...elements);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Create lint context
 | 
			
		||||
  const context: LintContext = {
 | 
			
		||||
    filePath: file,
 | 
			
		||||
    fileDir: fileDir,
 | 
			
		||||
    svgDoc: svgDoc,
 | 
			
		||||
    elements: allElements,
 | 
			
		||||
    warnings: [],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Run all lint checks
 | 
			
		||||
  runAllLints(context);
 | 
			
		||||
 | 
			
		||||
  // Output inspection details if requested
 | 
			
		||||
  if (options.inspect) {
 | 
			
		||||
    outputInspectDetails(allElements);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Output warnings
 | 
			
		||||
  outputWarnings(context);
 | 
			
		||||
 | 
			
		||||
  // Exit with appropriate code
 | 
			
		||||
  if (context.warnings.length > 0) {
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
  } else {
 | 
			
		||||
    process.exit(0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Execute the main function
 | 
			
		||||
main();
 | 
			
		||||
							
								
								
									
										917
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										917
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
				
			
			@ -17,12 +17,16 @@
 | 
			
		|||
    "vuex": "^4.0.0-0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@commander-js/extra-typings": "^14.0.0",
 | 
			
		||||
    "@types/stats.js": "^0.17.0",
 | 
			
		||||
    "@vue/cli-plugin-babel": "~4.5.0",
 | 
			
		||||
    "@vue/cli-plugin-typescript": "~4.5.0",
 | 
			
		||||
    "@vue/cli-plugin-vuex": "~4.5.0",
 | 
			
		||||
    "@vue/cli-service": "~4.5.0",
 | 
			
		||||
    "@vue/compiler-sfc": "^3.0.0",
 | 
			
		||||
    "@xmldom/xmldom": "^0.9.8",
 | 
			
		||||
    "commander": "^14.0.0",
 | 
			
		||||
    "tsx": "^4.20.3",
 | 
			
		||||
    "typescript": "~3.9.3"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue