commit c6dfbc0e018dc319859f86298278f737823e1d90
parent d64acf0bd3beeba45cd25f02d5ac1ba338e2aaab
Author: Michael Camilleri <[email protected]>
Date: Tue, 12 May 2026 17:04:50 +0900
Render circled cells in converted puzzles
This commit replaces specialCellIndices(body:) with specialCellInfo(body:)
which scans each per-cell <g> in certain JSON input files for both markers; the
same rect-fill check as prior to this commit plus a <circle> child element. The
function returns the affected indices together with the kind ("shaded" or
"circle"), and the metadata writer now emits the matching Special: header.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
2 files changed, 40 insertions(+), 27 deletions(-)
diff --git a/Crossmate/Models/XD.swift b/Crossmate/Models/XD.swift
@@ -4,7 +4,7 @@ import Foundation
/// full specification. Supports just enough of the format to parse our
/// bundled puzzles: metadata, grid (with rebus), and across/down clues.
struct XD: Sendable {
- static let currentCmVersion = 1
+ static let currentCmVersion = 2
let title: String?
let publisher: String?
diff --git a/Crossmate/Services/NYTToXDConverter.swift b/Crossmate/Services/NYTToXDConverter.swift
@@ -91,7 +91,7 @@ enum NYTToXDConverter {
// -- Find special (shaded/circled) cells from the SVG --
- let specialCells = specialCellIndices(body: body)
+ let (specialCells, specialKind) = specialCellInfo(body: body)
// -- Build grid lines --
@@ -203,8 +203,8 @@ enum NYTToXDConverter {
metadata.append("Rebus: \(rebusStr)")
}
- if !specialCells.isEmpty {
- metadata.append("Special: shaded")
+ if !specialCells.isEmpty, let specialKind {
+ metadata.append("Special: \(specialKind)")
}
let relatives = buildRelativeGroups(clues: clues)
@@ -413,20 +413,21 @@ enum NYTToXDConverter {
return "\(label)\(direction == "Across" ? "A" : "D")"
}
- /// Walks the structured SVG tree under `body.SVG` to find cells that are
- /// rendered with a non-default fill (e.g. `lightgray` for shading). The
- /// cells group is a `<g data-group="cells">` whose children are per-cell
- /// `<g>` nodes in `body.cells` order — so the child's array index is the
- /// cell index. Each per-cell group's first child is a `<rect>` whose
- /// `fill` attribute carries the theme marker.
+ /// Walks the structured SVG tree under `body.SVG` to find cells that
+ /// carry a theme marker. The cells group is a `<g data-group="cells">`
+ /// whose children are per-cell `<g>` nodes in `body.cells` order — so the
+ /// child's array index is the cell index. Two markers are recognised:
///
- /// Only `lightgray` (shading) is recognised today. Circle-based themes
- /// mark cells with a `<circle>` element instead and will need their own
- /// branch when a sample puzzle is available.
- private static func specialCellIndices(body: [String: Any]) -> Set<Int> {
+ /// * Shaded cells — the cell's `<rect>` background has `fill="lightgray"`.
+ /// * Circled cells — the cell group contains a `<circle>` element.
+ ///
+ /// The `.xd` format only supports one `Special:` kind per puzzle. If a
+ /// grid mixes both (not seen in the wild yet), circles take precedence
+ /// since they're the more visually distinct marker.
+ private static func specialCellInfo(body: [String: Any]) -> (indices: Set<Int>, kind: String?) {
guard let svg = body["SVG"] as? [String: Any],
let topChildren = svg["children"] as? [[String: Any]] else {
- return []
+ return ([], nil)
}
let cellsGroup = topChildren.first { node in
@@ -442,25 +443,37 @@ enum NYTToXDConverter {
guard let cellsGroup,
let cellGroups = cellsGroup["children"] as? [[String: Any]] else {
- return []
+ return ([], nil)
}
- var indices: Set<Int> = []
+ var shaded: Set<Int> = []
+ var circled: Set<Int> = []
for (index, cellGroup) in cellGroups.enumerated() {
- guard let inner = cellGroup["children"] as? [[String: Any]],
- let rect = inner.first,
- (rect["name"] as? String) == "rect",
- let attrs = rect["attributes"] as? [[String: Any]] else {
+ guard let inner = cellGroup["children"] as? [[String: Any]] else {
continue
}
- let fill = attrs.first {
- ($0["name"] as? String) == "fill"
- }?["value"] as? String
- if fill == "lightgray" {
- indices.insert(index)
+ if let rect = inner.first,
+ (rect["name"] as? String) == "rect",
+ let attrs = rect["attributes"] as? [[String: Any]] {
+ let fill = attrs.first {
+ ($0["name"] as? String) == "fill"
+ }?["value"] as? String
+ if fill == "lightgray" {
+ shaded.insert(index)
+ }
+ }
+ if inner.contains(where: { ($0["name"] as? String) == "circle" }) {
+ circled.insert(index)
}
}
- return indices
+
+ if !circled.isEmpty {
+ return (circled.union(shaded), "circle")
+ }
+ if !shaded.isEmpty {
+ return (shaded, "shaded")
+ }
+ return ([], nil)
}
/// Extracts an Int from a JSON value that may be NSNumber, Int, or Double.