SwiftXML

一个用 Swift 编写的用于处理 XML 的库。

本库基于 Apache License v2.0 许可发布,并附带运行时库例外条款。

let transformation = XTransformation {

    XRule(forElements: "table") { table in
        table.insertNext {
            XElement("caption") {
                "Table: "
                table.children({ $0.name.contains("title") }).content
            }
        }
    }

    XRule(forElements: "tbody", "tfoot") { tablePart in
        for cell in tablePart.children("tr").children("th") {
            cell.name = "td"
       }
    }
    
    XRule(forRegisteredAttributes: "label") { label in
        label.element["label"] = label.value + ")"
    }

}

注意

尽管版本号很高,但本库尚未处于“最终”状态,即可能仍然存在错误,或者会进行一些重大改进,并且可能会发生破坏性更改,而主版本号不会增加。此外,代码中将添加更多注释。而且,当达到最终状态时,该库可能会使用新的存储库 URL 进一步开发(并且版本号将设置回较低的版本)。更多通知将在此处添加。请访问那里获取联系信息。

我们计划在 2025 年初发布最终版本。(届时,该库将已在生产环境中使用。)对于所有已经对该库感兴趣的人,感谢您的耐心等待!

更新 1(2023 年 5 月): 我们最近对 API 做了一些小的更改(不再有公开的 XSpot,但您可以为 XText 设置 isolated),并修复了一些问题,目前正在努力为此库和 SwiftXMLParser 添加更多测试。

更新 2(2023 年 7 月): 为了保持 XML 树的小巧,我们移除了直接访问文档中具有特定名称的属性的功能, 因此也移除了为属性制定规则的功能(属性规则在应用程序中很少使用)。您将需要检查文档的后代(如果在解析期间没有捕获相应的事件),而不是直接访问具有特定名称的属性,也许可以保存结果。当我们添加验证工具时,将提供更简单的替代丢失功能的方案: 当使用适当的模式时,您将能够查找哪些元素——根据模式——可以设置某个属性,然后您可以直接访问这些元素。

更新 3(2023 年 7 月):havingProperties 重命名为 conformingTo

更新 4(2023 年 7 月): 命名空间处理现在处于最终状态,请参阅关于 XML 输入限制的新章节和关于如何处理 XML 命名空间的已更改章节。

更新 5(2023 年 7 月): 为了进一步简化库,移除了跟踪更改(属性的更改)的功能。在大多数情况下,当您必须跟踪更改时,您需要一种更好的设置这些属性的方法,因此在每次设置属性时都会有负担,但用处不大。

更新 6(2023 年 8 月):conformingTo 重命名为 when

更新 7(2023 年 8 月): 为了符合 Swift 5.9 中的一些类型检查,我们必须要求 Apple 平台为 macOS 13、iOS 16、tvOS 16 或 watchOS 9。

更新 8(2023 年 8 月):applying 重命名为 with

更新 9(2023 年 9 月): 再次将 with 重命名为 applying。将 when 重命名为 fullfilling。将 hasProperties 重命名为 fullfills。它们对于单个项目的实现现在通过协议完成。

更新 10(2023 年 10 月): 使用 element(_:) 而不是 element(ofName:),以更好地匹配其他接受名称的方法。

更新 11(2023 年 10 月): 现在使用 XProductionTemplateXActiveProduction 而不是 XProduction,请参阅下面更新的描述。

更新 11(2023 年 10 月): 删除 XProductionTemplateXActiveProduction 实现的 “X” 前缀。

更新 12(2023 年 10 月): XNode.write(toFile:) 重命名为 XNode.write(toPath:)XNode.write(toFileHandle:) 重命名为 XNode.write(toFile:)

更新 13(2023 年 12 月): texts 重命名为 immediateTexts,以免与 allTexts 混淆,text 重命名为 allTextsCollected。添加了 immediateTextsCollectedallTextsReversed 变体。

更新 14(2023 年 12 月): 具有整数值的 XContent、XElement 或 XText 序列的下标表示法现在从 1 开始计数。

更新 15(2023 年 12 月): 移除 immediateTextsCollected

更新 16(2023 年 12 月): 方法 child(...) 重命名为 firstChild(...)

更新 17(2023 年 12 月): 为复杂转换添加了一些跟踪功能。

更新 18(2024 年 1 月): XContentLike 重命名为 XContentConvertible。当使用 SwiftXML 时,新类型可以符合 XContentConvertible,因此可以作为 XML 插入。asContent 属性不再必要并已移除,... as XContentConvertible(以前是 ... as XContentLike)也应该不再必要。

更新 19(2024 年 3 月): descriptionXText 添加引号。

更新 20(2024 年 5 月):allTextsCollected 重命名为 allTextsCombined

更新 21(2024 年 9 月): 创建文档时,您可以指定要(再次)注册的属性名称。移除 extension FileHandle: TextOutputStream

更新 22(2024 年 10 月): 更多 ...IncludingSelf 版本用于迭代,例如 nextElementsIncludingSelf

更新 22(2024 年 10 月): 默认情况下,内部实体解析器现在必须解析呈现给它的所有实体。

更新 23(2024 年 10 月): 添加了 ...Close... 版本,例如 nextCloseElements

更新 24(2024 年 11 月): 使用 hasNexthasPrevious 而不是 hasNextTouchinghasPreviousTouching

更新 25(2024 年 11 月): 使用属性 cloneshallowCLone 而不是方法 clone()shallowCLone()

更新 26(2024 年 11 月): HTML 生成:将 suppressPrettyPrintBeforeLeadingAnchor 重命名为 suppressUncessaryPrettyPrintAtAnchors

更新 27(2024 年 11 月):backLink 重命名为 backlinkfinalBackLink 重命名为 finalBacklink。新方法 setting(backlink:)copyingBacklink(from:)

更新 28(2024 年 12 月): 新增:immediateTextsCombined

更新 29(2025 年 1 月): 新增:backlinkOrSelf


相关软件包

LoopsOnOptionals 软件包

For-in 循环不适用于可选类型,例如 Swift 中的可选链。但是,在使用此 XML 库时,能够这样做有时可能会很方便。为了能够循环遍历可选类型,请包含来自 https://github.com/stefanspringer1/LoopsOnOptionals 的非常小的 LoopsOnOptionals 软件包。

当具有以下 XDocument 的扩展时

extension XDocument {
   var metaDataSection: XElement? { ... }
}

那么使用 LoopsOnOptionals 软件包,您可以编写

for metaDataItem in myDocument.metaDataSection?.children("item") {
    ...
}

当然,尤其是在这种简单的情况下,您可以表达相同的含义,而无需使用 LoopsOnOptionals 软件包

if let metaDataSection = myDocument.metaDataSection {
    for metaDataItem in metaDataSection.children("item") {
        ...
    }
}

但即使在更复杂的情况下,引入这样的 if let(或 case let)表达式也会使代码更难理解。

Workflow 软件包

当在 SwiftWorkflow 框架的上下文中使用 SwiftXML 时,您可以包含 WorkflowUtilitiesForSwiftXML

库的属性

该库从源读取 XML 到 XML 文档实例中,并提供转换(或操作)文档的方法,以及将文档写入文件的方法。

该库应该是高效的,并且使用它的应用程序应该是非常易于理解的。

XML 输入的限制

XML 文档的操作

与其他一些 XML 库不同,对内存中构建的文档的操作是“就地”进行的,即不会构建新的 XML 文档。目标是能够有效地对 XML 文档应用许多隔离的操作。但是,始终可以轻松克隆文档,并带有来自旧版本的引用或指向旧版本的引用。

以下功能很重要

以下代码获取任何 <item>,其整数值 multiply 大于 1,并额外插入一个 multiply 数量少 1 的项目,同时删除现有项目上的 multiply 值(该库将在后续章节中更详细地解释)

let document = try parseXML(fromText: """
<a><item multiply="3"/></a>
""")

for item in document.elements("item") { in
    if let multiply = item["multiply"], let n = Int(multiply), n > 1 {
        item.insertPrevious {
            XElement("item", ["multiply": n > 2 ? String(n-1) : nil])
        }
        item["multiply"] = nil
    }
}

document.echo()

输出为

