PureXML/Sources/Parsing/DocumentType.swift
Mihaela Mihaljevic 7f1aada240 feat(dtd): conditional sections, notations, NDATA, external parameter entities
Deepen DTD processing in the doctype scanner:

- Conditional sections are processed, not skipped: INCLUDE sections have
  their declarations scanned, IGNORE sections are discarded, the keyword
  may be a parameter-entity reference, and nested sections are handled.
- <!NOTATION> declarations are parsed and stored on DocumentType.
- Unparsed entities (<!ENTITY ... NDATA n>) are parsed and stored with
  their notation, distinct from general entities.
- External parameter entities are loaded through the injected resolver so
  their declarations expand; the default refusing resolver loads nothing,
  keeping XXE closed.
- NOTATION attributes are a distinct type, validated against the declared
  notations (value must be listed and the notation declared).

Tracking unparsed entities also lets ENTITY/ENTITIES attribute values be
validated against the declared NDATA entities, closing the gap left when
#73 could only check name syntax.

Closes #72
2026-06-08 14:39:42 +02:00

49 lines
2.5 KiB
Swift

public extension PureXML.Parsing {
/// What the parser extracted from a `<!DOCTYPE>`: the internal general
/// entities, the `<!ELEMENT>` content-model strings, the `<!ATTLIST>` bodies,
/// the parameter entities, any external entity declarations, and the external
/// subset identifier. Empty when no DTD was processed.
struct DocumentType: Equatable, Sendable {
public var entities: [String: String]
public var elementModels: [String: String]
/// Raw `<!ATTLIST>` bodies keyed by element name (the text between the
/// element name and `>`, concatenated across multiple declarations).
public var attributeLists: [String: String]
/// Internal parameter entities (`<!ENTITY % name "value">`) keyed by name,
/// with their values already parameter-expanded. DTD-only; never visible
/// in document content.
public var parameterEntities: [String: String]
/// External general entity declarations keyed by name. Their replacement
/// text is loaded only through an injected ``EntityResolver``.
public var externalEntities: [String: ExternalID]
/// The external subset identifier from `<!DOCTYPE root SYSTEM/PUBLIC ...>`,
/// or nil when the DOCTYPE has no external subset.
public var externalSubset: ExternalID?
/// `<!NOTATION>` declarations keyed by name, with their external identifier.
public var notations: [String: ExternalID]
/// Unparsed general entities (`<!ENTITY name SYSTEM "..." NDATA n>`) keyed by
/// name. Their content is never parsed; the notation names how to handle it.
public var unparsedEntities: [String: UnparsedEntity]
public init(
entities: [String: String] = [:],
elementModels: [String: String] = [:],
attributeLists: [String: String] = [:],
parameterEntities: [String: String] = [:],
externalEntities: [String: ExternalID] = [:],
externalSubset: ExternalID? = nil,
notations: [String: ExternalID] = [:],
unparsedEntities: [String: UnparsedEntity] = [:],
) {
self.entities = entities
self.elementModels = elementModels
self.attributeLists = attributeLists
self.parameterEntities = parameterEntities
self.externalEntities = externalEntities
self.externalSubset = externalSubset
self.notations = notations
self.unparsedEntities = unparsedEntities
}
}
}