Add \operatorname{name} LaTeX command support
Agent-Logs-Url: https://github.com/wesleyel/swiftui-math/sessions/d2d346fe-819f-4ec5-8653-6582836c760d Co-authored-by: wesleyel <48174882+wesleyel@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
79b44fc96c
commit
aba7f31f95
@@ -571,9 +571,10 @@ extension Math {
|
|||||||
}
|
}
|
||||||
} else if atom.type == .largeOperator {
|
} else if atom.type == .largeOperator {
|
||||||
let op = atom as! LargeOperator
|
let op = atom as! LargeOperator
|
||||||
let command = AtomFactory.latexSymbolName(for: atom)
|
if let command = AtomFactory.latexSymbolName(for: atom) {
|
||||||
let originalOp = AtomFactory.atom(forLatexSymbol: command!) as! LargeOperator
|
// Known built-in operator (e.g. \sin, \sum)
|
||||||
str += "\\\(command!) "
|
let originalOp = AtomFactory.atom(forLatexSymbol: command) as! LargeOperator
|
||||||
|
str += "\\\(command) "
|
||||||
if originalOp.limits != op.limits {
|
if originalOp.limits != op.limits {
|
||||||
if op.limits {
|
if op.limits {
|
||||||
str += "\\limits "
|
str += "\\limits "
|
||||||
@@ -581,6 +582,10 @@ extension Math {
|
|||||||
str += "\\nolimits "
|
str += "\\nolimits "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Custom operator defined via \operatorname
|
||||||
|
str += "\\operatorname{\(op.nucleus)}"
|
||||||
|
}
|
||||||
} else if atom.type == .space {
|
} else if atom.type == .space {
|
||||||
if let space = atom as? Space {
|
if let space = atom as? Space {
|
||||||
if let command = Self.spaceToCommands[space.amount] {
|
if let command = Self.spaceToCommands[space.amount] {
|
||||||
@@ -848,6 +853,28 @@ extension Math {
|
|||||||
|
|
||||||
inner.innerList = innerList
|
inner.innerList = innerList
|
||||||
return inner
|
return inner
|
||||||
|
} else if command == "operatorname" {
|
||||||
|
// \operatorname{name} — renders argument as an upright operator (no limits by default)
|
||||||
|
guard self.expectCharacter("{") else {
|
||||||
|
self.setError(.characterNotFound, message: "Missing { after \\operatorname")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.skipSpaces()
|
||||||
|
var name = ""
|
||||||
|
while self.hasCharacters {
|
||||||
|
let ch = self.nextCharacter()
|
||||||
|
if ch == "}" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
name.append(ch)
|
||||||
|
}
|
||||||
|
name = name.trimmingCharacters(in: .whitespaces)
|
||||||
|
guard !name.isEmpty else {
|
||||||
|
self.setError(.invalidCommand, message: "\\operatorname requires a non-empty argument")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Create a LargeOperator with the given name, no limits (like \sin, \log)
|
||||||
|
return AtomFactory.operatorWithName(name, limits: false)
|
||||||
} else if command == "not" {
|
} else if command == "not" {
|
||||||
// Handle \not command with lookahead for comprehensive negation support
|
// Handle \not command with lookahead for comprehensive negation support
|
||||||
let nextCommand = self.peekNextCommand()
|
let nextCommand = self.peekNextCommand()
|
||||||
|
|||||||
@@ -1988,6 +1988,57 @@ struct ParserTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func operatorname() throws {
|
||||||
|
// Basic usage: \operatorname{Res}
|
||||||
|
var str = "\\operatorname{Res}"
|
||||||
|
var list = try #require(Math.Parser.build(fromString: str))
|
||||||
|
#expect(list.atoms.count == 1)
|
||||||
|
var op = try #require(list.atoms[0] as? Math.LargeOperator)
|
||||||
|
#expect(op.type == .largeOperator)
|
||||||
|
#expect(op.nucleus == "Res")
|
||||||
|
#expect(!op.limits)
|
||||||
|
|
||||||
|
// Serializes back as \operatorname{Res}
|
||||||
|
var latex = Math.Parser.atomListToString(list)
|
||||||
|
#expect(latex == "\\operatorname{Res}")
|
||||||
|
|
||||||
|
// With argument: \operatorname{sgn}(x)
|
||||||
|
str = "\\operatorname{sgn}(x)"
|
||||||
|
list = try #require(Math.Parser.build(fromString: str))
|
||||||
|
op = try #require(list.atoms.first(where: { $0.type == .largeOperator }) as? Math.LargeOperator)
|
||||||
|
#expect(op.nucleus == "sgn")
|
||||||
|
#expect(!op.limits)
|
||||||
|
|
||||||
|
// Another common operator: \operatorname{tr}(A)
|
||||||
|
str = "\\operatorname{tr}(A)"
|
||||||
|
list = try #require(Math.Parser.build(fromString: str))
|
||||||
|
op = try #require(list.atoms.first(where: { $0.type == .largeOperator }) as? Math.LargeOperator)
|
||||||
|
#expect(op.nucleus == "tr")
|
||||||
|
#expect(!op.limits)
|
||||||
|
|
||||||
|
// With subscript: \operatorname{Res}_{z=0} f(z)
|
||||||
|
str = "\\operatorname{Res}_{z=0} f(z)"
|
||||||
|
list = try #require(Math.Parser.build(fromString: str))
|
||||||
|
op = try #require(list.atoms.first(where: { $0.type == .largeOperator }) as? Math.LargeOperator)
|
||||||
|
#expect(op.nucleus == "Res")
|
||||||
|
#expect(op.subscript != nil)
|
||||||
|
|
||||||
|
// With superscript: \operatorname{Res}^{2}
|
||||||
|
str = "\\operatorname{Res}^{2}"
|
||||||
|
list = try #require(Math.Parser.build(fromString: str))
|
||||||
|
op = try #require(list.atoms.first(where: { $0.type == .largeOperator }) as? Math.LargeOperator)
|
||||||
|
#expect(op.nucleus == "Res")
|
||||||
|
#expect(op.superscript != nil)
|
||||||
|
|
||||||
|
// Error case: empty argument \operatorname{}
|
||||||
|
str = "\\operatorname{}"
|
||||||
|
var error: Math.ParserError? = nil
|
||||||
|
let errorList = Math.Parser.build(fromString: str, error: &error)
|
||||||
|
#expect(errorList == nil)
|
||||||
|
#expect(error != nil)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func arrows() throws {
|
func arrows() throws {
|
||||||
let arrows = [
|
let arrows = [
|
||||||
|
|||||||
Reference in New Issue
Block a user