<a><item/><item/><item/></a>

请注意,在本示例中——只是为了向您展示它可以工作——每个新项目都在当前节点之前插入,但随后仍会被处理。

甚至可以删除迭代返回的元素,而不会停止(惰性!)迭代

let document = try parseXML(fromText: """
<a><item id="1" remove="true"/><item id="2"/><item id="3" remove="true"/><item id="4"/></a>
""")

document.traverse { content in
    if let element = content as? XElement, element["remove"] == "true" {
        element.remove()
    }
}

document.echo()

输出为

<a><item id="2"/><item id="4"/></a>

当然,由于这些迭代是常规序列,因此可以使用所有相应的 Swift 库函数,如 mapfilter。但在许多情况下,最好使用内容迭代器上的条件(请参阅关于使用过滤器查找相关内容的章节)或内容迭代器的链接(请参阅关于链接迭代器的章节)。

库的用户还可以提供要应用的一组规则(请参阅开头的代码和关于规则章节中的完整示例)。在这样的规则中,用户定义如何处理具有特定名称的元素或属性。然后可以将一组规则应用于文档,即规则按照其定义的顺序应用。这将重复进行,保证规则仅对同一对象应用一次(如果未从文档中完全删除并再次添加,请参阅下面关于文档成员资格的章节),直到不再发生应用为止。因此,可以在规则应用期间添加元素,然后在以后由同一规则或另一规则处理。

其他属性

该库使用 SwiftXMLParser 来解析 XML,后者实现了来自 SwiftXMLInterfaces 的相应协议。

根据解析过程的配置,XML 源的所有部分都可以保留在 XML 文档中,包括所有注释和内部子集的部分,例如所有实体或元素定义。(元素定义和属性列表定义,除了它们报告的元素名称外,仅保留为其原始文本表示形式,它们不会被解析为任何其他表示形式。)

在当前的实现中,XML 库不实现任何验证,即针对 DTD 或其他 XML 模式的验证,例如,告诉我们是否可以将具有特定名称的元素包含在具有另一个特定名称的元素中。用户必须使用其他库(例如 Libxml2Validation)在读取或写入文档之前或之后进行此类验证。除了验证 XML 文档的结构外,验证对于了解空白文本的出现是否重要(即是否应保留)也很重要。(例如,表示文本文档段落的元素之间的空白文本通常被认为是不重要的。)为了弥补最后一个问题,库的用户可以提供一个函数来决定是否应保留元素之间的空白文本实例。此外,如果需要,一旦构建了文档树,用户必须设置属性的可能默认值。

此库完全控制如何处理实体。即使未定义,命名实体引用也可以在文档内部持久存在。命名实体引用在解析期间被评分为内部或外部实体引用,外部实体引用是由文档声明内部子集中的外部实体定义引用的那些实体引用。可以根据内部子集和/或由应用程序控制自动替换内部实体引用为文本。

可以配置自动包含外部解析实体的内容,然后内容可能会被具有实体相应信息的元素包装。

带有命名空间前缀的元素或属性被赋予全名“prefix:unprefixed”。请参阅关于处理命名空间的章节,了解动机以及如何处理命名空间。

对于解析期间的任何错误,都会抛出错误,并且不会提供文档。

不得并发检查或更改 XML 树(例如文档)。


注意

以下库的描述可能不包括所有类型和方法。请参阅 DocC 生成的文档或在相应的集成开发环境 (IDE) 中使用自动完成功能。


读取 XML

以下函数接受源并返回 XML 文档实例 (XDocument)。源可以作为 URL、文件路径、文本或二进制数据提供。

从引用本地文件的 URL 读取

func parseXML(
    fromURL: URL,
    registeringAttributes attributeRegisterMode: AttributeRegisterMode = .none,
    sourceInfo: String? = nil,
    textAllowedInElementWithName: ((String) -> Bool)? = nil,
    internalEntityAutoResolve: Bool = false,
    internalEntityResolver: InternalEntityResolver? = nil,
    internalEntityResolverHasToResolve: Bool = true,
    insertExternalParsedEntities: Bool = false,
    externalParsedEntitySystemResolver: ((String) -> URL?)? = nil,
    externalParsedEntityGetter: ((String) -> Data?)? = nil,
    externalWrapperElement: String? = nil,
    keepComments: Bool = false,
    keepCDATASections: Bool = false,
    eventHandlers: [XEventHandler]? = nil
) throws -> XDocument

并相应地

func parseXML(
    fromPath: String,
    ...
) throws -> XDocument
func parseXML(
    fromText: String,
    ...
) throws -> XDocument
func parseXML(
    fromData: Data,
    ...
) throws -> XDocument

如果您想对要处理的源的类型漠不关心,请使用 XDocumentSource 进行源定义并使用

func parseXML(
    from: XDocumentSource,
    ...
) throws -> XDocument

可选的 textAllowedInElementWithName 方法在元素内部找到文本时获取周围元素的名称,并应通知是否允许在特定上下文中出现文本。如果不允许,则文本将被丢弃(如果它是空白)。如果在此上下文中不允许文本,但文本不是空白,则会抛出错误。如果您需要比元素名称更具体的上下文来决定是否允许文本,请使用 XEventHandler 来跟踪更具体的上下文信息。

属性值中的所有内部实体引用都必须在解析期间替换为文本。为了实现这一点(以防内部实体引用出现在源的属性值中),可以提供 InternalEntityResolverInternalEntityResolver 必须实现以下方法

func resolve(
    entityWithName: String,
    forAttributeWithName: String?,
    atElementWithName: String?
) -> String?

当遇到命名实体引用时(在文本或属性中),此方法始终被调用,该实体引用被评分为内部实体。它要么返回实体的文本替换,要么通过返回 nil 来不解析实体。默认情况下,解析器必须解析呈现给它的所有实体,否则会抛出相应的错误。您可以通过在解析函数的调用中设置 internalEntityResolverHasToResolve: false 来移除此强制执行;然后,当解析器返回 nil 时,实体引用不会被文本替换,而是会被保留,而不会有任何进一步的通知。在属性值中的命名实体的情况下,当未给出替换时,始终会抛出错误。如果实体在属性值内部遇到,则函数参数 forAttributeWithName(属性的名称)和 atElementWithName(元素的名称)具有相应的值。

如果 internalEntityAutoResolve 设置为 true,则解析器首先尝试使用文档内部子集中的声明来替换内部实体,然后再调用 InternalEntityResolver

默认情况下,不插入外部解析实体的内容,但如果您将 insertExternalParsedEntities 设置为 true,则会插入。您可以在参数 externalParsedEntitySystemResolver 中提供一个方法,将外部解析实体的系统标识符解析为 URL。您还可以在参数 externalParsedEntityGetter 中提供一个方法来获取系统标识符的数据(如果提供了 externalParsedEntitySystemResolver,则 externalParsedEntitySystemResolver 必须首先返回 nil)。最后,系统标识符仅作为路径组件添加到源 URL(如果存在),并且解析器尝试从那里加载实体。

当插入外部解析实体的内容时,您可以声明一个元素名称 externalWrapperElement:然后插入的内容将被包装到一个具有该名称的元素中,并在属性 namesystemIDpath 中包含有关实体的信息(path 是可选的,因为外部解析实体可能在没有显式路径的情况下被解析)。(在以后的处理中,您可能希望更改此表示形式,例如,如果外部解析实体引用是元素的唯一内容,则您可以用其内容替换包装器,并将相应的信息设置为父元素的一些附件,以便文档验证成功。)

可以向 parseXML 调用提供一个或多个事件处理程序,这些处理程序实现来自 XMLInterfacesXEventHandler。这允许库的用户捕获解析期间的任何事件,例如进入或离开元素。例如,内部实体引用的解析可能取决于文档内部的位置(而不仅仅是元素的名称或属性),因此可以通过这样的事件处理程序收集此信息。

keepComments(默认值:false)决定是否应保留注释(作为 XComment),否则它们将被丢弃,恕不另行通知。keepCDATASections(默认值:false)决定是否应保留 CDATA 部分(作为 XCDATASection),否则所有 CDATA 部分都将解析为文本。

文档的内容

XML 文档 (XDocument) 可以包含以下内容

