Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,44 @@ provided below is an organized table of W3C HTML tags and their equivalent Slips
[`<template>`](https://html.spec.whatwg.org/multipage/sections.html#the-template-element) | ``Template``
[`<slot>`](https://html.spec.whatwg.org/multipage/sections.html#the-slot-element) | ``Slot``
[`<canvas>`](https://html.spec.whatwg.org/multipage/sections.html#the-canvas-element) | ``Canvas``

### SVG (Scalable Vector Graphics)

W3C tag | Slipstream view
:--------|:----------------
[`<svg>`](https://svgwg.org/svg2-draft/struct.html#SVGElement) | ``SVG``
[`<circle>`](https://svgwg.org/svg2-draft/shapes.html#CircleElement) | ``SVGCircle``
[`<defs>`](https://svgwg.org/svg2-draft/struct.html#DefsElement) | ``SVGDefs``
[`<desc>`](https://svgwg.org/svg2-draft/struct.html#DescElement) | ``SVGDesc``
[`<g>`](https://svgwg.org/svg2-draft/struct.html#GElement) | ``SVGGroup``
[`<linearGradient>`](https://svgwg.org/svg2-draft/pservers.html#LinearGradientElement) | ``SVGLinearGradient``
[`<path>`](https://svgwg.org/svg2-draft/paths.html#PathElement) | ``SVGPath``
[`<radialGradient>`](https://svgwg.org/svg2-draft/pservers.html#RadialGradientElement) | ``SVGRadialGradient``
[`<rect>`](https://svgwg.org/svg2-draft/shapes.html#RectElement) | ``SVGRect``
[`<stop>`](https://svgwg.org/svg2-draft/pservers.html#StopElement) | ``SVGStop``
[`<text>`](https://svgwg.org/svg2-draft/text.html#TextElement) | ``SVGText``
[`<title>`](https://svgwg.org/svg2-draft/struct.html#TitleElement) | ``SVGTitle``

### MathML (Mathematical Markup Language)

W3C tag | Slipstream view
:--------|:----------------
[`<math>`](https://www.w3.org/TR/MathML3/chapter2.html#interf.toplevel) | ``Math``
[`<mi>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.mi) | ``MI``
[`<mo>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.mo) | ``MO``
[`<mn>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.mn) | ``MN``
[`<mtext>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.mtext) | ``MText``
[`<ms>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.ms) | ``MS``
[`<mrow>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.mrow) | ``MRow``
[`<mfrac>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.mfrac) | ``MFrac``
[`<msup>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.msup) | ``MSup``
[`<msub>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.msub) | ``MSub``
[`<msubsup>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.msubsup) | ``MSubSup``
[`<msqrt>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.msqrt) | ``MSqrt``
[`<mroot>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.mroot) | ``MRoot``
[`<munder>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.munder) | ``MUnder``
[`<mover>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.mover) | ``MOver``
[`<munderover>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.munderover) | ``MUnderOver``
[`<mtable>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.mtable) | ``MTable``
[`<mtr>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.mtr) | ``MTr``
[`<mtd>`](https://www.w3.org/TR/MathML3/chapter3.html#presm.mtd) | ``MTd``
75 changes: 73 additions & 2 deletions Sources/Slipstream/Rendering/Render.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftSoup
import Foundation

/// Renders the given view as an HTML document and returns the HTML.
///
Expand Down Expand Up @@ -26,7 +27,46 @@ import SwiftSoup
public func renderHTML(_ view: any View) throws -> String {
let document = Document("/")
try view.render(document, environment: EnvironmentValues())
return try document.html()
var html = try document.html()

// Post-process to format MathML token elements inline
// SwiftSoup doesn't know MathML tags, so it formats them as block-level
// We need to collapse whitespace within these elements to render them inline
let mathmlTokenTags = ["mi", "mo", "mn", "ms", "mtext"]
for tag in mathmlTokenTags {
// Pattern captures: indentation before content, content, indentation before closing tag
// We'll strip the indentation (based on closing tag) while preserving content spaces
let pattern = "<\(tag)>\\r?\\n([ \\t]*)(.*?)\\r?\\n([ \\t]*)</\(tag)>"
let regex = try! NSRegularExpression(pattern: pattern, options: [])
var nsString = html as NSString
let matches = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsString.length))

// Process matches in reverse to maintain string indices
for match in matches.reversed() {
guard match.numberOfRanges == 4 else { continue }
let fullRange = match.range(at: 0)
let leadingSpacesRange = match.range(at: 1)
let contentRange = match.range(at: 2)
let closingSpacesRange = match.range(at: 3)

let leadingSpaces = nsString.substring(with: leadingSpacesRange)
let content = nsString.substring(with: contentRange)
let closingSpaces = nsString.substring(with: closingSpacesRange)

// The closing tag's indentation tells us the element's indent level
// Content is indented one level deeper, so we strip closingIndent + 1
let indentToStrip = closingSpaces.count + 1
// Combine leading spaces and content, then strip the indentation
let fullContent = leadingSpaces + content
let trimmedContent = String(fullContent.dropFirst(min(indentToStrip, fullContent.count)))

let replacement = "<\(tag)>\(trimmedContent)</\(tag)>"
html = nsString.replacingCharacters(in: fullRange, with: replacement) as String
nsString = html as NSString
}
}

return html
}

/// Renders the given view as an HTML document and returns the HTML.
Expand All @@ -43,7 +83,38 @@ public func inlineHTML<Content: View>(@ViewBuilder _ builder: () -> Content) ->
let document = Document("/")
do {
try builder().render(document, environment: EnvironmentValues())
return try document.html()
var html = try document.html()

// Post-process to format MathML token elements inline
let mathmlTokenTags = ["mi", "mo", "mn", "ms", "mtext"]
for tag in mathmlTokenTags {
let pattern = "<\(tag)>\\r?\\n([ \\t]*)(.*?)\\r?\\n([ \\t]*)</\(tag)>"
let regex = try! NSRegularExpression(pattern: pattern, options: [])
var nsString = html as NSString
let matches = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsString.length))

for match in matches.reversed() {
guard match.numberOfRanges == 4 else { continue }
let fullRange = match.range(at: 0)
let leadingSpacesRange = match.range(at: 1)
let contentRange = match.range(at: 2)
let closingSpacesRange = match.range(at: 3)

let leadingSpaces = nsString.substring(with: leadingSpacesRange)
let content = nsString.substring(with: contentRange)
let closingSpaces = nsString.substring(with: closingSpacesRange)

let indentToStrip = closingSpaces.count + 1
let fullContent = leadingSpaces + content
let trimmedContent = String(fullContent.dropFirst(min(indentToStrip, fullContent.count)))

let replacement = "<\(tag)>\(trimmedContent)</\(tag)>"
html = nsString.replacingCharacters(in: fullRange, with: replacement) as String
nsString = html as NSString
}
}

return html
} catch let error {
return "<!-- \(error) -->"
}
Expand Down
55 changes: 55 additions & 0 deletions Sources/Slipstream/W3C/Elements/EmbeddedContent/MathML/MFrac.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import SwiftSoup

/// A view that represents a MathML `<mfrac>` element for fractions.
///
/// The `<mfrac>` element is used to display fractions with a numerator and denominator
/// separated by a horizontal line.
///
/// ```swift
/// struct MySiteContent: View {
/// var body: some View {
/// Body {
/// Math {
/// MFrac {
/// MI("a")
/// MI("b")
/// }
/// }
/// }
/// }
/// }
/// ```
///
/// - SeeAlso: W3C [mfrac](https://www.w3.org/TR/MathML3/chapter3.html#presm.mfrac) specification.
@available(iOS 17.0, macOS 14.0, *)
public struct MFrac<Content>: View where Content: View {
/// Creates an MFrac element.
///
/// - Parameter content: The numerator and denominator expressions (must contain exactly two children).
public init(@ViewBuilder content: @escaping @Sendable () -> Content) {
self.content = content
self.linethicknessValue = nil
}

@_documentation(visibility: private)
public func render(_ container: Element, environment: EnvironmentValues) throws {
let element = try container.appendElement("mfrac")
if let linethickness = linethicknessValue {
try element.attr("linethickness", linethickness)
}
try self.content().render(element, environment: environment)
}

/// Sets the thickness of the fraction line.
///
/// - Parameter thickness: The line thickness (e.g., "thin", "medium", "thick", or a length value)
/// - Returns: A new MFrac with the specified line thickness.
public func linethickness(_ thickness: String) -> MFrac {
var newFrac = self
newFrac.linethicknessValue = thickness
return newFrac
}

@ViewBuilder private let content: @Sendable () -> Content
private var linethicknessValue: String?
}
41 changes: 41 additions & 0 deletions Sources/Slipstream/W3C/Elements/EmbeddedContent/MathML/MI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import SwiftSoup

/// A view that represents a MathML `<mi>` element for identifiers.
///
/// The `<mi>` element is used to represent identifier symbols in mathematical expressions,
/// such as variable names or function names. By default, content is rendered in italic.
///
/// ```swift
/// struct MySiteContent: View {
/// var body: some View {
/// Body {
/// Math {
/// MRow {
/// MI("x")
/// MO("+")
/// MI("y")
/// }
/// }
/// }
/// }
/// }
/// ```
///
/// - SeeAlso: W3C [mi](https://www.w3.org/TR/MathML3/chapter3.html#presm.mi) specification.
@available(iOS 17.0, macOS 14.0, *)
public struct MI: View {
private let text: String

/// Creates an MI element with text content.
///
/// - Parameter text: The identifier text content
public init(_ text: String) {
self.text = text
}

@_documentation(visibility: private)
public func render(_ container: Element, environment: EnvironmentValues) throws {
let element = try container.appendElement("mi")
try element.appendText(text)
}
}
40 changes: 40 additions & 0 deletions Sources/Slipstream/W3C/Elements/EmbeddedContent/MathML/MN.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import SwiftSoup

/// A view that represents a MathML `<mn>` element for numbers.
///
/// The `<mn>` element is used to represent numeric literals in mathematical expressions.
///
/// ```swift
/// struct MySiteContent: View {
/// var body: some View {
/// Body {
/// Math {
/// MRow {
/// MN("42")
/// MO("+")
/// MN("3.14")
/// }
/// }
/// }
/// }
/// }
/// ```
///
/// - SeeAlso: W3C [mn](https://www.w3.org/TR/MathML3/chapter3.html#presm.mn) specification.
@available(iOS 17.0, macOS 14.0, *)
public struct MN: View {
private let text: String

/// Creates an MN element with text content.
///
/// - Parameter text: The number text content
public init(_ text: String) {
self.text = text
}

@_documentation(visibility: private)
public func render(_ container: Element, environment: EnvironmentValues) throws {
let element = try container.appendElement("mn")
try element.appendText(text)
}
}
41 changes: 41 additions & 0 deletions Sources/Slipstream/W3C/Elements/EmbeddedContent/MathML/MO.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import SwiftSoup

/// A view that represents a MathML `<mo>` element for operators.
///
/// The `<mo>` element is used to represent operators in mathematical expressions,
/// such as +, -, ×, ÷, =, and other mathematical operators.
///
/// ```swift
/// struct MySiteContent: View {
/// var body: some View {
/// Body {
/// Math {
/// MRow {
/// MI("x")
/// MO("+")
/// MI("y")
/// }
/// }
/// }
/// }
/// }
/// ```
///
/// - SeeAlso: W3C [mo](https://www.w3.org/TR/MathML3/chapter3.html#presm.mo) specification.
@available(iOS 17.0, macOS 14.0, *)
public struct MO: View {
private let text: String

/// Creates an MO element with text content.
///
/// - Parameter text: The operator text content
public init(_ text: String) {
self.text = text
}

@_documentation(visibility: private)
public func render(_ container: Element, environment: EnvironmentValues) throws {
let element = try container.appendElement("mo")
try element.appendText(text)
}
}
40 changes: 40 additions & 0 deletions Sources/Slipstream/W3C/Elements/EmbeddedContent/MathML/MOver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import SwiftSoup

/// A view that represents a MathML `<mover>` element for overscripts.
///
/// The `<mover>` element is used to attach an overscript to a base expression,
/// commonly used for accents, limits, and other notations.
///
/// ```swift
/// struct MySiteContent: View {
/// var body: some View {
/// Body {
/// Math {
/// MOver {
/// MI("x")
/// MO("^")
/// }
/// }
/// }
/// }
/// }
/// ```
///
/// - SeeAlso: W3C [mover](https://www.w3.org/TR/MathML3/chapter3.html#presm.mover) specification.
@available(iOS 17.0, macOS 14.0, *)
public struct MOver<Content>: View where Content: View {
/// Creates an MOver element.
///
/// - Parameter content: The base and overscript expressions (must contain exactly two children).
public init(@ViewBuilder content: @escaping @Sendable () -> Content) {
self.content = content
}

@_documentation(visibility: private)
public func render(_ container: Element, environment: EnvironmentValues) throws {
let element = try container.appendElement("mover")
try self.content().render(element, environment: environment)
}

@ViewBuilder private let content: @Sendable () -> Content
}
Loading