Skip to content

Repository files navigation

Carto

A Swift port of Mapbox's archived carto compiler: it compiles CartoCSS stylesheets (.mss) together with a TileMill project file (project.mml) into Mapnik XML, for the swift-mapnik-based tile rendering pipeline.

Table of Contents

Features

  • Faithful port of carto 1.2.2, verified against carto's own rendering corpus (82/82 fixtures)
  • CartoCSS lexer/parser — a faithful port of carto's parser.js, as a chunked recursive descent parser
  • Variable frames and color functions with less.js semantics, including hex, RGB(A) and HSL output
  • HSLuv perceptual color functions (hsluv(), hsluva(), and the *p variants like lightenp, spinp, greyscalep), a port of the hsluv 0.0.2 reference implementation
  • Unit conversion for m, mm, cm, in, pt and pc to pixels at 90.714 ppi (carto's default)
  • mapnik-reference v3.0.22 property tables: validation, defaults, and per-property status
  • Style flattening with specificity sorting, inheritance and per-style zoom bookkeeping (carto's renderer.js)
  • Rule compilation to Mapnik XML with carto's jsonToXML semantics, including filter-mode="first" folding
  • MML loader for JSON/JSON5 project files with stylesheet file resolution (YAML project files via the opt-in EnableYAMLProjectFiles trait)
  • Renderer is a Sendable-friendly value type; compile errors and warnings are returned as Message values
  • A drop-in carto CLI mirroring node carto's command line options

Notes

This package intentionally mirrors node carto 1.2.2's semantics exactly, including the quirky parts: specificity sorting with descending source index, filter-mode="first" folding, the JS string-comparison quirks in Filterset.addable, per-style existing zoom bookkeeping shared across definitions, and JavaScript Number.toString formatting for attribute values.

  • Only Mapnik XML output is produced; -o json output and carto's renderMSS debug API are not implemented.
  • Only mapnik-reference v3.0.22 semantics (node carto's default) are built in; per-version differences are not switchable.
  • project.mml is parsed as JSON/JSON5; YAML project files are supported through the opt-in EnableYAMLProjectFiles package trait (see Package traits below).
  • Millstone resource localization never happens here — datasources pass through verbatim.

Requirements

This package requires Swift 6.3 or higher, and compiles on macOS (>= macOS 15) and Linux. By default it has no external dependencies besides swift-argument-parser (used by the CLI only); enabling the EnableYAMLProjectFiles trait additionally pulls in Yams.

Package traits

  • EnableYAMLProjectFiles — enables YAML (.yaml/.yml) project file parsing in the MML loader. Opt-in (SwiftPM traits can only be turned off from their default state, so YAML support is not on by default). When enabled, Yams is pulled in and MML.init(data:basedir:) falls back to YAML when JSON/JSON5 parsing fails (mirroring carto, which pipes every project file through js-yaml).
// Enable the trait for your target:
.target(
    name: "MyTarget",
    dependencies: [
        .product(name: "Carto", package: "swift-carto"),
    ],
    traits: ["EnableYAMLProjectFiles"]),
swift build --traits EnableYAMLProjectFiles    # build/CLI with YAML support
swift test --traits EnableYAMLProjectFiles     # tests including the YAML fixtures

Installation with Swift Package Manager

dependencies: [
    .package(url: "https://github.com/Outdooractive/swift-carto", from: "1.0.0"),
],
targets: [
    .target(name: "MyTarget", dependencies: [
        .product(name: "Carto", package: "swift-carto"),
    ]),
]

Quick start

import Carto
import Foundation

// Load a TileMill project file; stylesheets are resolved relative to `basedir`
let mml = try MML(
    data: try String(contentsOf: projectURL, encoding: .utf8),
    basedir: projectURL.deletingLastPathComponent())

// Render to Mapnik XML
var renderer = Renderer()
if let xml = renderer.render(mml) {
    print(xml)
}
else {
    for message in renderer.messages where message.kind == .error {
        print(message)
    }
}

Stylesheets can also be embedded directly in the project file as { id: 'style.mss', data: '...' } objects instead of file references, in which case basedir isn't needed.

See the tests for more examples.

The Renderer API

Implementation

Renderer is the central entry point: it loads stylesheets from an MML document, flattens and compiles the style definitions, and emits Mapnik XML:

/// Compile messages (errors/warnings) from the last render.
public private(set) var messages: [Message] = []

/// - Parameter ppi: Pixels per inch for unit conversion; carto's
///   default (and the CLI default) is 90.714.
public init(ppi: Double = 90.714)

/// Render an MML document (loaded stylesheets) to Mapnik XML.
/// Returns `nil` when compilation produced errors (messages carry the
/// details).
public mutating func render(_ mml: MML) -> String?

The render pipeline follows carto's Render.js: every stylesheet is parsed into an AST (variables are shared across stylesheets, carto passes the environment between them), definitions are flattened with specificity sorting and inheritance, validated against the mapnik-reference tables, and finally serialized to Mapnik XML.

Loading MML project files

Implementation

MML represents a TileMill project (project.mml), with its stylesheets, layers and global properties:

public struct MML: Sendable {
    public struct Stylesheet: Sendable {
        public let id: String
        public let data: String
        public init(id: String, data: String)
    }

    public struct Layer: Sendable {
        public struct Datasource: Sendable {
            public var values: [(String, JSONValue)]
            public init(values: [(String, JSONValue)])
        }

        public let id: String
        public var name: String?
        public var classes: [String]
        public var srs: String?
        public var status: String?
        public var geometry: String?
        public var extent: [Double]?
        public var properties: [(String, JSONValue)]?
        public var datasource: Datasource?
        public var extra: [String: JSONValue]
        public init(json: JSONValue) throws
    }

    public var name: String?
    public var description: String?
    public var attribution: String?
    public var bounds: JSONValue?
    public var center: JSONValue?
    public var format: String?
    public var minzoom: Int?
    public var maxzoom: Int?
    public var srs: String?
    public var interactivity: JSONValue?
    public var scale: Double?
    public var metatile: Int?
    public var bufferSize: Int?
    public var stylesheets: [Stylesheet]
    public var layers: [Layer]
    public var parameters: [String: JSONValue]
    public var rawMembers: [(String, JSONValue)]?
    public var globalProperties: [String: [(String, JSONValue)]]

    /// Parse a project file (JSON/JSON5) and resolve its stylesheet
    /// references relative to `basedir`.
    public init(data: String, basedir: URL?) throws
}

MML(data:basedir:) parses the project file, resolves @style.mss references against basedir, and collects carto's global properties (Map, _labels and friends). Unknown members are kept in rawMembers and passed through to the XML emitter.

Layer datasources pass through verbatim — no millstone localization, no downloading of remote resources.

Compile messages

Implementation

Compilation reports errors and warnings as Message values, mirroring carto's messages:

public struct CartoError: Error, Sendable, CustomStringConvertible {
    public let message: String
    public init(_ message: String)
    public var description: String { message }
}

public struct Message: Sendable, Equatable, CustomStringConvertible {
    public enum Kind: String, Sendable {
        case error
        case warning
    }

    public let kind: Kind
    public let message: String
    public let filename: String?
    public let line: Int
}

Example:

var renderer = Renderer(ppi: 96)
if renderer.render(mml) == nil {
    for message in renderer.messages where message.kind == .error {
        print(message.filename ?? "-", message.line, message.message)
    }
}

The carto CLI

Implementation

The CartoCLI target builds a carto executable that is a drop-in replacement for node carto's CLI (ArgumentParser, mirroring node carto's options):

carto project.mml > map.xml
carto -f map.xml -q project.mml   # quiet, write to file
carto --ppi 96 project.mml        # unit conversion ppi (default: 90.714)
carto -b project.mml              # print total compile time
carto --help

Supported flags

  • -q/--quiet — do not output any warnings
  • -b/--benchmark — output the total compile time
  • --ppi — pixels per inch for unit conversion (default: 90.714)
  • -a/--api — Mapnik API version (only 3.0.x semantics are supported)
  • -f/--file — output to the specified file instead of stdout
  • --output mapnik — output format (only mapnik; JSON output is not supported)
  • -l/--localize and -n/--nosymlink — rejected/no-op (millstone is not part of this port)

Package layout

Sources/Carto/
├── MML/              # project.mml loader (JSON5, Stylesheet file resolution)
├── MSS/              # CartoCSS lexer/parser (chunked recursive descent,
│                     #   faithful port of carto's parser.js) + FilterSet
├── Eval/             # variable frames, color functions (less.js semantics),
│                     #   unit conversion (pt/pc/in/mm/cm/m → px @90.714 ppi)
├── Style/            # reference tables (mapnik-reference v3.0.22),
│                     #   flatten/specificity/inheritance (renderer.js),
│                     #   rule compilation (Definition.toObject)
├── XML/              # Mapnik XML emitter (carto's jsonToXML semantics)
└── Render.swift      # public façade: MML → Mapnik XML
Sources/CartoCLI/     # `carto project.mml > map.xml` drop-in CLI
Tests/CartoTests/     # Swift Testing: rendering fixtures + unit tests
tools/                # differential test harnesses against node carto

Behavior notes

The port follows carto 1.2.2's semantics exactly, including the quirky parts (see Notes).

Missing or partial CartoCSS features

The port covers most the CartoCSS surface and was verified against carto's own rendering corpus (82/82). Features that node carto 1.2.2 supports but this port does not (or only partially):

Missing

  • mapnik-reference version selection (--api): only v3.0.22 semantics are built in (node carto's default). Per-version differences (e.g. maxzoom vs minimum-scale-denominator layer attributes, filter keyword lists) are not switchable.
  • -o json output and the Renderer.renderMSS debug API: only Mapnik XML is produced.
  • "Did you mean …?" suggestions: unrecognized rules/functions produce the error without carto's edit-distance suggestion.
  • Status warnings: deprecated/unstable/experimental property statuses are in the reference tables but no warnings are emitted.

Partial

  • Value-type validation: carto's validValue checks the declared type of every property value; this port validates unsigned (rounding), font values, keyword options, filter keywords and required properties, but lets most other value types through unvalidated.
  • colorize-alpha(): passes through as an image-filter call without argument validation (carto validates against the reference).
  • Geometry-transform functions (matrix, translate, scale, rotate, skewX, skewY): serialized verbatim; argument counts are not validated.

Out of scope by design

  • Millstone resource localization (carto's -l/--localize): downloading and localizing remote datasource files never happens here — datasources pass through verbatim.
  • Custom user-supplied references (Renderer({ reference })).

Tests

swift test                                # fixtures + unit tests (no YAML)
swift test --traits EnableYAMLProjectFiles  # include the YAML fixture tests
node tools/run_rendering_corpus.js        # differential test vs node carto
                                          # (requires a local carto checkout)

The rendering fixtures in Tests/CartoTests/Fixtures come from carto's test/rendering corpus (Apache-2.0). The unit tests are ported from carto's own test suites (filterset.test.js, color.test.js, zoom and specificity tests).

The differential test harnesses in tools/ compare this port against node carto directly. They require a local carto checkout with its dependencies installed (git clone https://github.com/mapbox/carto reference/carto && (cd reference/carto && npm install)); the checkout location and other paths can be overridden with the CARTO_REFERENCE, CARTO_BIN and CARTO_PROJECTS environment variables — see the scripts for details.

Acknowledgments

This package is MIT licensed and builds on third-party components with compatible licenses:

Component License How it is used
Component License How it is used
--------- ------- --------------
carto Apache-2.0 The original node.js compiler this package ports; also the source of the rendering test corpus
mapnik-reference Apache-2.0 Property tables (v3.0.22), vendored into Sources/Carto/Style
less.js Apache-2.0 Color function semantics, reimplemented in Swift
hsluv MIT HSLuv perceptual color space (the reference implementation, ported to Swift)
Yams MIT YAML project file parsing (with the EnableYAMLProjectFiles trait)

Related packages

  • swift-stb-image: Swift wrapper around stb_image/stb_image_write and libwebp for reading and writing PNG, JPG and WebP images
  • gis-tools: GIS tools for Swift, including a GeoJSON implementation and many algorithms
  • mvt-tools: Vector tiles reader/writer for Swift
  • swift-mapnik: Mapnik-style tile rendering for Swift, the target of the XML this package produces

Contributing

Please create an issue or open a pull request with a fix or enhancement.

License

MIT

Authors

Thomas Rasch, Outdooractive

About

A Swift port of Mapbox's archived carto tool

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages