diff --git a/src/data-model.typ b/src/data-model.typ index 4f23ee2..d0eec88 100644 --- a/src/data-model.typ +++ b/src/data-model.typ @@ -1,21 +1,53 @@ -#import "utils.typ": is-sequence, is-kind, is-heading, is-metadata, padright, get-all-children, hydrates, elements, shell-capacities, orbital-capacities, get-element-dict, get-molecule-dict, to-string +#import "utils.typ": is-sequence, is-kind, is-heading, is-metadata, padright, get-all-children, hydrates, elements, get-element-dict, get-molecule-dict, to-string #import "regex.typ": patterns -#import "formula-parser.typ": ce +#let get-element( + symbol: auto, + atomic-number:auto, + common-name:auto, + cas:auto, +)={ + let element = if symbol != auto { + elements.find(x=> x.symbol == symbol) + } else if atomic-number != auto{ + elements.find(x=> x.atomic-number == atomic-number) + } else if common-name != auto{ + elements.find(x=> x.common-name == common-name) + } else if cas != auto{ + elements.find(x=> x.cas == cas) + } + return metadata(element) +} + +#let validate-element(element)={ + let type = type(element) + if type == str{ + if element.len() > 2{ + return get-element(common-name:element) + } else { + return get-element(symbol:element) + } + } else if type == int{ + return get-element(atomic-number:element) + } else if type == content{ + return get-element-dict(element) + } else if type == dictionary{ + return element + } +} //TODO: properly parse bracket contents // maybe recursively with a bracket regex, passing in the bracket content and multiplier(?) //TODO: Properly apply stochiometry #let get-element-counts(molecule)={ - let found-elements = (:) let remaining = molecule.trim() while remaining.len() > 0 { - let match = remaining.match(regex_patterns.at("element")) + let match = remaining.match(patterns.element) if match != none { remaining = remaining.slice(match.end) let element = match.captures.at(0) - let count = int(if match.captures.at(1, default: "") == "" {1} else{match.captures.at(1)}) + let count = 1 //int(if match.captures.at(1, default: "") == "" {1} else{match.captures.at(1)}) let current = found-elements.at(element, default: 0) found-elements.insert(element, count) } @@ -43,116 +75,6 @@ return weight } -#let get-shell-configuration(element)={ - element = get-element-dict(element) - let charge = element.at("charge", default:0) - let electron-amount = element.atomic-number - charge - - let result = () - for value in shell-capacities { - if electron-amount <= 0{ - break - } - - if electron-amount >= value.at(1){ - result.push(value) - electron-amount -= value.at(1) - } else { - result.push((value.at(0), electron-amount)) - electron-amount = 0 - } - } - return result -} - -//TODO: fix Cr and Mo -#let get-electron-configuration(element)={ - element = get-element-dict(element) - let charge = element.at("charge", default:0) - let electron-amount = element.atomic-number - charge - - let result = () - for value in orbital-capacities { - if electron-amount <= 0{ - break - } - if electron-amount >= value.at(1){ - result.push(value) - electron-amount -= value.at(1) - } else { - result.push((value.at(0), electron-amount)) - electron-amount = 0 - } - } - return result -} - -#let display-electron-configuration(element, short: false)={ - let configuration = get-electron-configuration(element) - - if short{ - let prefix = "" - if configuration.at(14, default: (0,0)).at(1) == 6{ - configuration = configuration.slice(15) - prefix = "[Rn]" - } else if configuration.at(10, default: (0,0)).at(1) == 6{ - configuration = configuration.slice(11) - prefix = "[Xe]" - } else if configuration.at(7, default: (0,0)).at(1) == 6{ - configuration = configuration.slice(8) - prefix = "[Kr]" - } else if configuration.at(4, default: (0,0)).at(1) == 6{ - configuration = configuration.slice(5) - prefix = "[Ar]" - } else if configuration.at(2, default: (0,0)).at(1) == 6{ - configuration = configuration.slice(3) - prefix = "[Ne]" - } else if configuration.at(0, default: (0,0)).at(1) == 2{ - configuration = configuration.slice(1) - prefix = "[He]" - } - - return prefix + configuration.map(x=> $#x.at(0)^#str(x.at(1))$).sum() - } else{ - return configuration.map(x=> $#x.at(0)^#str(x.at(1))$).sum() - } -} - -#let get-element( - symbol: auto, - atomic-number:auto, - common-name:auto, - cas:auto, -)={ - let element = if symbol != auto { - elements.find(x=> x.symbol == symbol) - } else if atomic-number != auto{ - elements.find(x=> x.atomic-number == atomic-number) - } else if common-name != auto{ - elements.find(x=> x.common-name == common-name) - } else if cas != auto{ - elements.find(x=> x.cas == cas) - } - return metadata(element) -} - -#let validate-element(element)={ - let type = type(element) - if type == str{ - if element.len() > 2{ - return get-element(common-name:element) - } else { - return get-element(symbol:element) - } - } else if type == int{ - return get-element(atomic-number:element) - } else if type == content{ - return get-element-dict(element) - } else if type == dictionary{ - return element - } -} - #let define-ion( element, charge: 0, @@ -241,18 +163,18 @@ formula = smiles } - if CAS == ""{ - CAS = none + if cas == ""{ + cas = none } found-elements = get-element-counts(formula) - if InChI != ""{ + if inchi != ""{ // TODO: create InChI keys from provided InChI: // https://typst.app/universe/package/jumble // https://www.inchi-trust.org/download/104/InChI_TechMan.pdf }else{ - InChI = none + inchi = none } } diff --git a/src/display-intermediate-representation.typ b/src/display-intermediate-representation.typ new file mode 100644 index 0000000..6c50db1 --- /dev/null +++ b/src/display-intermediate-representation.typ @@ -0,0 +1,108 @@ +#import "utils.typ": try-at, count-to-content, charge-to-content, get-bracket, get-arrow, phase-to-content + +#let display-element(data) = { + let isotope = data.at("isotope", default: none) + math.attach( + data.symbol, + t: data.at("oxidation-number", default: none), + tr: charge-to-content(data.at("charge", default: none), radical: data.at("radical", default: false)), + br: count-to-content(data.at("count", default: none)), + tl: try-at(isotope, "mass-number"), + bl: try-at(isotope, "atomic-number"), + ) +} + +#let display-group(data) = { + let children = data.at("children", default:()) + let kind = data.at("kind", default: 1) + math.attach( + math.lr({ + get-bracket(kind, open:true) + for child in children { + if child.type == "content" { + child.body + } else if child.type == "element" { + display-element(child) + }else if data.type == "align"{ + $&$ + } else if child.type == "group" { + display-group(child) + } + } + get-bracket(kind, open:false) + }), + tr: charge-to-content(data.at("charge", default: none)), + br: count-to-content(data.at("count", default: none)), + ) +} + +#let display-molecule(data) = { + count-to-content(data.at("count", default: none)) + math.attach( + [ + #let children = data.at("children", default:()) + #for child in children { + if child.type == "content"{ + child.body + } else if data.type == "align"{ + $&$ + } else if child.type == "element"{ + display-element(child) + } else if child.type == "group" { + display-group(child) + } + } + ], + tr: charge-to-content(data.at("charge", default: none)), + // br: phase-to-content(data.at("phase", default:none)), + ) + context { + text(phase-to-content(data.at("phase", default:none)), size: text.size * 0.75) + } +} + +#let display-ir(data) = { + if data== none{ + none + } else if type(data) == array{ + for value in data { + display-ir(value) + //this removes spacing for groups that have long charges (looks better) + if value.type == "molecule" { + let last = value.children.last() + if last.type == "group" and (last.at("charge", default: none) != none or last.at("count", default: none) != none){ + h(-0.4em) + } + } + } + }else{ + if data.type == "molecule" { + display-molecule(data) + } else if data.type == "+"{ + h(0.4em, weak: true) + math.plus + h(0.4em, weak: true) + } else if data.type == "group"{ + display-group(data) + } else if data.type == "element"{ + display-element(data) + } else if data.type == "content"{ + data.body + }else if data.type == "align"{ + $&$ + } else if data.type == "arrow"{ + h(0.4em, weak: true) + let top = display-ir(data.at("top", default:none)) + let bottom = display-ir(data.at("bottom", default:none)) + math.attach( + math.stretch( + get-arrow(data.at("kind", default:0)), + size: 100% + 2em + ), + t: top, + b: bottom, + ) + h(0.4em, weak: true) + } + } +} \ No newline at end of file diff --git a/src/display-shell-configuration.typ b/src/display-shell-configuration.typ new file mode 100644 index 0000000..f4d25a5 --- /dev/null +++ b/src/display-shell-configuration.typ @@ -0,0 +1,76 @@ +#import "utils.typ": get-element-dict, shell-capacities, orbital-capacities + +#let get-shell-configuration(element)={ + element = get-element-dict(element) + let charge = element.at("charge", default:0) + let electron-amount = element.atomic-number - charge + + let result = () + for value in shell-capacities { + if electron-amount <= 0{ + break + } + + if electron-amount >= value.at(1){ + result.push(value) + electron-amount -= value.at(1) + } else { + result.push((value.at(0), electron-amount)) + electron-amount = 0 + } + } + return result +} + +//TODO: fix Cr and Mo +#let get-electron-configuration(element)={ + element = get-element-dict(element) + let charge = element.at("charge", default:0) + let electron-amount = element.atomic-number - charge + + let result = () + for value in orbital-capacities { + if electron-amount <= 0{ + break + } + if electron-amount >= value.at(1){ + result.push(value) + electron-amount -= value.at(1) + } else { + result.push((value.at(0), electron-amount)) + electron-amount = 0 + } + } + return result +} + +#let display-electron-configuration(element, short: false)={ + let configuration = get-electron-configuration(element) + + if short{ + let prefix = "" + if configuration.at(14, default: (0,0)).at(1) == 6{ + configuration = configuration.slice(15) + prefix = "[Rn]" + } else if configuration.at(10, default: (0,0)).at(1) == 6{ + configuration = configuration.slice(11) + prefix = "[Xe]" + } else if configuration.at(7, default: (0,0)).at(1) == 6{ + configuration = configuration.slice(8) + prefix = "[Kr]" + } else if configuration.at(4, default: (0,0)).at(1) == 6{ + configuration = configuration.slice(5) + prefix = "[Ar]" + } else if configuration.at(2, default: (0,0)).at(1) == 6{ + configuration = configuration.slice(3) + prefix = "[Ne]" + } else if configuration.at(0, default: (0,0)).at(1) == 2{ + configuration = configuration.slice(1) + prefix = "[He]" + } + + return prefix + configuration.map(x=> $#x.at(0)^#str(x.at(1))$).sum() + } else{ + return configuration.map(x=> $#x.at(0)^#str(x.at(1))$).sum() + } +} \ No newline at end of file diff --git a/src/formula-parser.typ b/src/formula-parser.typ deleted file mode 100644 index e22321c..0000000 --- a/src/formula-parser.typ +++ /dev/null @@ -1,139 +0,0 @@ -// Import required modules -#import "utils.typ": parser-config -#import "regex.typ": patterns - -// [CHANGE] Replaced direct pattern access with cached patterns for better performance -#let PATTERNS = { - let cache = (:) - for pattern in patterns.keys() { - cache.insert(pattern, patterns.at(pattern)) - } - cache -} - -// [CHANGE] Added symbol map for consistent rendering and better maintainability -#let SYMBOL_MAP = ( - arrows: ( - "<-": (sym.harpoons.rtlb, parser-config.arrow.reversible_size), - "=": ($=$, parser-config.arrow.arrow_size), - "->": ($->$, parser-config.arrow.arrow_size) - ), - charges: ( - "+": (math.plus, 0.8em), - "-": (math.minus, 0.75em) - ) -) - -// [CHANGE] Simplified charge processing with explicit symbol hiding -#let process_charge = (input, charge) => { - let first = charge.first() - show "-": text(size: 0.75em, baseline: -0.15em)[#math.minus] - show "+": text(size: 0.75em, baseline: -0.15em)[#math.plus] - show "^": none - context {$#block(height: measure(input).height)^#charge$} -} - -// Process reaction conditions (temperature, pressure, catalyst, etc.) -#let process_condition(cond) = { - let cond = cond.trim() - if cond.match(patterns.heating) != none { - return (none, { sym.Delta }) - } - - let is_bottom = ( - parser-config.conditions.bottom.identifiers.any(ids => ids.any(id => cond.starts-with(id))) - or parser-config.conditions.bottom.units.any(unit => cond.ends-with(unit)) - ) - - return if is_bottom { (none, cond) } else { (parse_formula(cond), none) } -} - -// Process reaction arrows with top and bottom conditions -#let process_arrow(arrow_text, condition: none) = { - let (arrow_match, bracket_content) = if arrow_text.contains("[") { - let match = arrow_text.match(patterns.arrow) - (match.captures.at(0), match.captures.at(1)) - } else { - (arrow_text, none) - } - - let arrow = if arrow_match.contains("<-") { - $stretch(#sym.harpoons.rtlb, size: #parser-config.arrow.reversible_size)$ - } else if arrow_match.contains("=") { - $stretch(=, size: #parser-config.arrow.arrow_size)$ - } else { - $stretch(->, size: #parser-config.arrow.arrow_size)$ - } - - let top = () - let bottom = () - - if bracket_content != none { - top.push(bracket_content) - } - - if condition != none { - for cond in condition.split(",") { - let (t, b) = process_condition(cond) - if t != none { top.push(t) } - if b != none { bottom.push(b) } - } - } - - $arrow^#top.join(",")_#bottom.join(",")$ -} - -// [CHANGE] Added pattern handlers for better code organization and reusability -#let PATTERN_HANDLERS = ( - charge: (r,c) => process_charge(r, c), - arrow: (t) => process_arrow(t), -) - -// [CHANGE] Optimized main parser with single-pass matching and improved error handling -#let ce = formula => { - let remaining = formula.trim() - if remaining.len() == 0 { return [] } - - let result = none - let pattern_group = parser-config.match_order.full - - while remaining.len() > 0 { - let best = (pattern: none, match: none) - - // [CHANGE] Single pass scan replaces multiple pattern attempts - for pattern in pattern_group { - let match = remaining.match(PATTERNS.at(pattern)) - if match != none { - best = (pattern: pattern, match: match) - break - } - } - - // [CHANGE] Simplified pattern handling using direct math mode - if best.match != none { - let pattern = best.pattern - let match = best.match - - result += if pattern == "plus" { $+$ } - else if pattern == "element" { $#match.captures.at(0) _#if match.captures.at(1) != none [#match.captures.at(1)]$ } - else if pattern == "bracket" { $#match.captures.at(0) _#if match.captures.at(1) != none [#match.captures.at(1)]$ } - else if pattern == "charge" { (PATTERN_HANDLERS.charge)(result, match.captures.at(0)) } - else { (PATTERN_HANDLERS.arrow)(match.text) } - - remaining = remaining.slice(match.end) - } else { - // [CHANGE] Better Unicode support with codepoints handling - result += text(remaining.at(0)) - remaining = remaining.slice(remaining.codepoints().at(0).len()) - } - } - - $upright(display(result))$ -} - -// Test section for previewing the output -#import "@preview/shadowed:0.2.0": shadowed - -#set page(margin: 0.3em, width: auto, height: auto) - -#shadowed(inset: 0.7em, radius: 6pt)[#ce("Cu^2+ +")] \ No newline at end of file diff --git a/src/lib.typ b/src/lib.typ index e2a741e..c54a58d 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -1,2 +1,8 @@ -#import "data-model.typ": get-element-counts, get-element, get-weight, define-molecule, define-hydrate, reaction, get-shell-configuration, get-electron-configuration, display-electron-configuration -#import "formula-parser.typ": ce \ No newline at end of file +#import "data-model.typ": get-element-counts, get-element, get-weight, define-molecule, define-hydrate, reaction +#import "display-shell-configuration.typ": get-electron-configuration, get-shell-configuration, display-electron-configuration +#import "display-intermediate-representation.typ": display-ir +#import "parse-formula-intermediate-representation.typ": string-to-ir + +#let ce(formula) = { + display-ir(string-to-ir(formula)) +} \ No newline at end of file diff --git a/src/parse-formula-intermediate-representation.typ b/src/parse-formula-intermediate-representation.typ new file mode 100644 index 0000000..cceb3a7 --- /dev/null +++ b/src/parse-formula-intermediate-representation.typ @@ -0,0 +1,367 @@ +#import "utils.typ": arrow-string-to-kind +#let patterns = ( + // element: regex("^(?P[A-Z][a-z]?)(?:(?P_?\d+)|(?P\^?[+-]?\d*\.?-?))?(?:(?P_?\d+)|(?P\^?[+-]?\d*\.?-?))?"), + element: regex("^(?P[A-Z][a-z]?)(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?"), + // group: regex("^(\((?:[^()]|(?R))*\)|\{(?:[^{}]|(?R))*\}|\[(?:[^\[\]]|(?R))*\])"), + group: regex("^(?P\((?:[^()]|(?R))*\)|\{(?:[^{}]|(?R))*\}|\[(?:[^\[\]]|(?R))*\])(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?"), + + reaction-plus: regex("^(\s?\+\s?)"), + reaction-arrow: regex("^\s?(<->|<=>|->|<-|=>|<=|-\/>|<\/-)(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?\s?"), + math: regex("^(\$[^$]*\$)"), + + // Match physical states (s/l/g/aq) + state: regex("^\((s|l|g|aq|solid|liquid|gas|aqueous)\)"), +) + +#let get-count-and-charge(count1, count2, charge1, charge2) = { + let radical = false + let count = if count1 != none and count1 != ""{ + int(count1.replace("_", "")) + } else if count2 != none and count2 != ""{ + int(count2.replace("_", "")) + } else { + none + } + + let charge = if charge1 != none and charge1 != ""{ + charge1.replace("^", "") + } else if charge2 != none and charge2 != ""{ + charge2.replace("^", "") + } else{ + none + } + + if charge != none and charge != ""{ + if charge.contains("."){ + charge = charge.replace(".", "") + radical = true + } + if charge == "-"{ + charge = -1 + } else if charge.contains("-"){ + charge = -int(charge.replace("-", "")) + } else if charge == "+" { + charge = 1 + } else if charge.replace("+", "").contains(regex("^[0-9]+$")){ + charge = int(charge.replace("+", "")) + } + } + + return (count, charge, radical) +} + +#let string-to-element(formula) = { + let element-match = formula.match(patterns.element) + + if element-match != none{ + let element = ( + type: "element", + symbol: element-match.captures.at(0), + ) + let x = get-count-and-charge(element-match.captures.at(1), + element-match.captures.at(3), + element-match.captures.at(2), + element-match.captures.at(4), + ) + if x.at(0) != none{ + element.count = x.at(0) + } + if x.at(1) != none{ + element.charge = x.at(1) + } + if x.at(2) { + element.radical = x.at(2) + } + return (true, element, element-match.end) + } + return (false,) +} + +#let string-to-math(formula) = { + let match = formula.match(patterns.math) + + if match != none{ + let math-content = ( + type: "content", + body: eval(match.text), + ) + return (true, math-content, match.end) + } + return (false,) +} + +#let string-to-group(formula) = { + let group-match = formula.match(patterns.group) + if group-match != none{ + + let group-content = group-match.captures.at(0) + let kind = if group-content.at(0) == "("{ + group-content = group-content.trim(regex("[()]"), repeat:false) + 0 + } else if group-content.at(0) == "["{ + group-content = group-content.trim(regex("[\[\]]"), repeat:false) + 1 + }else if group-content.at(0) == "{"{ + group-content = group-content.trim(regex("[{}]"), repeat:false) + 2 + } + let x = get-count-and-charge(group-match.captures.at(1), + group-match.captures.at(3), + group-match.captures.at(2), + group-match.captures.at(4), + ) + + let group = ( + type: "group", + kind:kind, + children:() + ) + if x.at(0) != none{ + group.count = x.at(0) + } + if x.at(1) != none{ + group.charge = x.at(1) + } + if x.at(2) { + group.radical = x.at(2) + } + + let random-content = "" + + let remaining = group-content + while remaining.len() > 0 { + + if remaining.at(0) == "&"{ + group.children.push((type:"align",)) + remaining = remaining.slice(1) + continue + } + let math-result = string-to-math(remaining) + if math-result.at(0) { + if random-content != none and random-content != ""{ + group.children.push((type:"content", body:[#random-content])) + } + random-content = "" + group.children.push(math-result.at(1)) + remaining = remaining.slice(math-result.at(2)) + continue + } + + let element = string-to-element(remaining) + if element.at(0) { + if random-content != none and random-content != ""{ + group.children.push((type:"content", body:[#random-content])) + } + random-content = "" + group.children.push(element.at(1)) + remaining = remaining.slice(element.at(2)) + continue + } + + let result = string-to-group(remaining) + if result.at(0) { + if random-content != none and random-content != ""{ + group.children.push((type:"content", body:[#random-content])) + } + random-content = "" + group.children.push(result.at(1)) + remaining = remaining.slice(result.at(2)) + continue + } + + random-content += remaining.codepoints().at(0) + remaining = remaining.slice(remaining.codepoints().at(0).len()) + } + + if random-content != none and random-content != ""{ + group.children.push((type:"content", body:[#random-content])) + } + return (true, group, group-match.end) + } + return (false,) +} + + +//this will assume that the string is a molecule for performance reasons +#let molecule-string-to-ir(formula) = { + let remaining = formula.trim() + if remaining.len() == 0 { + return none + } + + let molecule = ( + type: "molecule", + children:() + ) + + let random-content = "" + + while remaining.len() > 0 { + + if remaining.at(0) == "&"{ + molecule.children.push((type:"align",)) + remaining = remaining.slice(1) + continue + } + + let math-result = string-to-math(remaining) + if math-result.at(0) { + if random-content != none and random-content != ""{ + molecule.children.push((type:"content", body:[#random-content])) + } + random-content = "" + molecule.children.push(math-result.at(1)) + remaining = remaining.slice(math-result.at(2)) + continue + } + + let element = string-to-element(remaining) + + if element.at(0) { + if random-content != none and random-content != ""{ + molecule.children.push((type:"content", body:[#random-content])) + } + random-content = "" + molecule.children.push(element.at(1)) + remaining = remaining.slice(element.at(2)) + continue + } + + let group = string-to-group(remaining) + if group.at(0) { + if random-content != none and random-content != ""{ + molecule.children.push((type:"content", body:[#random-content])) + } + random-content = "" + molecule.children.push(group.at(1)) + remaining = remaining.slice(group.at(2)) + continue + } + + random-content += remaining.codepoints().at(0) + remaining = remaining.slice(remaining.codepoints().at(0).len()) + } + + if random-content != none and random-content != ""{ + molecule.children.push((type:"content", body:[#random-content])) + } + return molecule +} + +#let string-to-ir(reaction) = { + let remaining = reaction.trim() + if remaining.len() == 0 { + return none + } + let full-reaction = () + + let current-molecule = ( + type: "molecule", + children:() + ) + + let random-content = "" + while remaining.len() > 0 { + + if remaining.at(0) == "&"{ + if current-molecule.children.len() > 0{ + full-reaction.push(current-molecule) + current-molecule = (type:"molecule", children:()) + } + full-reaction.push((type:"align",)) + remaining = remaining.slice(1) + continue + } + let math-result = string-to-math(remaining) + if math-result.at(0) { + if random-content != none and random-content != ""{ + full-reaction.push((type:"content", body:[#random-content])) + } + random-content = "" + full-reaction.push(math-result.at(1)) + remaining = remaining.slice(math-result.at(2)) + continue + } + + let element = string-to-element(remaining) + if element.at(0) { + if random-content != none and random-content != ""{ + if current-molecule.children.len() == 0{ + full-reaction.push((type:"content", body:[#random-content])) + } else{ + current-molecule.children.push((type:"content", body:[#random-content])) + } + } + random-content = "" + current-molecule.children.push(element.at(1)) + remaining = remaining.slice(element.at(2)) + continue + } + + let group = string-to-group(remaining) + if group.at(0) { + if random-content != none and random-content != ""{ + if current-molecule.children.len() == 0{ + full-reaction.push((type:"content", body:[#random-content])) + } else{ + current-molecule.children.push((type:"content", body:[#random-content])) + } + } + random-content = "" + current-molecule.children.push(group.at(1)) + remaining = remaining.slice(group.at(2)) + continue + } + + let plus-match = remaining.match(patterns.reaction-plus) + if plus-match != none{ + if current-molecule.children.len() > 0{ + full-reaction.push(current-molecule) + current-molecule = (type:"molecule", children:()) + } + if random-content != none and random-content != ""{ + full-reaction.push((type:"content", body:[#random-content])) + } + random-content = "" + full-reaction.push((type:"+")) + remaining = remaining.slice(plus-match.end) + continue + } + + let arrow-match = remaining.match(patterns.reaction-arrow) + if arrow-match != none{ + if current-molecule.children.len() > 0{ + full-reaction.push(current-molecule) + current-molecule = (type:"molecule", children:()) + } + if random-content != none and random-content != ""{ + full-reaction.push((type:"content", body:[#random-content])) + } + random-content = "" + let arrow = ( + type:"arrow", + kind:arrow-string-to-kind(arrow-match.captures.at(0)), + ) + if arrow-match.captures.at(1) != none{ + arrow.top = string-to-ir(arrow-match.captures.at(1)) + } + if arrow-match.captures.at(2) != none{ + arrow.bottom = string-to-ir(arrow-match.captures.at(2)) + } + full-reaction.push(arrow) + remaining = remaining.slice(arrow-match.end) + continue + } + + random-content += remaining.codepoints().at(0) + remaining = remaining.slice(remaining.codepoints().at(0).len()) + } + if current-molecule.children.len() != 0{ + full-reaction.push(current-molecule) + } + if random-content != none and random-content != ""{ + full-reaction.push((type:"content", body:[#random-content])) + } + + return full-reaction +} \ No newline at end of file diff --git a/src/utils.typ b/src/utils.typ index 9a74644..d986251 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -67,6 +67,98 @@ "7p": 6, ) +#let brackets = ( + math.paren.l, + math.bracket.l, + math.brace.l, + math.bar.v, + math.paren.r, + math.bracket.r, + math.brace.r, + math.bar.v, +) + +#let arrows = ( + sym.arrow.r.l, + sym.arrow.r, + sym.arrow.l, + sym.arrow.r.double, + sym.arrow.l.double, + sym.arrow.r.not, + sym.arrow.l.not, + sym.harpoons.rtlb +) +#let arrow-kinds = ( + "<->":0, + "->":1, + "<-":2, + "=>":3, + "<+":4, + "-/>":5, + "":7, +) + +#let get-bracket(kind, open: true) = { + if not open{ + kind += 4 + } + brackets.at(kind, default:none) +} +#let get-arrow(kind) = { + arrows.at(kind, default:sym.arrow.r) +} + +#let phase-to-content(phase) = { + if phase == none{ + none + } else if type(phase) == str{ + "(" + phase + ")" + } +} + +#let count-to-content(factor) = { + if factor == none{ + none + } else if type(factor) == int{ + if factor > 1{ + str(factor) + } + } +} +#let arrow-string-to-kind(arrow) = { + arrow = arrow.trim() + arrow-kinds.at(arrow, default: 1) +} +#let charge-to-content(charge, radical: false) = { + if charge == none{ + none + } else if type(charge) == int { + if radical{ + sym.bullet + } + if charge < 0 { + if calc.abs(charge) > 1{ + str(calc.abs(charge)) + } + math.minus + } + else if charge > 0 { + if charge > 1{ + str(charge) + } + math.plus + } + else { + none + } + } else if type(charge) == str{ + charge.replace(".", sym.bullet) + .replace("-", math.minus) + .replace("+", math.plus) + } +} + #let parser-config = ( arrow: (arrow_size: 120%, reversible_size: 120%), conditions: ( @@ -107,7 +199,7 @@ type(it) == content and it.func() == heading and it.depth <= depth } -// Following utility methods are from: +// Following utility method is from: // https://github.com/typst-community/linguify/blob/b220a5993c7926b1d2edcc155cda00d2050da9ba/lib/utils.typ#L3 #let if-auto-then(val,ret) = { if (val == auto){ @@ -117,6 +209,13 @@ } } +#let try-at(value, field, default:none) = { + if (value == none){ + none + } else { + value.at(field, default: default) + } +} // own utils diff --git a/tests/README-graphic1/ref/1.png b/tests/README-graphic1/ref/1.png index 8978ab0..88ec8da 100644 Binary files a/tests/README-graphic1/ref/1.png and b/tests/README-graphic1/ref/1.png differ diff --git a/tests/README-graphic1/ref/formula-parser.svg b/tests/README-graphic1/ref/formula-parser.svg deleted file mode 100644 index b637b48..0000000 --- a/tests/README-graphic1/ref/formula-parser.svg +++ /dev/null @@ -1,268 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/README-graphic1/test.typ b/tests/README-graphic1/test.typ index f1feff5..2c7e2a8 100644 --- a/tests/README-graphic1/test.typ +++ b/tests/README-graphic1/test.typ @@ -1,4 +1,4 @@ #import "../../src/lib.typ" : ce #set page(width: auto, height: auto, margin: 0.5em) -#ce("[Cu(H2O)4]^2+ + 4NH3 -> [Cu(NH3)4]^2+ + 4H2O") +$#ce("[Cu(H2O)4]^2+ + 4NH3 -> [Cu(NH3)4]^2+ + 4H2O")$ diff --git a/tests/arrow-align/ref/1.png b/tests/arrow-align/ref/1.png index f857144..b63e8a0 100644 Binary files a/tests/arrow-align/ref/1.png and b/tests/arrow-align/ref/1.png differ diff --git a/tests/brackets/ref/1.png b/tests/brackets/ref/1.png index 5883d03..b6da07b 100644 Binary files a/tests/brackets/ref/1.png and b/tests/brackets/ref/1.png differ diff --git a/tests/charges/ref/1.png b/tests/charges/ref/1.png index 4bbcda0..3d34e73 100644 Binary files a/tests/charges/ref/1.png and b/tests/charges/ref/1.png differ diff --git a/tests/charges/test.typ b/tests/charges/test.typ index f626f90..931f744 100644 --- a/tests/charges/test.typ +++ b/tests/charges/test.typ @@ -9,8 +9,8 @@ #linebreak() #ce("OH- + H+ -> H2O") #linebreak() -#ce("OH 1-") +#ce("OH-3") #linebreak() -#ce("Fe(OH)2 ^0") +#ce("Fe(OH)2^0") #linebreak() -#ce("PO4 3-") +#ce("PO4-3") diff --git a/tests/intermediate-representation-molecules/.gitignore b/tests/intermediate-representation-molecules/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/intermediate-representation-molecules/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/intermediate-representation-molecules/ref/1.png b/tests/intermediate-representation-molecules/ref/1.png new file mode 100644 index 0000000..59bf7b8 Binary files /dev/null and b/tests/intermediate-representation-molecules/ref/1.png differ diff --git a/tests/intermediate-representation-molecules/test.typ b/tests/intermediate-representation-molecules/test.typ new file mode 100644 index 0000000..c8fb9d2 --- /dev/null +++ b/tests/intermediate-representation-molecules/test.typ @@ -0,0 +1,90 @@ +#import "../../src/display-intermediate-representation.typ" : display-ir +#set page(width: auto, height: auto, margin: 0.5em) + +#let co2 = ( + type:"molecule", + count:1, + phase:"g", + charge:0, + align:none, + arrow:none, + children:( + ( + type:"element", + count:1, + symbol:"C", + charge:0, + oxidation-number:none, + isotope:none, + align:none, + ), + ( + type:"element", + count:2, + symbol:"O", + charge:0, + oxidation-number:none, + isotope:none, + align:none, + ) + ) +) + +#let hexacyanidoferrat = ( + type:"molecule", + count:3, + phase:"s", + charge:0, + align:none, + arrow:none, + children:( + ( + type:"group", + count:2, + kind:1, + charge:4, + align:none, + children:( + ( + type:"element", + count:1, + symbol:"Fe", + charge:0, + oxidation-number:none, + isotope:none, + align:none, + ), + ( + type:"group", + count:6, + kind:0, + charge:0, + align:none, + children:( + ( + type:"element", + count:1, + symbol:"C", + charge:0, + oxidation-number:none, + isotope:none, + align:none, + ), + ( + type:"element", + count:1, + symbol:"N", + charge:0, + oxidation-number:none, + isotope:none, + align:none, + ), + ) + ), + ) + ), + ) +) + +#display-ir(co2)\ +#display-ir(hexacyanidoferrat) \ No newline at end of file diff --git a/tests/intermediate-representation-reactions/.gitignore b/tests/intermediate-representation-reactions/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/intermediate-representation-reactions/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/intermediate-representation-reactions/ref/1.png b/tests/intermediate-representation-reactions/ref/1.png new file mode 100644 index 0000000..c3fbed2 Binary files /dev/null and b/tests/intermediate-representation-reactions/ref/1.png differ diff --git a/tests/intermediate-representation-reactions/test.typ b/tests/intermediate-representation-reactions/test.typ new file mode 100644 index 0000000..c7e294d --- /dev/null +++ b/tests/intermediate-representation-reactions/test.typ @@ -0,0 +1,196 @@ +#import "../../src/display-intermediate-representation.typ" : display-ir +#set page(width: auto, height: auto, margin: 0.5em) + +#let reaction1 = ( + ( + type: "molecule", + charge:2, + children:( + ( + type:"group", + kind:1, + children:( + ( + type:"element", + symbol:"Cu", + ), + ( + type:"group", + kind:0, + count:4, + children:( + ( + type:"element", + count:2, + symbol:"H", + ), + ( + type:"element", + symbol:"O", + ), + ) + ), + ) + ), + ) + ), + (type: "align"), + ( + type: "arrow", + kind: 1, + top: none, + bottom: none, + ), + ( + type: "molecule", + charge:2, + children:( + ( + type:"group", + kind:1, + children:( + ( + type:"element", + symbol:"Cu", + ), + ( + type:"group", + kind:0, + count:4, + children:( + ( + type:"element", + symbol:"N", + ), + ( + type:"element", + count:3, + symbol:"H", + ), + ) + ), + ) + ), + ) + ), + (type: "+"), + ( + type: "molecule", + count:4, + children:( + ( + type:"element", + count:2, + symbol:"H", + ), + ( + type:"element", + symbol:"O", + ), + ) + ) +) + +#let reaction2 = ( + ( + type: "molecule", + charge:2, + children:( + ( + type:"group", + kind:1, + children:( + ( + type:"element", + symbol:"Cu", + ), + ( + type:"group", + kind:0, + count:4, + children:( + ( + type:"element", + count:2, + symbol:"H", + ), + ( + type:"element", + symbol:"O", + ), + ) + ), + ) + ), + ) + ), + ( + type:"+" + ), + ( + type: "molecule", + count:4, + children:( + ( + type:"element", + symbol:"N", + ), + ( + type:"element", + count:3, + symbol:"H", + ), + ) + ), + (type: "align"), + ( + type: "arrow", + kind: 1, + top: ( + ( + type:"content", + body:[dissolve in ] + ), + ( + type: "molecule", + children:( + ( + type:"element", + count:2, + symbol:"H", + ), + ( + type:"element", + symbol:"O", + ), + ) + ), + ), + bottom: ( + ( + type:"content", + body:$Delta H^0$ + ), + ), + ), + ( + type: "molecule", + count:4, + children:( + ( + type:"element", + count:2, + symbol:"H", + ), + ( + type:"element", + symbol:"O", + ), + ) + ), +) + +$ + #display-ir(reaction1)\ + #display-ir(reaction2)\ +$ \ No newline at end of file diff --git a/tests/parse-ir-elements/.gitignore b/tests/parse-ir-elements/.gitignore new file mode 100644 index 0000000..03681f9 --- /dev/null +++ b/tests/parse-ir-elements/.gitignore @@ -0,0 +1,5 @@ +# generated by tytanic, do not edit + +diff/** +out/** +ref/** diff --git a/tests/parse-ir-elements/test.typ b/tests/parse-ir-elements/test.typ new file mode 100644 index 0000000..0b48ce8 --- /dev/null +++ b/tests/parse-ir-elements/test.typ @@ -0,0 +1,79 @@ +#import "../../src/parse-formula-intermediate-representation.typ" : molecule-string-to-ir +#import "../../src/lib.typ" : display-ir +#set page(width: auto, height: auto, margin: 0.5em) + +#let co2 = ( + type: "molecule", + children: ( + (type: "element", symbol: "C"), + (type: "element", symbol: "O", count: 2), + ), + ) +#let ir-co2 = molecule-string-to-ir("CO2") + +#let no = ( + type: "molecule", + children: ( + (type: "element", symbol: "N"), + ( + type: "element", + symbol: "O", + charge: -2, + radical: true, + ), + ), + ) +#let ir-no = molecule-string-to-ir("NO^2.-") + +#let na = ( + type: "molecule", + children: ( + (type: "element", symbol: "Na", count: 3, charge: 1), + ), + ) +#let ir-na1 = molecule-string-to-ir("Na_3^+") +#let ir-na2 = molecule-string-to-ir("Na_3^+") + +#let cl = ( + type: "molecule", + children: ( + ( + type: "element", + symbol: "Cl", + count: 2, + charge: -1, + ), + ), + ) +#let ir-cl = molecule-string-to-ir("Cl2-1") + +#let fe = ( + type: "molecule", + children: ( + ( + type: "element", + symbol: "Fe", + count: 2, + charge: "III", + ), + ), + ) +#let ir-fe = molecule-string-to-ir("Fe2^III") + +#display-ir(ir-co2) +#display-ir(ir-no) +#display-ir(ir-cl) +#display-ir(ir-fe) +#display-ir(ir-na1) +#display-ir(ir-na2)\ +#display-ir(co2) +#display-ir(no) +#display-ir(cl) +#display-ir(fe) +#display-ir(na) +#assert(co2 == ir-co2) +#assert(no == ir-no) +#assert(na == ir-na1) +#assert(na == ir-na2) +#assert(cl == ir-cl) +#assert(fe == ir-fe) \ No newline at end of file diff --git a/tests/parse-ir-groups/.gitignore b/tests/parse-ir-groups/.gitignore new file mode 100644 index 0000000..03681f9 --- /dev/null +++ b/tests/parse-ir-groups/.gitignore @@ -0,0 +1,5 @@ +# generated by tytanic, do not edit + +diff/** +out/** +ref/** diff --git a/tests/parse-ir-groups/test.typ b/tests/parse-ir-groups/test.typ new file mode 100644 index 0000000..b171a3d --- /dev/null +++ b/tests/parse-ir-groups/test.typ @@ -0,0 +1,43 @@ +#import "../../src/parse-formula-intermediate-representation.typ" : molecule-string-to-ir + +#let trisethylendiamin = ( + type: "molecule", + children: ( + ( + type: "group", + kind: 1, + children: ( + (type: "element", symbol: "Co"), + ( + type: "group", + kind: 0, + children: ((type: "content", body: [en]),), + count: 3, + ), + ), + ), + (type: "element", symbol: "Cl", count: 3), + ), + ) +#let ir-trisethylendiamin = molecule-string-to-ir("[Co(en)3]Cl3") + +#let fenh3 = ( + type: "molecule", + children: ( + (type: "element", symbol: "Fe"), + ( + type: "group", + kind: 1, + children: ( + (type: "element", symbol: "N"), + (type: "element", symbol: "H", count: 3), + ), + count: 2, + charge: 1, + ), + ), + ) +#let ir-fenh3 = molecule-string-to-ir("Fe[NH3]2+") + +#assert(trisethylendiamin == ir-trisethylendiamin) +#assert(fenh3 == ir-fenh3) \ No newline at end of file diff --git a/tests/shell-configuration/.gitignore b/tests/shell-configuration/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/shell-configuration/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/shell-configuration/ref/1.png b/tests/shell-configuration/ref/1.png new file mode 100644 index 0000000..4e897ee Binary files /dev/null and b/tests/shell-configuration/ref/1.png differ diff --git a/tests/shell-configuration/test.typ b/tests/shell-configuration/test.typ new file mode 100644 index 0000000..f0d7e80 --- /dev/null +++ b/tests/shell-configuration/test.typ @@ -0,0 +1,8 @@ +#import "../../src/display-shell-configuration.typ" : get-shell-configuration, display-electron-configuration, get-electron-configuration +#import "../../src/lib.typ" : get-element +#set page(width: auto, height: auto, margin: 0.5em) + +#let carbon = get-element(symbol:"Y") +#let shells = get-shell-configuration(carbon) +#let orbitals = get-electron-configuration(carbon) +#display-electron-configuration(carbon) diff --git a/tests/simple-formulas/ref/1.png b/tests/simple-formulas/ref/1.png index a9d934e..a7eccaf 100644 Binary files a/tests/simple-formulas/ref/1.png and b/tests/simple-formulas/ref/1.png differ diff --git a/tests/simple-formulas/test.typ b/tests/simple-formulas/test.typ index bfcb174..10ef719 100644 --- a/tests/simple-formulas/test.typ +++ b/tests/simple-formulas/test.typ @@ -3,11 +3,11 @@ #set page(width: auto, height: auto, margin: 0.5em) -#ce("H2SO4") +$#ce("H2SO4")$ #linebreak() -#ce("12Fe2 (SO4)3") +$#ce("12Fe2(SO4)3")$ #linebreak() -#ce("514H2O") +$#ce("514H2O")$ #linebreak() -#ce("9Fe(OH)3") +$#ce("9Fe(OH)3")$