XLiteral 永远不是解析 XML 的结果,但可能会被应用程序添加。后续的 XLiteral 内容(就像 XText 一样,请参阅关于处理文本的章节)始终会自动合并。

这些内容是 XContent 类型,而更通用的类型 XNode 可能是内容或 XDocument

以下内容从内部子集中读取

可以通过属性 declarationsInInternalSubset 访问它们。

文档从 XML 源获取以下附加属性(某些值可能为 nil

当未在 XML 源中显式设置时,其中一些值将设置为合理的值。

显示 XML

当通过 print(...) 打印内容时,仅打印顶级表示形式,如开始标记,而永远不会打印整个树。当您想打印整个树或文档时,请使用

func echo(pretty: Bool, indentation: String, terminator: String)

pretty 默认为 false;如果设置为 true,则添加换行符和空格以进行美观打印。indentation 默认为两个空格,terminator 默认为 "\n",即在输出后打印换行符。

具有更多控制

func echo(usingProductionTemplate: XProductionTemplate, terminator: String)

将在下一节中解释生成。

当您想要将整个树或文档序列化为文本 (String) 时,请使用以下方法

func serialized(pretty: Bool) -> String

pretty 再次默认为 false 并具有相同的效果。

具有更多控制

func serialized(usingProductionTemplate: XProductionTemplate) -> String

不要使用 serialized 打印树或文档,而应使用 echo,因为在这种情况下使用 echo 更有效。

写入 XML

可以写入任何 XML 节点(包括 XML 文档),包括由它开始的节点树,通过以下方法。

func write(toURL: URL, usingProductionTemplate: XProductionTemplate) throws
func write(toPath: String, usingProductionTemplate: XProductionTemplate) throws
func write(toFile: FileHandle, usingProductionTemplate: XProductionTemplate) throws
func write(toWriter: Writer, usingProductionTemplate: XProductionTemplate) throws

您还可以使用 WriteTarget 协议来允许上述所有可能性

func write(to writeTarget: WriteTarget, usingProductionTemplate: XProductionTemplate) throws

通过参数 usingProductionTemplate:,您可以定义一个生成,即序列化的详细信息,例如是否插入换行符以使结果看起来美观。其值默认为 XActiveProductionTemplate 的实例,这将给出标准输出。

这种生成的定义分为两个部分,一个模板,可以使用值初始化该模板,以进一步配置序列化,以及一个活动生成,该活动生成要应用于特定目标。这样,用户就能够完全定义序列化的外观,然后将此定义应用于一个或多个序列化。更详细地说

XProductionTemplate 具有方法 activeProduction(for writer: Writer) -> XActiveProduction,它通过使用 writer 初始化 XActiveProduction,其中相应的事件触发写入 writer。此类生成的配置应通过 XProductionTemplate 的初始化器的参数提供。

因此,XActiveProduction 定义了如何写入文档的每个部分,例如,是否将 >" 按字面写入,还是作为文本部分中预定义的 XML 实体写入。上述函数调用中的生成默认为 DefaultProductionTemplate 的实例,这将导致 ActiveDefaultProduction 的实例。如果仅要更改文档写入方式的某些细节,则应扩展 ActiveDefaultProduction。生成 ActivePrettyPrintProduction(可以通过定义 PrettyPrintProductionTemplate 来使用)和 ActiveHTMLProduction(可以通过定义 HTMLProductionTemplate 来使用)已经扩展了 ActiveDefaultProduction,可以用于美观打印 XML 或输出 HTML。(请注意,HTMLProductionTemplate 可以给定 NamespaceReference 以考虑 HTML 元素的可能命名空间前缀。)但是您也可以自己扩展这些类之一,例如,您可以覆盖 func writeText(text: XText)func writeAttributeValue(name: String, value: String, element: XElement) 以再次将某些字符写为命名实体引用。或者,您只需提供 DefaultProduction 本身的实例,并更改其 linebreak 属性以定义应如何写入换行符(例如 Unix 或 Windows 样式)。您可能还想考虑 func sortAttributeNames(attributeNames: [String], element: XElement) -> [String] 以对输出的属性进行排序。

示例:在所有元素之前写入换行符

class MyProduction: DefaultProduction {

    override func writeElementStartBeforeAttributes(element: XElement) throws {
        try write(linebreak)
        try super.writeElementStartBeforeAttributes(element: element)
    }

}

try document.write(toFile: "myFile.xml", usingProduction: MyProduction())

为了通用性,提供了以下方法,将任何 XActiveProduction 应用于节点及其包含的树

func applyProduction(activeProduction: XActiveProduction) throws

克隆和文档版本

可以使用以下方法克隆任何节点(包括 XML 文档),包括以它为根的节点树

var clone: XNode

(如果主题已知更具体,则结果将更具体。)

任何内容和文档本身都拥有属性 backlink,可以用作克隆和原始节点之间的关系。如果您使用 clone 属性创建克隆,则克隆中节点的 backlink 值指向原始节点。因此,当使用克隆时,您可以轻松查看原始节点。

(回溯链接也可以通过方法 setting(backlink:)copyingBacklink(from:) 手动设置,这在转换中可能会派上用场。)

请注意,backlink 引用弱引用原始节点,即如果您不保存对原始节点或树的引用,则原始节点将消失,并且 backlink 属性将为 nil

如果您想使用克隆仅将文档的版本保存到副本,请使用其以下方法

func makeVersion()

在这种情况下,将创建文档的克隆,但原始节点的 backlink 属性指向克隆,并且克隆的 backlink 属性将指向原始节点的旧 backlink 值。即,如果您多次应用 saveVersion(),则在从原始文档中的节点开始跟踪 backlink 值时,您将遍历此节点的所有版本,从较新的版本到较旧的版本。backlinks 属性为您提供正是回溯链接链。与使用 clone 不同,文档将记住对此文档版本的强引用,因此将保留克隆的节点。在文档上使用 forgetVersions(keeping:Int) 以停止此记忆,仅保留由参数 keeping 定义的最后版本数(keeping 默认为 0)。在仍然记住的最旧版本中,或者如果没有留下记住的版本,则在文档本身中,所有 backlink 值都将设置为 nil

finalBacklink 属性跟踪整个 backlink 值链,并为您提供此链中的最后一个值。

有时,只需要“浅”克隆,即节点本身,而无需以该节点为根的整个节点树。在这种情况下,只需使用

func shallowClone(forwardref: Bool) -> XNode

然后,backlink 的设置方式与使用 clone 时相同。

属性 backlinkOrSelf 提供回溯链接,或者——如果它是 nil——则提供主题本身。

内容属性

源范围

如果解析器(正如 SwiftXMLParser 的情况一样)报告了文档的哪个部分在文本中(即它在第几行和第几列开始,在第几行和第几列结束),则属性 sourceRange: XTextRange(使用来自 SwiftXMLInterfacesXTextRange)为相应的节点返回它

示例

let document = try parseXML(fromText: """
<a>
    <b>Hello</b>
</a>
""", textAllowedInElementWithName: { $0 == "b" })

for content in document.allContent {
    if let sourceRange = content.sourceRange {
        print("\(sourceRange): \(content)")
    }
    else {
        content.echo()
    }
}

输出

1:1 - 3:4: <a>
2:5 - 2:16: <b>
2:8 - 2:12: Hello

元素名称

可以使用元素的属性 name 读取和设置元素名称。在设置与现有名称不同的新名称后,如果元素是文档的一部分,则该元素将在文档中以新名称注册。设置相同的名称不会更改任何内容(这是一种有效的非更改)。

文本

对于文本内容 (XText),可以通过其 value 属性读取和设置文本。因此,无需通过替换另一个 XText 内容来更改文本。另请参阅下面关于处理文本的部分。

更改和读取属性

元素的属性可以通过“索引符号”读取和设置。如果未设置属性,则返回 nil;反之,将属性设置为 nil 会将其删除。如果元素是文档的一部分,则设置具有新名称的属性或删除属性会更改文档中属性的注册。将已存在属性的非 nil 值设置为属性,对于属性的注册来说,是一种高效的非更改操作。

示例

// setting the "id" attribute to "1":
myElement["id"] = "1"

// reading an attribute:
if let id = myElement["id"] {
    print("the ID is \(id)")
}

您还可以从元素序列中获取属性值(可选字符串)的序列。

示例

let document = try parseXML(fromText: """
    <test>
      <b id="1"/>
      <b id="2"/>
      <b id="3"/>
    </test>
    """)
print(document.children.children["id"].joined(separator: ", "))

结果

1, 2, 3

如果您想获取属性值并同时删除该属性,请使用元素的 pullAttribute(...) 方法。

要获取元素的所有属性名称,请使用

var attributeNames: [String]

请注意,您还可以通过使用相同的索引符号来获取(惰性)元素序列的特定属性名称的属性值(惰性)序列

print(myElement.children("myChildName")["myAttributeName"].joined(separator: ", "))

附件

所有节点都可以拥有“附件”。这些是通过文本键附加的对象。这些附件不被视为属于正式的 XML 树。

这些附件以字典 attached 的形式实现,作为每个节点的成员。

您还可以在创建元素或文档时立即设置附件,方法是使用初始化器的 attached: 参数。(请注意,在此参数中,为了方便起见,某些值可能是 nil。)

XPath

通过以下方式获取节点的 XPath

var xPath: String

遍历

从节点(包括文档)开始的深度优先遍历可以通过以下方法完成

func traverse(down: (XNode) throws -> (), up: ((XNode) throws -> ())? = nil) rethrows
func traverse(down: (XNode) async throws -> (), up: ((XNode) async throws -> ())? = nil) async rethrows

对于“分支”,即可能包含其他节点的节点(例如元素,与不包含其他节点的文本相对),当从其内容的遍历返回时(也包括空分支的情况),将调用给定可选 up: 参数的闭包。

示例

document.traverse { node in
    if let element = node as? XElement {
        print("entering element \(element.name)")
    }
}
up: { node in
    if let element = node as? XElement {
        print("leaving element \(element.name)")
    }
}

请注意,在遍历期间不得删除遍历的根节点。

直接访问元素

如前所述和一般描述,该库允许在文档中高效查找具有特定名称的元素,而无需遍历整个树。

查找具有特定名称的元素

func elements(_: String) -> XElementsOfSameNameSequence

示例

for paragraph in myDocument.elements("paragraph") {
    if let id = paragraph["id"] {
        print("found paragraph with ID \"\(ID)\"")
    }
}

通过在 elements(_:) 中使用多个名称来查找具有多个名称备选项的元素。请注意,与单个名称的方法一样,您在迭代期间添加的内容也将被考虑在内。

直接访问属性

要直接查找在何处设置了具有特定名称的属性,您可以使用类似于直接访问元素的方法,但出于效率原因,您必须指定可用于此类直接访问的属性名称。您可以在创建文档时指定这些属性名称(例如 XDocument(registeringAttributes: .selected(["id", "label"])))或在使用解析函数时间接指定(例如 try parseXML(fromText: "...", registeringAttributes: .selected(["id", "label"])))。

示例

let document = try parseXML(fromText: """
    <test>
      <x a="1"/>
      <x b="2"/>
      <x c="3"/>
      <x d="4"/>
    </test>
    """, registeringAttributes: .selected(["a", "c"]))

let registeredValuesInfo = document.registeredAttributes("a", "b", "c", "d").map{ "\($0.name)=\"\($0.value)\" in \($0.element)" }.joined(separator: ", ")
print(registeredValuesInfo) // "a="1" in <x a="1">, c="3" in <x c="3">"

let allValuesInfo = document.elements("x").compactMap{
    if let name = $0.attributeNames.first, let value = $0[name] { "\(name)=\"\(value)\" in \($0)" } else { nil }
}.joined(separator: ", ")
print(allValuesInfo) // "a="1" in <x a="1">, b="2" in <x b="2">, c="3" in <x c="3">, d="4" in <x d="4">"

查找相关内容

从某些内容开始,您可能想要查找相关内容,例如其子项。为相应方法选择的名称来自以下想法:所有内容都具有自然的顺序,即深度优先遍历的顺序,这与 XML 文档的内容存储在文本文件中的顺序相同。此顺序为方法名称(例如 nextTouching)赋予了意义。请注意,与您通过 elements(_:) 获取的迭代不同,即使停留在同一文档中的节点,如果在迭代期间相应地移动,也可能在这样的迭代中多次出现。

返回的序列始终是惰性序列,迭代它们会给出显而易见的类型的项目。正如库的一般描述中所提到的,允许在这样的迭代期间操作 XML 树。

查找节点所在的文档

var document: XDocument?

查找父元素

var parent: XElement?

其所有祖先元素

var ancestors: XElementSequence

获取分支的第一个内容

var firstContent: XContent?

获取分支的最后一个内容

var lastContent: XContent?

如果只包含一个节点,则获取它,否则获取 nil

var singleContent: XContent?

文档或元素的直接内容(“直接”表示它们的父级是此文档或元素)

var content: XContentSequence

作为元素的直接内容,即所有子元素

var children: XElementSequence

作为文本的直接内容

var immediateTexts: XTextSequence

对于 contentchildren 序列,还存在序列 contentReversedchildrenReversedimmediateTextsReversed,它们从最后一个对应的项目迭代到第一个。

节点本身开始的节点树中的所有内容,不包括节点本身,按照深度优先遍历的顺序

var allContent: XContentSequence

节点开始的节点树中的所有内容,包括节点本身

var allContentIncludingSelf: XContentSequence

树中的所有文本

var allTexts: XTextSequence

后代,即节点本身开始的节点树中的所有内容,不包括节点本身,并且是元素

var descendants: XElementSequence

如果节点是一个元素,则元素本身和后代,从元素本身开始

var descendantsIncludingSelf: XElementSequence

节点本身开始的节点树中的所有文本,不包括节点本身,按照深度优先遍历的顺序

var allTexts: XTextSequence

相同,但仅适用于作为直接内容包含的节点

var immediateTexts: XTextSequence

分支(元素或文档)的(直接)内容彼此是“兄弟姐妹”。

主题之前的内容项

var previousTouching: XContent?

主题之后的内容项

var nextTouching: XContent?

(请注意,为了自动完成,最好输入“touch...”而不是“prev...”或“next...”。)

您可能只想知道是否存在上一个或下一个节点

var hasPrevious: Bool
var hasNext: Bool

以下非常短的方法名称 previousnext 实际上意味着“上一个内容”和“下一个内容”。之所以选择如此短的方法名称,是因为它们是非常常见的用例。

节点之前的所有节点(即上一个兄弟节点),在同一级别上,即具有相同的父节点,从节点开始排序

var previous: XContentSequence

其中,作为元素的节点

var previousElements: XElementSequence

类似地,节点之后的内容

var next: XContentSequence

其中,作为元素的节点

var nextElements: XElementSequence

nextElementspreviousElements 跳过任何非元素。如果您想查找中间没有非元素的元素,请使用 nextCloseElementspreviousCloseElements


注意

请记住,没有 ...Close... 的版本只是忽略中间的所有其他节点类型。


还有 ...IncludingSelf 版本,其中包含主题本身,例如 nextElementsIncludingSelf

示例

for descendant in myElement.descendants {
    print("the name of the descendant is \(descendant.name)")
}

请注意,一个序列可能会被多次使用

let document = try parseXML(fromText: """
<a><c/><d/><e/></a>
""")

let insideA = document.children.children

insideA.echo()
print("again:")
insideA.echo()

输出

<c/>
<d/>
<e/>
again:
<c/>
<d/>
<e/>

一旦您有了这样的序列,您就可以通过其 first 属性获取序列中的第一个项目(这是此包除了已定义的 first(where:) 之外引入的)。

可以使用序列的常用方法。例如,使用 mySequence.dropFirst(n) 删除序列 mySequence 的前 n 个项目。例如,要获取序列的第三个项目,请使用 mySequence.dropFirst(2).first

请注意,没有属性可以获取这些序列的最后一个项目,因为这将非常低效。最好将 contentReversedchildrenReversedfirst 结合使用。

使用 exist 测试序列中是否存在某些内容

var exist: Bool

请注意,在使用 exist 后,您仍然可以沿着同一序列正常迭代,而不会丢失任何项目。

使用 absent 测试序列中是否不存在任何内容

var absent: Bool

如果您想测试某些项目是否存在,并且在许多情况下您也会使用这些项目。内容或元素序列的属性 existing 在项目存在时返回序列本身,否则返回 nil

var existing: XContentSequence?
var existing: XElementSequence?

在以下示例中,首先测试序列中是否存在项目,如果存在项目,则使用该序列

let document = try parseXML(fromText: """
<a><c/><b id="1"/><b id="2"/><d/><b id="3"/></a>
""")

if let theBs = document.descendants("b").existing {
    theBs.echo()
}

请注意,您通过使用 existing 获取的仍然是惰性序列,即,如果您在 existing 测试和使用其结果之间更改内容,则可能不再有项目可以找到。

您还可以按深度优先遍历的顺序,询问树中上一个或下一个内容项。例如,如果一个节点是以某个元素为根的子树的最后一个节点,并且该元素有下一个兄弟节点,则该下一个兄弟节点是该子树的最后一个节点的“树中的下一个节点”。获取树中的下一个或上一个节点非常高效,因为库无论如何都会跟踪它们。

树中的下一个内容项

var nextInTreeTouching: XContent?

树中的上一个内容项

var previousInTreeTouching: XContent?

查找节点树中包含的所有文本,并将它们组合成一个 String

var allTextsCombined: String

即使您知道只有一个文本要“组合”,也可以使用这些文本收集属性,这种情况已得到高效实现。

您还可以使用以下方法将单个内容项,或更具体地说,将元素转换为适当的序列

对于任何内容

var asSequence: XContentSequence

对于元素

var asElementSequence: XElementSequence

(这两种方法在库的测试中使用。)

使用过滤器查找相关节点

上一节中返回序列的所有方法也允许将条件作为第一个参数进行过滤。我们区分以下情况:序列的所有项目都满足条件,当条件满足时所有项目,以及直到条件满足时所有项目(不包括条件满足时找到的项目)

func content((XContent) -> Bool) -> XContentSequence
func content(while: (XContent) -> Bool) -> XContentSequence
func content(until: (XContent) -> Bool) -> XContentSequence
func content(untilAndIncluding: (XContent) -> Bool) -> XContentSequence

untilAndIncluding 版本也在条件满足时停止,但包括相应的项目。

在合理的情况下,会返回更具体类型的序列。

示例

let document = try parseXML(fromText: """
<a><b/><c take="true"/><d/><e take="true"/></a>
""")

for descendant in document.descendants({ element in element["take"] == "true" }) {
    print(descendant)
}

输出

<c take="true">
<e take="true">

请注意,示例中条件周围的圆括号“(...)”是必需的,以便将其与 while:until: 版本区分开来。(没有 where: 参数名称,因为没有它,不太常见的 while: – 以及在较小程度上 until: – 更容易在视觉上与它区分开来,更常见的情况在语法上是最短的。这在实际代码中效果很好。)

还存在根据名称过滤元素的常用快捷方式

for _ in document.descendants("paragraph") {
    print("found a paragraph!")"
}

您还可以使用多个名称(例如 descendants("paragraph", "table"))。如果未给出名称,则结果中将给出所有元素,而与名称无关,例如 children()children 含义相同。


节点

请注意,nextElements("paragraph")(按名称过滤下一个元素)与 nextElements(while: { $0.name == "paragraph" }) 不同。


如果您知道最多只有一个具有特定名称的子元素,请使用以下方法(如果存在,它将返回具有此名称的第一个子元素)

func firstChild(_ name: String) -> XElement?

然后您还可以考虑备用名称(为您提供名称匹配的第一个子元素)

func firstChild(_ names: String...) -> XElement?

如果您想获取具有特定名称的第一个祖先,请使用以下方法之一

func ancestor(_ name: String) -> XElement?
func ancestor(_ names: String...) -> XElement?

链式迭代器

迭代器也可以链接在一起。第二个迭代器在第一个迭代器遇到的每个节点上执行。所有这些迭代都是惰性的,因此只有当第二个迭代器完成第一个迭代器找到的当前节点时,第一个迭代器才会搜索下一个节点。

示例

let document = try parseXML(fromText: """
<a>
    <b>
        <c>
            <d/>
        </c>
    </b>
</a>
""")

for element in document.descendants.descendants { print(element) }

输出

<b>
<c>
<d>
<c>
<d>
<d>

此外,在应用于单个节点(如 parent)的链式操作中,查找单个节点的操作也有效,您可以使用例如 insertNext(请参阅关于树操作的部分)或 with(请参阅下一节关于构造 XML 的部分)或 echo()

当使用带有 String 的索引时,您将获得相应属性值(设置的位置)的序列

for childID in element.children["id"] {
    print("found child ID \(childID)")
}

请注意,当将 Int 用作内容序列的下标值时,您将获得相应索引的子项

if let secondCHild = element.children[2] {
    print("second child: \(secondChild)")
}

注意

如果您对 XContent、XElement 或 XText 序列使用此下标表示法 [n],那么 – 尽管使用了整数值 – 这不是 (!) 对元素的随机访问(每次使用这样的下标时,都会跟踪序列,直到通过计数找到相应的项目),并且计数从 1 开始,就像在 XPath 语言中一样,而不是像例如 Swift 数组那样从 0 开始。

您应该将此整数下标更多地视为带有名称的下标,整数值是位置在 XML 中给出的名称,其中从 1 开始计数是常见的。


构造 XML

构造一个空元素

在构造元素(没有内容)时,名称作为第一个(无名)参数给出,属性值作为(无名)字典给出。

示例:构造一个带有属性 id="1"style="note" 的空“paragraph”元素

let myElement = XElement("paragraph", ["id": "1", "style": "note"])

关于内容插入

在我们详细解释相应的功能之前,我们首先想给出一些重要的提示。

请注意,当将内容插入到元素或文档中,并且该内容已存在于其他位置时,插入的内容是其原始位置移动,而不是复制。如果您想插入副本,请插入使用内容的 clone 属性的结果。

在编写代码时要“大胆”,可能比您想象的更多功能可以工作。例如,预测以下各节中的解释,以下代码示例确实有效

将元素的“a”子元素和“b”子元素移动到元素的开头

element.addFirst {
  element.children(“a”)
  element.children(“b”)
}

由于内容是先构造然后再插入,因此这里没有无限循环。

请注意,在结果中,内容的顺序就像在括号 {...} 内定义的那样,因此在示例中,在生成的 element 内部,首先是“a”子元素,然后是“b”子元素。

用另一个元素包装一个元素

element.replace {
   XElement("wrapper") {
      element
   }
}

您在括号 {...} 内定义的内容是从内向外构造的。从上面的注释中,您可能会认为示例中的 element 在“wrapper”元素的内容被构造时不再在其原始位置,在实际发生替换之前。是的,这是真的,但尽管如此,replace 方法仍然知道在哪里插入这个“wrapper”元素。该操作确实像您从朴素的角度期望的那样工作。

任何符合 XContentConvertible 类型的实例(它必须实现其 collectXML(by:) 方法)都可以作为 XML 插入

struct MyStruct: XContentConvertible {
    
    let text1: String
    let text2: String
    
    func collectXML(by xmlCollector: inout XMLCollector) {
        xmlCollector.collect(XElement("text1") { text1 })
        xmlCollector.collect(XElement("text2") { text2 })
    }
    
}

let myStruct1 = MyStruct(text1: "hello", text2: "world")
let myStruct2 = MyStruct(text1: "greeting", text2: "you")

let element = XElement("x") {
    myStruct1
    myStruct2
}

element.echo(pretty: true)

结果

<x>
  <text1>hello</text1>
  <text2>world</text2>
  <text1>greeting</text1>
  <text2>you</text2>
</x>

对于 XContentConvertible,还有 xml 属性,它返回一个 XContent 的相应数组。

定义内容

在构造元素时,其内容在括号 {...} 中给出(这些括号是初始化器的 builder 参数)。

let myElement = XElement("div") {
    XElement("hr")
    XElement("paragraph") {
        "Hello World"
    }
    XElement("hr")
}

(文本 "Hello World" 也可以作为 XText("Hello World") 给出。文本将在这样的 XML 节点中自动转换。)

内容可以作为数组或适当的序列给出

let myElement = XElement("div") {
    XElement("hr")
    myOtherElement.content
    XElement("hr")
}

在不定义内容时,使用 map 可能是一个明智的选择

let element = XElement("z") {
    XElement("a") {
        XElement("a1")
        XElement("a2")
    }
    XElement("b") {
        XElement("b1")
        XElement("b2")
    }
}

for content in element.children.map({ $0.children.first }) { print(content?.name ?? "-") }

输出

a1
b1

这同样适用于例如 filter 方法,除了让代码看起来更复杂(当用作代替上面描述的过滤器选项时),在定义内容时不是一个好的选择。

在定义内容时,包含其他元素的元素的内容是从内向外构建的:考虑以下示例

let b = XElement("b")

let a = XElement("a") {
    b
    "Hello"
}

a.echo(pretty: true)

print("\n------\n")

b.replace {
    XElement("wrapper1") {
        b
        XElement("wrapper2") {
            b.next
        }
    }
}

a.echo(pretty: true)

首先,构建元素“wrapper2”,此时序列 b.next 包含文本 "Hello"。因此,我们将得到以下输出

<a><b/>Hello</a>

------

<a>
  <wrapper1>
    <b/>
    <wrapper2>Hello</wrapper2>
  </wrapper1>
</a>

构造元素中的文档成员资格

作为文档 (XDocument) 一部分的元素在文档中注册。原因是这允许通过 elements(_:) 分别 attributes(_:) 快速访问特定名称的元素或属性,并且规则(请参阅关于规则的部分)使用这些注册(请注意,出于效率原因,要以这种方式使用的属性名称必须在创建文档时配置)。

在构造新元素时,其内容在构造期间的 {...} 括号中定义,该元素不属于任何文档。插入其中的节点离开文档树,但它们不会 (!) 从文档中注销。即,迭代 elements(_:) 仍会找到它们,并且相应的规则将应用于它们。这种行为的原因是新元素插入到同一文档的常见情况。如果新元素的内容首先从文档中注销,然后再重新插入到同一文档中,则它们将被视为新元素,并且提到的迭代可能会再次迭代它们。

如果您想让新构建元素的内容从文档中注销,请使用其方法 adjustDocument()。此方法将元素的当前文档扩散到其内容。对于新构建的元素,此文档是 nil,这会将节点从其文档中注销。您还可以在元素的初始化器中将属性 adjustDocument 设置为 true,以便在新元素的构建完成时自动调用 adjustDocument()。此调用或设置为调整文档仅在顶层元素上是必要的,它会分散到整个树中。

请注意,如果您将元素插入到作为文档一部分的另一个文档中,则如果新子元素尚未在其新父元素的文档中注册,则会注册在该文档中(并从之前注册的任何不同文档中注销)。

示例:将新构造的元素添加到文档

let document = try parseXML(fromText: """
<a><b id="1"/><b id="2"/></a>
""")

for element in document.elements("b") {
    print("applying the rule to \(element)")
    if element["id"] == "2" {
        element.insertNext {
            XElement("c") {
                element.previous
            }
        }
    }
}

print("\n-----------------\n")

document.echo()

输出

applying the rule to <b id="1">
applying the rule to <b id="2">

-----------------

<a><b id="2"/><c><b id="1"/></c></a>

正如您可以从最后一个示例中的 print 命令中看到的那样,元素 <b id="1"> 不会失去其与文档的“连接”(尽管它似乎又被添加到文档中),因此它在迭代中只被迭代一次。

树操作

除了更改节点属性外,还可以通过以下方法更改 XML 树。其中一些方法返回主题本身作为可丢弃的结果。对于在 {...}(构建器)中指定的内容,顺序会被保留。

分别在元素或文档的内容末尾添加节点

func add(builder: () -> [XContent])

分别在元素或文档的内容开头添加节点

func addFirst(builder: () -> [XContent])

将节点添加为节点之前的节点

func insertPrevious(_ insertionMode: InsertionMode = .following, builder: () -> [XContent])

将节点添加为节点之后的节点

func insertNext(_ insertionMode: InsertionMode = .following, builder: () -> [XContent])

如果主题的类型更精确地已知,则从 insertPreviousinsertNext 返回更精确的类型。

通过使用接下来的两种方法,节点将被删除。

从树结构和文档中删除节点

func remove()

您还可以使用节点的 removed() 方法来删除节点,但也使用该节点。

用其他节点替换节点

func replace(_ insertionMode: InsertionMode = .following, builder: () -> [XContent])

请注意,替换节点的内容允许包含节点本身。

分别清除元素或文档的内容

func clear()

测试元素或文档是否为空

var isEmpty: Bool

分别设置元素或文档的内容

func setContent(builder: () -> [XContent])

示例

for table in myDocument.elements("table") {
    table.insertNext {
        XElement("legend") {
            "this is the table legend"
        }
        XElement("caption") {
            "this is the table caption"
        }
    }
}

请注意,默认情况下,迭代会继续,并且新插入的节点(由 insertPreviousinsertNext 插入)也会被考虑在内。在以下情况下,您必须添加 .skipping 指令才能获得如下所示的输出(在第二种情况下,如果您不设置 .skipping,甚至会得到无限循环)

let element = XElement("top") {
    XElement("a1") {
        XElement("a2")
    }
    XElement("b1") {
        XElement("b2")
    }
    XElement("c1") {
        XElement("c2")
    }
}

element.echo(pretty: true)

print("\n---- 1 ----\n")

for content in element.content {
    content.replace(.skipping) {
        content.content
    }
}

element.echo(pretty: true)

print("\n---- 2 ----\n")

for content in element.contentReversed {
    content.insertPrevious(.skipping) {
        XElement("I" + ((content as? XElement)?.name ?? "?"))
    }
}

element.echo(pretty: true)

输出

<top>
  <a1>
    <a2/>
  </a1>
  <b1>
    <b2/>
  </b1>
  <c1>
    <c2/>
  </c1>
</top>

---- 1 ----

<top>
  <a2/>
  <b2/>
  <c2/>
</top>

---- 2 ----

<top>
  <Ia2/>
  <a2/>
  <Ib2/>
  <b2/>
  <Ic2/>
  <c2/>
</top>

请注意,当不使用 insertPreviousinsertNextreplace(例如,当使用 add 时)时,没有这种跳过插入内容的机制。考虑组合 descendants.add:那么就没有“自然”的方法来纠正树的遍历。(更常见的用例可能是类似 descendants("table").add { XElement("caption") } 的情况,因此这在常见情况下不应该是一个问题,但您应该意识到这一点。)

当在链式迭代器中使用 insertNextreplace 等时,会发生的情况是,括号 {...} 中内容的定义会为序列中的每个项目执行。您可能应该使用 collect 函数来专门为当前项目构建内容。例如,在最后一个示例中,您可以使用相同的结果

print("\n---- 1 ----\n")

element.content.replace { content in
    collect {
        content.content
    }
}

element.echo(pretty: true)

print("\n---- 2 ----\n")

element.contentReversed.insertPrevious { content in
    find {
        XElement("I" + ((content as? XElement)?.name ?? "?"))
    }
}

element.echo(pretty: true)

您也可以不使用 collect

let e = XElement("a") {
    XElement("b")
    XElement("c")
}

for descendant in e.descendants({ $0.name != "added" }) {
    descendant.add { XElement("added") }
}

e.echo(pretty: true)

输出

<a>
  <b>
    <added/>
  </b>
  <c>
    <added/>
  </c>
</a>

请注意,每次都会创建一个新的 <added/>。从已经说过的内容来看,应该清楚的是,这种“重复”不适用于现有内容(除非您使用 cloneshallowClone

let myElement = XElement("a") {
    XElement("to-add")
    XElement("b")
    XElement("c")
}

for descendant in myElement.descendants({ $0.name != "to-add" }) {
    descendant.add {
        myElement.descendants("to-add")
    }
}

myElement.echo(pretty: true)

输出

<a>
  <b/>
  <c>
    <to-add/>
  </c>
</a>

作为一般规则,当插入内容时,如果该内容已经是另一个元素或文档的一部分,则该内容不会被复制,而是从其原始位置删除。

当您确实希望内容被复制时,请使用 clone(或 shallowClone),例如,在最后一个示例中使用 myElement.descendants("to-add").clone 将输出

<a>
  <to-add/>
  <b>
    <to-add/>
  </b>
  <c>
    <to-add/>
    <to-add/>
  </c>
</a>

默认情况下,当您插入内容时,此新内容也会被跟踪(插入模式 .following),因为这最能反映此库的动态特性。如果您不希望这样做,请将 .skipping 设置为 insertPreviousinsertNext 的第一个参数。例如,考虑以下代码

let myElement = XElement("top") {
    XElement("a")
}

for element in myElement.descendants {
    if element.name == "a" {
        element.insertNext() {
            XElement("b")
        }
    }
    else if element.name == "b" {
        element.insertNext {
            XElement("c")
        }
    }
}

myElement.echo(pretty: true)

输出

<top>
  <a/>
  <b/>
  <c/>
</top>

当插入 <b/> 时,遍历也会跟踪此插入的内容。当您想跳过插入的内容时,请使用 .skipping 作为 insertNext 的第一个参数

    ...
        element.insertNext(.skipping) {
            XElement("b")
        }
    ...

输出

<top>
  <a/>
  <b/>
</top>

类似地,如果您替换一个节点,则默认情况下,插入到节点位置的内容会包含在迭代中。示例:假设您想用其内容替换每个出现的 <bold> 元素

let document = try parseXML(fromText: """
    <text><bold><bold>Hello</bold></bold></text>
    """)
for bold in document.descendants("bold") { bold.replace { bold.content } }
document.echo()

输出为

<text>Hello</text>

文本处理

后续的文本节点 (XText) 始终会自动合并,并且具有空文本的文本节点会自动删除。相同的处理也适用于 XLiteral 节点。

当处理文本时,这可能非常方便,例如,然后可以直接将正则表达式应用于文档中的文本。但是,当文本节点和其他节点的不同行为影响您的操作结果时,可能会有一些绊脚石。

您可以通过将 isolated 属性设置为 true 来避免将文本 text 与其他文本合并(您也可以选择在 XText 的初始化期间设置此值)。考虑以下示例,其中搜索文本的出现会获得绿色的背景。在此示例中,您不希望在迭代中将 part 添加到 text

let searchText = "world"

document.traverse { node in
    if let text = node as? XText {
        if text.value.contains(searchText) {
            text.isolated = true
            var addSearchText = false
            for part in text.value.components(separatedBy: searchText) {
                text.insertPrevious {
                    addSearchText ? XElement("span", ["style": "background:LightGreen"]) {
                        searchText
                    } : nil
                    part
                }
                addSearchText = true
            }
            text.remove()
            text.isolated = false
        }
    }
}

document.echo()

输出

<a>Hello <span style="background:LightGreen">world</span>, the <span style="background:LightGreen">world</span> is nice.</a>

请注意,例如在插入节点时,它们的 XText 节点在移动时被视为 isolated

在需要 XText 的地方可以使用 String,例如,您可以编写 "Hello" as XText"

XText 以及 XLiteralXCDATASection 都符合 XTextualContentRepresentation 协议,即它们都具有名为 valueString 属性,该属性可以读取和设置,并且表示将写入序列化文档的内容(在编写 XText 时需要一些字符转义)。请注意,XComment 不符合 XTextualContentRepresentation 协议。

规则

当您只想对文档应用一些更改时,只需直接转到一些相应的元素并应用您想要的更改即可。但是,如果您想将整个文档转换为“其他内容”,则需要更好的工具来组织您对文档的操作,您需要“转换”。

正如一般描述中提到的,规则集 XRuleXTransformation 类型的转换实例的形式可以使用,如下所示。

在规则中,用户定义如何处理具有特定名称的元素或属性。然后可以将规则集应用于文档,即规则按照其定义顺序应用。这将重复进行,保证规则仅对同一对象应用一次(如果未从文档中删除并再次添加),直到没有应用发生为止。因此,可以在规则应用期间添加元素,然后稍后由相同或另一个规则处理。

示例

let document = try parseXML(fromText: """
<a><formula id="1"/></a>
""")

var count = 1

let transformation = XTransformation {

    XRule(forElements: "formula") { element in
        print("\n----- Rule for element \"formula\" -----\n")
        print("  \(element)")
        if count == 1 {
            count += 1
            print("  add image")
            element.insertPrevious {
                XElement("image", ["id": "\(count)"])
            }

        }
    }

    XRule(forElements: "image") { element in
        print("\n----- Rule for element \"image\" -----\n")
        print("  \(element)")
        if count == 2 {
            count += 1
            print("  add formula")
            element.insertPrevious {
                XElement("formula", ["id": "\(count)"])
            }
        }
    }

}

transformation.execute(inDocument: document)

print("\n----------------------------------------\n")

document.echo()

----- Rule for element "formula" -----

  <formula id="1">
  add image

----- Rule for element "image" -----

  <image id="2">
  add formula

----- Rule for element "formula" -----

  <formula id="3">

----------------------------------------

<a><formula id="3"/><image id="2"/><formula id="1"/></a>

作为旁注,对于这样的 XTransformation,元素名称的长度实际上并不重要:除了执行之前转换的初始化以及规则内部发生的事情之外,如果元素名称更长,规则的应用效率也不会降低。

您应该使用多个转换,每个转换都专用于一个单独的“主题”,而不是使用具有大量规则的转换。例如,对于某些文档格式,您可以先转换内联元素,然后再转换块元素。将转换拆分为多个转换实际上不会损害性能。

另请注意,规则的顺序很重要:如果您需要在规则中查找例如元素的父级,则了解此父级是否已被另一个规则更改很重要,即,先前的规则是否已转换此元素。以下部分“反向顺序的转换”中给出了一个示例。如前一段所述,使用多个转换可能对此有所帮助。在以下各节“带有上下文信息附件的转换”、“带有文档版本的转换”和“带有遍历的转换”中描述了使用更好上下文信息的方法。

另请注意,使用 XTransformation 您只能转换整个文档。在下面的“带有遍历的转换”部分中,描述了另一种转换任何 XML 树的选项。

可以通过在转换上调用 stop() 来停止转换,尽管这仅间接有效

var transformationAlias: XTransformation? = nil

let transformation = XTransformation {

    XRule(forElements: "a") { _ in
        transformationAlias?.stop()
    }

}

transformationAlias = transformation

transformation.execute(inDocument: myDocument)

反向顺序的转换

正如上一节中指出的那样,规则的顺序在某些转换中至关重要,例如,如果原始上下文很重要。

规则的“反向顺序”从内部元素到外部元素,以便在应用规则时上下文仍然不变,请注意查找 element.parent?.name 以区分文本的颜色

let document = try parseXML(fromText: """
    <document>
        <section>
            <hint>
                <paragraph>This is a hint.</paragraph>
            </hint>
            <warning>
                <paragraph>This is a warning.</paragraph>
            </warning>
        </section>
    </document>
    """, textAllowedInElementWithName: { $0 == "paragraph" })

let transformation = XTransformation {

    XRule(forElements: "paragraph") { element in
        let style: String? = if element.parent?.name == "warning" {
            "color:Red"
        } else {
            nil
        }
        element.replace {
            XElement("p", ["style": style]) {
                element.content
            }
        }
    }

    XRule(forElements: "hint", "warning") { element in
        element.replace {
            XElement("div") {
                XElement("p", ["style": "bold"]) {
                    element.name.uppercased()
                }
                element.content
            }
        }
    }
}

transformation.execute(inDocument: document)

document.echo(pretty: true)

结果

<document>
  <section>
    <div>
      <p style="bold">HINT</p>
      <p>This is a hint.</p>
    </div>
    <div>
      <p style="bold">WARNING</p>
      <p style="color:Red">This is a warning.</p>
    </div>
  </section>
</document>

此方法可能不完全适用于某些转换。

带有上下文信息附件的转换

为了获取有关已转换元素在原始文档中的上下文信息,可以使用附件。请参阅以下代码中如何在构建 div 元素时使用 attached: ["source": element.name],以及如何在 paragraph 元素的规则中使用此信息(输入文档与上面“具有逆序的转换”部分中的文档相同;请注意,该部分中描述的逆序在此处使用)。

let transformation = XTransformation {

    XRule(forElements: "hint", "warning") { element in
        element.replace {
            XElement("div", attached: ["source": element.name]) {
                XElement("p", ["style": "bold"]) {
                    element.name.uppercased()
                }
                element.content
            }
        }
    }

    XRule(forElements: "paragraph") { element in
        let style: String? = if element.parent?.attached["source"] as? String == "warning" {
            "color:Red"
        } else {
            nil
        }
        element.replace {
            XElement("p", ["style": style]) {
                element.content
            }
        }
    }
}

transformation.execute(inDocument: document)

document.echo(pretty: true)

结果与上面“具有逆序的转换”部分中的结果相同。

使用文档版本的转换

如上面关于规则的部分所述,有时您需要了解已转换元素的原始上下文。为此,您可以使用文档版本,如下所述。

请注意,此方法会降低效率,因为需要创建一个(临时)克隆,但对于非常困难的转换,这可能会派上用场。当您需要以复杂的方式检查原始上下文时,可以使用此方法。

您首先创建一个文档版本(这会创建一个克隆,以便您当前的文档包含指向该克隆的反向链接),然后在某些规则中,您可以使用元素创建中的 withBackLinkFrom: 参数复制要替换的节点的反向链接(输入文档与上面“具有逆序的转换”部分中的文档相同)。

let transformation = XTransformation {

    XRule(forElements: "hint", "warning") { element in
        element.replace {
            XElement("div", withBackLinkFrom: element) {
                XElement("p", ["style": "bold"]) {
                    element.name.uppercased()
                }
                element.content
            }
        }
    }

    XRule(forElements: "paragraph") { element in
        let style: String? = if element.parent?.backlink?.name == "warning" {
            "color:Red"
        } else {
            nil
        }
        element.replace {
            XElement("p", ["style": style]) {
                element.content
            }
        }
    }
}

// make a clone with inverse backlinks,
// pointing from the original document to the clone:
document.makeVersion()

transformation.execute(inDocument: document)

// remove the clone:
document.forgetLastVersion()

document.echo(pretty: true)

结果与上面“具有逆序的转换”部分中的结果相同。

使用遍历的转换

还有另一种制定转换的可能性,它使用遍历,并且也可以应用于文档的某些部分或不属于文档的 XML 树。

由于 XML 树可以在遍历期间更改,因此您可以遍历 XML 树并在遍历期间更改树,例如,根据 switch 语句中当前元素的名称制定操作。

如果您在遍历的向下方向制定操作,您知道当前节点的父节点或其他祖先节点已被转换。相反,如果您仅在 up: 遍历部分中制定操作,并且从不操作当前元素的任何祖先,您知道父节点和其他祖先仍然是原始的(输入文档与上面“具有逆序的转换”部分中的文档相同)。

for section in document.elements("section") {
    section.traverse { node in
        // -
    } up: { node in
        if let element = node as? XElement {
            guard node !== section else { return }
            switch element.name {
            case "paragraph":
                let style: String? = if element.parent?.name == "warning" {
                    "color:Red"
                } else {
                    nil
                }
                element.replace {
                    XElement("p", ["style": style]) {
                        element.content
                    }
                }
            case "hint", "warning":
                element.replace {
                    XElement("div") {
                        XElement("p", ["style": "bold"]) {
                            element.name.uppercased()
                        }
                        element.content
                    }
                }
            default:
                break
            }
        }
    }
}

document.echo(pretty: true)

由于遍历的根节点在遍历期间不会被删除,因此有一个相应的 guard 语句。

结果与上面“具有逆序的转换”部分中的结果相同。

请注意,当使用遍历来转换 XML 树时,使用多个转换而不是一个转换会对效率产生负面影响。

命名空间的处理

当涉及到跟踪特定名称的元素并制定相应的规则时,该库非常强大。通过直接在这些点支持命名空间来添加额外的层将使库的实现更加复杂且效率更低。那么,让我们看看如何处理使用命名空间的 XML 文档。

首先,您始终可以在文档中查找命名空间前缀设置(属性 xmlns:...)。正如在关于 XML 输入限制的部分中提到的,命名空间前缀通过 xmlns:... 属性的注释应仅位于 XML 源的根元素。然后,以下两个辅助方法可帮助您完成处理命名空间的任务。

从根元素读取命名空间 URL 字符串的完整前缀

XDocument.fullPrefix(forNamespace:) -> String

“完整”意味着会自动添加一个结束 :。如果未定义前缀,则返回空字符串。

从根元素获取从命名空间 URL 字符串到完整前缀的映射

XDocument.fullPrefixesForNamespaces

当您想访问或更改该命名空间中的元素时,请在代码中动态添加相应的前缀

let fullMathMLPrefix = myDocument.fullPrefix(forNamespace: "http://www.w3.org/1998/Math/MathML")

let transformation = XTransformation {

    XRule(forElements: "\(fullMathMLPrefix)a") { a in
        ...
    }

    ...

如果您想在根元素中添加命名空间声明,请使用以下方法

XDocument.setNamespace(:withPossiblyFullPrefix:)

这里的前缀可能是“完整”前缀,即它可以包含一个结束 :。对于同一命名空间但具有另一个前缀的现有命名空间声明不会 (!) 被删除。

请注意,这三个辅助方法也适用于元素。

使用 async/await

您可以将 traverse 与使用 await 的闭包一起使用。您可以使用 Swift Async Algorithms packageasync 属性(提供 AsyncLazySequence)将 map 等应用于使用 await 的闭包(例如 element.children.async.map { await a.f($0) })。

目前,SwiftXML 包为使用 await 的闭包参数定义了 forEachAsync 方法,但如果 Swift Async Algorithms 包应为 AsyncLazySequence 定义此方法,则此方法可能会在包的未来版本中删除。

便捷扩展

当以复杂方式处理 XML 时,XContent 具有以下非常方便的扩展

fullfilling 原则上是仅针对一项的 filter 方法的变体。)

很难用简单的示例来展示这些扩展的便利性,在简单的示例中,很容易在没有它们的情况下制定代码。但是,如果情况变得更加复杂,它们就会派上用场。

示例

let element1 = XElement("a") {
    XElement("child-of-a") {
        XElement("more", ["special": "yes"])
    }
}

let element2 = XElement("b")

if let childOfA = element1.fullfilling({ $0.name == "a" })?.children.first,
   childOfA.children.first?.fullfills({ $0["special"] == "yes" && $0["moved"] != "yes"  }) == true {
    element2.add {
        childOfA.applying { $0["moved"] = "yes" }
    }
}

element2.echo()

结果

<b><child-of-a moved="yes"><more special="yes"/></child-of-a></b>

applying 也为内容序列或元素序列预定义,在一般情况下(其中可能必须包含 return 语句)它比使用 map 方法更短,您可以直接使用它来定义内容(无需使用上面描述的 asContent 属性)

let myElement = XElement("a") {
    XElement("b", ["inserted": "yes"]) {
        XElement("c", ["inserted": "yes"])
    }
}

print(Array(myElement.descendants.applying{ $0["inserted"] = "yes" }))

结果

[<b inserted="yes">, <c inserted="yes">]

工具

copyXStructure

public func copyXStructure(from start: XContent, to end: XContent, upTo: XElement? = nil, correction: ((StructureCopyInfo) -> XContent)?) -> XContent?

start 复制结构到 end,可以选择复制到 upTo 值。startend 必须有一个共同的祖先。如果没有共同的祖先,则返回 nil。如果 a) upTo 不为 nil 且 b) upTo 是共同祖先或祖先本身的祖先,则返回的元素是 upTo 值的克隆。否则,它是共同祖先的克隆(但通常在两种情况下内容都不同)。correction 可以进行一些更正。

调试

如果使用捆绑到 XTRansformation 中的 XRule 的多个实例来转换整个文档,则了解哪些属于哪些规则的动作“触及”了元素可能很有用。在调试版本中,转换执行期间执行的所有文件名和行号都记录在 encounteredActionsAt 属性中。