as Schönes aus. https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9e3&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Sehen Sie dieses E-Mail in einem https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9e4&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824:::&p1=%40hRefGTjjF5tIwcD7odIOh%2FfwR7XIYrfwJFP1A9Kv9%2Fs%3D Web-Browser an Cards & KeyClub https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9e5&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Für besondere Geschenke Ihre Punkte warten auf Sie Sehr geehrte Frau Pfeiffer Lösen Sie Ihre Punkte ein, bevor sie Ende Jahr verfallen: Finden Sie Ihre Lieblingsangebote im eStore oder spenden Sie Ihre Punkte und unterstützen Sie wohltätige Organisationen. Wir wünschen Ihnen wunderbare Festtage. https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9e6&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Angebote entdecken 〉 Mit freundlichen Grüssen https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9e7&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9e8&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Festlich einschenken mit Mövenpick Wein Im Dezember erhalten Sie im KeyClub eStore eine Digitale Geschenkkarte von Mövenpick Wein im Wert von CHF 120 für nur CHF 100. https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9e9&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: CHF 20 sparen 〉 https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9ea&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9eb&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Weihnachten mit der Geschenkkarte von Manor Sichern Sie sich jetzt 10% Rabatt beim Kauf einer Digitalen Geschenkkarte von Manor im Wert von CHF 50 und CHF 100. Einlösbar auf manor.ch oder vor Ort in allen Manor Warenhäusern. Angebot gültig, solange der Vorrat reicht. https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9ec&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: 10% Rabatt 〉 https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9ed&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9ee&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9ef&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Schönes zum Fest von Zalando Bei Zalando entdecken Sie Styles und Marken, die Freude bereiten. Beim Kauf eines E-Gutscheins im Wert von CHF 50 erhalten Sie jetzt einen 20%‑Beauty‑Voucher gratis dazu. https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9f0&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Zum Angebot 〉 https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9f1&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9f2&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Exklusive Hotelvorteile mit Ihrer UBS Kreditkarte Sichern Sie sich Ihre kostenlose suitespot Premium Mitgliedschaft und profitieren Sie von flexiblen Buchungen, kostenlosem Frühstück und Upgrades (nach Verfügbarkeit) mit Ihrer UBS Gold oder Platinum Kreditkarte. https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9f3&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Sich jetzt registrieren 〉 https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9f4&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9f5&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9f6&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Auf Geschenksuche für die Liebsten? Beim Kauf einer Digitalen Geschenkkarte von Globus für CHF 200 erhalten Sie jetzt ein Extraguthaben von CHF 20. Schnell sein lohnt sich: Das Angebot ist limitiert auf 250 Exemplare. https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9f7&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Sich Guthaben sichern 〉 https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9f8&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9f9&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Manchmal braucht es mehr als ein Pflaster Die UBS Optimus Foundation engagiert sich für die psychische Gesundheit von jungen Menschen in der Schweiz und weltweit. Alle Spenden, die vor dem 31. Dezember 2025 eingehen, werden bis zu einem Total von CHF 500 000 durch UBS und Partner verdoppelt. https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9fa&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Jetzt spenden 〉 https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9fb&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Abmelden https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9fd&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824:::&p1=43130732&p2=%40FKQqQBul1y7sqvb4Us%2B34A%3D%3D&p3=43473897&p4=de&p5= #end --> Impressum Absender: UBS Switzerland AG, Bahnhofstrasse 45, 8001 Zürich Diese Informationen wurden von UBS Switzerland AG und/oder ihren Tochtergesellschaften und/oder verbundenen Unternehmen ("UBS", "wir") herausgegeben. Aufgrund des bisherigen E-Mail-Verkehrs mit Ihnen beziehungsweise der mit Ihnen getroffenen Absprachen halten wir uns für berechtigt, Sie per ungesicherter E-Mail zu kontaktieren. Sollten Sie diese E-Mail nicht mehr von uns erhalten wollen, können Sie diesen Newsletter jederzeit über den Abmeldelink oben abbestellen. Sollten Sie diese E-Mail irrtümlich erhalten haben, informieren Sie uns bitte und löschen Sie die E-Mail (mit allen Anhängen) unwiderruflich. Leiten Sie den Inhalt nicht weiter und machen Sie ihn nicht für andere Personen zugänglich. E-Mails werden in der Regel über offene, allfällig für Dritte zugängliche Systeme grenzüberschreitend versandt. Daher birgt die Nutzung von E-Mails gewisse Risiken, insbesondere (1) mangelnde Vertraulichkeit und, je nach den an der Übermittlung beteiligten Rechtsordnungen, ein geringeres Datenschutzniveau, (2) Manipulation oder Fälschung der Adresse der Absenderin beziehungsweise des Absenders oder des Inhalts, (3) Missbrauch mit der Folge von Schäden durch das Abfangen von E-Mails durch Dritte, (4) Systemausfälle und andere Übertragungsfehler, die dazu führen können, dass E-Mails und ihre Anhänge verzögert, beschädigt, fehlgeleitet oder gelöscht werden, und (5) Schadprogramme, die von Dritten unbemerkt per E-Mail verbreitet werden und erheblichen Schaden anrichten können. Die Empfängerin beziehungsweise der Empfänger akzeptiert die Risiken der Nutzung von E-Mails. Kundinnen und Kunden nehmen insbesondere das Risiko in Kauf, dass die Bankbeziehung und die damit verbundenen vertraulichen Informationen gegenüber Dritten offengelegt werden könnten. Es liegt in der eigenen Verantwortung der Empfängerin beziehungsweise des Empfängers, die für öffentliche elektronische Netze üblichen Sicherheitsvorkehrungen zu kennen und umzusetzen (zum Beispiel durch Installation einer Firewall und Verwendung eines regelmässig zu aktualisierenden Antivirenprogramms) und die E-Mail-Adresse nur von Geräten aus zu nutzen, die gegen elektronische Angriffe und unbefugte Nutzung geschützt sind. Im Falle von Zweifeln bezüglich der Herkunft einer E-Mail muss die Empfängerin beziehungsweise der Empfänger UBS telefonisch kontaktieren. Soweit gesetzlich erlaubt, ist UBS nicht haftbar für Verluste oder Schäden, die aus der Verwendung von E-Mails resultieren. UBS akzeptiert keine per E-Mail übermittelten Transaktionsaufträge, darunter unter anderem die Eröffnung von Konten, Zahlungs- und Börsenaufträge, der Widerruf von Aufträgen oder Genehmigungen, die Sperrung von Kreditkarten, Adressänderungen und so weiter. Gehen trotzdem solche E-Mails ein, ist UBS nicht verpflichtet, auf diese zu reagieren oder zu antworten. Obwohl alle Informationen und Meinungen aus Quellen stammen, die als zuverlässig eingestuft und in gutem Glauben abgegeben werden, lehnen wir jede Haftung für unrichtige oder unvollständige Informationen ab. Die Meinungsäusserungen von externen Autorinnen beziehungsweise Autoren entsprechen ihren eigenen Ansichten, die von der offiziellen Position von UBS abweichen können. Die Aktualität der Informationen beschränkt sich auf das Datum der Veröffentlichung oder den Zeitpunkt des Versands (zum Beispiel bei E-Mails). Daher müssen wir diese Informationen nicht auf dem neuesten Stand halten. Die zur Verfügung gestellten Inhalte dienen zu reinen Informationszwecken. Sie sind nicht als Empfehlung, Offerte oder Aufforderung zur Offertstellung zum Kauf oder Verkauf von Anlage- oder anderen spezifischen Produkten zu verstehen. Sie stellen keine Anlage-, Rechts- oder Steuerberatung dar und sollten nicht als Grundlage für Anlageentscheide dienen. UBS behält sich das Recht vor, Dienstleistungen, Produkte und Preise jederzeit ohne Vorankündigung zu ändern. Einzelne Dienstleistungen und Produkte können von Personen mit Wohnsitz in bestimmten Ländern oder von bestimmten Kategorien von Investoren unter Umständen nicht erworben werden. Diese E-Mail kann Inhalte Dritter oder Links zu Webseiten Dritter enthalten. Diese Inhalte und Links dienen ausschliesslich der Benutzerfreundlichkeit und der Information. UBS besitzt keine Kontrolle über die Inhalte oder Webseiten Dritter, übernimmt keinerlei Verantwortung oder Gewähr für diese Inhalte oder Webseiten und macht diesbezüglich keinerlei Zusicherungen. Dies schliesst unter anderem die Richtigkeit, den Inhalt, die Qualität oder die Aktualität dieser Webseiten ein. UBS ist nicht für die Inhalte oder Webseiten Dritter sowie für Webseiten haftbar, die auf die UBS-Webseite verlinken oder diese in Frames anzeigen. Um unser Angebot auf Ihre Interessen abzustimmen, erheben wir zu Analysezwecken Daten über Ihr Leseverhalten dieser E-Mail, zum Beispiel ob Sie die E-Mail öffnen, welche Inhalte Sie interessieren und ob Sie die E-Mail weiterleiten. Dies geschieht mithilfe einer eindeutigen E-Mail-ID. Es werden keine Cookies oder ähnliche Programme auf Ihrem Gerät installiert. Wenn Sie keine Nachverfolgung wünschen, können Sie diesen Newsletter jederzeit über den oben genannten Abmeldelink abbestellen. Weitere Informationen über die Verarbeitung Ihrer Daten, über Ihre Rechte im Zusammenhang mit Ihren Daten sowie die Kontaktdaten unseres Group Data Protection Office finden Sie in unserer https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9fe&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: Datenschutzerklärung . © UBS 2025. Das Schlüsselsymbol und UBS gehören zu den geschützten Marken von UBS. Alle Rechte vorbehalten. https://secure.ubs.com/campaign/r/?id=t59581894,6ec6122,28c3e9ff&campID=UC:E:601216:601221:43130732:0:116154656:116154658:de:657424824::: ------=_NextPart_231_E519BAD3.E519BAD3 Content-Type: text/html; charset="utf-8" Content-Transfer-Encoding: quoted-printable = Bevor sie verfallen: KeyClub-Punkte einl=C3=B6sen oder spenden</ti= tle> <meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=3D= 1"> <meta http-equiv=3D"X-UA-Compatible" content=3D"IE=3Dedge"> <meta name=3D"keywords"> <!--[if !mso]><!--> <style media=3D"all" type=3D"text/css"> @font-face { font-family: "Frutiger 55 Roman"; src: local("Frutiger 55 Roman"), url("https://www.ubs.com/campa= ign/res/ubs_extfront_prod/41993bac9a92cc785f0035c9615457e3.eot=3F#iefix"); src: local("Frutiger 55 Roman"), url("https://www.ubs.com/campa= ign/res/ubs_extfront_prod/41993bac9a92cc785f0035c9615457e3.eot=3F#iefix") f= ormat("eot"), url("https://www.ubs.com/campaign/res/ubs_extfront_prod/36= 5019ed069cc0924e3430da4aa5c705.woff") format("woff"), url("https://www.ubs.com/campaign/res/ubs_extfront_prod/a5= d9a697ffbb66493b1253a18160f95a.ttf") format("truetype"), url("https://www.ubs.com/campaign/res/ubs_extfront_prod/99= 007186c1e0f342b6a8cf166d9e88ad.svg#c59a36ab-4ef8-4ee1-a2f8-48aa79d4f877") f= ormat("svg"); font-style: normal; font-weight: 400; font-display: swap; } @font-face { font-family: "Frutiger 45 Light"; src: local("Frutiger 45 Light"), url("https://www.ubs.com/campa= ign/res/ubs_extfront_prod/b8b1c28214bb7671bf9537b3940709c8.eot=3F#iefix"); src: local("Frutiger 45 Light"), url("https://www.ubs.com/campa= ign/res/ubs_extfront_prod/b8b1c28214bb7671bf9537b3940709c8.eot=3F#iefix") f= ormat("eot"), url("https://www.ubs.com/campaign/res/ubs_extfront_prod/2b= 70981d4208186bc9e71e8b51662738.woff") format("woff"), url("https://www.ubs.com/campaign/res/ubs_extfront_prod/09= c9dc3e3fcc26d4a8b7ce335d0db826.ttf") format("truetype"), url("https://www.ubs.com/campaign/res/ubs_extfront_prod/ad= 90bb8ddeea12eb0505b9a83f6701f7.svg#3f5a5b87-e71e-4544-be0c-da4daa132710") f= ormat("svg"); font-style: normal; font-weight: 300; font-display: swap; } </style> <!--[endif]----> <style media=3D"all" type=3D"text/css"> /* Boilerplate styles */ a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } body { width: 100% !important; min-width: 100%; padding: 0; margin: 0; font-family: "Frutiger 55 Roman", Segoe UI Light, Arial, Helvet= ica, sans-serif; font-size: 16px; font-weight: 400; color: #1c1c1c; background-color: #FFFFFF; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .ReadMsgBody, /* Outlook.com wrapper class */ .ExternalClass { width: 100%; background-color: #fff; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } p { margin: 0; padding: 0; } table { border-collapse: collapse; border-spacing: 0; mso-table-lspace: 0pt !important; mso-table-rspace: 0pt !important; } img { text-decoration: none; outline: 0; border: 0; vertical-align: middle; -ms-interpolation-mode: bicubic; } /* End boilerplate styles */ /* Link specific css */ span.MsoHyperlink, span.MsoHyperlinkFollowed { mso-style-priority: 99; color: inherit; } a { color: inherit; text-decoration: underline; /* Breaks the inheritance of text-decoration from a parent elem= ent so it can be overridden */ display: inline-block; } .owaContextualHighlight span { border: none !important; color: inherit !important; } /* End link specific css */ /* Hyperlink platform fixes */ /* Prevent Apple blue links. */ a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } /* Prevent Gmail blue links. */ u+#body a { color: inherit; text-decoration: none; font-size: inherit; font-family: inherit; font-weight: inherit; line-height: inherit; } /* Prevent Samsung blue links. */ #MessageViewBody a { color: inherit; text-decoration: none; font-size: inherit; font-family: inherit; font-weight: inherit; line-height: inherit; } /* End hyperlink platform fixes section */ /* Mobile styles */ @media screen and (max-width: 480px) { .wrapper, .container, .module { width: 100% !important; } .moduleInner { width: auto !important; } .mobileHide { display: none !important; mso-hide: all !important; } .mobileShow { display: block !important; mso-hide: none !important; } .twoColMobileShow { display: table !important; mso-hide: none !important; } .mobilePadding--left0 { padding-left: 0 !important; } .button__wrapper, .link_list__wrapper, .hpadding--mobile { padding-left: 10px !important; padding-right: 10px !important; } .two-col-hpadding--mobile { width: 10px !important; } } /* End Mobile styles */ </style> <style media=3D"all" type=3D"text/css"> .textimage__rte a { color: #da0000 !important; font-weight: 400 !important; text-decoration: underline !important; font-style: normal !important; } =2Etextimage__rte p { margin: 0 0 20px !important; } =2Etextimage__rte ul, =2Etextimage__rte ol { margin-top: 0; margin-bottom: 20px; padding-left: 20px; mso-text-indent-alt: 0; mso-line-height-rule: exactly; line-height: 20px; mso-border-bottom-alt: solid 0 #FFFFFF; mso-padding-bottom-alt: 20px; } =2Etextimage__rte li { margin: 0 0 5px; } =2Etextimage__rte li a { display: inline; } =2Emso .textimage__rte li { text-indent: -20px; margin-left: -25px !important; } =2Etextimage .button__wrapper, =2Etextimage .link_list__wrapper { padding-top: 0 !important; padding-right: 0 !important; padding-left: 0 !important; } @media screen and (max-width: 480px) { .textimage__img--regular, .textimage__img--wide, .textimage__img--full-width, .textimage__text { width: 100%; } .textimage__img--narrow { display: inline-flex; vertical-align: top; width: 84px; } .textimage__text--narrow { display: inline-flex; vertical-align: top; width: 72%; width: calc(100% - 84px); } } =2EsubscriptionInfo__text a, =2EsubscriptionInfo__links a { color: #da0000 !important; text-decoration: underline; font-style: normal; font-weight: 400; display: inline-block; } =2EsubscriptionInfo__text p { margin: 0 0 0px !important; } =2Etable .table-title a, =2Etable .ubs-table a, =2Etable .ubs-disclaimer a { color: #DA0000 !important; font-weight: 400 !important; text-decoration: none !important; font-style: normal !important; } =2Eubs-table td.cell-title { padding: 10px 0px; vertical-align: top; } =2Eubs-table td.cell-value { padding: 10px; vertical-align: top; } =2Etable .button__wrapper { padding: 0 !important; } =2Etable .button__body { height: 44px !important; } =2Etable .button--hasBorder .button__body { padding-left: 15px !important; padding-right: 15px !important; } =2Etable .button__body a { font-family: 'Frutiger 45 Light', Segoe UI Light, Arial, Helvetica, san= s-serif !important; font-size: 16px !important; font-weight: 700 !important; line-height: 20px !important; } =2Everticalteaser__text p { margin: 0 0 20px !important; } =2Emso .verticalteaser__text li { /* Magic numbers courtesy of Microsoft */ text-indent: -20px; margin-left: -25px !important; } =2Everticalteaser__text li a { display: inline; } =2EpublicationInfo__text a { color: #e60000 !important;; text-decoration: none; font-style: normal; font-weight: 400; display: inline-block; } =2EpublicationInfo__text p { margin: 0 0 0px !important; } =2Edisclaimer__text a, =2Edisclaimer__copyright a { color: #e60000 !important; text-decoration: underline !important; font-style: normal !important; font-weight: 400 !important; } =2Edisclaimer__text p { font-size: 12px !important; line-height: 20px !important; color: #1c1c1c !important; margin: 0 0 0px !important; } =2Edisclaimer__copyright p { margin: 0 0 0px !important; } =2Edisclaimer__text .highlightColor { color: #DA0000 !important; } =2Edisclaimer__text li a, =2Edisclaimer__copyright li a { display: inline; } =2EfreeTextDisclaimer .free_text_disclaimer_2 a { color: #e60000 !important; text-decoration: underline !important; font-style: normal !important; font-weight: 400 !important; } =2EfreeTextDisclaimer .highlightColor { color: #DA0000 !important; } =2EfreeTextDisclaimer .free_text_disclaimer a { color: #e60000 !important; text-decoration: none !important; font-style: normal !important; font-weight: 400 !important; } =2EfreeTextDisclaimer .highlightColor { color: #DA0000 !important; } =2Eprimary_headline__keyline a, =2Eprimary_headline__infoline a { color: #da0000; font-weight: 400; text-decoration: underline !important; font-style: normal !important; } =2Eprimary_headline__infoline .highlightColor { color: #da0000 !important; } =2Efwteaser__text a { display: inline; color: #da0000 !important; font-weight: 400 !important; text-decoration: underline !important; font-style: normal !important; } =2Efwteaser__text p { margin: 0 0 20px !important; } =2Efwteaser__text ul, =2Efwteaser__text ol { margin-top: 0; margin-bottom: 20px; padding-left: 20px; mso-text-indent-alt: 0; mso-line-height-rule: exactly; line-height: 20px; } =2Efwteaser__text li { margin: 0 0 5px; } =2Efwteaser__text li a { display: inline; } =2Emso .fwteaser__text li { text-indent: -20px; margin-left: -25px !important; } =2Efull_width_teaser .button__wrapper { padding: 0px 0 0 !important; } =2Efull_width_teaser .button__body { height: 42px !important; } =2Efull_width_teaser .button--hasBorder .button__body { padding-left: 24px !important; padding-right: 24px !important; } =2Efull_width_teaser .button__body a { font-family: 'Frutiger 45 Light', Segoe UI Light, Arial, Helvetica, san= s-serif !important; font-size: 16px !important; font-weight: 700 !important; line-height: 20px !important; } @media (max-width: 480px) { .fwteaser__body { width: 100%; } .mobile-element { display: block; width: 100%; } .mobile-element img { max-width: 100% !important; width: 100% !important; height: auto !important; display: block; } } =2Emso .fwteaser__body { width: 396px; } =2Elink_list__text a { color: #1c1c1c; font-weight: 400; text-decoration: none !important; font-style: normal !important; } @media screen and (max-width: 480px) { .descriptor-mobile { padding-left: 10px !important; padding-right: 10px !important; } } @media (max-width: 480px) { .button--hasBorder { width: 100%; } .button__spacing { padding-right: 0 !important; } .button__body a { line-height: 1; } } =2Edropdown__colorSwatch { display: inline-block; width: 30px; height: 15px; border: 1px solid; margin: 0px 5px; } =2Everticalteaser__text a { color: #da0000 !important; text-decoration: underline !important; font-style: normal !important; font-weight: 400 !important; text-indent: 0px; } =2Everticalteaser__text ul, =2Everticalteaser__text ol { margin-top: 0; margin-bottom: 20px; padding-left: 30px; mso-text-indent-alt: 0; mso-line-height-rule: exactly; line-height: 20px; } =2Everticalteaser__text li { margin: 0 0 5px; } </style> <!--[if gte mso 9]> <xml> <o:OfficeDocumentSettings> <o:AllowPNG/> <o:PixelsPerInch>96</o:PixelsPerInch> </o:OfficeDocumentSettings> <w:WordDocument> <w:DontUseAdvancedTypographyReadingMail/> </w:WordDocument> </xml> <![endif]--> </head> <body bgcolor=3D"#FFFFFF" style=3D"background-color: #FFFFFF;"> <!--[if mso]> <table role=3D"presentation" width=3D"100%" border=3D"0" cellpadding=3D"0" = cellspacing=3D"0" bgcolor=3D"#FFFFFF" class=3D"mso" style=3D"background-col= or: #FFFFFF;"> <![endif]--> <!--[if !mso]><!--> <table role=3D"presentation" width=3D"100%" border=3D"0" cellpadding=3D"0= " cellspacing=3D"0" bgcolor=3D"#FFFFFF" style=3D"background-color: #FFFFFF;= "> <!--<![endif]--> <tbody> <tr> <td width=3D"100%" align=3D"center" valign=3D"top" bgcolor=3D"#FFFFFF"= style=3D"background-color: #FFFFFF; vertical-align: top;"> <div class=3D"container"> <div class=3D"root responsivegrid"> <div> <div class=3D"preheader"> <table role=3D"presentation" border=3D"0" cellspacing=3D"0" cellp= adding=3D"0" class=3D"module" width=3D"640" style=3D"border-collapse: colla= pse; width: 640px;"> <tbody> <tr> <td> <table role=3D"presentation" border=3D"0" cellspacing=3D"0" c= ellpadding=3D"0" width=3D"100%" style=3D"display: none; border-collapse: co= llapse;"> <tbody> <tr> <td> <div style=3D"margin: 0; display: inline-block; color: #6= 46464; font-size:0px;line-height:0px;padding-top:0;padding-bottom:0"> <p>Entdecken Sie unsere Angebote f=C3=BCr die Festtage u= nd suchen Sie sich etwas Sch=C3=B6nes aus.<br> </p> </div> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <span> <a href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581= 894,6ec6122,28c3e9c4&campID=3DUC:E:601216:601221:43130732:0:116154656:11615= 4658:de:657424824:::" _label=3D"e_start" _category=3D"no-cta" _urltype=3D"1= 3" aria-hidden=3D"true" style=3D" display: block; overflow: hidden; height:= 0; font-size: 0; line-height: 0; visibility: hidden; opacity: 0; position:= absolute; left: -99999px; margin: 0; padding: 0; text-decoration: none;"><= /a> </span> <div class=3D"online_version_link"> <table role=3D"presentation" border=3D"0" cellspacing=3D"0" cellp= adding=3D"0" width=3D"100%" bgcolor=3D"#f4f3ee" style=3D"width: 100%;"> <tbody> <tr> <td> <table role=3D"presentation" border=3D"0" cellspacing=3D"0" c= ellpadding=3D"0" align=3D"center" class=3D"module" width=3D"640" style=3D"w= idth: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 10px 20px= 10px;"> <table role=3D"presentation" border=3D"0" cellspacing=3D"= 0" cellpadding=3D"0" width=3D"100%" style=3D"width: 100%;"> <tbody> <tr> <td class=3D"onlineVersionLink__text" align=3D"left" s= tyle=3D" font-family: 'Frutiger 45 Light', S= egoe UI Light, Arial, Helvetica, sans-serif; color: #444444; font-size: 12px; text-decoration: none; font-style: normal; text-align: left; font-weight: 400; "> <p> Sehen Sie dieses E-Mail in e= inem <a href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6122= ,28c3e9c5&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:de:657= 424824:::&p1=3D%40hRefGTjjF5tIwcD7odIOh%2FfwR7XIYrfwJFP1A9Kv9%2Fs%3D" targe= t=3D"_blank" _label=3D"mirrorLink" _category=3D"no-cta" _urltype=3D"6" styl= e=3D"color: #444444;text-decoration: underline;font-style: normal;font-weig= ht: 400;"> Web-Browser</a> an </p> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> <table role=3D"presentation" border=3D"0" cellspacing=3D"0" cellp= adding=3D"0"> <tbody> <tr> <td height=3D"5" style=3D" height: 5px; line-height: 5px; mso-line-height-rule: exactly; ">  </td> </tr> </tbody> </table> </div> <div class=3D"free_text_disclaimer"> </div> <div class=3D"client_navigation"> <table role=3D"presentation" width=3D"640" cellspacing=3D"0" cell= padding=3D"0" border=3D"0" class=3D"module" style=3D"border-collapse: colla= pse; width: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 10px 20px 10p= x 20px;"> <table role=3D"presentation" cellspacing=3D"0" cellpadding=3D= "0" border=3D"0" width=3D"100%" style=3D"width: 100%; border-collapse: coll= apse;"> <tbody> <tr> <td align=3D"right" aria-label=3D"Classification - Cards &= amp; KeyClub" style=3D" color: #474747; font-family: 'Frutiger 45 Light', Segoe UI Ligh= t, Arial, Helvetica, sans-serif; font-size: 14px; font-style: normal; font-weight: 400; line-height: 24px; text-align: right; text-decoration: none; "> Cards & KeyClub </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"header"> <table cellspacing=3D"0" cellpadding=3D"0" border=3D"0" width=3D"= 640" role=3D"presentation" class=3D"module" style=3D"width: 640px; border-c= ollapse: collapse"> <tbody> <tr> <td width=3D"22%" style=3D"width: 22%; padding-top: 25px;"> <table role=3D"presentation" border=3D"0" cellspacing=3D"0" c= ellpadding=3D"0" width=3D"100%" style=3D"width: 100%;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 0 20px 0 = 20px;"> <a href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6= 122,28c3e9c6&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:de:= 657424824:::" target=3D"_blank" _category=3D"no-cta" _urltype=3D"14"><img s= rc=3D"https://www.ubs.com/campaign/res/img/44199dd610ccf116434f9c218b2af246= .png" width=3D"113" style=3D"min-width: 90px; height: auto" alt=3D"UBS Logo= " border=3D"0"> </a> </td> </tr> </tbody> </table> </td> <td width=3D"78%" style=3D"padding-top: 25px;"> <table align=3D"left" cellspacing=3D"0" cellpadding=3D"0" bor= der=3D"0" width=3D"100%" role=3D"presentation"> <tbody> <tr align=3D"left"> <td class=3D"descriptor-mobile" style=3D" padding-left: 32px; padding-right: 20px;"> </td> </tr> </tbody> </table> <div class=3D"cq-placeholder cmp-text" data-emptytext=3D"Desc= riptor"></div> </td> </tr> </tbody> </table> </div> <div class=3D"mailing_content"> <table class=3D"module" cellspacing=3D"0" cellpadding=3D"0" borde= r=3D"0" align=3D"center" role=3D"presentation" width=3D"640" style=3D"borde= r-collapse: collapse; width: 640px;"> <tbody> <tr> <td style=3D"padding: 20px 0 0px;"> <div class=3D"primary_headline adaptiveimage image parbase"> <table class=3D"cq-dd-image cq-dd-mobileimage module" role= =3D"presentation" cellspacing=3D"0" cellpadding=3D"0" border=3D"0" align=3D= "center" width=3D"640" style=3D"width: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 0 20px 0= 20px; word-break: break-word;"> <h1 class=3D"primary_headli= ne__keyline" style=3D" margin: 0; font-family: 'Frutiger 45 Light', Segoe UI Light, Arial= , Helvetica, sans-serif; font-size: 42px; line-height: 50px; font-weight: 400; color: #1c1c1c; padding: 20px 0 15px; mso-margin-top-alt: 20px; mso-margin-bottom-alt: 15px; word-break: break-word; "> F=C3=BCr besondere Geschenke </h1> <p class=3D"prima= ry_headline__infoline" style=3D" font-family: 'Frutiger 45 Light', Segoe UI Light, Arial= , Helvetica, sans-serif; font-size: 24px; line-height: 33px; font-weight: 400; color: #1c1c1c; padding: 0px 0 20px; mso-para-margin-top: 0px; mso-para-margin-bottom: 20px; word-break: break-word; "> <span class=3D"highlightColor">Ihre Punkte</span> wa= rten auf Sie </p> </td> </tr> </tbody> </table> </div> <div class=3D"text_and_image adaptiveimage image parbase"> <!--[if mso]> <style media=3D"all" type=3D"text/css"> .textimage__text { width: 600px; } </style> <![endif]--> <table class=3D"textimage cq-dd-image cq-dd-mobileimage modu= le" role=3D"presentation" cellpadding=3D"0" cellspacing=3D"0" border=3D"0" = align=3D"center" style=3D"width: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 20px 20p= x 0px;"> <table role=3D"presentation" width=3D"100%" cellpadding= =3D"0" cellspacing=3D"0" border=3D"0" align=3D"center"> <tbody> <tr> <td valign=3D"top"> <table cellpadding=3D"0" cellspacing=3D"0" border=3D= "0" width=3D"600" role=3D"presentation" class=3D"textimage__text textimage_= _text--"> <tbody> <tr> <td> <table cellpadding=3D"0" cellspacing=3D"0" borde= r=3D"0" role=3D"presentation"> <tbody> <tr> <td class=3D"textimage__rte" style=3D" font-family: 'Frutiger = 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 16px; color: #1c1c1c; line-height: 24px; font-weight: 400; padding-bottom: 0px; word-break: break-word; "> <p> Sehr geehrte Frau Pfeiffer</p> <p>L=C3=B6sen Sie Ihre Punkte ein, bevor sie= Ende Jahr verfallen: Finden Sie Ihre Lieblingsangebote im eStore oder spen= den Sie Ihre Punkte und unterst=C3=BCtzen Sie wohlt=C3=A4tige Organisatione= n. </p> <p>Wir w=C3=BCnschen Ihnen wunderbare Festtage.</p> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"button"> <table class=3D"module" role=3D"presentation" cellspacing=3D= "0" cellpadding=3D"0" border=3D"0" width=3D"640" align=3D"center" style=3D"= width: 640px;"> <tbody> <tr> <td class=3D"button__wrapper" style=3D" mso-line-height-rule: exactly; line-height: 0; padding: 20px 20px 20px"> <table role=3D"presentation" cellspacing=3D"0" cellpaddi= ng=3D"0" border=3D"0" align=3D"left" class=3D"button--hasBorder"> <tbody> <tr> <td class=3D"button__spacing" style=3D"padding-right:= 20px"> <!--[if mso]> <table role=3D"presentation" cellspacing=3D"0" cellpadding= =3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#e60000" style=3D" background-color: #e60000; border: solid 1px transparent; border-color: #e60000; "> <![endif]--> <!--[if !mso]><!--> <table role=3D"presentation" cellspacing=3D"0" cellp= adding=3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#e60000" style=3D" background-color: #e60000; border: solid 1px transparent; border-color: #e60000; border-collapse: collapse; border-radius: 0; "> <!--[endif]----> <tbody> <tr> <td valign=3D"middle" align=3D"center" class=3D"b= utton__body" style=3D" height: 42px; padding-left: 24px; padding-right: 24px; "> <a x-cq-linkchecker=3D"skip" _label=3D"M= ain CTA: C&KC_CTA1_NWS_LANDPAGE" href=3D"https://secure.ubs.com/campaign/r/= =3Fid=3Dh59581894,6ec6122,28c3e9c7&campID=3DUC:E:601216:601221:43130732:0:1= 16154656:116154658:de:657424824:::" target=3D"_blank" style=3D" width: 100%= ; text-decoration: none; -webkit-text-size-adjust: none; font-family: 'Frut= iger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 16= px; font-weight: 700; color: #ffffff;"> Angebote entdecken   <span cla= ss=3D"button__chevron" aria-hidden=3D"true" style=3D"display: inline-block; font-weight: 700; width: 6px; color: #ffffff">=E2=8C=AA</span> </a> </td= > </tr> <!--[if mso]></table><![endif]--> <!--[if !mso]><!--> </tbody> </table> <!--[endif]----> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"text_and_image adaptiveimage image parbase"> <!--[if mso]> <style media=3D"all" type=3D"text/css"> .textimage__text { width: 600px; } </style> <![endif]--> <table class=3D"textimage cq-dd-image cq-dd-mobileimage modu= le" role=3D"presentation" cellpadding=3D"0" cellspacing=3D"0" border=3D"0" = align=3D"center" style=3D"width: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 20px 20p= x 0px;"> <table role=3D"presentation" width=3D"100%" cellpadding= =3D"0" cellspacing=3D"0" border=3D"0" align=3D"center"> <tbody> <tr> <td valign=3D"top"> <table cellpadding=3D"0" cellspacing=3D"0" border=3D= "0" width=3D"600" role=3D"presentation" class=3D"textimage__text textimage_= _text--"> <tbody> <tr> <td> <table cellpadding=3D"0" cellspacing=3D"0" borde= r=3D"0" role=3D"presentation"> <tbody> <tr> <td class=3D"textimage__rte" style=3D" font-family: 'Frutiger = 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 16px; color: #1c1c1c; line-height: 24px; font-weight: 400; padding-bottom: 0px; word-break: break-word; "> <p>Mit freundlichen Gr= =C3=BCssen<br> <!-- SIGNATURE --> <style> @media screen and (max-width: 480px) { .signatureTable {width: 100%; min-width: 320px !important; max-width: 3= 20px !important;} .signatureTable td.signatureOne, .signatureTable td.signatureTwo {width= : 100%; display: block;} .signatureTable td.signatureTwo {margin-top: 20px;} .spacerColumn {display: none;} } </style> <table width=3D"480" role=3D"presentation" border=3D"0" cellspacing=3D"0"= cellpadding=3D"0" valign=3D"top" style=3D"min-width: 480px; max-width: 480= px; margin-bottom: 20px;" class=3D"signatureTable"> <tbody> <tr> <td width=3D"45%" valign=3D"top" style=3D"font-family:'Frutiger 45 Li= ght',Segoe UI Light,Arial,Helvetica,sans-serif; line-height:24px;" class=3D= "signatureOne"> <div style=3D"display:inline-block; width:100%; min-width:150px; max-wi= dth:150px;"> <strong>Rahel Metin</strong><br>UBS Credit Cards </div> </td> <td width=3D"10%" valign=3D"top" class=3D"spacerColumn"></td> <td width=3D"45%" valign=3D"top" style=3D"font-family:'Frutiger 45 Li= ght',Segoe UI Light,Arial,Helvetica,sans-serif; line-height:24px;" class=3D= "signatureTwo"> <div style=3D"display:inline-block; width:100%; min-width:150px; max-wi= dth:150px;"> <strong>Marc Tutzauer</strong><br>UBS KeyClub </div> </td> </tr> </tbody> </table> <!-- END SIGNATURE --></p> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"full_width_teaser"> <!--[if mso]> <style media=3D"all" type=3D"text/css"> .fwteaser__text ul, .fwteaser__text ol { mso-border-bottom-alt: solid 0 #f4f3ee; mso-padding-bottom-alt: 20px; } </style> <![endif]--> <table class=3D"cq-dd-image cq-dd-mobileimage module" role= =3D"presentation" cellspacing=3D"0" cellpadding=3D"0" border=3D"0" align=3D= "center" width=3D"640" style=3D"width: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 20px 20p= x 20px 20px;"> <table class=3D"module" role=3D"presentation" cellspacin= g=3D"0" cellpadding=3D"0" align=3D"center" bgcolor=3D"#f4f3ee" width=3D"600= " style=3D"width: 600px"> <tbody> <tr> <!--[if mso]> <td valign=3D"top"> <table width=3D"600" align=3D"center" role=3D"presentation" cellp= adding=3D"0" cellspacing=3D"0" border=3D"0"> <tr> <td valign=3D"top" width=3D"200"> <![endif]--> <!--[if !mso]><!--> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <table width=3D"200" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"module "= > <tbody> <tr> <td> <a _label=3D"Teaser 1: M=C3=B6venpick_CTA2_N= WS_PRODPAGE" href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6e= c6122,28c3e9c8&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:d= e:657424824:::"><img src=3D"https://www.ubs.com/campaign/res/img/c477634d69= 96fae54c25bc3eb621bd19.png" width=3D"200" border=3D"0" alt=3D"M=C3=B6venpic= k Wein" style=3D"max-width: 100%" class=3D"mobileHide"> <!--[if !mso]><!-- --> <span class=3D"mobileSho= w" style=3D"display: none; mso-hide: all;"> <img src=3D"https://www.ubs.com= /campaign/res/img/afa4bb66f900cb69ca12e70072e9ed40.png" width=3D"460" borde= r=3D"0" style=3D"max-width: 100%" alt=3D"M=C3=B6venpick Wein"> </span> <!--<![endif]--> </a> </td> </tr> </tbody> </table> <!--[if mso]> </td> <td valign=3D"top" width=3D"400"> <![endif]--> <!--[if !mso]><!--> </td> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <table width=3D"400" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"fwteaser_= _body"> <tbody> <tr> <td style=3D"padding: 20px 20px 20px 20px;"> <table width=3D"100%" cellspacing=3D"0" cellpadd= ing=3D"0" border=3D"0" role=3D"presentation"> <tbody> <tr> <td> <h2 style=3D" margin: 0 0 10px; font-family: 'Fru= tiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 24px; line-height: 32px= ; font-weight: 400; color: #1c1c1c; word-break: break= -word; "> <a _label=3D"Teaser = 1: M=C3=B6venpick_CTA2_NWS_PRODPAGE" href=3D"https://secure.ubs.com/campaig= n/r/=3Fid=3Dh59581894,6ec6122,28c3e9c9&campID=3DUC:E:601216:601221:43130732= :0:116154656:116154658:de:657424824:::" target=3D"_blank" style=3D" text-de= coration: none; color: #1c1c1c;"> Festlich einschenken mit M=C3=B6venpick W= ein </a> </h2> </td> </tr> <tr> <td class=3D"fwteaser__text" style=3D" font-family: 'Frutiger 45= Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; color: #1c1c1c; word-break: break-word; "> <p>Im Dezember erhalten Sie = im KeyClub eStore eine Digitale Geschenkkarte von M=C3=B6venpick Wein im We= rt von CHF 120 f=C3=BCr nur CHF 100.</p> </td> </tr> <tr> <td> <table class=3D"module" role=3D"presentation= " cellspacing=3D"0" cellpadding=3D"0" border=3D"0" width=3D"100%" align=3D"= center" style=3D"width: 100%px;"> <tbody> <tr> <td class=3D"button__wrapper" style=3D" mso-line-height-rule: exactly; line-height: 0; padding: 0px 0px 20px"> <table role=3D"presentation" cellspacing= =3D"0" cellpadding=3D"0" border=3D"0" class=3D"button--hasBorder"> <tbody> <tr> <td> <!--[if mso]> <table role=3D"presentation" cellspacing=3D"0" cellpadding= =3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; "> <![endif]--> <!--[if !mso]><!--> <table role=3D"presentation" cellspa= cing=3D"0" cellpadding=3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c= 1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; border-collapse: collapse; border-radius: 0; "> <!--[endif]----> <tbody> <tr> <td valign=3D"middle" align=3D"ce= nter" class=3D"button__body" style=3D" height: 44px; padding-left: 20px; padding-right: 20px; "> <a x-cq-linkchecker=3D"skip" _label=3D"T= easer 1: M=C3=B6venpick_CTA2_NWS_PRODPAGE" href=3D"https://secure.ubs.com/c= ampaign/r/=3Fid=3Dh59581894,6ec6122,28c3e9ca&campID=3DUC:E:601216:601221:43= 130732:0:116154656:116154658:de:657424824:::" target=3D"_blank" style=3D" w= idth: 100%; text-decoration: none; -webkit-text-size-adjust: none; font-fam= ily: 'Frutiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; fon= t-size: 16px; font-weight: 700; color: #ffffff;"> CHF 20 sparen   <spa= n class=3D"button__chevron" aria-hidden=3D"true" style=3D"display: inline-b= lock; font-weight: 700; width: 6px; color: #ffffff">=E2=8C=AA</span> </a> </td= > </tr> <!--[if mso]></table><![endif]--> <!--[if !mso]><!--> </tbody> </table> <!--[endif]----> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> <td valign=3D"top"> </td> <!--[if mso]> </td> </tr> </table> </td> <![endif]--> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"full_width_teaser"> <!--[if mso]> <style media=3D"all" type=3D"text/css"> .fwteaser__text ul, .fwteaser__text ol { mso-border-bottom-alt: solid 0 #f4f3ee; mso-padding-bottom-alt: 20px; } </style> <![endif]--> <table class=3D"cq-dd-image cq-dd-mobileimage module" role= =3D"presentation" cellspacing=3D"0" cellpadding=3D"0" border=3D"0" align=3D= "center" width=3D"640" style=3D"width: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 20px 20p= x 20px 20px;"> <table class=3D"module" role=3D"presentation" cellspacin= g=3D"0" cellpadding=3D"0" align=3D"center" bgcolor=3D"#f4f3ee" width=3D"600= " style=3D"width: 600px"> <tbody> <tr> <!--[if mso]> <td valign=3D"top"> <table width=3D"600" align=3D"center" role=3D"presentation" cellp= adding=3D"0" cellspacing=3D"0" border=3D"0"> <tr> <td valign=3D"top" width=3D"0"> <![endif]--> <!--[if !mso]><!--> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <!--[if !mso]><!--> <table width=3D"200" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"module m= obileShow" style=3D"display: none; mso-hide: all"> <tbody> <tr> <td> <a _label=3D"Teaser 2: Manor_CTA3_NWS_PRODPA= GE" href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6122,28c= 3e9cb&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:de:6574248= 24:::"><img src=3D"https://www.ubs.com/campaign/res/img/e73108675daeea8c57e= f47d2aaef5e19.png" width=3D"200" border=3D"0" alt=3D"Eine Frau mit einem ri= esigen Kuchen und Weihnachtskugeln" style=3D"max-width: 100%" class=3D"mobi= leHide"> <!--[if !mso]><!-- --> <span class=3D"mobileSho= w" style=3D"display: none; mso-hide: all;"> <img src=3D"https://www.ubs.com= /campaign/res/img/042dc050e963e7cc1e3efed7d6df5622.png" width=3D"460" borde= r=3D"0" style=3D"max-width: 100%" alt=3D"Eine Frau mit einem riesigen Kuche= n und Weihnachtskugeln"> </span> <!--<![endif]--> </a> </td> </tr> </tbody> </table> <!--<![endif]--> <!--[if mso]> </td> <td valign=3D"top" width=3D"400"> <![endif]--> <!--[if !mso]><!--> </td> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <table width=3D"400" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"fwteaser_= _body"> <tbody> <tr> <td style=3D"padding: 20px 20px 20px 20px;"> <table width=3D"100%" cellspacing=3D"0" cellpadd= ing=3D"0" border=3D"0" role=3D"presentation"> <tbody> <tr> <td> <h2 style=3D" margin: 0 0 10px; font-family: 'Fru= tiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 24px; line-height: 32px= ; font-weight: 400; color: #1c1c1c; word-break: break= -word; "> <a _label=3D"Teaser = 2: Manor_CTA3_NWS_PRODPAGE" href=3D"https://secure.ubs.com/campaign/r/=3Fid= =3Dh59581894,6ec6122,28c3e9cc&campID=3DUC:E:601216:601221:43130732:0:116154= 656:116154658:de:657424824:::" target=3D"_blank" style=3D" text-decoration:= none; color: #1c1c1c;"> Weihnachten mit der Geschenkkarte von Manor </a> <= /h2> </td> </tr> <tr> <td class=3D"fwteaser__text" style=3D" font-family: 'Frutiger 45= Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; color: #1c1c1c; word-break: break-word; "> <p>Sichern Sie sich jetzt 10= % Rabatt beim Kauf einer Digitalen Geschenkkarte von Manor im Wert von CHF&= nbsp;50 und CHF 100. Einl=C3=B6sbar auf manor.ch oder vor Ort in allen= Manor Warenh=C3=A4usern. Angebot g=C3=BCltig, solange der Vorrat reicht.</= p> </td> </tr> <tr> <td> <table class=3D"module" role=3D"presentation= " cellspacing=3D"0" cellpadding=3D"0" border=3D"0" width=3D"100%" align=3D"= center" style=3D"width: 100%px;"> <tbody> <tr> <td class=3D"button__wrapper" style=3D" mso-line-height-rule: exactly; line-height: 0; padding: 0px 0px 20px"> <table role=3D"presentation" cellspacing= =3D"0" cellpadding=3D"0" border=3D"0" class=3D"button--hasBorder"> <tbody> <tr> <td> <!--[if mso]> <table role=3D"presentation" cellspacing=3D"0" cellpadding= =3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; "> <![endif]--> <!--[if !mso]><!--> <table role=3D"presentation" cellspa= cing=3D"0" cellpadding=3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c= 1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; border-collapse: collapse; border-radius: 0; "> <!--[endif]----> <tbody> <tr> <td valign=3D"middle" align=3D"ce= nter" class=3D"button__body" style=3D" height: 44px; padding-left: 20px; padding-right: 20px; "> <a x-cq-linkchecker=3D"skip" _label=3D"T= easer 2: Manor_CTA3_NWS_PRODPAGE" href=3D"https://secure.ubs.com/campaign/r= /=3Fid=3Dh59581894,6ec6122,28c3e9cd&campID=3DUC:E:601216:601221:43130732:0:= 116154656:116154658:de:657424824:::" target=3D"_blank" style=3D" width: 100= %; text-decoration: none; -webkit-text-size-adjust: none; font-family: 'Fru= tiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 1= 6px; font-weight: 700; color: #ffffff;"> 10% Rabatt   <span class=3D"b= utton__chevron" aria-hidden=3D"true" style=3D"display: inline-block; font-weight: 700; width: 6px; color: #ffffff">=E2=8C=AA</span> </a> </td= > </tr> <!--[if mso]></table><![endif]--> <!--[if !mso]><!--> </tbody> </table> <!--[endif]----> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> <td valign=3D"top"> <table width=3D"200" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"module mo= bileHide "> <tbody> <tr> <td> <a _label=3D"Teaser 2: Manor_CTA3_NWS_PRODPA= GE" href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6122,28c= 3e9ce&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:de:6574248= 24:::"><img src=3D"https://www.ubs.com/campaign/res/img/e73108675daeea8c57e= f47d2aaef5e19.png" width=3D"200" border=3D"0" alt=3D"Eine Frau mit einem ri= esigen Kuchen und Weihnachtskugeln" style=3D"max-width: 100%" class=3D"mobi= leHide"> <!--[if !mso]><!-- --> <span class=3D"mobileSho= w" style=3D"display: none; mso-hide: all;"> <img src=3D"https://www.ubs.com= /campaign/res/img/042dc050e963e7cc1e3efed7d6df5622.png" width=3D"460" borde= r=3D"0" style=3D"max-width: 100%" alt=3D"Eine Frau mit einem riesigen Kuche= n und Weihnachtskugeln"> </span> <!--<![endif]--> </a> </td> </tr> </tbody> </table> </td> <!--[if mso]> </td> </tr> </table> </td> <![endif]--> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"full_width_teaser"> <!--[if mso]> <style media=3D"all" type=3D"text/css"> .fwteaser__text ul, .fwteaser__text ol { mso-border-bottom-alt: solid 0 #f4f3ee; mso-padding-bottom-alt: 20px; } </style> <![endif]--> <table class=3D"cq-dd-image cq-dd-mobileimage module" role= =3D"presentation" cellspacing=3D"0" cellpadding=3D"0" border=3D"0" align=3D= "center" width=3D"640" style=3D"width: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 20px 20p= x 20px 20px;"> <table class=3D"module" role=3D"presentation" cellspacin= g=3D"0" cellpadding=3D"0" align=3D"center" bgcolor=3D"#f4f3ee" width=3D"600= " style=3D"width: 600px"> <tbody> <tr> <!--[if mso]> <td valign=3D"top"> <table width=3D"600" align=3D"center" role=3D"presentation" cellp= adding=3D"0" cellspacing=3D"0" border=3D"0"> <tr> <td valign=3D"top" width=3D"200"> <![endif]--> <!--[if !mso]><!--> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <table width=3D"200" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"module "= > <tbody> <tr> <td> <a _label=3D"Teaser 3: Zalando_CTA4_NWS_PROD= PAGE" href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6122,2= 8c3e9cf&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:de:65742= 4824:::"><img src=3D"https://www.ubs.com/campaign/res/img/ed0274ce030aff210= 5cd8eaac1cf1364.png" width=3D"200" border=3D"0" alt=3D"Kosmetiken stehen au= f einem St=C3=BCck Holz" style=3D"max-width: 100%" class=3D"mobileHide"> <!--[if !mso]><!-- --> <span class=3D"mobileSho= w" style=3D"display: none; mso-hide: all;"> <img src=3D"https://www.ubs.com= /campaign/res/img/03be4d592b395793072edfb7945a0cfa.png" width=3D"460" borde= r=3D"0" style=3D"max-width: 100%" alt=3D"Kosmetiken stehen auf einem St=C3= =BCck Holz"> </span> <!--<![endif]--> </a> </td> </tr> </tbody> </table> <!--[if mso]> </td> <td valign=3D"top" width=3D"400"> <![endif]--> <!--[if !mso]><!--> </td> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <table width=3D"400" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"fwteaser_= _body"> <tbody> <tr> <td style=3D"padding: 20px 20px 20px 20px;"> <table width=3D"100%" cellspacing=3D"0" cellpadd= ing=3D"0" border=3D"0" role=3D"presentation"> <tbody> <tr> <td> <h2 style=3D" margin: 0 0 10px; font-family: 'Fru= tiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 24px; line-height: 32px= ; font-weight: 400; color: #1c1c1c; word-break: break= -word; "> <a _label=3D"Teaser = 3: Zalando_CTA4_NWS_PRODPAGE" href=3D"https://secure.ubs.com/campaign/r/=3F= id=3Dh59581894,6ec6122,28c3e9d0&campID=3DUC:E:601216:601221:43130732:0:1161= 54656:116154658:de:657424824:::" target=3D"_blank" style=3D" text-decoratio= n: none; color: #1c1c1c;"> Sch=C3=B6nes zum Fest von Zalando </a> </h2> </t= d> </tr> <tr> <td class=3D"fwteaser__text" style=3D" font-family: 'Frutiger 45= Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; color: #1c1c1c; word-break: break-word; "> <p>Bei Zalando entdecken Sie= Styles und Marken, die Freude bereiten. Beim Kauf eines E-Gutscheins im We= rt von CHF 50 erhalten Sie jetzt einen 20%=E2=80=91Beauty=E2=80=91Vouc= her gratis dazu.</p> </td> </tr> <tr> <td> <table class=3D"module" role=3D"presentation= " cellspacing=3D"0" cellpadding=3D"0" border=3D"0" width=3D"100%" align=3D"= center" style=3D"width: 100%px;"> <tbody> <tr> <td class=3D"button__wrapper" style=3D" mso-line-height-rule: exactly; line-height: 0; padding: 0px 0px 20px"> <table role=3D"presentation" cellspacing= =3D"0" cellpadding=3D"0" border=3D"0" class=3D"button--hasBorder"> <tbody> <tr> <td> <!--[if mso]> <table role=3D"presentation" cellspacing=3D"0" cellpadding= =3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; "> <![endif]--> <!--[if !mso]><!--> <table role=3D"presentation" cellspa= cing=3D"0" cellpadding=3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c= 1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; border-collapse: collapse; border-radius: 0; "> <!--[endif]----> <tbody> <tr> <td valign=3D"middle" align=3D"ce= nter" class=3D"button__body" style=3D" height: 44px; padding-left: 20px; padding-right: 20px; "> <a x-cq-linkchecker=3D"skip" _label=3D"T= easer 3: Zalando_CTA4_NWS_PRODPAGE" href=3D"https://secure.ubs.com/campaign= /r/=3Fid=3Dh59581894,6ec6122,28c3e9d1&campID=3DUC:E:601216:601221:43130732:= 0:116154656:116154658:de:657424824:::" target=3D"_blank" style=3D" width: 1= 00%; text-decoration: none; -webkit-text-size-adjust: none; font-family: 'F= rutiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size:= 16px; font-weight: 700; color: #ffffff;"> Zum Angebot   <span class= =3D"button__chevron" aria-hidden=3D"true" style=3D"display: inline-block; font-weight: 700; width: 6px; color: #ffffff">=E2=8C=AA</span> </a> </td= > </tr> <!--[if mso]></table><![endif]--> <!--[if !mso]><!--> </tbody> </table> <!--[endif]----> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> <td valign=3D"top"> </td> <!--[if mso]> </td> </tr> </table> </td> <![endif]--> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"full_width_teaser"> <!--[if mso]> <style media=3D"all" type=3D"text/css"> .fwteaser__text ul, .fwteaser__text ol { mso-border-bottom-alt: solid 0 #f4f3ee; mso-padding-bottom-alt: 20px; } </style> <![endif]--> <table class=3D"cq-dd-image cq-dd-mobileimage module" role= =3D"presentation" cellspacing=3D"0" cellpadding=3D"0" border=3D"0" align=3D= "center" width=3D"640" style=3D"width: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 20px 20p= x 20px 20px;"> <table class=3D"module" role=3D"presentation" cellspacin= g=3D"0" cellpadding=3D"0" align=3D"center" bgcolor=3D"#f4f3ee" width=3D"600= " style=3D"width: 600px"> <tbody> <tr> <!--[if mso]> <td valign=3D"top"> <table width=3D"600" align=3D"center" role=3D"presentation" cellp= adding=3D"0" cellspacing=3D"0" border=3D"0"> <tr> <td valign=3D"top" width=3D"0"> <![endif]--> <!--[if !mso]><!--> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <!--[if !mso]><!--> <table width=3D"200" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"module m= obileShow" style=3D"display: none; mso-hide: all"> <tbody> <tr> <td> <a _label=3D"Teaser 4: Suitespot_CTA5_NWS_PR= ODPAGE" href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6122= ,28c3e9d2&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:de:657= 424824:::"><img src=3D"https://www.ubs.com/campaign/res/img/0d64cfe2efeefe2= cc6f321c015a2d4a1.jpeg" width=3D"200" border=3D"0" alt=3D"Suitespot" style= =3D"max-width: 100%" class=3D"mobileHide"> <!--[if !mso]><!-- --> <span class=3D"mobileSho= w" style=3D"display: none; mso-hide: all;"> <img src=3D"https://www.ubs.com= /campaign/res/img/18c3acca77e123d159f05ca1141d2115.jpeg" width=3D"460" bord= er=3D"0" style=3D"max-width: 100%" alt=3D"Suitespot"> </span> <!--<![endif]--> </a> </td> </tr> </tbody> </table> <!--<![endif]--> <!--[if mso]> </td> <td valign=3D"top" width=3D"400"> <![endif]--> <!--[if !mso]><!--> </td> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <table width=3D"400" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"fwteaser_= _body"> <tbody> <tr> <td style=3D"padding: 20px 20px 20px 20px;"> <table width=3D"100%" cellspacing=3D"0" cellpadd= ing=3D"0" border=3D"0" role=3D"presentation"> <tbody> <tr> <td> <h2 style=3D" margin: 0 0 10px; font-family: 'Fru= tiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 24px; line-height: 32px= ; font-weight: 400; color: #1c1c1c; word-break: break= -word; "> <a _label=3D"Teaser = 4: Suitespot_CTA5_NWS_PRODPAGE" href=3D"https://secure.ubs.com/campaign/r/= =3Fid=3Dh59581894,6ec6122,28c3e9d3&campID=3DUC:E:601216:601221:43130732:0:1= 16154656:116154658:de:657424824:::" target=3D"_blank" style=3D" text-decora= tion: none; color: #1c1c1c;"> Exklusive Hotelvorteile mit Ihrer UBS Kreditk= arte </a> </h2> </td> </tr> <tr> <td class=3D"fwteaser__text" style=3D" font-family: 'Frutiger 45= Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; color: #1c1c1c; word-break: break-word; "> <p>Sichern Sie sich Ihre kos= tenlose suitespot Premium Mitgliedschaft und profitieren Sie von flexiblen = Buchungen, kostenlosem Fr=C3=BChst=C3=BCck und Upgrades (nach Verf=C3=BCgba= rkeit) mit Ihrer UBS Gold oder Platinum Kreditkarte.</p> </td> </tr> <tr> <td> <table class=3D"module" role=3D"presentation= " cellspacing=3D"0" cellpadding=3D"0" border=3D"0" width=3D"100%" align=3D"= center" style=3D"width: 100%px;"> <tbody> <tr> <td class=3D"button__wrapper" style=3D" mso-line-height-rule: exactly; line-height: 0; padding: 0px 0px 20px"> <table role=3D"presentation" cellspacing= =3D"0" cellpadding=3D"0" border=3D"0" class=3D"button--hasBorder"> <tbody> <tr> <td> <!--[if mso]> <table role=3D"presentation" cellspacing=3D"0" cellpadding= =3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; "> <![endif]--> <!--[if !mso]><!--> <table role=3D"presentation" cellspa= cing=3D"0" cellpadding=3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c= 1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; border-collapse: collapse; border-radius: 0; "> <!--[endif]----> <tbody> <tr> <td valign=3D"middle" align=3D"ce= nter" class=3D"button__body" style=3D" height: 44px; padding-left: 20px; padding-right: 20px; "> <a x-cq-linkchecker=3D"skip" _label=3D"T= easer 4: Suitespot_CTA5_NWS_PRODPAGE" href=3D"https://secure.ubs.com/campai= gn/r/=3Fid=3Dh59581894,6ec6122,28c3e9d4&campID=3DUC:E:601216:601221:4313073= 2:0:116154656:116154658:de:657424824:::" target=3D"_blank" style=3D" width:= 100%; text-decoration: none; -webkit-text-size-adjust: none; font-family: = 'Frutiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-siz= e: 16px; font-weight: 700; color: #ffffff;"> Sich jetzt registrieren  = <span class=3D"button__chevron" aria-hidden=3D"true" style=3D"display: inl= ine-block; font-weight: 700; width: 6px; color: #ffffff">=E2=8C=AA</span> </a> </td= > </tr> <!--[if mso]></table><![endif]--> <!--[if !mso]><!--> </tbody> </table> <!--[endif]----> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> <td valign=3D"top"> <table width=3D"200" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"module mo= bileHide "> <tbody> <tr> <td> <a _label=3D"Teaser 4: Suitespot_CTA5_NWS_PR= ODPAGE" href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6122= ,28c3e9d5&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:de:657= 424824:::"><img src=3D"https://www.ubs.com/campaign/res/img/0d64cfe2efeefe2= cc6f321c015a2d4a1.jpeg" width=3D"200" border=3D"0" alt=3D"Suitespot" style= =3D"max-width: 100%" class=3D"mobileHide"> <!--[if !mso]><!-- --> <span class=3D"mobileSho= w" style=3D"display: none; mso-hide: all;"> <img src=3D"https://www.ubs.com= /campaign/res/img/18c3acca77e123d159f05ca1141d2115.jpeg" width=3D"460" bord= er=3D"0" style=3D"max-width: 100%" alt=3D"Suitespot"> </span> <!--<![endif]--> </a> </td> </tr> </tbody> </table> </td> <!--[if mso]> </td> </tr> </table> </td> <![endif]--> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"full_width_teaser"> <!--[if mso]> <style media=3D"all" type=3D"text/css"> .fwteaser__text ul, .fwteaser__text ol { mso-border-bottom-alt: solid 0 #f4f3ee; mso-padding-bottom-alt: 20px; } </style> <![endif]--> <table class=3D"cq-dd-image cq-dd-mobileimage module" role= =3D"presentation" cellspacing=3D"0" cellpadding=3D"0" border=3D"0" align=3D= "center" width=3D"640" style=3D"width: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 20px 20p= x 20px 20px;"> <table class=3D"module" role=3D"presentation" cellspacin= g=3D"0" cellpadding=3D"0" align=3D"center" bgcolor=3D"#f4f3ee" width=3D"600= " style=3D"width: 600px"> <tbody> <tr> <!--[if mso]> <td valign=3D"top"> <table width=3D"600" align=3D"center" role=3D"presentation" cellp= adding=3D"0" cellspacing=3D"0" border=3D"0"> <tr> <td valign=3D"top" width=3D"200"> <![endif]--> <!--[if !mso]><!--> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <table width=3D"200" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"module "= > <tbody> <tr> <td> <a _label=3D"Teaser 5: Globus_CTA6_NWS_PRODP= AGE" href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6122,28= c3e9d6&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:de:657424= 824:::"><img src=3D"https://www.ubs.com/campaign/res/img/3c81e50f145300299b= ba977984c1d62a.png" width=3D"200" border=3D"0" alt=3D"Globus" style=3D"max-= width: 100%" class=3D"mobileHide"> <!--[if !mso]><!-- --> <span class=3D"mobileSho= w" style=3D"display: none; mso-hide: all;"> <img src=3D"https://www.ubs.com= /campaign/res/img/a1d1ec7dd362148daca40cfe7cd527a5.png" width=3D"460" borde= r=3D"0" style=3D"max-width: 100%" alt=3D"Globus"> </span> <!--<![endif]--> </a> </td> </tr> </tbody> </table> <!--[if mso]> </td> <td valign=3D"top" width=3D"400"> <![endif]--> <!--[if !mso]><!--> </td> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <table width=3D"400" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"fwteaser_= _body"> <tbody> <tr> <td style=3D"padding: 20px 20px 20px 20px;"> <table width=3D"100%" cellspacing=3D"0" cellpadd= ing=3D"0" border=3D"0" role=3D"presentation"> <tbody> <tr> <td> <h2 style=3D" margin: 0 0 10px; font-family: 'Fru= tiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 24px; line-height: 32px= ; font-weight: 400; color: #1c1c1c; word-break: break= -word; "> <a _label=3D"Teaser = 5: Globus_CTA6_NWS_PRODPAGE" href=3D"https://secure.ubs.com/campaign/r/=3Fi= d=3Dh59581894,6ec6122,28c3e9d7&campID=3DUC:E:601216:601221:43130732:0:11615= 4656:116154658:de:657424824:::" target=3D"_blank" style=3D" text-decoration= : none; color: #1c1c1c;"> Auf Geschenksuche f=C3=BCr die Liebsten=3F </a> <= /h2> </td> </tr> <tr> <td class=3D"fwteaser__text" style=3D" font-family: 'Frutiger 45= Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; color: #1c1c1c; word-break: break-word; "> <p>Beim Kauf einer Digitalen= Geschenkkarte von Globus f=C3=BCr CHF 200 erhalten Sie jetzt ein Extr= aguthaben von CHF 20. Schnell sein lohnt sich: Das Angebot ist limitie= rt auf 250 Exemplare.</p> </td> </tr> <tr> <td> <table class=3D"module" role=3D"presentation= " cellspacing=3D"0" cellpadding=3D"0" border=3D"0" width=3D"100%" align=3D"= center" style=3D"width: 100%px;"> <tbody> <tr> <td class=3D"button__wrapper" style=3D" mso-line-height-rule: exactly; line-height: 0; padding: 0px 0px 20px"> <table role=3D"presentation" cellspacing= =3D"0" cellpadding=3D"0" border=3D"0" class=3D"button--hasBorder"> <tbody> <tr> <td> <!--[if mso]> <table role=3D"presentation" cellspacing=3D"0" cellpadding= =3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; "> <![endif]--> <!--[if !mso]><!--> <table role=3D"presentation" cellspa= cing=3D"0" cellpadding=3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c= 1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; border-collapse: collapse; border-radius: 0; "> <!--[endif]----> <tbody> <tr> <td valign=3D"middle" align=3D"ce= nter" class=3D"button__body" style=3D" height: 44px; padding-left: 20px; padding-right: 20px; "> <a x-cq-linkchecker=3D"skip" _label=3D"T= easer 5: Globus_CTA6_NWS_PRODPAGE" href=3D"https://secure.ubs.com/campaign/= r/=3Fid=3Dh59581894,6ec6122,28c3e9d8&campID=3DUC:E:601216:601221:43130732:0= :116154656:116154658:de:657424824:::" target=3D"_blank" style=3D" width: 10= 0%; text-decoration: none; -webkit-text-size-adjust: none; font-family: 'Fr= utiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: = 16px; font-weight: 700; color: #ffffff;"> Sich Guthaben sichern   <spa= n class=3D"button__chevron" aria-hidden=3D"true" style=3D"display: inline-b= lock; font-weight: 700; width: 6px; color: #ffffff">=E2=8C=AA</span> </a> </td= > </tr> <!--[if mso]></table><![endif]--> <!--[if !mso]><!--> </tbody> </table> <!--[endif]----> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> <td valign=3D"top"> </td> <!--[if mso]> </td> </tr> </table> </td> <![endif]--> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"full_width_teaser"> <!--[if mso]> <style media=3D"all" type=3D"text/css"> .fwteaser__text ul, .fwteaser__text ol { mso-border-bottom-alt: solid 0 #f4f3ee; mso-padding-bottom-alt: 20px; } </style> <![endif]--> <table class=3D"cq-dd-image cq-dd-mobileimage module" role= =3D"presentation" cellspacing=3D"0" cellpadding=3D"0" border=3D"0" align=3D= "center" width=3D"640" style=3D"width: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 20px 20p= x 20px 20px;"> <table class=3D"module" role=3D"presentation" cellspacin= g=3D"0" cellpadding=3D"0" align=3D"center" bgcolor=3D"#f4f3ee" width=3D"600= " style=3D"width: 600px"> <tbody> <tr> <!--[if mso]> <td valign=3D"top"> <table width=3D"600" align=3D"center" role=3D"presentation" cellp= adding=3D"0" cellspacing=3D"0" border=3D"0"> <tr> <td valign=3D"top" width=3D"0"> <![endif]--> <!--[if !mso]><!--> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <!--[if !mso]><!--> <table width=3D"200" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"module m= obileShow" style=3D"display: none; mso-hide: all"> <tbody> <tr> <td> <a _label=3D"Teaser 6: Optimus_CTA7_NWS_PROD= PAGE" href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6122,2= 8c3e9d9&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:de:65742= 4824:::"><img src=3D"https://www.ubs.com/campaign/res/img/80f82e02dd5c29522= 099685c089baa9b.jpeg" width=3D"200" border=3D"0" alt=3D"Optimus" style=3D"m= ax-width: 100%" class=3D"mobileHide"> <!--[if !mso]><!-- --> <span class=3D"mobileSho= w" style=3D"display: none; mso-hide: all;"> <img src=3D"https://www.ubs.com= /campaign/res/img/2b5cec8a31e91dab26d1e5ad996d2807.jpeg" width=3D"460" bord= er=3D"0" style=3D"max-width: 100%" alt=3D"Optimus"> </span> <!--<![endif]--> </a> </td> </tr> </tbody> </table> <!--<![endif]--> <!--[if mso]> </td> <td valign=3D"top" width=3D"400"> <![endif]--> <!--[if !mso]><!--> </td> <td valign=3D"top" class=3D"mobile-element"> <!--[endif]--> <table width=3D"400" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"fwteaser_= _body"> <tbody> <tr> <td style=3D"padding: 20px 20px 20px 20px;"> <table width=3D"100%" cellspacing=3D"0" cellpadd= ing=3D"0" border=3D"0" role=3D"presentation"> <tbody> <tr> <td> <h2 style=3D" margin: 0 0 10px; font-family: 'Fru= tiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 24px; line-height: 32px= ; font-weight: 400; color: #1c1c1c; word-break: break= -word; "> <a _label=3D"Teaser = 6: Optimus_CTA7_NWS_PRODPAGE" href=3D"https://secure.ubs.com/campaign/r/=3F= id=3Dh59581894,6ec6122,28c3e9da&campID=3DUC:E:601216:601221:43130732:0:1161= 54656:116154658:de:657424824:::" target=3D"_blank" style=3D" text-decoratio= n: none; color: #1c1c1c;"> Manchmal braucht es mehr als ein Pflaster </a> <= /h2> </td> </tr> <tr> <td class=3D"fwteaser__text" style=3D" font-family: 'Frutiger 45= Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; color: #1c1c1c; word-break: break-word; "> <p>Die UBS Optimus Foundatio= n engagiert sich f=C3=BCr die psychische Gesundheit von jungen Menschen in = der Schweiz und weltweit. Alle Spenden, die vor dem 31. Dezember 2025 = eingehen, werden bis zu einem Total von CHF 500 000 durch UBS und= Partner verdoppelt.</p> </td> </tr> <tr> <td> <table class=3D"module" role=3D"presentation= " cellspacing=3D"0" cellpadding=3D"0" border=3D"0" width=3D"100%" align=3D"= center" style=3D"width: 100%px;"> <tbody> <tr> <td class=3D"button__wrapper" style=3D" mso-line-height-rule: exactly; line-height: 0; padding: 0px 0px 20px"> <table role=3D"presentation" cellspacing= =3D"0" cellpadding=3D"0" border=3D"0" class=3D"button--hasBorder"> <tbody> <tr> <td> <!--[if mso]> <table role=3D"presentation" cellspacing=3D"0" cellpadding= =3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; "> <![endif]--> <!--[if !mso]><!--> <table role=3D"presentation" cellspa= cing=3D"0" cellpadding=3D"0" width=3D"100%" border=3D"1" bordercolor=3D"#1c= 1c1c" style=3D" background-color: #1c1c1c; border: solid 1px transparent; border-color: #1c1c1c; border-collapse: collapse; border-radius: 0; "> <!--[endif]----> <tbody> <tr> <td valign=3D"middle" align=3D"ce= nter" class=3D"button__body" style=3D" height: 44px; padding-left: 20px; padding-right: 20px; "> <a x-cq-linkchecker=3D"skip" _label=3D"T= easer 6: Optimus_CTA7_NWS_PRODPAGE" href=3D"https://secure.ubs.com/campaign= /r/=3Fid=3Dh59581894,6ec6122,28c3e9db&campID=3DUC:E:601216:601221:43130732:= 0:116154656:116154658:de:657424824:::" target=3D"_blank" style=3D" width: 1= 00%; text-decoration: none; -webkit-text-size-adjust: none; font-family: 'F= rutiger 45 Light', Segoe UI Light, Arial, Helvetica, sans-serif; font-size:= 16px; font-weight: 700; color: #ffffff;"> Jetzt spenden   <span class= =3D"button__chevron" aria-hidden=3D"true" style=3D"display: inline-block; font-weight: 700; width: 6px; color: #ffffff">=E2=8C=AA</span> </a> </td= > </tr> <!--[if mso]></table><![endif]--> <!--[if !mso]><!--> </tbody> </table> <!--[endif]----> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> <td valign=3D"top"> <table width=3D"200" cellpadding=3D"0" cellspacing= =3D"0" border=3D"0" role=3D"presentation" align=3D"left" class=3D"module mo= bileHide "> <tbody> <tr> <td> <a _label=3D"Teaser 6: Optimus_CTA7_NWS_PROD= PAGE" href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6122,2= 8c3e9dc&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:de:65742= 4824:::"><img src=3D"https://www.ubs.com/campaign/res/img/80f82e02dd5c29522= 099685c089baa9b.jpeg" width=3D"200" border=3D"0" alt=3D"Optimus" style=3D"m= ax-width: 100%" class=3D"mobileHide"> <!--[if !mso]><!-- --> <span class=3D"mobileSho= w" style=3D"display: none; mso-hide: all;"> <img src=3D"https://www.ubs.com= /campaign/res/img/2b5cec8a31e91dab26d1e5ad996d2807.jpeg" width=3D"460" bord= er=3D"0" style=3D"max-width: 100%" alt=3D"Optimus"> </span> <!--<![endif]--> </a> </td> </tr> </tbody> </table> </td> <!--[if mso]> </td> </tr> </table> </td> <![endif]--> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> </td> </tr> </tbody> </table> </div> <div class=3D"divider"> <table class=3D"module" role=3D"presentation" cellspacing=3D"0" c= ellpadding=3D"0" border=3D"0" align=3D"center" width=3D"640" style=3D"width= : 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 20px 20px 20p= x 20px;"> <table cellspacing=3D"0" cellpadding=3D"0" border=3D"0" width= =3D"100%" role=3D"presentation"> <tbody> <tr> <td height=3D"1" style=3D" border-bottom: 1px solid #DDDDDD; font-size: 0; line-height: 0; mso-line-height-rule: exactly "></td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"subscription_info"> <table class=3D"module" role=3D"presentation" cellspacing=3D"0" c= ellpadding=3D"0" border=3D"0" align=3D"center" width=3D"640" bgcolor=3D"#ff= ffff" style=3D"border-collapse: collapse; width: 640px; background-color: #ffffff; "> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 30px 20px 10p= x;"> <table cellspacing=3D"0" cellpadding=3D"0" border=3D"0" role= =3D"presentation" width=3D"100%" style=3D"width: 100%; border-collapse: col= lapse"> <tbody> <tr> <td class=3D"subscriptionInfo__text" align=3D"left" style= =3D" margin: 0; color: #1c1c1c; font-family: 'Frutiger 45 Light', Segoe UI = Light, Arial, Helvetica, sans-serif; font-size: 12px; font-style: normal; font-weight: 400; line-height: 20px; text-align: left; text-decoration: none; word-break: break-word; "> <a href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6122,28c= 3e9df&campID=3DUC:E:601216:601221:43130732:0:116154656:116154658:de:6574248= 24:::&p1=3D43130732&p2=3D%40FKQqQBul1y7sqvb4Us%2B34A%3D%3D&p3=3D43473897&p4= =3Dde&p5=3D" _label=3D"Opt-out link" _category=3D"no-cta" _type=3D"optout" = _urltype=3D"3">Abmelden</a> </td> </tr> <tr> <td height=3D"20" style=3D"height: 20px;"></td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"publication_info"> <table class=3D"module" role=3D"presentation" cellspacing=3D"0" c= ellpadding=3D"0" border=3D"0" align=3D"center" width=3D"640" bgcolor=3D"#ff= ffff" style=3D" border-collapse: collapse; width: 640px; background-color: #ffffff; "> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D"padding: 10px 20px 20p= x;"> <table role=3D"presentation" cellspacing=3D"0" cellpadding=3D= "0" border=3D"0" width=3D"100%" style=3D"width :100%; border-collapse: coll= apse"> <tbody> <tr> <td align=3D"left" class=3D"publicationInfo__text" style= =3D" color: #1c1c1c; font-family: 'Frutiger 45 Light', Segoe UI Li= ght, Arial, Helvetica, sans-serif; font-size: 12px; font-style: normal; font-weight: 400; line-height: 20px; text-align: left; text-decoration: none; word-break: break-word; "> Impressum <br> Absender: UBS Switzerl= and AG, Bahnhofstrasse 45, 8001 Z=C3=BCrich </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <div class=3D"free_text_disclaimer_2 free_text_disclaimer"> </div> <div class=3D"disclaimer"> <table class=3D"module" role=3D"presentation" cellspacing=3D"0" c= ellpadding=3D"0" border=3D"0" align=3D"center" width=3D"640" style=3D"borde= r-collapse: collapse; width: 640px;"> <tbody> <tr> <td class=3D"hpadding--mobile" style=3D" text-align: left; padding: 10px 20px 40px; "> <table cellspacing=3D"0" cellpadding=3D"0" border=3D"0" role= =3D"presentation" width=3D"100%" style=3D"width: 100%; border-collapse: col= lapse"> <tbody> <tr> <td class=3D"disclaimer__text" style=3D" color: #1c1c1c; font-family: 'Frutiger 45 Light', Segoe UI = Light, Arial, Helvetica, sans-serif; font-size: 12px; font-style: normal; font-weight: 400; line-height: 20px; text-decoration: none; word-break: break-word; highlightColor: #DA0000 "> <p>Diese Informationen wurden von UBS Sw= itzerland AG und/oder ihren Tochtergesellschaften und/oder verbundenen Unte= rnehmen («UBS», «wir») herausgegeben. </p> <p> = ;</p> <p>Aufgrund des bisherigen E-Mail-Verkehrs mit Ihnen beziehungsweise = der mit Ihnen getroffenen Absprachen halten wir uns f=C3=BCr berechtigt, Si= e per ungesicherter E-Mail zu kontaktieren. Sollten Sie diese E-Mail nicht = mehr von uns erhalten wollen, k=C3=B6nnen Sie diesen Newsletter jederzeit = =C3=BCber den Abmeldelink oben abbestellen. Sollten Sie diese E-Mail irrt= =C3=BCmlich erhalten haben, informieren Sie uns bitte und l=C3=B6schen Sie = die E-Mail (mit allen Anh=C3=A4ngen) unwiderruflich. Leiten Sie den Inhalt = nicht weiter und machen Sie ihn nicht f=C3=BCr andere Personen zug=C3=A4ngl= ich. </p> <p> </p> <p>E-Mails werden in der Regel =C3=BCber offene, al= lf=C3=A4llig f=C3=BCr Dritte zug=C3=A4ngliche Systeme grenz=C3=BCberschreit= end versandt. Daher birgt die Nutzung von E-Mails gewisse Risiken, insbeson= dere (1) mangelnde Vertraulichkeit und, je nach den an der =C3=9Cbermittlun= g beteiligten Rechtsordnungen, ein geringeres Datenschutzniveau, (2) Manipu= lation oder F=C3=A4lschung der Adresse der Absenderin beziehungsweise des A= bsenders oder des Inhalts, (3) Missbrauch mit der Folge von Sch=C3=A4den du= rch das Abfangen von E-Mails durch Dritte, (4) Systemausf=C3=A4lle und ande= re =C3=9Cbertragungsfehler, die dazu f=C3=BChren k=C3=B6nnen, dass E-Mails = und ihre Anh=C3=A4nge verz=C3=B6gert, besch=C3=A4digt, fehlgeleitet oder ge= l=C3=B6scht werden, und (5) Schadprogramme, die von Dritten unbemerkt per E= -Mail verbreitet werden und erheblichen Schaden anrichten k=C3=B6nnen. Die = Empf=C3=A4ngerin beziehungsweise der Empf=C3=A4nger akzeptiert die Risiken = der Nutzung von E-Mails. Kundinnen und Kunden nehmen insbesondere das Risik= o in Kauf, dass die Bankbeziehung und die damit verbundenen vertraulichen I= nformationen gegen=C3=BCber Dritten offengelegt werden k=C3=B6nnten. Es lie= gt in der eigenen Verantwortung der Empf=C3=A4ngerin beziehungsweise des Em= pf=C3=A4ngers, die f=C3=BCr =C3=B6ffentliche elektronische Netze =C3=BCblic= hen Sicherheitsvorkehrungen zu kennen und umzusetzen (zum Beispiel durch In= stallation einer Firewall und Verwendung eines regelm=C3=A4ssig zu aktualis= ierenden Antivirenprogramms) und die E-Mail-Adresse nur von Ger=C3=A4ten au= s zu nutzen, die gegen elektronische Angriffe und unbefugte Nutzung gesch= =C3=BCtzt sind. Im Falle von Zweifeln bez=C3=BCglich der Herkunft einer E-M= ail muss die Empf=C3=A4ngerin beziehungsweise der Empf=C3=A4nger UBS telefo= nisch kontaktieren. Soweit gesetzlich erlaubt, ist UBS nicht haftbar f=C3= =BCr Verluste oder Sch=C3=A4den, die aus der Verwendung von E-Mails resulti= eren. </p> <p> </p> <p>UBS akzeptiert keine per E-Mail =C3=BCbermittel= ten Transaktionsauftr=C3=A4ge, darunter unter anderem die Er=C3=B6ffnung vo= n Konten, Zahlungs- und B=C3=B6rsenauftr=C3=A4ge, der Widerruf von Auftr=C3= =A4gen oder Genehmigungen, die Sperrung von Kreditkarten, Adress=C3=A4nderu= ngen und so weiter. Gehen trotzdem solche E-Mails ein, ist UBS nicht verpfl= ichtet, auf diese zu reagieren oder zu antworten. </p> <p> </p> <p>Obw= ohl alle Informationen und Meinungen aus Quellen stammen, die als zuverl=C3= =A4ssig eingestuft und in gutem Glauben abgegeben werden, lehnen wir jede H= aftung f=C3=BCr unrichtige oder unvollst=C3=A4ndige Informationen ab. Die M= einungs=C3=A4usserungen von externen Autorinnen beziehungsweise Autoren ent= sprechen ihren eigenen Ansichten, die von der offiziellen Position von UBS = abweichen k=C3=B6nnen. Die Aktualit=C3=A4t der Informationen beschr=C3=A4nk= t sich auf das Datum der Ver=C3=B6ffentlichung oder den Zeitpunkt des Versa= nds (zum Beispiel bei E-Mails). Daher m=C3=BCssen wir diese Informationen n= icht auf dem neuesten Stand halten. Die zur Verf=C3=BCgung gestellten Inhal= te dienen zu reinen Informationszwecken. Sie sind nicht als Empfehlung, Off= erte oder Aufforderung zur Offertstellung zum Kauf oder Verkauf von Anlage-= oder anderen spezifischen Produkten zu verstehen. Sie stellen keine Anlage= -, Rechts- oder Steuerberatung dar und sollten nicht als Grundlage f=C3=BCr= Anlageentscheide dienen. UBS beh=C3=A4lt sich das Recht vor, Dienstleistun= gen, Produkte und Preise jederzeit ohne Vorank=C3=BCndigung zu =C3=A4ndern.= Einzelne Dienstleistungen und Produkte k=C3=B6nnen von Personen mit Wohnsi= tz in bestimmten L=C3=A4ndern oder von bestimmten Kategorien von Investoren= unter Umst=C3=A4nden nicht erworben werden. </p> <p> </p> <p>Diese E-= Mail kann Inhalte Dritter oder Links zu Webseiten Dritter enthalten. Diese = Inhalte und Links dienen ausschliesslich der Benutzerfreundlichkeit und der= Information. UBS besitzt keine Kontrolle =C3=BCber die Inhalte oder Websei= ten Dritter, =C3=BCbernimmt keinerlei Verantwortung oder Gew=C3=A4hr f=C3= =BCr diese Inhalte oder Webseiten und macht diesbez=C3=BCglich keinerlei Zu= sicherungen. Dies schliesst unter anderem die Richtigkeit, den Inhalt, die = Qualit=C3=A4t oder die Aktualit=C3=A4t dieser Webseiten ein. UBS ist nicht = f=C3=BCr die Inhalte oder Webseiten Dritter sowie f=C3=BCr Webseiten haftba= r, die auf die UBS-Webseite verlinken oder diese in Frames anzeigen.</p> <p= > </p> <p>Um unser Angebot auf Ihre Interessen abzustimmen, erheben wi= r zu Analysezwecken Daten =C3=BCber Ihr Leseverhalten dieser E-Mail, zum Be= ispiel ob Sie die E-Mail =C3=B6ffnen, welche Inhalte Sie interessieren und = ob Sie die E-Mail weiterleiten. Dies geschieht mithilfe einer eindeutigen E= -Mail-ID. Es werden keine Cookies oder =C3=A4hnliche Programme auf Ihrem Ge= r=C3=A4t installiert. Wenn Sie keine Nachverfolgung w=C3=BCnschen, k=C3=B6n= nen Sie diesen Newsletter jederzeit =C3=BCber den oben genannten Abmeldelin= k abbestellen. Weitere Informationen =C3=BCber die Verarbeitung Ihrer Daten= , =C3=BCber Ihre Rechte im Zusammenhang mit Ihren Daten sowie die Kontaktda= ten unseres Group Data Protection Office finden Sie in unserer <a href=3D"h= ttps://secure.ubs.com/campaign/r/=3Fid=3Dh59581894,6ec6122,28c3e9e1&campID= =3DUC:E:601216:601221:43130732:0:116154656:116154658:de:657424824:::" _urlt= ype=3D"14" x-cq-linkchecker=3D"skip">Datenschutzerkl=C3=A4rung</a>.</p> </t= d> </tr> <tr> <td style=3D"height: 3px; line-height: 3px; mso-line-heigh= t-rule: exactly;" height=3D"3">   </td> </tr> <tr> <td class=3D"disclaimer__copyright" style=3D" color: #1c1c1c; font-family: 'Frutiger 45 Light', Segoe UI = Light, Arial, Helvetica, sans-serif; font-size: 12px; font-style: normal; font-weight: 400; line-height: 20px; text-decoration: none; word-break: break-word; "> =C2=A9 UBS 2025. Das Schl=C3=BCsselsymbol und UBS geh=C3=B6ren zu den gesch= =C3=BCtzten Marken von UBS. Alle Rechte vorbehalten. </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> <span> <a href=3D"https://secure.ubs.com/campaign/r/=3Fid=3Dh59581= 894,6ec6122,28c3e9e2&campID=3DUC:E:601216:601221:43130732:0:116154656:11615= 4658:de:657424824:::" _label=3D"e_end" _category=3D"no-cta" _urltype=3D"13"= aria-hidden=3D"true" style=3D" display: block; overflow: hidden; height: 0= ; font-size: 0; line-height: 0; visibility: hidden; opacity: 0; position: a= bsolute; left: -99999px; margin: 0; padding: 0; text-decoration: none;"></a= > </span> </div> </div> </div> </td> </tr> </tbody> </table> <img height=3D'0' width=3D'0' alt=3D'' src=3D'https://secure.ubs.com/campa= ign/r/=3Fid=3Dh59581894,6ec6122,1'/></body> </html> ------=_NextPart_231_E519BAD3.E519BAD3-- From - Mon Dec 01 20:14:38 2025 X-Mozilla-Status: 0001 X-Mozilla-Status2: 00000000 Return-Path: <arch-general-bounces@lists.archlinux.org> Received: from witcher.mxrouting.net by witcher.mxrouting.net with LMTP id 4MAcGmfzLWm7ngAAYBR5ng (envelope-from <arch-general-bounces@lists.archlinux.org>); Mon, 01 Dec 2025 19:58:31 +0000 Return-path: <arch-general-bounces@lists.archlinux.org> Envelope-to: pfeifferj@archlinux.ch Delivery-date: Mon, 01 Dec 2025 19:58:36 +0000 Received: from lists.archlinux.org ([95.217.236.249]) by witcher.mxrouting.net with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.98) (envelope-from <arch-general-bounces@lists.archlinux.org>) id 1vQA2Y-00000000KAC-3ART for pfeifferj@archlinux.ch; Mon, 01 Dec 2025 19:58:31 +0000 Received: from [95.217.236.249] (localhost [IPv6:::1]) by lists.archlinux.org (Postfix) with ESMTP id 0C843601A36C; Mon, 01 Dec 2025 19:58:27 +0000 (UTC) Received: from out-180.mta0.migadu.com (out-180.mta0.migadu.com [91.218.175.180]) by lists.archlinux.org (Postfix) with ESMTPS id 6F863601A24A for <arch-general@lists.archlinux.org>; Mon, 01 Dec 2025 19:57:59 +0000 (UTC) Authentication-Results: lists.archlinux.org; dkim=pass header.d=ariadnavigo.xyz header.s=key1 header.b=hmUF6xfV; dmarc=pass (policy=quarantine) header.from=ariadnavigo.xyz; spf=pass (lists.archlinux.org: domain of ariadna@ariadnavigo.xyz designates 91.218.175.180 as permitted sender) smtp.mailfrom=ariadna@ariadnavigo.xyz Mime-Version: 1.0 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=ariadnavigo.xyz; s=key1; t=1764619078; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: in-reply-to:in-reply-to:references:references; bh=hpsxVNXa0HJQs7b2bWw9CYubC8zdS5JjvdzK8pOt4Lc=; b=hmUF6xfVbObWwRqpvGaLvcN0iYzuhET+QGin+rkjEgVSDzjTKlfxl1RvJufPZLNxoxGOvE j4D54vcc3IujPGH81evMRbEsMQ/HwQIFHoY/3rQiPDqTGmLdbDhW/sIO+lmlNBtFAsETEQ tk6yp77ga+ea8ZXK+4rjZ3AEgaNi4ZU= Content-Type: multipart/signed; boundary=3f963a3bb20f6b64f867ecab281c37450fa0981e2f1a6836331b012a3d46; micalg=pgp-sha512; protocol="application/pgp-signature" Date: Mon, 01 Dec 2025 20:57:38 +0100 Message-Id: <DEN5901V5VZM.M8QVX6C76201@ariadnavigo.xyz> Subject: Re: archlinux-keyring-wkd-sync.service: Errors refreshing keys X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. From: "Ariadna Vigo" <ariadna@ariadnavigo.xyz> To: "Ariadna Vigo" <ariadna@ariadnavigo.xyz>, <arch-general@lists.archlinux.org> References: <DEMYTBMLWOBR.MKHJLQVZAW2X@ariadnavigo.xyz> In-Reply-To: <DEMYTBMLWOBR.MKHJLQVZAW2X@ariadnavigo.xyz> X-Migadu-Flow: FLOW_OUT X-Rspamd-Queue-Id: 6F863601A24A X-Spamd-Result: default: False [-2.60 / 15.00]; SIGNED_PGP(-2.00)[]; MV_CASE(0.50)[]; DMARC_POLICY_ALLOW(-0.50)[ariadnavigo.xyz,quarantine]; R_SPF_ALLOW(-0.20)[+ip4:91.218.175.0/24]; MIME_GOOD(-0.20)[multipart/signed,text/plain]; R_DKIM_ALLOW(-0.20)[ariadnavigo.xyz:s=key1]; ONCE_RECEIVED(0.10)[]; RCVD_IN_DNSWL_LOW(-0.10)[91.218.175.180:from]; FROM_EQ_ENVFROM(0.00)[]; ARC_NA(0.00)[]; MISSING_XM_UA(0.00)[]; MIME_TRACE(0.00)[0:+,1:+,2:~]; TO_DN_SOME(0.00)[]; TO_MATCH_ENVRCPT_SOME(0.00)[]; FUZZY_RATELIMITED(0.00)[rspamd.com]; FROM_HAS_DN(0.00)[]; RCPT_COUNT_TWO(0.00)[2]; RCVD_COUNT_ZERO(0.00)[0]; MID_RHS_MATCH_FROM(0.00)[]; NEURAL_HAM(-0.00)[-0.898]; DKIM_TRACE(0.00)[ariadnavigo.xyz:+] X-Rspamd-Server: lists.archlinux.org X-Rspamd-Action: no action Message-ID-Hash: LPEQRFY72DACIDJH2VCYREL6MYJKO65R X-Message-ID-Hash: LPEQRFY72DACIDJH2VCYREL6MYJKO65R X-MailFrom: ariadna@ariadnavigo.xyz X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: General Discussion about Arch Linux <arch-general.lists.archlinux.org> Archived-At: <https://lists.archlinux.org/archives/list/arch-general@lists.archlinux.org/message/LPEQRFY72DACIDJH2VCYREL6MYJKO65R/> List-Archive: <https://lists.archlinux.org/archives/list/arch-general@lists.archlinux.org/> List-Help: <mailto:arch-general-request@lists.archlinux.org?subject=help> List-Owner: <mailto:arch-general-owner@lists.archlinux.org> List-Post: <mailto:arch-general@lists.archlinux.org> List-Subscribe: <mailto:arch-general-join@lists.archlinux.org> List-Unsubscribe: <mailto:arch-general-leave@lists.archlinux.org> X-DKIM: signer='ariadnavigo.xyz' status='pass' reason='' DKIMCheck: Server passes DKIM test, 0 Spam score X-Spam-Score: -96.1 (---------------------------------------------------) X-Spam-Report: Spam detection software, running on the system "witcher.mxrouting.net", has performed the tests listed below against this email. Information: https://mxroutedocs.com/directadmin/spamfilters/ --- Content analysis details: (-96.1 points) --- pts rule name description ---- ---------------------- ----------------------------------------- 0.0 URIBL_BLOCKED ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [URIs: archlinux.org] 0.0 RCVD_IN_DNSWL_BLOCKED RBL: ADMINISTRATOR NOTICE: The query to DNSWL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#DnsBlocklists-dnsbl-block for more information. [95.217.236.249 listed in list.dnswl.org] -0.0 USER_IN_WELCOMELIST User is listed in 'welcomelist_from' -100 USER_IN_WHITELIST DEPRECATED: See USER_IN_WELCOMELIST 1.5 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level mail domains are different 2.0 PDS_OTHER_BAD_TLD Untrustworthy TLDs [URI: ariadnavigo.xyz (xyz)] -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily valid -0.1 DKIM_VALID_AU Message has a valid DKIM or DK signature from author's domain 1.0 BULK_RE_SUSP_NTLD Precedence bulk and RE: from a suspicious TLD 0.5 FROM_SUSPICIOUS_NTLD From abused NTLD -1.0 MAILING_LIST_MULTI Multiple indicators imply a widely-seen list manager SpamTally: Final spam score: -960 --3f963a3bb20f6b64f867ecab281c37450fa0981e2f1a6836331b012a3d46 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 On Mon Dec 1, 2025 at 3:55 PM CET, Ariadna Vigo wrote: > Hi everyone, > For a couple of weeks now, I have been noticing that > archlinux-keyring-wkd-sync.service fails every single time it is fired up= by its > accompanying timer. Restarts are attempted, but fail nonetheless. The rea= son why > is that many keys are not retrievable via WKD, I imagine because they are= not > set up to be so by their respective owners. I have waited enough to confi= rm that > it wasn't some network error on my side before sending this message. > > Journal info on the failing keys is available at [1]. All other keys are > correctly retrieved via WKD and refreshed if that's the case, so this isn= 't a > critical issue. > > However, if some key isn't available via WKD, maybe should it be silently > dropped, so that errors are reserved to something bad happening to keys t= hat are > available via the protocol? Keys that aren't available via WKD will get u= pdated > via regular updates to the archlinux-keyring package in any case. > > The thing is that I find having a service failing this way (with a timer = that is > enabled by default) avoidable. That 'systemctl status' shows a degraded s= tatus > every week because of this false positive makes it harder to detect when > something else might have gone wrong that could require proper attention. > > Best, > Ariadna > > [1]: https://paste.sr.ht/~ariadna/0f509b0c5823ab225523c989617967820db07a3= a Following up on this, after several tests: 1. Changing DNS servers doesn't fix the issue, as neither does changing the resolver (from NetworkManager to systemd-resolved). 2. Resetting the keyring (deleting /etc/pacman.d/gnupg) and afterwards call= ing the pacman-key --init, pacman-key --populate combo doesn't work either. 3. Connecting to a *different* network whatsoever doesn't fix this either. My observation is that only keys under the archlinux.org domain fail, but n= ot all of them. Keys under other domains never fail, on the other hand. Any ideas? --=20 Ariadna Vigo https://ariadnavigo.xyz gpg 0xC948873069856D6D --3f963a3bb20f6b64f867ecab281c37450fa0981e2f1a6836331b012a3d46 Content-Type: application/pgp-signature; name="signature.asc" -----BEGIN PGP SIGNATURE----- iI4EABYKADYWIQQHuuJG0mwWGLjvk27JSIcwaYVtbQUCaS3zPxgcYXJpYWRuYUBh cmlhZG5hdmlnby54eXoACgkQyUiHMGmFbW3jEAD8CKZBLW1K52gaaKNBiwejPSUg N0f0w7+vtk36ys7TbdABAIqA6lMwlB8spPn5LxOoA5ko12QUExRjtw2OLzpQo6wC =Emo5 -----END PGP SIGNATURE----- --3f963a3bb20f6b64f867ecab281c37450fa0981e2f1a6836331b012a3d46-- From - Mon Dec 01 20:24:19 2025 X-Mozilla-Status: 0001 X-Mozilla-Status2: 00000000 Return-Path: <feedback@service.alibaba.com> Received: from witcher.mxrouting.net by witcher.mxrouting.net with LMTP id cFTJA1X3LWldawMAYBR5ng (envelope-from <feedback@service.alibaba.com>); Mon, 01 Dec 2025 20:15:17 +0000 Return-path: <feedback@service.alibaba.com> Envelope-to: pfeifferj@archlinux.ch Delivery-date: Mon, 01 Dec 2025 20:15:17 +0000 Received: from out197-206.us.a.dm.aliyun.com ([47.90.197.206]) by witcher.mxrouting.net with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.98) (envelope-from <feedback@service.alibaba.com>) id 1vQAIl-00000000y1Y-0nHH for pfeifferj@archlinux.ch; Mon, 01 Dec 2025 20:15:17 +0000 X-AliDM-RcptTo: cGZlaWZmZXJqQGFyY2hsaW51eC5jaA== DKIM-Signature:v=1; a=rsa-sha256; c=relaxed/relaxed; d=service.alibaba.com; s=aliyun-cn-hangzhou; t=1764620111; h=Date:From:To:Message-ID:Subject:MIME-Version:Content-Type; bh=REaj/zOmEGqCg0UAkZPJnkxUSKdHzSNdLYp6fFwwHCc=; b=OjtnWER9WNQiyQSI03I7d3C+IdOL80e8An/2MzUv3DrdAt1U8lCWrieVm7MarWLj981hK2Z/zcZTKDNQEmds9P9CeFx8CwhuuUHoa7n1sLy5glddWDuyUOJZyKewCWvfcVDR5c229iIdAUJZLmgWOVWI6Ud/IeHDVapHRRUOddY= Received: from mobile-messages-service033065085204.center.na610(mailfrom:feedback@service.alibaba.com fp:SMTPD_-VHcEnoL7nV cluster:AY35D) by smtp.aliyun-inc.com(127.0.0.1); Tue, 02 Dec 2025 04:15:10 +0800 X-EnvId: 600000252381797652 Date: Mon, 1 Dec 2025 12:15:10 -0800 (PSvar temml = (function () { 'use strict'; /** * This is the ParseError class, which is the main error thrown by Temml * functions when something has gone wrong. This is used to distinguish internal * errors from errors in the expression that the user provided. * * If possible, a caller should provide a Token or ParseNode with information * about where in the source string the problem occurred. */ class ParseError { constructor( message, // The error message token // An object providing position information ) { let error = " " + message; let start; const loc = token && token.loc; if (loc && loc.start <= loc.end) { // If we have the input and a position, make the error a bit fancier // Get the input const input = loc.lexer.input; // Prepend some information start = loc.start; const end = loc.end; if (start === input.length) { error += " at end of input: "; } else { error += " at position " + (start + 1) + ": \n"; } // Underline token in question using combining underscores const underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332"); // Extract some context from the input and add it to the error let left; if (start > 15) { left = "…" + input.slice(start - 15, start); } else { left = input.slice(0, start); } let right; if (end + 15 < input.length) { right = input.slice(end, end + 15) + "…"; } else { right = input.slice(end); } error += left + underlined + right; } // Some hackery to make ParseError a prototype of Error // See http://stackoverflow.com/a/8460753 const self = new Error(error); self.name = "ParseError"; self.__proto__ = ParseError.prototype; self.position = start; return self; } } ParseError.prototype.__proto__ = Error.prototype; // /** * This file contains a list of utility functions which are useful in other * files. */ /** * Provide a default value if a setting is undefined */ const deflt = function(setting, defaultIfUndefined) { return setting === undefined ? defaultIfUndefined : setting; }; // hyphenate and escape adapted from Facebook's React under Apache 2 license const uppercase = /([A-Z])/g; const hyphenate = function(str) { return str.replace(uppercase, "-$1").toLowerCase(); }; const ESCAPE_LOOKUP = { "&": "&", ">": ">", "<": "<", '"': """, "'": "'" }; const ESCAPE_REGEX = /[&><"']/g; /** * Escapes text to prevent scripting attacks. */ function escape(text) { return String(text).replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]); } /** * Sometimes we want to pull out the innermost element of a group. In most * cases, this will just be the group itself, but when ordgroups and colors have * a single element, we want to pull that out. */ const getBaseElem = function(group) { if (group.type === "ordgroup") { if (group.body.length === 1) { return getBaseElem(group.body[0]); } else { return group; } } else if (group.type === "color") { if (group.body.length === 1) { return getBaseElem(group.body[0]); } else { return group; } } else if (group.type === "font") { return getBaseElem(group.body); } else { return group; } }; /** * TeXbook algorithms often reference "character boxes", which are simply groups * with a single character in them. To decide if something is a character box, * we find its innermost group, and see if it is a single character. */ const isCharacterBox = function(group) { const baseElem = getBaseElem(group); // These are all the types of groups which hold single characters return baseElem.type === "mathord" || baseElem.type === "textord" || baseElem.type === "atom" }; const assert = function(value) { if (!value) { throw new Error("Expected non-null, but got " + String(value)); } return value; }; /** * Return the protocol of a URL, or "_relative" if the URL does not specify a * protocol (and thus is relative), or `null` if URL has invalid protocol * (so should be outright rejected). */ const protocolFromUrl = function(url) { // Check for possible leading protocol. // https://url.spec.whatwg.org/#url-parsing strips leading whitespace // (\x00) or C0 control (\x00-\x1F) characters. // eslint-disable-next-line no-control-regex const protocol = /^[\x00-\x20]*([^\\/#?]*?)(:|�*58|�*3a|&colon)/i.exec(url); if (!protocol) { return "_relative"; } // Reject weird colons if (protocol[2] !== ":") { return null; } // Reject invalid characters in scheme according to // https://datatracker.ietf.org/doc/html/rfc3986#section-3.1 if (!/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(protocol[1])) { return null; } // Lowercase the protocol return protocol[1].toLowerCase(); }; /** * Round `n` to 4 decimal places, or to the nearest 1/10,000th em. The TeXbook * gives an acceptable rounding error of 100sp (which would be the nearest * 1/6551.6em with our ptPerEm = 10): * http://www.ctex.org/documents/shredder/src/texbook.pdf#page=69 */ const round = function(n) { return +n.toFixed(4); }; var utils = { deflt, escape, hyphenate, getBaseElem, isCharacterBox, protocolFromUrl, round }; /** * This is a module for storing settings passed into Temml. It correctly handles * default settings. */ /** * The main Settings object */ class Settings { constructor(options) { // allow null options options = options || {}; this.displayMode = utils.deflt(options.displayMode, false); // boolean this.annotate = utils.deflt(options.annotate, false); // boolean this.leqno = utils.deflt(options.leqno, false); // boolean this.throwOnError = utils.deflt(options.throwOnError, false); // boolean this.errorColor = utils.deflt(options.errorColor, "#b22222"); // string this.macros = options.macros || {}; this.wrap = utils.deflt(options.wrap, "tex"); // "tex" | "=" this.xml = utils.deflt(options.xml, false); // boolean this.colorIsTextColor = utils.deflt(options.colorIsTextColor, false); // booelean this.strict = utils.deflt(options.strict, false); // boolean this.trust = utils.deflt(options.trust, false); // trust context. See html.js. this.maxSize = (options.maxSize === undefined ? [Infinity, Infinity] : Array.isArray(options.maxSize) ? options.maxSize : [Infinity, Infinity] ); this.maxExpand = Math.max(0, utils.deflt(options.maxExpand, 1000)); // number } /** * Check whether to test potentially dangerous input, and return * `true` (trusted) or `false` (untrusted). The sole argument `context` * should be an object with `command` field specifying the relevant LaTeX * command (as a string starting with `\`), and any other arguments, etc. * If `context` has a `url` field, a `protocol` field will automatically * get added by this function (changing the specified object). */ isTrusted(context) { if (context.url && !context.protocol) { const protocol = utils.protocolFromUrl(context.url); if (protocol == null) { return false } context.protocol = protocol; } const trust = typeof this.trust === "function" ? this.trust(context) : this.trust; return Boolean(trust); } } /** * All registered functions. * `functions.js` just exports this same dictionary again and makes it public. * `Parser.js` requires this dictionary. */ const _functions = {}; /** * All MathML builders. Should be only used in the `define*` and the `build*ML` * functions. */ const _mathmlGroupBuilders = {}; function defineFunction({ type, names, props, handler, mathmlBuilder }) { // Set default values of functions const data = { type, numArgs: props.numArgs, argTypes: props.argTypes, allowedInArgument: !!props.allowedInArgument, allowedInText: !!props.allowedInText, allowedInMath: props.allowedInMath === undefined ? true : props.allowedInMath, numOptionalArgs: props.numOptionalArgs || 0, infix: !!props.infix, primitive: !!props.primitive, handler: handler }; for (let i = 0; i < names.length; ++i) { _functions[names[i]] = data; } if (type) { if (mathmlBuilder) { _mathmlGroupBuilders[type] = mathmlBuilder; } } } /** * Use this to register only the MathML builder for a function(e.g. * if the function's ParseNode is generated in Parser.js rather than via a * stand-alone handler provided to `defineFunction`). */ function defineFunctionBuilders({ type, mathmlBuilder }) { defineFunction({ type, names: [], props: { numArgs: 0 }, handler() { throw new Error("Should never be called.") }, mathmlBuilder }); } const normalizeArgument = function(arg) { return arg.type === "ordgroup" && arg.body.length === 1 ? arg.body[0] : arg }; // Since the corresponding buildMathML function expects a // list of elements, we normalize for different kinds of arguments const ordargument = function(arg) { return arg.type === "ordgroup" ? arg.body : [arg] }; /** * This node represents a document fragment, which contains elements, but when * placed into the DOM doesn't have any representation itself. It only contains * children and doesn't have any DOM node properties. */ class DocumentFragment { constructor(children) { this.children = children; this.classes = []; this.style = {}; } hasClass(className) { return this.classes.includes(className); } /** Convert the fragment into a node. */ toNode() { const frag = document.createDocumentFragment(); for (let i = 0; i < this.children.length; i++) { frag.appendChild(this.children[i].toNode()); } return frag; } /** Convert the fragment into HTML markup. */ toMarkup() { let markup = ""; // Simply concatenate the markup for the children together. for (let i = 0; i < this.children.length; i++) { markup += this.children[i].toMarkup(); } return markup; } /** * Converts the math node into a string, similar to innerText. Applies to * MathDomNode's only. */ toText() { // To avoid this, we would subclass documentFragment separately for // MathML, but polyfills for subclassing is expensive per PR 1469. const toText = (child) => child.toText(); return this.children.map(toText).join(""); } } /** * These objects store the data about the DOM nodes we create, as well as some * extra data. They can then be transformed into real DOM nodes with the * `toNode` function or HTML markup using `toMarkup`. They are useful for both * storing extra properties on the nodes, as well as providing a way to easily * work with the DOM. * * Similar functions for working with MathML nodes exist in mathMLTree.js. * */ /** * Create an HTML className based on a list of classes. In addition to joining * with spaces, we also remove empty classes. */ const createClass = function(classes) { return classes.filter((cls) => cls).join(" "); }; const initNode = function(classes, style) { this.classes = classes || []; this.attributes = {}; this.style = style || {}; }; /** * Convert into an HTML node */ const toNode = function(tagName) { const node = document.createElement(tagName); // Apply the class node.className = createClass(this.classes); // Apply inline styles for (const style in this.style) { if (Object.prototype.hasOwnProperty.call(this.style, style )) { node.style[style] = this.style[style]; } } // Apply attributes for (const attr in this.attributes) { if (Object.prototype.hasOwnProperty.call(this.attributes, attr )) { node.setAttribute(attr, this.attributes[attr]); } } // Append the children, also as HTML nodes for (let i = 0; i < this.children.length; i++) { node.appendChild(this.children[i].toNode()); } return node; }; /** * Convert into an HTML markup string */ const toMarkup = function(tagName) { let markup = `<${tagName}`; // Add the class if (this.classes.length) { markup += ` class="${utils.escape(createClass(this.classes))}"`; } let styles = ""; // Add the styles, after hyphenation for (const style in this.style) { if (Object.prototype.hasOwnProperty.call(this.style, style )) { styles += `${utils.hyphenate(style)}:${this.style[style]};`; } } if (styles) { markup += ` style="${styles}"`; } // Add the attributes for (const attr in this.attributes) { if (Object.prototype.hasOwnProperty.call(this.attributes, attr )) { markup += ` ${attr}="${utils.escape(this.attributes[attr])}"`; } } markup += ">"; // Add the markup of the children, also as markup for (let i = 0; i < this.children.length; i++) { markup += this.children[i].toMarkup(); } markup += `</${tagName}>`; return markup; }; /** * This node represents a span node, with a className, a list of children, and * an inline style. * */ class Span { constructor(classes, children, style) { initNode.call(this, classes, style); this.children = children || []; } setAttribute(attribute, value) { this.attributes[attribute] = value; } toNode() { return toNode.call(this, "span"); } toMarkup() { return toMarkup.call(this, "span"); } } let TextNode$1 = class TextNode { constructor(text) { this.text = text; } toNode() { return document.createTextNode(this.text); } toMarkup() { return utils.escape(this.text); } }; // Create an <a href="…"> node. class AnchorNode { constructor(href, classes, children) { this.href = href; this.classes = classes; this.children = children || []; } toNode() { const node = document.createElement("a"); node.setAttribute("href", this.href); if (this.classes.length > 0) { node.className = createClass(this.classes); } for (let i = 0; i < this.children.length; i++) { node.appendChild(this.children[i].toNode()); } return node } toMarkup() { let markup = `<a href='${utils.escape(this.href)}'`; if (this.classes.length > 0) { markup += ` class="${utils.escape(createClass(this.classes))}"`; } markup += ">"; for (let i = 0; i < this.children.length; i++) { markup += this.children[i].toMarkup(); } markup += "</a>"; return markup } } /* * This node represents an image embed (<img>) element. */ class Img { constructor(src, alt, style) { this.alt = alt; this.src = src; this.classes = ["mord"]; this.style = style; } hasClass(className) { return this.classes.includes(className); } toNode() { const node = document.createElement("img"); node.src = this.src; node.alt = this.alt; node.className = "mord"; // Apply inline styles for (const style in this.style) { if (Object.prototype.hasOwnProperty.call(this.style, style )) { node.style[style] = this.style[style]; } } return node; } toMarkup() { let markup = `<img src='${this.src}' alt='${this.alt}'`; // Add the styles, after hyphenation let styles = ""; for (const style in this.style) { if (Object.prototype.hasOwnProperty.call(this.style, style )) { styles += `${utils.hyphenate(style)}:${this.style[style]};`; } } if (styles) { markup += ` style="${utils.escape(styles)}"`; } markup += ">"; return markup; } } // /** * These objects store data about MathML nodes. * The `toNode` and `toMarkup` functions create namespaced DOM nodes and * HTML text markup respectively. */ function newDocumentFragment(children) { return new DocumentFragment(children); } /** * This node represents a general purpose MathML node of any type, * for example, `"mo"` or `"mspace"`, corresponding to `<mo>` and * `<mspace>` tags). */ class MathNode { constructor(type, children, classes, style) { this.type = type; this.attributes = {}; this.children = children || []; this.classes = classes || []; this.style = style || {}; // Used for <mstyle> elements this.label = ""; } /** * Sets an attribute on a MathML node. MathML depends on attributes to convey a * semantic content, so this is used heavily. */ setAttribute(name, value) { this.attributes[name] = value; } /** * Gets an attribute on a MathML node. */ getAttribute(name) { return this.attributes[name]; } setLabel(value) { this.label = value; } /** * Converts the math node into a MathML-namespaced DOM element. */ toNode() { const node = document.createElementNS("http://www.w3.org/1998/Math/MathML", this.type); for (const attr in this.attributes) { if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { node.setAttribute(attr, this.attributes[attr]); } } if (this.classes.length > 0) { node.className = createClass(this.classes); } // Apply inline styles for (const style in this.style) { if (Object.prototype.hasOwnProperty.call(this.style, style )) { node.style[style] = this.style[style]; } } for (let i = 0; i < this.children.length; i++) { node.appendChild(this.children[i].toNode()); } return node; } /** * Converts the math node into an HTML markup string. */ toMarkup() { let markup = "<" + this.type; // Add the attributes for (const attr in this.attributes) { if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { markup += " " + attr + '="'; markup += utils.escape(this.attributes[attr]); markup += '"'; } } if (this.classes.length > 0) { markup += ` class="${utils.escape(createClass(this.classes))}"`; } let styles = ""; // Add the styles, after hyphenation for (const style in this.style) { if (Object.prototype.hasOwnProperty.call(this.style, style )) { styles += `${utils.hyphenate(style)}:${this.style[style]};`; } } if (styles) { markup += ` style="${styles}"`; } markup += ">"; for (let i = 0; i < this.children.length; i++) { markup += this.children[i].toMarkup(); } markup += "</" + this.type + ">"; return markup; } /** * Converts the math node into a string, similar to innerText, but escaped. */ toText() { return this.children.map((child) => child.toText()).join(""); } } /** * This node represents a piece of text. */ class TextNode { constructor(text) { this.text = text; } /** * Converts the text node into a DOM text node. */ toNode() { return document.createTextNode(this.text); } /** * Converts the text node into escaped HTML markup * (representing the text itself). */ toMarkup() { return utils.escape(this.toText()); } /** * Converts the text node into a string * (representing the text itself). */ toText() { return this.text; } } // Do not make an <mrow> the only child of a <mstyle>. // An <mstyle> acts as its own implicit <mrow>. const wrapWithMstyle = expression => { let node; if (expression.length === 1 && expression[0].type === "mrow") { node = expression.pop(); node.type = "mstyle"; } else { node = new MathNode("mstyle", expression); } return node }; var mathMLTree = { MathNode, TextNode, newDocumentFragment }; /** * This file provides support for building horizontal stretchy elements. */ // TODO: Remove when Chromium stretches \widetilde & \widehat const estimatedWidth = node => { let width = 0; if (node.body && Array.isArray(node.body)) { for (const item of node.body) { width += estimatedWidth(item); } } else if (node.body) { width += estimatedWidth(node.body); } else if (node.type === "supsub") { width += estimatedWidth(node.base); if (node.sub) { width += 0.7 * estimatedWidth(node.sub); } if (node.sup) { width += 0.7 * estimatedWidth(node.sup); } } else if (node.type === "mathord" || node.type === "textord") { for (const ch of node.text.split('')) { const codePoint = ch.codePointAt(0); if ((0x60 < codePoint && codePoint < 0x7B) || (0x03B0 < codePoint && codePoint < 0x3CA)) { width += 0.56; // lower case latin or greek. Use advance width of letter n } else if (0x2F < codePoint && codePoint < 0x3A) { width += 0.50; // numerals. } else { width += 0.92; // advance width of letter M } } } else { width += 1.0; } return width }; const stretchyCodePoint = { widehat: "^", widecheck: "ˇ", widetilde: "~", wideparen: "⏜", // \u23dc utilde: "~", overleftarrow: "\u2190", underleftarrow: "\u2190", xleftarrow: "\u2190", overrightarrow: "\u2192", underrightarrow: "\u2192", xrightarrow: "\u2192", underbrace: "\u23df", overbrace: "\u23de", overgroup: "\u23e0", overparen: "⏜", undergroup: "\u23e1", underparen: "\u23dd", overleftrightarrow: "\u2194", underleftrightarrow: "\u2194", xleftrightarrow: "\u2194", Overrightarrow: "\u21d2", xRightarrow: "\u21d2", overleftharpoon: "\u21bc", xleftharpoonup: "\u21bc", overrightharpoon: "\u21c0", xrightharpoonup: "\u21c0", xLeftarrow: "\u21d0", xLeftrightarrow: "\u21d4", xhookleftarrow: "\u21a9", xhookrightarrow: "\u21aa", xmapsto: "\u21a6", xrightharpoondown: "\u21c1", xleftharpoondown: "\u21bd", xtwoheadleftarrow: "\u219e", xtwoheadrightarrow: "\u21a0", xlongequal: "=", xrightleftarrows: "\u21c4", xtofrom: "\u21c4", xleftrightharpoons: "\u21cb", xrightleftharpoons: "\u21cc", yields: "\u2192", yieldsLeft: "\u2190", mesomerism: "\u2194", longrightharpoonup: "\u21c0", longleftharpoondown: "\u21bd", eqrightharpoonup: "\u21c0", eqleftharpoondown: "\u21bd", "\\cdrightarrow": "\u2192", "\\cdleftarrow": "\u2190", "\\cdlongequal": "=", yieldsLeftRight: "\u21c4", chemequilibrium: "\u21cc" }; const mathMLnode = function(label) { const child = new mathMLTree.TextNode(stretchyCodePoint[label.slice(1)]); const node = new mathMLTree.MathNode("mo", [child]); node.setAttribute("stretchy", "true"); return node }; const crookedWides = ["\\widetilde", "\\widehat", "\\widecheck", "\\utilde"]; // TODO: Remove when Chromium stretches \widetilde & \widehat const accentNode = (group) => { const mo = mathMLnode(group.label); if (crookedWides.includes(group.label)) { const width = estimatedWidth(group.base); if (1 < width && width < 1.6) { mo.classes.push("tml-crooked-2"); } else if (1.6 <= width && width < 2.5) { mo.classes.push("tml-crooked-3"); } else if (2.5 <= width) { mo.classes.push("tml-crooked-4"); } } return mo }; var stretchy = { mathMLnode, accentNode }; /** * This file holds a list of all no-argument functions and single-character * symbols (like 'a' or ';'). * * For each of the symbols, there are two properties they can have: * - group (required): the ParseNode group type the symbol should have (i.e. "textord", "mathord", etc). * - replace: the character that this symbol or function should be * replaced with (i.e. "\phi" has a replace value of "\u03d5", the phi * character in the main font). * * The outermost map in the table indicates what mode the symbols should be * accepted in (e.g. "math" or "text"). */ // Some of these have a "-token" suffix since these are also used as `ParseNode` // types for raw text tokens, and we want to avoid conflicts with higher-level // `ParseNode` types. These `ParseNode`s are constructed within `Parser` by // looking up the `symbols` map. const ATOMS = { bin: 1, close: 1, inner: 1, open: 1, punct: 1, rel: 1 }; const NON_ATOMS = { "accent-token": 1, mathord: 1, "op-token": 1, spacing: 1, textord: 1 }; const symbols = { math: {}, text: {} }; /** `acceptUnicodeChar = true` is only applicable if `replace` is set. */ function defineSymbol(mode, group, replace, name, acceptUnicodeChar) { symbols[mode][name] = { group, replace }; if (acceptUnicodeChar && replace) { symbols[mode][replace] = symbols[mode][name]; } } // Some abbreviations for commonly used strings. // This helps minify the code, and also spotting typos using jshint. // modes: const math = "math"; const text = "text"; // groups: const accent = "accent-token"; const bin = "bin"; const close = "close"; const inner = "inner"; const mathord = "mathord"; const op = "op-token"; const open = "open"; const punct = "punct"; const rel = "rel"; const spacing = "spacing"; const textord = "textord"; // Now comes the symbol table // Relation Symbols defineSymbol(math, rel, "\u2261", "\\equiv", true); defineSymbol(math, rel, "\u227a", "\\prec", true); defineSymbol(math, rel, "\u227b", "\\succ", true); defineSymbol(math, rel, "\u223c", "\\sim", true); defineSymbol(math, rel, "\u27c2", "\\perp", true); defineSymbol(math, rel, "\u2aaf", "\\preceq", true); defineSymbol(math, rel, "\u2ab0", "\\succeq", true); defineSymbol(math, rel, "\u2243", "\\simeq", true); defineSymbol(math, rel, "\u224c", "\\backcong", true); defineSymbol(math, rel, "|", "\\mid", true); defineSymbol(math, rel, "\u226a", "\\ll", true); defineSymbol(math, rel, "\u226b", "\\gg", true); defineSymbol(math, rel, "\u224d", "\\asymp", true); defineSymbol(math, rel, "\u2225", "\\parallel"); defineSymbol(math, rel, "\u2323", "\\smile", true); defineSymbol(math, rel, "\u2291", "\\sqsubseteq", true); defineSymbol(math, rel, "\u2292", "\\sqsupseteq", true); defineSymbol(math, rel, "\u2250", "\\doteq", true); defineSymbol(math, rel, "\u2322", "\\frown", true); defineSymbol(math, rel, "\u220b", "\\ni", true); defineSymbol(math, rel, "\u220c", "\\notni", true); defineSymbol(math, rel, "\u221d", "\\propto", true); defineSymbol(math, rel, "\u22a2", "\\vdash", true); defineSymbol(math, rel, "\u22a3", "\\dashv", true); defineSymbol(math, rel, "\u220b", "\\owns"); defineSymbol(math, rel, "\u2258", "\\arceq", true); defineSymbol(math, rel, "\u2259", "\\wedgeq", true); defineSymbol(math, rel, "\u225a", "\\veeeq", true); defineSymbol(math, rel, "\u225b", "\\stareq", true); defineSymbol(math, rel, "\u225d", "\\eqdef", true); defineSymbol(math, rel, "\u225e", "\\measeq", true); defineSymbol(math, rel, "\u225f", "\\questeq", true); defineSymbol(math, rel, "\u2260", "\\ne", true); defineSymbol(math, rel, "\u2260", "\\neq"); // unicodemath defineSymbol(math, rel, "\u2a75", "\\eqeq", true); defineSymbol(math, rel, "\u2a76", "\\eqeqeq", true); // mathtools.sty defineSymbol(math, rel, "\u2237", "\\dblcolon", true); defineSymbol(math, rel, "\u2254", "\\coloneqq", true); defineSymbol(math, rel, "\u2255", "\\eqqcolon", true); defineSymbol(math, rel, "\u2239", "\\eqcolon", true); defineSymbol(math, rel, "\u2A74", "\\Coloneqq", true); // Punctuation defineSymbol(math, punct, "\u002e", "\\ldotp"); defineSymbol(math, punct, "\u00b7", "\\cdotp"); // Misc Symbols defineSymbol(math, textord, "\u0023", "\\#"); defineSymbol(text, textord, "\u0023", "\\#"); defineSymbol(math, textord, "\u0026", "\\&"); defineSymbol(text, textord, "\u0026", "\\&"); defineSymbol(math, textord, "\u2135", "\\aleph", true); defineSymbol(math, textord, "\u2200", "\\forall", true); defineSymbol(math, textord, "\u210f", "\\hbar", true); defineSymbol(math, textord, "\u2203", "\\exists", true); // ∇ is actually a unary operator, not binary. But this works. defineSymbol(math, bin, "\u2207", "\\nabla", true); defineSymbol(math, textord, "\u266d", "\\flat", true); defineSymbol(math, textord, "\u2113", "\\ell", true); defineSymbol(math, textord, "\u266e", "\\natural", true); defineSymbol(math, textord, "Å", "\\Angstrom", true); defineSymbol(text, textord, "Å", "\\Angstrom", true); defineSymbol(math, textord, "\u2663", "\\clubsuit", true); defineSymbol(math, textord, "\u2667", "\\varclubsuit", true); defineSymbol(math, textord, "\u2118", "\\wp", true); defineSymbol(math, textord, "\u266f", "\\sharp", true); defineSymbol(math, textord, "\u2662", "\\diamondsuit", true); defineSymbol(math, textord, "\u2666", "\\vardiamondsuit", true); defineSymbol(math, textord, "\u211c", "\\Re", true); defineSymbol(math, textord, "\u2661", "\\heartsuit", true); defineSymbol(math, textord, "\u2665", "\\varheartsuit", true); defineSymbol(math, textord, "\u2111", "\\Im", true); defineSymbol(math, textord, "\u2660", "\\spadesuit", true); defineSymbol(math, textord, "\u2664", "\\varspadesuit", true); defineSymbol(math, textord, "\u2640", "\\female", true); defineSymbol(math, textord, "\u2642", "\\male", true); defineSymbol(math, textord, "\u00a7", "\\S", true); defineSymbol(text, textord, "\u00a7", "\\S"); defineSymbol(math, textord, "\u00b6", "\\P", true); defineSymbol(text, textord, "\u00b6", "\\P"); defineSymbol(text, textord, "\u263a", "\\smiley", true); defineSymbol(math, textord, "\u263a", "\\smiley", true); // Math and Text defineSymbol(math, textord, "\u2020", "\\dag"); defineSymbol(text, textord, "\u2020", "\\dag"); defineSymbol(text, textord, "\u2020", "\\textdagger"); defineSymbol(math, textord, "\u2021", "\\ddag"); defineSymbol(text, textord, "\u2021", "\\ddag"); defineSymbol(text, textord, "\u2021", "\\textdaggerdbl"); // Large Delimiters defineSymbol(math, close, "\u23b1", "\\rmoustache", true); defineSymbol(math, open, "\u23b0", "\\lmoustache", true); defineSymbol(math, close, "\u27ef", "\\rgroup", true); defineSymbol(math, open, "\u27ee", "\\lgroup", true); // Binary Operators defineSymbol(math, bin, "\u2213", "\\mp", true); defineSymbol(math, bin, "\u2296", "\\ominus", true); defineSymbol(math, bin, "\u228e", "\\uplus", true); defineSymbol(math, bin, "\u2293", "\\sqcap", true); defineSymbol(math, bin, "\u2217", "\\ast"); defineSymbol(math, bin, "\u2294", "\\sqcup", true); defineSymbol(math, bin, "\u25ef", "\\bigcirc", true); defineSymbol(math, bin, "\u2219", "\\bullet", true); defineSymbol(math, bin, "\u2021", "\\ddagger"); defineSymbol(math, bin, "\u2240", "\\wr", true); defineSymbol(math, bin, "\u2a3f", "\\amalg"); defineSymbol(math, bin, "\u0026", "\\And"); // from amsmath defineSymbol(math, bin, "\u2AFD", "\\sslash", true); // from stmaryrd // Arrow Symbols defineSymbol(math, rel, "\u27f5", "\\longleftarrow", true); defineSymbol(math, rel, "\u21d0", "\\Leftarrow", true); defineSymbol(math, rel, "\u27f8", "\\Longleftarrow", true); defineSymbol(math, rel, "\u27f6", "\\longrightarrow", true); defineSymbol(math, rel, "\u21d2", "\\Rightarrow", true); defineSymbol(math, rel, "\u27f9", "\\Longrightarrow", true); defineSymbol(math, rel, "\u2194", "\\leftrightarrow", true); defineSymbol(math, rel, "\u27f7", "\\longleftrightarrow", true); defineSymbol(math, rel, "\u21d4", "\\Leftrightarrow", true); defineSymbol(math, rel, "\u27fa", "\\Longleftrightarrow", true); defineSymbol(math, rel, "\u21a4", "\\mapsfrom", true); defineSymbol(math, rel, "\u21a6", "\\mapsto", true); defineSymbol(math, rel, "\u27fc", "\\longmapsto", true); defineSymbol(math, rel, "\u2197", "\\nearrow", true); defineSymbol(math, rel, "\u21a9", "\\hookleftarrow", true); defineSymbol(math, rel, "\u21aa", "\\hookrightarrow", true); defineSymbol(math, rel, "\u2198", "\\searrow", true); defineSymbol(math, rel, "\u21bc", "\\leftharpoonup", true); defineSymbol(math, rel, "\u21c0", "\\rightharpoonup", true); defineSymbol(math, rel, "\u2199", "\\swarrow", true); defineSymbol(math, rel, "\u21bd", "\\leftharpoondown", true); defineSymbol(math, rel, "\u21c1", "\\rightharpoondown", true); defineSymbol(math, rel, "\u2196", "\\nwarrow", true); defineSymbol(math, rel, "\u21cc", "\\rightleftharpoons", true); defineSymbol(math, mathord, "\u21af", "\\lightning", true); defineSymbol(math, mathord, "\u220E", "\\QED", true); defineSymbol(math, mathord, "\u2030", "\\permil", true); defineSymbol(text, textord, "\u2030", "\\permil"); defineSymbol(math, mathord, "\u2609", "\\astrosun", true); defineSymbol(math, mathord, "\u263c", "\\sun", true); defineSymbol(math, mathord, "\u263e", "\\leftmoon", true); defineSymbol(math, mathord, "\u263d", "\\rightmoon", true); defineSymbol(math, mathord, "\u2295", "\\Earth"); // AMS Negated Binary Relations defineSymbol(math, rel, "\u226e", "\\nless", true); // Symbol names preceeded by "@" each have a corresponding macro. defineSymbol(math, rel, "\u2a87", "\\lneq", true); defineSymbol(math, rel, "\u2268", "\\lneqq", true); defineSymbol(math, rel, "\u2268\ufe00", "\\lvertneqq"); defineSymbol(math, rel, "\u22e6", "\\lnsim", true); defineSymbol(math, rel, "\u2a89", "\\lnapprox", true); defineSymbol(math, rel, "\u2280", "\\nprec", true); // unicode-math maps \u22e0 to \npreccurlyeq. We'll use the AMS synonym. defineSymbol(math, rel, "\u22e0", "\\npreceq", true); defineSymbol(math, rel, "\u22e8", "\\precnsim", true); defineSymbol(math, rel, "\u2ab9", "\\precnapprox", true); defineSymbol(math, rel, "\u2241", "\\nsim", true); defineSymbol(math, rel, "\u2224", "\\nmid", true); defineSymbol(math, rel, "\u2224", "\\nshortmid"); defineSymbol(math, rel, "\u22ac", "\\nvdash", true); defineSymbol(math, rel, "\u22ad", "\\nvDash", true); defineSymbol(math, rel, "\u22ea", "\\ntriangleleft"); defineSymbol(math, rel, "\u22ec", "\\ntrianglelefteq", true); defineSymbol(math, rel, "\u2284", "\\nsubset", true); defineSymbol(math, rel, "\u2285", "\\nsupset", true); defineSymbol(math, rel, "\u228a", "\\subsetneq", true); defineSymbol(math, rel, "\u228a\ufe00", "\\varsubsetneq"); defineSymbol(math, rel, "\u2acb", "\\subsetneqq", true); defineSymbol(math, rel, "\u2acb\ufe00", "\\varsubsetneqq"); defineSymbol(math, rel, "\u226f", "\\ngtr", true); defineSymbol(math, rel, "\u2a88", "\\gneq", true); defineSymbol(math, rel, "\u2269", "\\gneqq", true); defineSymbol(math, rel, "\u2269\ufe00", "\\gvertneqq"); defineSymbol(math, rel, "\u22e7", "\\gnsim", true); defineSymbol(math, rel, "\u2a8a", "\\gnapprox", true); defineSymbol(math, rel, "\u2281", "\\nsucc", true); // unicode-math maps \u22e1 to \nsucccurlyeq. We'll use the AMS synonym. defineSymbol(math, rel, "\u22e1", "\\nsucceq", true); defineSymbol(math, rel, "\u22e9", "\\succnsim", true); defineSymbol(math, rel, "\u2aba", "\\succnapprox", true); // unicode-math maps \u2246 to \simneqq. We'll use the AMS synonym. defineSymbol(math, rel, "\u2246", "\\ncong", true); defineSymbol(math, rel, "\u2226", "\\nparallel", true); defineSymbol(math, rel, "\u2226", "\\nshortparallel"); defineSymbol(math, rel, "\u22af", "\\nVDash", true); defineSymbol(math, rel, "\u22eb", "\\ntriangleright"); defineSymbol(math, rel, "\u22ed", "\\ntrianglerighteq", true); defineSymbol(math, rel, "\u228b", "\\supsetneq", true); defineSymbol(math, rel, "\u228b", "\\varsupsetneq"); defineSymbol(math, rel, "\u2acc", "\\supsetneqq", true); defineSymbol(math, rel, "\u2acc\ufe00", "\\varsupsetneqq"); defineSymbol(math, rel, "\u22ae", "\\nVdash", true); defineSymbol(math, rel, "\u2ab5", "\\precneqq", true); defineSymbol(math, rel, "\u2ab6", "\\succneqq", true); defineSymbol(math, bin, "\u22b4", "\\unlhd"); defineSymbol(math, bin, "\u22b5", "\\unrhd"); // AMS Negated Arrows defineSymbol(math, rel, "\u219a", "\\nleftarrow", true); defineSymbol(math, rel, "\u219b", "\\nrightarrow", true); defineSymbol(math, rel, "\u21cd", "\\nLeftarrow", true); defineSymbol(math, rel, "\u21cf", "\\nRightarrow", true); defineSymbol(math, rel, "\u21ae", "\\nleftrightarrow", true); defineSymbol(math, rel, "\u21ce", "\\nLeftrightarrow", true); // AMS Misc defineSymbol(math, rel, "\u25b3", "\\vartriangle"); defineSymbol(math, textord, "\u210f", "\\hslash"); defineSymbol(math, textord, "\u25bd", "\\triangledown"); defineSymbol(math, textord, "\u25ca", "\\lozenge"); defineSymbol(math, textord, "\u24c8", "\\circledS"); defineSymbol(math, textord, "\u00ae", "\\circledR", true); defineSymbol(text, textord, "\u00ae", "\\circledR"); defineSymbol(text, textord, "\u00ae", "\\textregistered"); defineSymbol(math, textord, "\u2221", "\\measuredangle", true); defineSymbol(math, textord, "\u2204", "\\nexists"); defineSymbol(math, textord, "\u2127", "\\mho"); defineSymbol(math, textord, "\u2132", "\\Finv", true); defineSymbol(math, textord, "\u2141", "\\Game", true); defineSymbol(math, textord, "\u2035", "\\backprime"); defineSymbol(math, textord, "\u2036", "\\backdprime"); defineSymbol(math, textord, "\u2037", "\\backtrprime"); defineSymbol(math, textord, "\u25b2", "\\blacktriangle"); defineSymbol(math, textord, "\u25bc", "\\blacktriangledown"); defineSymbol(math, textord, "\u25a0", "\\blacksquare"); defineSymbol(math, textord, "\u29eb", "\\blacklozenge"); defineSymbol(math, textord, "\u2605", "\\bigstar"); defineSymbol(math, textord, "\u2222", "\\sphericalangle", true); defineSymbol(math, textord, "\u2201", "\\complement", true); defineSymbol(math, textord, "\u2571", "\\diagup"); defineSymbol(math, textord, "\u2572", "\\diagdown"); defineSymbol(math, textord, "\u25a1", "\\square"); defineSymbol(math, textord, "\u25a1", "\\Box"); defineSymbol(math, textord, "\u25ca", "\\Diamond"); // unicode-math maps U+A5 to \mathyen. We map to AMS function \yen defineSymbol(math, textord, "\u00a5", "\\yen", true); defineSymbol(text, textord, "\u00a5", "\\yen", true); defineSymbol(math, textord, "\u2713", "\\checkmark", true); defineSymbol(text, textord, "\u2713", "\\checkmark"); defineSymbol(math, textord, "\u2717", "\\ballotx", true); defineSymbol(text, textord, "\u2717", "\\ballotx"); defineSymbol(text, textord, "\u2022", "\\textbullet"); // AMS Hebrew defineSymbol(math, textord, "\u2136", "\\beth", true); defineSymbol(math, textord, "\u2138", "\\daleth", true); defineSymbol(math, textord, "\u2137", "\\gimel", true); // AMS Greek defineSymbol(math, textord, "\u03dd", "\\digamma", true); defineSymbol(math, textord, "\u03f0", "\\varkappa"); // AMS Delimiters defineSymbol(math, open, "\u231C", "\\ulcorner", true); defineSymbol(math, close, "\u231D", "\\urcorner", true); defineSymbol(math, open, "\u231E", "\\llcorner", true); defineSymbol(math, close, "\u231F", "\\lrcorner", true); // AMS Binary Relations defineSymbol(math, rel, "\u2266", "\\leqq", true); defineSymbol(math, rel, "\u2a7d", "\\leqslant", true); defineSymbol(math, rel, "\u2a95", "\\eqslantless", true); defineSymbol(math, rel, "\u2272", "\\lesssim", true); defineSymbol(math, rel, "\u2a85", "\\lessapprox", true); defineSymbol(math, rel, "\u224a", "\\approxeq", true); defineSymbol(math, bin, "\u22d6", "\\lessdot"); defineSymbol(math, rel, "\u22d8", "\\lll", true); defineSymbol(math, rel, "\u2276", "\\lessgtr", true); defineSymbol(math, rel, "\u22da", "\\lesseqgtr", true); defineSymbol(math, rel, "\u2a8b", "\\lesseqqgtr", true); defineSymbol(math, rel, "\u2251", "\\doteqdot"); defineSymbol(math, rel, "\u2253", "\\risingdotseq", true); defineSymbol(math, rel, "\u2252", "\\fallingdotseq", true); defineSymbol(math, rel, "\u223d", "\\backsim", true); defineSymbol(math, rel, "\u22cd", "\\backsimeq", true); defineSymbol(math, rel, "\u2ac5", "\\subseteqq", true); defineSymbol(math, rel, "\u22d0", "\\Subset", true); defineSymbol(math, rel, "\u228f", "\\sqsubset", true); defineSymbol(math, rel, "\u227c", "\\preccurlyeq", true); defineSymbol(math, rel, "\u22de", "\\curlyeqprec", true); defineSymbol(math, rel, "\u227e", "\\precsim", true); defineSymbol(math, rel, "\u2ab7", "\\precapprox", true); defineSymbol(math, rel, "\u22b2", "\\vartriangleleft"); defineSymbol(math, rel, "\u22b4", "\\trianglelefteq"); defineSymbol(math, rel, "\u22a8", "\\vDash", true); defineSymbol(math, rel, "\u22ab", "\\VDash", true); defineSymbol(math, rel, "\u22aa", "\\Vvdash", true); defineSymbol(math, rel, "\u2323", "\\smallsmile"); defineSymbol(math, rel, "\u2322", "\\smallfrown"); defineSymbol(math, rel, "\u224f", "\\bumpeq", true); defineSymbol(math, rel, "\u224e", "\\Bumpeq", true); defineSymbol(math, rel, "\u2267", "\\geqq", true); defineSymbol(math, rel, "\u2a7e", "\\geqslant", true); defineSymbol(math, rel, "\u2a96", "\\eqslantgtr", true); defineSymbol(math, rel, "\u2273", "\\gtrsim", true); defineSymbol(math, rel, "\u2a86", "\\gtrapprox", true); defineSymbol(math, bin, "\u22d7", "\\gtrdot"); defineSymbol(math, rel, "\u22d9", "\\ggg", true); defineSymbol(math, rel, "\u2277", "\\gtrless", true); defineSymbol(math, rel, "\u22db", "\\gtreqless", true); defineSymbol(math, rel, "\u2a8c", "\\gtreqqless", true); defineSymbol(math, rel, "\u2256", "\\eqcirc", true); defineSymbol(math, rel, "\u2257", "\\circeq", true); defineSymbol(math, rel, "\u225c", "\\triangleq", true); defineSymbol(math, rel, "\u223c", "\\thicksim"); defineSymbol(math, rel, "\u2248", "\\thickapprox"); defineSymbol(math, rel, "\u2ac6", "\\supseteqq", true); defineSymbol(math, rel, "\u22d1", "\\Supset", true); defineSymbol(math, rel, "\u2290", "\\sqsupset", true); defineSymbol(math, rel, "\u227d", "\\succcurlyeq", true); defineSymbol(math, rel, "\u22df", "\\curlyeqsucc", true); defineSymbol(math, rel, "\u227f", "\\succsim", true); defineSymbol(math, rel, "\u2ab8", "\\succapprox", true); defineSymbol(math, rel, "\u22b3", "\\vartriangleright"); defineSymbol(math, rel, "\u22b5", "\\trianglerighteq"); defineSymbol(math, rel, "\u22a9", "\\Vdash", true); defineSymbol(math, rel, "\u2223", "\\shortmid"); defineSymbol(math, rel, "\u2225", "\\shortparallel"); defineSymbol(math, rel, "\u226c", "\\between", true); defineSymbol(math, rel, "\u22d4", "\\pitchfork", true); defineSymbol(math, rel, "\u221d", "\\varpropto"); defineSymbol(math, rel, "\u25c0", "\\blacktriangleleft"); // unicode-math says that \therefore is a mathord atom. // We kept the amssymb atom type, which is rel. defineSymbol(math, rel, "\u2234", "\\therefore", true); defineSymbol(math, rel, "\u220d", "\\backepsilon"); defineSymbol(math, rel, "\u25b6", "\\blacktriangleright"); // unicode-math says that \because is a mathord atom. // We kept the amssymb atom type, which is rel. defineSymbol(math, rel, "\u2235", "\\because", true); defineSymbol(math, rel, "\u22d8", "\\llless"); defineSymbol(math, rel, "\u22d9", "\\gggtr"); defineSymbol(math, bin, "\u22b2", "\\lhd"); defineSymbol(math, bin, "\u22b3", "\\rhd"); defineSymbol(math, rel, "\u2242", "\\eqsim", true); defineSymbol(math, rel, "\u2251", "\\Doteq", true); defineSymbol(math, rel, "\u297d", "\\strictif", true); defineSymbol(math, rel, "\u297c", "\\strictfi", true); // AMS Binary Operators defineSymbol(math, bin, "\u2214", "\\dotplus", true); defineSymbol(math, bin, "\u2216", "\\smallsetminus"); defineSymbol(math, bin, "\u22d2", "\\Cap", true); defineSymbol(math, bin, "\u22d3", "\\Cup", true); defineSymbol(math, bin, "\u2a5e", "\\doublebarwedge", true); defineSymbol(math, bin, "\u229f", "\\boxminus", true); defineSymbol(math, bin, "\u229e", "\\boxplus", true); defineSymbol(math, bin, "\u29C4", "\\boxslash", true); defineSymbol(math, bin, "\u22c7", "\\divideontimes", true); defineSymbol(math, bin, "\u22c9", "\\ltimes", true); defineSymbol(math, bin, "\u22ca", "\\rtimes", true); defineSymbol(math, bin, "\u22cb", "\\leftthreetimes", true); defineSymbol(math, bin, "\u22cc", "\\rightthreetimes", true); defineSymbol(math, bin, "\u22cf", "\\curlywedge", true); defineSymbol(math, bin, "\u22ce", "\\curlyvee", true); defineSymbol(math, bin, "\u229d", "\\circleddash", true); defineSymbol(math, bin, "\u229b", "\\circledast", true); defineSymbol(math, bin, "\u22ba", "\\intercal", true); defineSymbol(math, bin, "\u22d2", "\\doublecap"); defineSymbol(math, bin, "\u22d3", "\\doublecup"); defineSymbol(math, bin, "\u22a0", "\\boxtimes", true); defineSymbol(math, bin, "\u22c8", "\\bowtie", true); defineSymbol(math, bin, "\u22c8", "\\Join"); defineSymbol(math, bin, "\u27d5", "\\leftouterjoin", true); defineSymbol(math, bin, "\u27d6", "\\rightouterjoin", true); defineSymbol(math, bin, "\u27d7", "\\fullouterjoin", true); // stix Binary Operators defineSymbol(math, bin, "\u2238", "\\dotminus", true); defineSymbol(math, bin, "\u27D1", "\\wedgedot", true); defineSymbol(math, bin, "\u27C7", "\\veedot", true); defineSymbol(math, bin, "\u2A62", "\\doublebarvee", true); defineSymbol(math, bin, "\u2A63", "\\veedoublebar", true); defineSymbol(math, bin, "\u2A5F", "\\wedgebar", true); defineSymbol(math, bin, "\u2A60", "\\wedgedoublebar", true); defineSymbol(math, bin, "\u2A54", "\\Vee", true); defineSymbol(math, bin, "\u2A53", "\\Wedge", true); defineSymbol(math, bin, "\u2A43", "\\barcap", true); defineSymbol(math, bin, "\u2A42", "\\barcup", true); defineSymbol(math, bin, "\u2A48", "\\capbarcup", true); defineSymbol(math, bin, "\u2A40", "\\capdot", true); defineSymbol(math, bin, "\u2A47", "\\capovercup", true); defineSymbol(math, bin, "\u2A46", "\\cupovercap", true); defineSymbol(math, bin, "\u2A4D", "\\closedvarcap", true); defineSymbol(math, bin, "\u2A4C", "\\closedvarcup", true); defineSymbol(math, bin, "\u2A2A", "\\minusdot", true); defineSymbol(math, bin, "\u2A2B", "\\minusfdots", true); defineSymbol(math, bin, "\u2A2C", "\\minusrdots", true); defineSymbol(math, bin, "\u22BB", "\\Xor", true); defineSymbol(math, bin, "\u22BC", "\\Nand", true); defineSymbol(math, bin, "\u22BD", "\\Nor", true); defineSymbol(math, bin, "\u22BD", "\\barvee"); defineSymbol(math, bin, "\u2AF4", "\\interleave", true); defineSymbol(math, bin, "\u29E2", "\\shuffle", true); defineSymbol(math, bin, "\u2AF6", "\\threedotcolon", true); defineSymbol(math, bin, "\u2982", "\\typecolon", true); defineSymbol(math, bin, "\u223E", "\\invlazys", true); defineSymbol(math, bin, "\u2A4B", "\\twocaps", true); defineSymbol(math, bin, "\u2A4A", "\\twocups", true); defineSymbol(math, bin, "\u2A4E", "\\Sqcap", true); defineSymbol(math, bin, "\u2A4F", "\\Sqcup", true); defineSymbol(math, bin, "\u2A56", "\\veeonvee", true); defineSymbol(math, bin, "\u2A55", "\\wedgeonwedge", true); defineSymbol(math, bin, "\u29D7", "\\blackhourglass", true); defineSymbol(math, bin, "\u29C6", "\\boxast", true); defineSymbol(math, bin, "\u29C8", "\\boxbox", true); defineSymbol(math, bin, "\u29C7", "\\boxcircle", true); defineSymbol(math, bin, "\u229C", "\\circledequal", true); defineSymbol(math, bin, "\u29B7", "\\circledparallel", true); defineSymbol(math, bin, "\u29B6", "\\circledvert", true); defineSymbol(math, bin, "\u29B5", "\\circlehbar", true); defineSymbol(math, bin, "\u27E1", "\\concavediamond", true); defineSymbol(math, bin, "\u27E2", "\\concavediamondtickleft", true); defineSymbol(math, bin, "\u27E3", "\\concavediamondtickright", true); defineSymbol(math, bin, "\u22C4", "\\diamond", true); defineSymbol(math, bin, "\u29D6", "\\hourglass", true); defineSymbol(math, bin, "\u27E0", "\\lozengeminus", true); defineSymbol(math, bin, "\u233D", "\\obar", true); defineSymbol(math, bin, "\u29B8", "\\obslash", true); defineSymbol(math, bin, "\u2A38", "\\odiv", true); defineSymbol(math, bin, "\u29C1", "\\ogreaterthan", true); defineSymbol(math, bin, "\u29C0", "\\olessthan", true); defineSymbol(math, bin, "\u29B9", "\\operp", true); defineSymbol(math, bin, "\u2A37", "\\Otimes", true); defineSymbol(math, bin, "\u2A36", "\\otimeshat", true); defineSymbol(math, bin, "\u22C6", "\\star", true); defineSymbol(math, bin, "\u25B3", "\\triangle", true); defineSymbol(math, bin, "\u2A3A", "\\triangleminus", true); defineSymbol(math, bin, "\u2A39", "\\triangleplus", true); defineSymbol(math, bin, "\u2A3B", "\\triangletimes", true); defineSymbol(math, bin, "\u27E4", "\\whitesquaretickleft", true); defineSymbol(math, bin, "\u27E5", "\\whitesquaretickright", true); defineSymbol(math, bin, "\u2A33", "\\smashtimes", true); // AMS Arrows // Note: unicode-math maps \u21e2 to their own function \rightdasharrow. // We'll map it to AMS function \dashrightarrow. It produces the same atom. defineSymbol(math, rel, "\u21e2", "\\dashrightarrow", true); // unicode-math maps \u21e0 to \leftdasharrow. We'll use the AMS synonym. defineSymbol(math, rel, "\u21e0", "\\dashleftarrow", true); defineSymbol(math, rel, "\u21c7", "\\leftleftarrows", true); defineSymbol(math, rel, "\u21c6", "\\leftrightarrows", true); defineSymbol(math, rel, "\u21da", "\\Lleftarrow", true); defineSymbol(math, rel, "\u219e", "\\twoheadleftarrow", true); defineSymbol(math, rel, "\u21a2", "\\leftarrowtail", true); defineSymbol(math, rel, "\u21ab", "\\looparrowleft", true); defineSymbol(math, rel, "\u21cb", "\\leftrightharpoons", true); defineSymbol(math, rel, "\u21b6", "\\curvearrowleft", true); // unicode-math maps \u21ba to \acwopencirclearrow. We'll use the AMS synonym. defineSymbol(math, rel, "\u21ba", "\\circlearrowleft", true); defineSymbol(math, rel, "\u21b0", "\\Lsh", true); defineSymbol(math, rel, "\u21c8", "\\upuparrows", true); defineSymbol(math, rel, "\u21bf", "\\upharpoonleft", true); defineSymbol(math, rel, "\u21c3", "\\downharpoonleft", true); defineSymbol(math, rel, "\u22b6", "\\origof", true); defineSymbol(math, rel, "\u22b7", "\\imageof", true); defineSymbol(math, rel, "\u22b8", "\\multimap", true); defineSymbol(math, rel, "\u21ad", "\\leftrightsquigarrow", true); defineSymbol(math, rel, "\u21c9", "\\rightrightarrows", true); defineSymbol(math, rel, "\u21c4", "\\rightleftarrows", true); defineSymbol(math, rel, "\u21a0", "\\twoheadrightarrow", true); defineSymbol(math, rel, "\u21a3", "\\rightarrowtail", true); defineSymbol(math, rel, "\u21ac", "\\looparrowright", true); defineSymbol(math, rel, "\u21b7", "\\curvearrowright", true); // unicode-math maps \u21bb to \cwopencirclearrow. We'll use the AMS synonym. defineSymbol(math, rel, "\u21bb", "\\circlearrowright", true); defineSymbol(math, rel, "\u21b1", "\\Rsh", true); defineSymbol(math, rel, "\u21ca", "\\downdownarrows", true); defineSymbol(math, rel, "\u21be", "\\upharpoonright", true); defineSymbol(math, rel, "\u21c2", "\\downharpoonright", true); defineSymbol(math, rel, "\u21dd", "\\rightsquigarrow", true); defineSymbol(math, rel, "\u21dd", "\\leadsto"); defineSymbol(math, rel, "\u21db", "\\Rrightarrow", true); defineSymbol(math, rel, "\u21be", "\\restriction"); defineSymbol(math, textord, "\u2018", "`"); defineSymbol(math, textord, "$", "\\$"); defineSymbol(text, textord, "$", "\\$"); defineSymbol(text, textord, "$", "\\textdollar"); defineSymbol(math, textord, "¢", "\\cent"); defineSymbol(text, textord, "¢", "\\cent"); defineSymbol(math, textord, "%", "\\%"); defineSymbol(text, textord, "%", "\\%"); defineSymbol(math, textord, "_", "\\_"); defineSymbol(text, textord, "_", "\\_"); defineSymbol(text, textord, "_", "\\textunderscore"); defineSymbol(text, textord, "\u2423", "\\textvisiblespace", true); defineSymbol(math, textord, "\u2220", "\\angle", true); defineSymbol(math, textord, "\u221e", "\\infty", true); defineSymbol(math, textord, "\u2032", "\\prime"); defineSymbol(math, textord, "\u2033", "\\dprime"); defineSymbol(math, textord, "\u2034", "\\trprime"); defineSymbol(math, textord, "\u2057", "\\qprime"); defineSymbol(math, textord, "\u25b3", "\\triangle"); defineSymbol(text, textord, "\u0391", "\\Alpha", true); defineSymbol(text, textord, "\u0392", "\\Beta", true); defineSymbol(text, textord, "\u0393", "\\Gamma", true); defineSymbol(text, textord, "\u0394", "\\Delta", true); defineSymbol(text, textord, "\u0395", "\\Epsilon", true); defineSymbol(text, textord, "\u0396", "\\Zeta", true); defineSymbol(text, textord, "\u0397", "\\Eta", true); defineSymbol(text, textord, "\u0398", "\\Theta", true); defineSymbol(text, textord, "\u0399", "\\Iota", true); defineSymbol(text, textord, "\u039a", "\\Kappa", true); defineSymbol(text, textord, "\u039b", "\\Lambda", true); defineSymbol(text, textord, "\u039c", "\\Mu", true); defineSymbol(text, textord, "\u039d", "\\Nu", true); defineSymbol(text, textord, "\u039e", "\\Xi", true); defineSymbol(text, textord, "\u039f", "\\Omicron", true); defineSymbol(text, textord, "\u03a0", "\\Pi", true); defineSymbol(text, textord, "\u03a1", "\\Rho", true); defineSymbol(text, textord, "\u03a3", "\\Sigma", true); defineSymbol(text, textord, "\u03a4", "\\Tau", true); defineSymbol(text, textord, "\u03a5", "\\Upsilon", true); defineSymbol(text, textord, "\u03a6", "\\Phi", true); defineSymbol(text, textord, "\u03a7", "\\Chi", true); defineSymbol(text, textord, "\u03a8", "\\Psi", true); defineSymbol(text, textord, "\u03a9", "\\Omega", true); defineSymbol(math, mathord, "\u0391", "\\Alpha", true); defineSymbol(math, mathord, "\u0392", "\\Beta", true); defineSymbol(math, mathord, "\u0393", "\\Gamma", true); defineSymbol(math, mathord, "\u0394", "\\Delta", true); defineSymbol(math, mathord, "\u0395", "\\Epsilon", true); defineSymbol(math, mathord, "\u0396", "\\Zeta", true); defineSymbol(math, mathord, "\u0397", "\\Eta", true); defineSymbol(math, mathord, "\u0398", "\\Theta", true); defineSymbol(math, mathord, "\u0399", "\\Iota", true); defineSymbol(math, mathord, "\u039a", "\\Kappa", true); defineSymbol(math, mathord, "\u039b", "\\Lambda", true); defineSymbol(math, mathord, "\u039c", "\\Mu", true); defineSymbol(math, mathord, "\u039d", "\\Nu", true); defineSymbol(math, mathord, "\u039e", "\\Xi", true); defineSymbol(math, mathord, "\u039f", "\\Omicron", true); defineSymbol(math, mathord, "\u03a0", "\\Pi", true); defineSymbol(math, mathord, "\u03a1", "\\Rho", true); defineSymbol(math, mathord, "\u03a3", "\\Sigma", true); defineSymbol(math, mathord, "\u03a4", "\\Tau", true); defineSymbol(math, mathord, "\u03a5", "\\Upsilon", true); defineSymbol(math, mathord, "\u03a6", "\\Phi", true); defineSymbol(math, mathord, "\u03a7", "\\Chi", true); defineSymbol(math, mathord, "\u03a8", "\\Psi", true); defineSymbol(math, mathord, "\u03a9", "\\Omega", true); defineSymbol(math, open, "\u00ac", "\\neg", true); defineSymbol(math, open, "\u00ac", "\\lnot"); defineSymbol(math, textord, "\u22a4", "\\top"); defineSymbol(math, textord, "\u22a5", "\\bot"); defineSymbol(math, textord, "\u2205", "\\emptyset"); defineSymbol(math, textord, "\u2300", "\\varnothing"); defineSymbol(math, mathord, "\u03b1", "\\alpha", true); defineSymbol(math, mathord, "\u03b2", "\\beta", true); defineSymbol(math, mathord, "\u03b3", "\\gamma", true); defineSymbol(math, mathord, "\u03b4", "\\delta", true); defineSymbol(math, mathord, "\u03f5", "\\epsilon", true); defineSymbol(math, mathord, "\u03b6", "\\zeta", true); defineSymbol(math, mathord, "\u03b7", "\\eta", true); defineSymbol(math, mathord, "\u03b8", "\\theta", true); defineSymbol(math, mathord, "\u03b9", "\\iota", true); defineSymbol(math, mathord, "\u03ba", "\\kappa", true); defineSymbol(math, mathord, "\u03bb", "\\lambda", true); defineSymbol(math, mathord, "\u03bc", "\\mu", true); defineSymbol(math, mathord, "\u03bd", "\\nu", true); defineSymbol(math, mathord, "\u03be", "\\xi", true); defineSymbol(math, mathord, "\u03bf", "\\omicron", true); defineSymbol(math, mathord, "\u03c0", "\\pi", true); defineSymbol(math, mathord, "\u03c1", "\\rho", true); defineSymbol(math, mathord, "\u03c3", "\\sigma", true); defineSymbol(math, mathord, "\u03c4", "\\tau", true); defineSymbol(math, mathord, "\u03c5", "\\upsilon", true); defineSymbol(math, mathord, "\u03d5", "\\phi", true); defineSymbol(math, mathord, "\u03c7", "\\chi", true); defineSymbol(math, mathord, "\u03c8", "\\psi", true); defineSymbol(math, mathord, "\u03c9", "\\omega", true); defineSymbol(math, mathord, "\u03b5", "\\varepsilon", true); defineSymbol(math, mathord, "\u03d1", "\\vartheta", true); defineSymbol(math, mathord, "\u03d6", "\\varpi", true); defineSymbol(math, mathord, "\u03f1", "\\varrho", true); defineSymbol(math, mathord, "\u03c2", "\\varsigma", true); defineSymbol(math, mathord, "\u03c6", "\\varphi", true); defineSymbol(math, mathord, "\u03d8", "\\Coppa", true); defineSymbol(math, mathord, "\u03d9", "\\coppa", true); defineSymbol(math, mathord, "\u03d9", "\\varcoppa", true); defineSymbol(math, mathord, "\u03de", "\\Koppa", true); defineSymbol(math, mathord, "\u03df", "\\koppa", true); defineSymbol(math, mathord, "\u03e0", "\\Sampi", true); defineSymbol(math, mathord, "\u03e1", "\\sampi", true); defineSymbol(math, mathord, "\u03da", "\\Stigma", true); defineSymbol(math, mathord, "\u03db", "\\stigma", true); defineSymbol(math, mathord, "\u2aeb", "\\Bot"); // unicode-math maps U+F0 to \matheth. We map to AMS function \eth defineSymbol(math, textord, "\u00f0", "\\eth", true); // ð defineSymbol(text, textord, "\u00f0", "\u00f0"); // Extended ASCII and non-ASCII Letters defineSymbol(math, textord, "\u00C5", "\\AA"); // Å defineSymbol(text, textord, "\u00C5", "\\AA", true); defineSymbol(math, textord, "\u00C6", "\\AE", true); // Æ defineSymbol(text, textord, "\u00C6", "\\AE", true); defineSymbol(math, textord, "\u00D0", "\\DH", true); // Ð defineSymbol(text, textord, "\u00D0", "\\DH", true); defineSymbol(math, textord, "\u00DE", "\\TH", true); // Þ defineSymbol(text, textord, "\u00DE", "\\TH", true); defineSymbol(math, textord, "\u00DF", "\\ss", true); // ß defineSymbol(text, textord, "\u00DF", "\\ss", true); defineSymbol(math, textord, "\u00E5", "\\aa"); // å defineSymbol(text, textord, "\u00E5", "\\aa", true); defineSymbol(math, textord, "\u00E6", "\\ae", true); // æ defineSymbol(text, textord, "\u00E6", "\\ae", true); defineSymbol(math, textord, "\u00F0", "\\dh"); // ð defineSymbol(text, textord, "\u00F0", "\\dh", true); defineSymbol(math, textord, "\u00FE", "\\th", true); // þ defineSymbol(text, textord, "\u00FE", "\\th", true); defineSymbol(math, textord, "\u0110", "\\DJ", true); // Đ defineSymbol(text, textord, "\u0110", "\\DJ", true); defineSymbol(math, textord, "\u0111", "\\dj", true); // đ defineSymbol(text, textord, "\u0111", "\\dj", true); defineSymbol(math, textord, "\u0141", "\\L", true); // Ł defineSymbol(text, textord, "\u0141", "\\L", true); defineSymbol(math, textord, "\u0141", "\\l", true); // ł defineSymbol(text, textord, "\u0141", "\\l", true); defineSymbol(math, textord, "\u014A", "\\NG", true); // Ŋ defineSymbol(text, textord, "\u014A", "\\NG", true); defineSymbol(math, textord, "\u014B", "\\ng", true); // ŋ defineSymbol(text, textord, "\u014B", "\\ng", true); defineSymbol(math, textord, "\u0152", "\\OE", true); // Œ defineSymbol(text, textord, "\u0152", "\\OE", true); defineSymbol(math, textord, "\u0153", "\\oe", true); // œ defineSymbol(text, textord, "\u0153", "\\oe", true); defineSymbol(math, bin, "\u2217", "\u2217", true); defineSymbol(math, bin, "+", "+"); defineSymbol(math, bin, "\u2217", "*"); defineSymbol(math, bin, "\u2044", "/", true); defineSymbol(math, bin, "\u2044", "\u2044"); defineSymbol(math, bin, "\u2212", "-", true); defineSymbol(math, bin, "\u22c5", "\\cdot", true); defineSymbol(math, bin, "\u2218", "\\circ", true); defineSymbol(math, bin, "\u00f7", "\\div", true); defineSymbol(math, bin, "\u00b1", "\\pm", true); defineSymbol(math, bin, "\u00d7", "\\times", true); defineSymbol(math, bin, "\u2229", "\\cap", true); defineSymbol(math, bin, "\u222a", "\\cup", true); defineSymbol(math, bin, "\u2216", "\\setminus", true); defineSymbol(math, bin, "\u2227", "\\land"); defineSymbol(math, bin, "\u2228", "\\lor"); defineSymbol(math, bin, "\u2227", "\\wedge", true); defineSymbol(math, bin, "\u2228", "\\vee", true); defineSymbol(math, open, "\u27e6", "\\llbracket", true); // stmaryrd/semantic packages defineSymbol(math, close, "\u27e7", "\\rrbracket", true); defineSymbol(math, open, "\u27e8", "\\langle", true); defineSymbol(math, open, "\u27ea", "\\lAngle", true); defineSymbol(math, open, "\u2989", "\\llangle", true); defineSymbol(math, open, "|", "\\lvert"); defineSymbol(math, open, "\u2016", "\\lVert", true); defineSymbol(math, textord, "!", "\\oc"); // cmll package defineSymbol(math, textord, "?", "\\wn"); defineSymbol(math, textord, "\u2193", "\\shpos"); defineSymbol(math, textord, "\u2195", "\\shift"); defineSymbol(math, textord, "\u2191", "\\shneg"); defineSymbol(math, close, "?", "?"); defineSymbol(math, close, "!", "!"); defineSymbol(math, close, "‼", "‼"); defineSymbol(math, close, "\u27e9", "\\rangle", true); defineSymbol(math, close, "\u27eb", "\\rAngle", true); defineSymbol(math, close, "\u298a", "\\rrangle", true); defineSymbol(math, close, "|", "\\rvert"); defineSymbol(math, close, "\u2016", "\\rVert"); defineSymbol(math, open, "\u2983", "\\lBrace", true); // stmaryrd/semantic packages defineSymbol(math, close, "\u2984", "\\rBrace", true); defineSymbol(math, rel, "=", "\\equal", true); defineSymbol(math, rel, ":", ":"); defineSymbol(math, rel, "\u2248", "\\approx", true); defineSymbol(math, rel, "\u2245", "\\cong", true); defineSymbol(math, rel, "\u2265", "\\ge"); defineSymbol(math, rel, "\u2265", "\\geq", true); defineSymbol(math, rel, "\u2190", "\\gets"); defineSymbol(math, rel, ">", "\\gt", true); defineSymbol(math, rel, "\u2208", "\\in", true); defineSymbol(math, rel, "\u2209", "\\notin", true); defineSymbol(math, rel, "\ue020", "\\@not"); defineSymbol(math, rel, "\u2282", "\\subset", true); defineSymbol(math, rel, "\u2283", "\\supset", true); defineSymbol(math, rel, "\u2286", "\\subseteq", true); defineSymbol(math, rel, "\u2287", "\\supseteq", true); defineSymbol(math, rel, "\u2288", "\\nsubseteq", true); defineSymbol(math, rel, "\u2288", "\\nsubseteqq"); defineSymbol(math, rel, "\u2289", "\\nsupseteq", true); defineSymbol(math, rel, "\u2289", "\\nsupseteqq"); defineSymbol(math, rel, "\u22a8", "\\models"); defineSymbol(math, rel, "\u2190", "\\leftarrow", true); defineSymbol(math, rel, "\u2264", "\\le"); defineSymbol(math, rel, "\u2264", "\\leq", true); defineSymbol(math, rel, "<", "\\lt", true); defineSymbol(math, rel, "\u2192", "\\rightarrow", true); defineSymbol(math, rel, "\u2192", "\\to"); defineSymbol(math, rel, "\u2271", "\\ngeq", true); defineSymbol(math, rel, "\u2271", "\\ngeqq"); defineSymbol(math, rel, "\u2271", "\\ngeqslant"); defineSymbol(math, rel, "\u2270", "\\nleq", true); defineSymbol(math, rel, "\u2270", "\\nleqq"); defineSymbol(math, rel, "\u2270", "\\nleqslant"); defineSymbol(math, rel, "\u2aeb", "\\Perp", true); //cmll package defineSymbol(math, spacing, "\u00a0", "\\ "); defineSymbol(math, spacing, "\u00a0", "\\space"); // Ref: LaTeX Source 2e: \DeclareRobustCommand{\nobreakspace}{% defineSymbol(math, spacing, "\u00a0", "\\nobreakspace"); defineSymbol(text, spacing, "\u00a0", "\\ "); defineSymbol(text, spacing, "\u00a0", " "); defineSymbol(text, spacing, "\u00a0", "\\space"); defineSymbol(text, spacing, "\u00a0", "\\nobreakspace"); defineSymbol(math, spacing, null, "\\nobreak"); defineSymbol(math, spacing, null, "\\allowbreak"); defineSymbol(math, punct, ",", ","); defineSymbol(text, punct, ":", ":"); defineSymbol(math, punct, ";", ";"); defineSymbol(math, bin, "\u22bc", "\\barwedge"); defineSymbol(math, bin, "\u22bb", "\\veebar"); defineSymbol(math, bin, "\u2299", "\\odot", true); // Firefox turns ⊕ into an emoji. So append \uFE0E. Define Unicode character in macros, not here. defineSymbol(math, bin, "\u2295\uFE0E", "\\oplus"); defineSymbol(math, bin, "\u2297", "\\otimes", true); defineSymbol(math, textord, "\u2202", "\\partial", true); defineSymbol(math, bin, "\u2298", "\\oslash", true); defineSymbol(math, bin, "\u229a", "\\circledcirc", true); defineSymbol(math, bin, "\u22a1", "\\boxdot", true); defineSymbol(math, bin, "\u25b3", "\\bigtriangleup"); defineSymbol(math, bin, "\u25bd", "\\bigtriangledown"); defineSymbol(math, bin, "\u2020", "\\dagger"); defineSymbol(math, bin, "\u22c4", "\\diamond"); defineSymbol(math, bin, "\u25c3", "\\triangleleft"); defineSymbol(math, bin, "\u25b9", "\\triangleright"); defineSymbol(math, open, "{", "\\{"); defineSymbol(text, textord, "{", "\\{"); defineSymbol(text, textord, "{", "\\textbraceleft"); defineSymbol(math, close, "}", "\\}"); defineSymbol(text, textord, "}", "\\}"); defineSymbol(text, textord, "}", "\\textbraceright"); defineSymbol(math, open, "{", "\\lbrace"); defineSymbol(math, close, "}", "\\rbrace"); defineSymbol(math, open, "[", "\\lbrack", true); defineSymbol(text, textord, "[", "\\lbrack", true); defineSymbol(math, close, "]", "\\rbrack", true); defineSymbol(text, textord, "]", "\\rbrack", true); defineSymbol(math, open, "(", "\\lparen", true); defineSymbol(math, close, ")", "\\rparen", true); defineSymbol(math, open, "⦇", "\\llparenthesis", true); defineSymbol(math, close, "⦈", "\\rrparenthesis", true); defineSymbol(text, textord, "<", "\\textless", true); // in T1 fontenc defineSymbol(text, textord, ">", "\\textgreater", true); // in T1 fontenc defineSymbol(math, open, "\u230a", "\\lfloor", true); defineSymbol(math, close, "\u230b", "\\rfloor", true); defineSymbol(math, open, "\u2308", "\\lceil", true); defineSymbol(math, close, "\u2309", "\\rceil", true); defineSymbol(math, textord, "\\", "\\backslash"); defineSymbol(math, textord, "|", "|"); defineSymbol(math, textord, "|", "\\vert"); defineSymbol(text, textord, "|", "\\textbar", true); // in T1 fontenc defineSymbol(math, textord, "\u2016", "\\|"); defineSymbol(math, textord, "\u2016", "\\Vert"); defineSymbol(text, textord, "\u2016", "\\textbardbl"); defineSymbol(text, textord, "~", "\\textasciitilde"); defineSymbol(text, textord, "\\", "\\textbackslash"); defineSymbol(text, textord, "^", "\\textasciicircum"); defineSymbol(math, rel, "\u2191", "\\uparrow", true); defineSymbol(math, rel, "\u21d1", "\\Uparrow", true); defineSymbol(math, rel, "\u2193", "\\downarrow", true); defineSymbol(math, rel, "\u21d3", "\\Downarrow", true); defineSymbol(math, rel, "\u2195", "\\updownarrow", true); defineSymbol(math, rel, "\u21d5", "\\Updownarrow", true); defineSymbol(math, op, "\u2210", "\\coprod"); defineSymbol(math, op, "\u22c1", "\\bigvee"); defineSymbol(math, op, "\u22c0", "\\bigwedge"); defineSymbol(math, op, "\u2a04", "\\biguplus"); defineSymbol(math, op, "\u2a04", "\\bigcupplus"); defineSymbol(math, op, "\u2a03", "\\bigcupdot"); defineSymbol(math, op, "\u2a07", "\\bigdoublevee"); defineSymbol(math, op, "\u2a08", "\\bigdoublewedge"); defineSymbol(math, op, "\u22c2", "\\bigcap"); defineSymbol(math, op, "\u22c3", "\\bigcup"); defineSymbol(math, op, "\u222b", "\\int"); defineSymbol(math, op, "\u222b", "\\intop"); defineSymbol(math, op, "\u222c", "\\iint"); defineSymbol(math, op, "\u222d", "\\iiint"); defineSymbol(math, op, "\u220f", "\\prod"); defineSymbol(math, op, "\u2211", "\\sum"); defineSymbol(math, op, "\u2a02", "\\bigotimes"); defineSymbol(math, op, "\u2a01", "\\bigoplus"); defineSymbol(math, op, "\u2a00", "\\bigodot"); defineSymbol(math, op, "\u2a09", "\\bigtimes"); defineSymbol(math, op, "\u222e", "\\oint"); defineSymbol(math, op, "\u222f", "\\oiint"); defineSymbol(math, op, "\u2230", "\\oiiint"); defineSymbol(math, op, "\u2231", "\\intclockwise"); defineSymbol(math, op, "\u2232", "\\varointclockwise"); defineSymbol(math, op, "\u2a0c", "\\iiiint"); defineSymbol(math, op, "\u2a0d", "\\intbar"); defineSymbol(math, op, "\u2a0e", "\\intBar"); defineSymbol(math, op, "\u2a0f", "\\fint"); defineSymbol(math, op, "\u2a12", "\\rppolint"); defineSymbol(math, op, "\u2a13", "\\scpolint"); defineSymbol(math, op, "\u2a15", "\\pointint"); defineSymbol(math, op, "\u2a16", "\\sqint"); defineSymbol(math, op, "\u2a17", "\\intlarhk"); defineSymbol(math, op, "\u2a18", "\\intx"); defineSymbol(math, op, "\u2a19", "\\intcap"); defineSymbol(math, op, "\u2a1a", "\\intcup"); defineSymbol(math, op, "\u2a05", "\\bigsqcap"); defineSymbol(math, op, "\u2a06", "\\bigsqcup"); defineSymbol(math, op, "\u222b", "\\smallint"); defineSymbol(text, inner, "\u2026", "\\textellipsis"); defineSymbol(math, inner, "\u2026", "\\mathellipsis"); defineSymbol(text, inner, "\u2026", "\\ldots", true); defineSymbol(math, inner, "\u2026", "\\ldots", true); defineSymbol(math, inner, "\u22f0", "\\iddots", true); defineSymbol(math, inner, "\u22ef", "\\@cdots", true); defineSymbol(math, inner, "\u22f1", "\\ddots", true); defineSymbol(math, textord, "\u22ee", "\\varvdots"); // \vdots is a macro defineSymbol(text, textord, "\u22ee", "\\varvdots"); defineSymbol(math, accent, "\u00b4", "\\acute"); defineSymbol(math, accent, "\u0060", "\\grave"); defineSymbol(math, accent, "\u00a8", "\\ddot"); defineSymbol(math, accent, "\u2026", "\\dddot"); defineSymbol(math, accent, "\u2026\u002e", "\\ddddot"); defineSymbol(math, accent, "\u007e", "\\tilde"); defineSymbol(math, accent, "\u203e", "\\bar"); defineSymbol(math, accent, "\u02d8", "\\breve"); defineSymbol(math, accent, "\u02c7", "\\check"); defineSymbol(math, accent, "\u005e", "\\hat"); defineSymbol(math, accent, "\u2192", "\\vec"); defineSymbol(math, accent, "\u02d9", "\\dot"); defineSymbol(math, accent, "\u02da", "\\mathring"); defineSymbol(math, mathord, "\u0131", "\\imath", true); defineSymbol(math, mathord, "\u0237", "\\jmath", true); defineSymbol(math, textord, "\u0131", "\u0131"); defineSymbol(math, textord, "\u0237", "\u0237"); defineSymbol(text, textord, "\u0131", "\\i", true); defineSymbol(text, textord, "\u0237", "\\j", true); defineSymbol(text, textord, "\u00f8", "\\o", true); defineSymbol(math, mathord, "\u00f8", "\\o", true); defineSymbol(text, textord, "\u00d8", "\\O", true); defineSymbol(math, mathord, "\u00d8", "\\O", true); defineSymbol(text, accent, "\u02ca", "\\'"); // acute defineSymbol(text, accent, "\u02cb", "\\`"); // grave defineSymbol(text, accent, "\u02c6", "\\^"); // circumflex defineSymbol(text, accent, "\u007e", "\\~"); // tilde defineSymbol(text, accent, "\u02c9", "\\="); // macron defineSymbol(text, accent, "\u02d8", "\\u"); // breve defineSymbol(text, accent, "\u02d9", "\\."); // dot above defineSymbol(text, accent, "\u00b8", "\\c"); // cedilla defineSymbol(text, accent, "\u02da", "\\r"); // ring above defineSymbol(text, accent, "\u02c7", "\\v"); // caron defineSymbol(text, accent, "\u00a8", '\\"'); // diaeresis defineSymbol(text, accent, "\u02dd", "\\H"); // double acute defineSymbol(math, accent, "\u02ca", "\\'"); // acute defineSymbol(math, accent, "\u02cb", "\\`"); // grave defineSymbol(math, accent, "\u02c6", "\\^"); // circumflex defineSymbol(math, accent, "\u007e", "\\~"); // tilde defineSymbol(math, accent, "\u02c9", "\\="); // macron defineSymbol(math, accent, "\u02d8", "\\u"); // breve defineSymbol(math, accent, "\u02d9", "\\."); // dot above defineSymbol(math, accent, "\u00b8", "\\c"); // cedilla defineSymbol(math, accent, "\u02da", "\\r"); // ring above defineSymbol(math, accent, "\u02c7", "\\v"); // caron defineSymbol(math, accent, "\u00a8", '\\"'); // diaeresis defineSymbol(math, accent, "\u02dd", "\\H"); // double acute // These ligatures are detected and created in Parser.js's `formLigatures`. const ligatures = { "--": true, "---": true, "``": true, "''": true }; defineSymbol(text, textord, "\u2013", "--", true); defineSymbol(text, textord, "\u2013", "\\textendash"); defineSymbol(text, textord, "\u2014", "---", true); defineSymbol(text, textord, "\u2014", "\\textemdash"); defineSymbol(text, textord, "\u2018", "`", true); defineSymbol(text, textord, "\u2018", "\\textquoteleft"); defineSymbol(text, textord, "\u2019", "'", true); defineSymbol(text, textord, "\u2019", "\\textquoteright"); defineSymbol(text, textord, "\u201c", "``", true); defineSymbol(text, textord, "\u201c", "\\textquotedblleft"); defineSymbol(text, textord, "\u201d", "''", true); defineSymbol(text, textord, "\u201d", "\\textquotedblright"); // \degree from gensymb package defineSymbol(math, textord, "\u00b0", "\\degree", true); defineSymbol(text, textord, "\u00b0", "\\degree"); // \textdegree from inputenc package defineSymbol(text, textord, "\u00b0", "\\textdegree", true); // TODO: In LaTeX, \pounds can generate a different character in text and math // mode, but among our fonts, only Main-Regular defines this character "163". defineSymbol(math, textord, "\u00a3", "\\pounds"); defineSymbol(math, textord, "\u00a3", "\\mathsterling", true); defineSymbol(text, textord, "\u00a3", "\\pounds"); defineSymbol(text, textord, "\u00a3", "\\textsterling", true); defineSymbol(math, textord, "\u2720", "\\maltese"); defineSymbol(text, textord, "\u2720", "\\maltese"); defineSymbol(math, textord, "\u20ac", "\\euro", true); defineSymbol(text, textord, "\u20ac", "\\euro", true); defineSymbol(text, textord, "\u20ac", "\\texteuro"); defineSymbol(math, textord, "\u00a9", "\\copyright", true); defineSymbol(text, textord, "\u00a9", "\\textcopyright"); defineSymbol(math, textord, "\u2300", "\\diameter", true); defineSymbol(text, textord, "\u2300", "\\diameter"); // Italic Greek defineSymbol(math, textord, "𝛤", "\\varGamma"); defineSymbol(math, textord, "𝛥", "\\varDelta"); defineSymbol(math, textord, "𝛩", "\\varTheta"); defineSymbol(math, textord, "𝛬", "\\varLambda"); defineSymbol(math, textord, "𝛯", "\\varXi"); defineSymbol(math, textord, "𝛱", "\\varPi"); defineSymbol(math, textord, "𝛴", "\\varSigma"); defineSymbol(math, textord, "𝛶", "\\varUpsilon"); defineSymbol(math, textord, "𝛷", "\\varPhi"); defineSymbol(math, textord, "𝛹", "\\varPsi"); defineSymbol(math, textord, "𝛺", "\\varOmega"); defineSymbol(text, textord, "𝛤", "\\varGamma"); defineSymbol(text, textord, "𝛥", "\\varDelta"); defineSymbol(text, textord, "𝛩", "\\varTheta"); defineSymbol(text, textord, "𝛬", "\\varLambda"); defineSymbol(text, textord, "𝛯", "\\varXi"); defineSymbol(text, textord, "𝛱", "\\varPi"); defineSymbol(text, textord, "𝛴", "\\varSigma"); defineSymbol(text, textord, "𝛶", "\\varUpsilon"); defineSymbol(text, textord, "𝛷", "\\varPhi"); defineSymbol(text, textord, "𝛹", "\\varPsi"); defineSymbol(text, textord, "𝛺", "\\varOmega"); // There are lots of symbols which are the same, so we add them in afterwards. // All of these are textords in math mode const mathTextSymbols = '0123456789/@."'; for (let i = 0; i < mathTextSymbols.length; i++) { const ch = mathTextSymbols.charAt(i); defineSymbol(math, textord, ch, ch); } // All of these are textords in text mode const textSymbols = '0123456789!@*()-=+";:?/.,'; for (let i = 0; i < textSymbols.length; i++) { const ch = textSymbols.charAt(i); defineSymbol(text, textord, ch, ch); } // All of these are textords in text mode, and mathords in math mode const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; for (let i = 0; i < letters.length; i++) { const ch = letters.charAt(i); defineSymbol(math, mathord, ch, ch); defineSymbol(text, textord, ch, ch); } // Some more letters in Unicode Basic Multilingual Plane. const narrow = "ÇÐÞçþℂℍℕℙℚℝℤℎℏℊℋℌℐℑℒℓ℘ℛℜℬℰℱℳℭℨ"; for (let i = 0; i < narrow.length; i++) { const ch = narrow.charAt(i); defineSymbol(math, mathord, ch, ch); defineSymbol(text, textord, ch, ch); } // The next loop loads wide (surrogate pair) characters. // We support some letters in the Unicode range U+1D400 to U+1D7FF, // Mathematical Alphanumeric Symbols. let wideChar = ""; for (let i = 0; i < letters.length; i++) { // The hex numbers in the next line are a surrogate pair. // 0xD835 is the high surrogate for all letters in the range we support. // 0xDC00 is the low surrogate for bold A. wideChar = String.fromCharCode(0xd835, 0xdc00 + i); // A-Z a-z bold defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); wideChar = String.fromCharCode(0xd835, 0xdc34 + i); // A-Z a-z italic defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); wideChar = String.fromCharCode(0xd835, 0xdc68 + i); // A-Z a-z bold italic defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); wideChar = String.fromCharCode(0xd835, 0xdd04 + i); // A-Z a-z Fractur defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); wideChar = String.fromCharCode(0xd835, 0xdda0 + i); // A-Z a-z sans-serif defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); wideChar = String.fromCharCode(0xd835, 0xddd4 + i); // A-Z a-z sans bold defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); wideChar = String.fromCharCode(0xd835, 0xde08 + i); // A-Z a-z sans italic defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); wideChar = String.fromCharCode(0xd835, 0xde70 + i); // A-Z a-z monospace defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); wideChar = String.fromCharCode(0xd835, 0xdd38 + i); // A-Z a-z double struck defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); const ch = letters.charAt(i); wideChar = String.fromCharCode(0xd835, 0xdc9c + i); // A-Z a-z calligraphic defineSymbol(math, mathord, ch, wideChar); defineSymbol(text, textord, ch, wideChar); } // Next, some wide character numerals for (let i = 0; i < 10; i++) { wideChar = String.fromCharCode(0xd835, 0xdfce + i); // 0-9 bold defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); wideChar = String.fromCharCode(0xd835, 0xdfe2 + i); // 0-9 sans serif defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); wideChar = String.fromCharCode(0xd835, 0xdfec + i); // 0-9 bold sans defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); wideChar = String.fromCharCode(0xd835, 0xdff6 + i); // 0-9 monospace defineSymbol(math, mathord, wideChar, wideChar); defineSymbol(text, textord, wideChar, wideChar); } /* * Neither Firefox nor Chrome support hard line breaks or soft line breaks. * (Despite https://www.w3.org/Math/draft-spec/mathml.html#chapter3_presm.lbattrs) * So Temml has work-arounds for both hard and soft breaks. * The work-arounds sadly do not work simultaneously. Any top-level hard * break makes soft line breaks impossible. * * Hard breaks are simulated by creating a <mtable> and putting each line in its own <mtr>. * * To create soft line breaks, Temml avoids using the <semantics> and <annotation> tags. * Then the top level of a <math> element can be occupied by <mrow> elements, and the browser * will break after a <mrow> if the expression extends beyond the container limit. * * The default is for soft line breaks after each top-level binary or * relational operator, per TeXbook p. 173. So we gather the expression into <mrow>s so that * each <mrow> ends in a binary or relational operator. * * An option is for soft line breaks before an "=" sign. That changes the <mrow>s. * * Soft line breaks will not work in Chromium and Safari, only Firefox. * * Hopefully browsers will someday do their own linebreaking and we will be able to delete * much of this module. */ const openDelims = "([{⌊⌈⟨⟮⎰⟦⦃"; const closeDelims = ")]}⌋⌉⟩⟯⎱⟦⦄"; function setLineBreaks(expression, wrapMode, isDisplayMode) { const mtrs = []; let mrows = []; let block = []; let numTopLevelEquals = 0; let i = 0; let level = 0; while (i < expression.length) { while (expression[i] instanceof DocumentFragment) { expression.splice(i, 1, ...expression[i].children); // Expand the fragment. } const node = expression[i]; if (node.attributes && node.attributes.linebreak && node.attributes.linebreak === "newline") { // A hard line break. Create a <mtr> for the current block. if (block.length > 0) { mrows.push(new mathMLTree.MathNode("mrow", block)); } mrows.push(node); block = []; const mtd = new mathMLTree.MathNode("mtd", mrows); mtd.style.textAlign = "left"; mtrs.push(new mathMLTree.MathNode("mtr", [mtd])); mrows = []; i += 1; continue } block.push(node); if (node.type && node.type === "mo" && node.children.length === 1 && !Object.prototype.hasOwnProperty.call(node.attributes, "movablelimits")) { const ch = node.children[0].text; if (openDelims.indexOf(ch) > -1) { level += 1; } else if (closeDelims.indexOf(ch) > -1) { level -= 1; } else if (level === 0 && wrapMode === "=" && ch === "=") { numTopLevelEquals += 1; if (numTopLevelEquals > 1) { block.pop(); // Start a new block. (Insert a soft linebreak.) const element = new mathMLTree.MathNode("mrow", block); mrows.push(element); block = [node]; } } else if (level === 0 && wrapMode === "tex" && ch !== "∇") { // Check if the following node is a \nobreak text node, e.g. "~"" const next = i < expression.length - 1 ? expression[i + 1] : null; let glueIsFreeOfNobreak = true; if ( !( next && next.type === "mtext" && next.attributes.linebreak && next.attributes.linebreak === "nobreak" ) ) { // We may need to start a new block. // First, put any post-operator glue on same line as operator. for (let j = i + 1; j < expression.length; j++) { const nd = expression[j]; if ( nd.type && nd.type === "mspace" && !(nd.attributes.linebreak && nd.attributes.linebreak === "newline") ) { block.push(nd); i += 1; if ( nd.attributes && nd.attributes.linebreak && nd.attributes.linebreak === "nobreak" ) { glueIsFreeOfNobreak = false; } } else { break; } } } if (glueIsFreeOfNobreak) { // Start a new block. (Insert a soft linebreak.) const element = new mathMLTree.MathNode("mrow", block); mrows.push(element); block = []; } } } i += 1; } if (block.length > 0) { const element = new mathMLTree.MathNode("mrow", block); mrows.push(element); } if (mtrs.length > 0) { const mtd = new mathMLTree.MathNode("mtd", mrows); mtd.style.textAlign = "left"; const mtr = new mathMLTree.MathNode("mtr", [mtd]); mtrs.push(mtr); const mtable = new mathMLTree.MathNode("mtable", mtrs); if (!isDisplayMode) { mtable.setAttribute("columnalign", "left"); mtable.setAttribute("rowspacing", "0em"); } return mtable } return mathMLTree.newDocumentFragment(mrows); } /** * This file converts a parse tree into a corresponding MathML tree. The main * entry point is the `buildMathML` function, which takes a parse tree from the * parser. */ /** * Takes a symbol and converts it into a MathML text node after performing * optional replacement from symbols.js. */ const makeText = function(text, mode, style) { if ( symbols[mode][text] && symbols[mode][text].replace && text.charCodeAt(0) !== 0xd835 && !( Object.prototype.hasOwnProperty.call(ligatures, text) && style && ((style.fontFamily && style.fontFamily.slice(4, 6) === "tt") || (style.font && style.font.slice(4, 6) === "tt")) ) ) { text = symbols[mode][text].replace; } return new mathMLTree.TextNode(text); }; const copyChar = (newRow, child) => { if (newRow.children.length === 0 || newRow.children[newRow.children.length - 1].type !== "mtext") { const mtext = new mathMLTree.MathNode( "mtext", [new mathMLTree.TextNode(child.children[0].text)] ); newRow.children.push(mtext); } else { newRow.children[newRow.children.length - 1].children[0].text += child.children[0].text; } }; const consolidateText = mrow => { // If possible, consolidate adjacent <mtext> elements into a single element. if (mrow.type !== "mrow" && mrow.type !== "mstyle") { return mrow } if (mrow.children.length === 0) { return mrow } // empty group, e.g., \text{} const newRow = new mathMLTree.MathNode("mrow"); for (let i = 0; i < mrow.children.length; i++) { const child = mrow.children[i]; if (child.type === "mtext" && Object.keys(child.attributes).length === 0) { copyChar(newRow, child); } else if (child.type === "mrow") { // We'll also check the children of an mrow. One level only. No recursion. let canConsolidate = true; for (let j = 0; j < child.children.length; j++) { const grandChild = child.children[j]; if (grandChild.type !== "mtext" || Object.keys(child.attributes).length !== 0) { canConsolidate = false; break } } if (canConsolidate) { for (let j = 0; j < child.children.length; j++) { const grandChild = child.children[j]; copyChar(newRow, grandChild); } } else { newRow.children.push(child); } } else { newRow.children.push(child); } } for (let i = 0; i < newRow.children.length; i++) { if (newRow.children[i].type === "mtext") { const mtext = newRow.children[i]; // Firefox does not render a space at either end of an <mtext> string. // To get proper rendering, we replace leading or trailing spaces with no-break spaces. if (mtext.children[0].text.charAt(0) === " ") { mtext.children[0].text = "\u00a0" + mtext.children[0].text.slice(1); } const L = mtext.children[0].text.length; if (L > 0 && mtext.children[0].text.charAt(L - 1) === " ") { mtext.children[0].text = mtext.children[0].text.slice(0, -1) + "\u00a0"; } for (const [key, value] of Object.entries(mrow.attributes)) { mtext.attributes[key] = value; } } } if (newRow.children.length === 1 && newRow.children[0].type === "mtext") { return newRow.children[0]; // A consolidated <mtext> } else { return newRow } }; /** * Wrap the given array of nodes in an <mrow> node if needed, i.e., * unless the array has length 1. Always returns a single node. */ const makeRow = function(body, semisimple = false) { if (body.length === 1 && !(body[0] instanceof DocumentFragment)) { return body[0]; } else if (!semisimple) { // Suppress spacing on <mo> nodes at both ends of the row. if (body[0] instanceof MathNode && body[0].type === "mo" && !body[0].attributes.fence) { body[0].attributes.lspace = "0em"; body[0].attributes.rspace = "0em"; } const end = body.length - 1; if (body[end] instanceof MathNode && body[end].type === "mo" && !body[end].attributes.fence) { body[end].attributes.lspace = "0em"; body[end].attributes.rspace = "0em"; } } return new mathMLTree.MathNode("mrow", body); }; /** * Check for <mi>.</mi> which is how a dot renders in MathML, * or <mo separator="true" lspace="0em" rspace="0em">,</mo> * which is how a braced comma {,} renders in MathML */ function isNumberPunctuation(group) { if (!group) { return false } if (group.type === 'mi' && group.children.length === 1) { const child = group.children[0]; return child instanceof TextNode && child.text === '.' } else if (group.type === "mtext" && group.children.length === 1) { const child = group.children[0]; return child instanceof TextNode && child.text === '\u2008' // punctuation space } else if (group.type === 'mo' && group.children.length === 1 && group.getAttribute('separator') === 'true' && group.getAttribute('lspace') === '0em' && group.getAttribute('rspace') === '0em') { const child = group.children[0]; return child instanceof TextNode && child.text === ',' } else { return false } } const isComma = (expression, i) => { const node = expression[i]; const followingNode = expression[i + 1]; return (node.type === "atom" && node.text === ",") && // Don't consolidate if there is a space after the comma. node.loc && followingNode.loc && node.loc.end === followingNode.loc.start }; const isRel = item => { return (item.type === "atom" && item.family === "rel") || (item.type === "mclass" && item.mclass === "mrel") }; /** * Takes a list of nodes, builds them, and returns a list of the generated * MathML nodes. Also do a couple chores along the way: * (1) Suppress spacing when an author wraps an operator w/braces, as in {=}. * (2) Suppress spacing between two adjacent relations. */ const buildExpression = function(expression, style, semisimple = false) { if (!semisimple && expression.length === 1) { const group = buildGroup$1(expression[0], style); if (group instanceof MathNode && group.type === "mo") { // When TeX writers want to suppress spacing on an operator, // they often put the operator by itself inside braces. group.setAttribute("lspace", "0em"); group.setAttribute("rspace", "0em"); } return [group]; } const groups = []; const groupArray = []; let lastGroup; for (let i = 0; i < expression.length; i++) { groupArray.push(buildGroup$1(expression[i], style)); } for (let i = 0; i < groupArray.length; i++) { const group = groupArray[i]; // Suppress spacing between adjacent relations if (i < expression.length - 1 && isRel(expression[i]) && isRel(expression[i + 1])) { group.setAttribute("rspace", "0em"); } if (i > 0 && isRel(expression[i]) && isRel(expression[i - 1])) { group.setAttribute("lspace", "0em"); } // Concatenate numbers if (group.type === 'mn' && lastGroup && lastGroup.type === 'mn') { // Concatenate <mn>...</mn> followed by <mi>.</mi> lastGroup.children.push(...group.children); continue } else if (isNumberPunctuation(group) && lastGroup && lastGroup.type === 'mn') { // Concatenate <mn>...</mn> followed by <mi>.</mi> lastGroup.children.push(...group.children); continue } else if (lastGroup && lastGroup.type === "mn" && i < groupArray.length - 1 && groupArray[i + 1].type === "mn" && isComma(expression, i)) { lastGroup.children.push(...group.children); continue } else if (group.type === 'mn' && isNumberPunctuation(lastGroup)) { // Concatenate <mi>.</mi> followed by <mn>...</mn> group.children = [...lastGroup.children, ...group.children]; groups.pop(); } else if ((group.type === 'msup' || group.type === 'msub') && group.children.length >= 1 && lastGroup && (lastGroup.type === 'mn' || isNumberPunctuation(lastGroup))) { // Put preceding <mn>...</mn> or <mi>.</mi> inside base of // <msup><mn>...base...</mn>...exponent...</msup> (or <msub>) const base = group.children[0]; if (base instanceof MathNode && base.type === 'mn' && lastGroup) { base.children = [...lastGroup.children, ...base.children]; groups.pop(); } } groups.push(group); lastGroup = group; } return groups }; /** * Equivalent to buildExpression, but wraps the elements in an <mrow> * if there's more than one. Returns a single node instead of an array. */ const buildExpressionRow = function(expression, style, semisimple = false) { return makeRow(buildExpression(expression, style, semisimple), semisimple); }; /** * Takes a group from the parser and calls the appropriate groupBuilders function * on it to produce a MathML node. */ const buildGroup$1 = function(group, style) { if (!group) { return new mathMLTree.MathNode("mrow"); } if (_mathmlGroupBuilders[group.type]) { // Call the groupBuilders function const result = _mathmlGroupBuilders[group.type](group, style); return result; } else { throw new ParseError("Got group of unknown type: '" + group.type + "'"); } }; const glue$1 = _ => { return new mathMLTree.MathNode("mtd", [], [], { padding: "0", width: "50%" }) }; const labelContainers = ["mrow", "mtd", "mtable", "mtr"]; const getLabel = parent => { for (const node of parent.children) { if (node.type && labelContainers.includes(node.type)) { if (node.classes && node.classes[0] === "tml-label") { const label = node.label; return label } else { const label = getLabel(node); if (label) { return label } } } else if (!node.type) { const label = getLabel(node); if (label) { return label } } } }; const taggedExpression = (expression, tag, style, leqno) => { tag = buildExpressionRow(tag[0].body, style); tag = consolidateText(tag); // tag is now an <mtext> element tag.classes.push("tml-tag"); // to be available for \ref const label = getLabel(expression); // from a \label{} function. expression = new mathMLTree.MathNode("mtd", [expression]); const rowArray = [glue$1(), expression, glue$1()]; rowArray[leqno ? 0 : 2].children.push(tag); const mtr = new mathMLTree.MathNode("mtr", rowArray, ["tml-tageqn"]); if (label) { mtr.setAttribute("id", label); } const table = new mathMLTree.MathNode("mtable", [mtr]); table.style.width = "100%"; table.setAttribute("displaystyle", "true"); return table }; /** * Takes a full parse tree and settings and builds a MathML representation of * it. */ function buildMathML(tree, texExpression, style, settings) { // Strip off outer tag wrapper for processing below. let tag = null; if (tree.length === 1 && tree[0].type === "tag") { tag = tree[0].tag; tree = tree[0].body; } const expression = buildExpression(tree, style); if (expression.length === 1 && expression[0] instanceof AnchorNode) { return expression[0] } const wrap = (settings.displayMode || settings.annotate) ? "none" : settings.wrap; const n1 = expression.length === 0 ? null : expression[0]; let wrapper = expression.length === 1 && tag === null && (n1 instanceof MathNode) ? expression[0] : setLineBreaks(expression, wrap, settings.displayMode); if (tag) { wrapper = taggedExpression(wrapper, tag, style, settings.leqno); } if (settings.annotate) { // Build a TeX annotation of the source const annotation = new mathMLTree.MathNode( "annotation", [new mathMLTree.TextNode(texExpression)]); annotation.setAttribute("encoding", "application/x-tex"); wrapper = new mathMLTree.MathNode("semantics", [wrapper, annotation]); } const math = new mathMLTree.MathNode("math", [wrapper]); if (settings.xml) { math.setAttribute("xmlns", "http://www.w3.org/1998/Math/MathML"); } if (settings.displayMode) { math.setAttribute("display", "block"); math.style.display = "block math"; // necessary in Chromium. // Firefox and Safari do not recognize display: "block math". // Set a class so that the CSS file can set display: block. math.classes = ["tml-display"]; } return math; } // Identify letters to which we'll attach a combining accent character const smalls = "acegıȷmnopqrsuvwxyzαγεηικμνοπρςστυχωϕ𝐚𝐜𝐞𝐠𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐮𝐯𝐰𝐱𝐲𝐳"; // From the KaTeX font metrics, identify letters whose accents need a italic correction. const smallNudge = "DHKLUcegorsuvxyzΠΥΨαδηιμνοτυχϵ"; const mediumNudge = "BCEGIMNOPQRSTXZlpqtwΓΘΞΣΦΩβεζθξρςφψϑϕϱ"; const largeNudge = "AFJdfΔΛ"; const mathmlBuilder$a = (group, style) => { const accentNode = group.isStretchy ? stretchy.accentNode(group) : new mathMLTree.MathNode("mo", [makeText(group.label, group.mode)]); if (!group.isStretchy) { accentNode.setAttribute("stretchy", "false"); // Keep Firefox from stretching \check } if (group.label !== "\\vec") { accentNode.style.mathDepth = "0"; // not scriptstyle // Don't use attribute accent="true" because MathML Core eliminates a needed space. } const tag = group.label === "\\c" ? "munder" : "mover"; const needsWbkVertShift = needsWebkitVerticalShift.has(group.label); if (tag === "mover" && group.mode === "math" && (!group.isStretchy) && group.base.text && group.base.text.length === 1) { const text = group.base.text; const isVec = group.label === "\\vec"; const vecPostfix = isVec === "\\vec" ? "-vec" : ""; if (isVec) { accentNode.classes.push("tml-vec"); // Firefox sizing of \vec arrow } const wbkPostfix = isVec ? "-vec" : needsWbkVertShift ? "-acc" : ""; if (smallNudge.indexOf(text) > -1) { accentNode.classes.push(`chr-sml${vecPostfix}`); accentNode.classes.push(`wbk-sml${wbkPostfix}`); } else if (mediumNudge.indexOf(text) > -1) { accentNode.classes.push(`chr-med${vecPostfix}`); accentNode.classes.push(`wbk-med${wbkPostfix}`); } else if (largeNudge.indexOf(text) > -1) { accentNode.classes.push(`chr-lrg${vecPostfix}`); accentNode.classes.push(`wbk-lrg${wbkPostfix}`); } else if (isVec) { accentNode.classes.push(`wbk-vec`); } else if (needsWbkVertShift) { accentNode.classes.push(`wbk-acc`); } } else if (needsWbkVertShift) { // text-mode accents accentNode.classes.push("wbk-acc"); } const node = new mathMLTree.MathNode(tag, [buildGroup$1(group.base, style), accentNode]); return node; }; const nonStretchyAccents = new Set([ "\\acute", "\\check", "\\grave", "\\ddot", "\\dddot", "\\ddddot", "\\tilde", "\\bar", "\\breve", "\\check", "\\hat", "\\vec", "\\dot", "\\mathring" ]); const needsWebkitVerticalShift = new Set([ "\\acute", "\\bar", "\\breve", "\\check", "\\dot", "\\ddot", "\\grave", "\\hat", "\\mathring", "\\`", "\\'", "\\^", "\\=", "\\u", "\\.", '\\"', "\\r", "\\H", "\\v" ]); const combiningChar = { "\\`": "\u0300", "\\'": "\u0301", "\\^": "\u0302", "\\~": "\u0303", "\\=": "\u0304", "\\u": "\u0306", "\\.": "\u0307", '\\"': "\u0308", "\\r": "\u030A", "\\H": "\u030B", "\\v": "\u030C", "\\c": "\u0327" }; // Accents defineFunction({ type: "accent", names: [ "\\acute", "\\grave", "\\ddot", "\\dddot", "\\ddddot", "\\tilde", "\\bar", "\\breve", "\\check", "\\hat", "\\vec", "\\dot", "\\mathring", "\\overparen", "\\widecheck", "\\widehat", "\\wideparen", "\\widetilde", "\\overrightarrow", "\\overleftarrow", "\\Overrightarrow", "\\overleftrightarrow", "\\overgroup", "\\overleftharpoon", "\\overrightharpoon" ], props: { numArgs: 1 }, handler: (context, args) => { const base = normalizeArgument(args[0]); const isStretchy = !nonStretchyAccents.has(context.funcName); return { type: "accent", mode: context.parser.mode, label: context.funcName, isStretchy, base }; }, mathmlBuilder: mathmlBuilder$a }); // Text-mode accents defineFunction({ type: "accent", names: ["\\'", "\\`", "\\^", "\\~", "\\=", "\\c", "\\u", "\\.", '\\"', "\\r", "\\H", "\\v"], props: { numArgs: 1, allowedInText: true, allowedInMath: true, argTypes: ["primitive"] }, handler: (context, args) => { const base = normalizeArgument(args[0]); const mode = context.parser.mode; if (mode === "math" && context.parser.settings.strict) { // LaTeX only writes a warning. It doesn't stop. We'll issue the same warning. // eslint-disable-next-line no-console console.log(`Temml parse error: Command ${context.funcName} is invalid in math mode.`); } if (mode === "text" && base.text && base.text.length === 1 && context.funcName in combiningChar && smalls.indexOf(base.text) > -1) { // Return a combining accent character return { type: "textord", mode: "text", text: base.text + combiningChar[context.funcName] } } else if (context.funcName === "\\c" && mode === "text" && base.text && base.text.length === 1) { // combining cedilla return { type: "textord", mode: "text", text: base.text + "\u0327" } } else { // Build up the accent return { type: "accent", mode, label: context.funcName, isStretchy: false, base } } }, mathmlBuilder: mathmlBuilder$a }); defineFunction({ type: "accentUnder", names: [ "\\underleftarrow", "\\underrightarrow", "\\underleftrightarrow", "\\undergroup", "\\underparen", "\\utilde" ], props: { numArgs: 1 }, handler: ({ parser, funcName }, args) => { const base = args[0]; return { type: "accentUnder", mode: parser.mode, label: funcName, base: base }; }, mathmlBuilder: (group, style) => { const accentNode = stretchy.accentNode(group); accentNode.style["math-depth"] = 0; const node = new mathMLTree.MathNode("munder", [ buildGroup$1(group.base, style), accentNode ]); return node; } }); /** * This file does conversion between units. In particular, it provides * calculateSize to convert other units into CSS units. */ const ptPerUnit = { // Convert to CSS (Postscipt) points, not TeX points // https://en.wikibooks.org/wiki/LaTeX/Lengths and // https://tex.stackexchange.com/a/8263 pt: 800 / 803, // convert TeX point to CSS (Postscript) point pc: (12 * 800) / 803, // pica dd: ((1238 / 1157) * 800) / 803, // didot cc: ((14856 / 1157) * 800) / 803, // cicero (12 didot) nd: ((685 / 642) * 800) / 803, // new didot nc: ((1370 / 107) * 800) / 803, // new cicero (12 new didot) sp: ((1 / 65536) * 800) / 803, // scaled point (TeX's internal smallest unit) mm: (25.4 / 72), cm: (2.54 / 72), in: (1 / 72), px: (96 / 72) }; /** * Determine whether the specified unit (either a string defining the unit * or a "size" parse node containing a unit field) is valid. */ const validUnits = [ "em", "ex", "mu", "pt", "mm", "cm", "in", "px", "bp", "pc", "dd", "cc", "nd", "nc", "sp" ]; const validUnit = function(unit) { if (typeof unit !== "string") { unit = unit.unit; } return validUnits.indexOf(unit) > -1 }; const emScale = styleLevel => { const scriptLevel = Math.max(styleLevel - 1, 0); return [1, 0.7, 0.5][scriptLevel] }; /* * Convert a "size" parse node (with numeric "number" and string "unit" fields, * as parsed by functions.js argType "size") into a CSS value. */ const calculateSize = function(sizeValue, style) { let number = sizeValue.number; if (style.maxSize[0] < 0 && number > 0) { return { number: 0, unit: "em" } } const unit = sizeValue.unit; switch (unit) { case "mm": case "cm": case "in": case "px": { const numInCssPts = number * ptPerUnit[unit]; if (numInCssPts > style.maxSize[1]) { return { number: style.maxSize[1], unit: "pt" } } return { number, unit }; // absolute CSS units. } case "em": case "ex": { // In TeX, em and ex do not change size in \scriptstyle. if (unit === "ex") { number *= 0.431; } number = Math.min(number / emScale(style.level), style.maxSize[0]); return { number: utils.round(number), unit: "em" }; } case "bp": { if (number > style.maxSize[1]) { number = style.maxSize[1]; } return { number, unit: "pt" }; // TeX bp is a CSS pt. (1/72 inch). } case "pt": case "pc": case "dd": case "cc": case "nd": case "nc": case "sp": { number = Math.min(number * ptPerUnit[unit], style.maxSize[1]); return { number: utils.round(number), unit: "pt" } } case "mu": { number = Math.min(number / 18, style.maxSize[0]); return { number: utils.round(number), unit: "em" } } default: throw new ParseError("Invalid unit: '" + unit + "'") } }; // Helper functions const padding = width => { const node = new mathMLTree.MathNode("mspace"); node.setAttribute("width", width + "em"); return node }; const paddedNode = (group, lspace = 0.3, rspace = 0, mustSmash = false) => { if (group == null && rspace === 0) { return padding(lspace) } const row = group ? [group] : []; if (lspace !== 0) { row.unshift(padding(lspace)); } if (rspace > 0) { row.push(padding(rspace)); } if (mustSmash) { // Used for the bottom arrow in a {CD} environment const mpadded = new mathMLTree.MathNode("mpadded", row); mpadded.setAttribute("height", "0.1px"); // Don't use 0. WebKit would hide it. return mpadded } else { return new mathMLTree.MathNode("mrow", row) } }; const labelSize = (size, scriptLevel) => Number(size) / emScale(scriptLevel); const munderoverNode = (fName, body, below, style) => { const arrowNode = stretchy.mathMLnode(fName); // Is this the short part of a mhchem equilibrium arrow? const isEq = fName.slice(1, 3) === "eq"; const minWidth = fName.charAt(1) === "x" ? "1.75" // mathtools extensible arrows are ≥ 1.75em long : fName.slice(2, 4) === "cd" ? "3.0" // cd package arrows : isEq ? "1.0" // The shorter harpoon of a mhchem equilibrium arrow : "2.0"; // other mhchem arrows // TODO: When Firefox supports minsize, use the next line. //arrowNode.setAttribute("minsize", String(minWidth) + "em") arrowNode.setAttribute("lspace", "0"); arrowNode.setAttribute("rspace", (isEq ? "0.5em" : "0")); // <munderover> upper and lower labels are set to scriptlevel by MathML // So we have to adjust our label dimensions accordingly. const labelStyle = style.withLevel(style.level < 2 ? 2 : 3); const minArrowWidth = labelSize(minWidth, labelStyle.level); // The dummyNode will be inside a <mover> inside a <mover> // So it will be at scriptlevel 3 const dummyWidth = labelSize(minWidth, 3); const emptyLabel = paddedNode(null, minArrowWidth.toFixed(4), 0); const dummyNode = paddedNode(null, dummyWidth.toFixed(4), 0); // The arrow is a little longer than the label. Set a spacer length. const space = labelSize((isEq ? 0 : 0.3), labelStyle.level).toFixed(4); let upperNode; let lowerNode; const gotUpper = (body && body.body && // \hphantom visible content (body.body.body || body.body.length > 0)); if (gotUpper) { let label = buildGroup$1(body, labelStyle); const mustSmash = (fName === "\\\\cdrightarrow" || fName === "\\\\cdleftarrow"); label = paddedNode(label, space, space, mustSmash); // Since Firefox does not support minsize, stack a invisible node // on top of the label. Its width will serve as a min-width. // TODO: Refactor this after Firefox supports minsize. upperNode = new mathMLTree.MathNode("mover", [label, dummyNode]); } const gotLower = (below && below.body && (below.body.body || below.body.length > 0)); if (gotLower) { let label = buildGroup$1(below, labelStyle); label = paddedNode(label, space, space); lowerNode = new mathMLTree.MathNode("munder", [label, dummyNode]); } let node; if (!gotUpper && !gotLower) { node = new mathMLTree.MathNode("mover", [arrowNode, emptyLabel]); } else if (gotUpper && gotLower) { node = new mathMLTree.MathNode("munderover", [arrowNode, lowerNode, upperNode]); } else if (gotUpper) { node = new mathMLTree.MathNode("mover", [arrowNode, upperNode]); } else { node = new mathMLTree.MathNode("munder", [arrowNode, lowerNode]); } if (minWidth === "3.0") { node.style.height = "1em"; } // CD environment node.setAttribute("accent", "false"); // Necessary for MS Word return node }; // Stretchy arrows with an optional argument defineFunction({ type: "xArrow", names: [ "\\xleftarrow", "\\xrightarrow", "\\xLeftarrow", "\\xRightarrow", "\\xleftrightarrow", "\\xLeftrightarrow", "\\xhookleftarrow", "\\xhookrightarrow", "\\xmapsto", "\\xrightharpoondown", "\\xrightharpoonup", "\\xleftharpoondown", "\\xleftharpoonup", "\\xlongequal", "\\xtwoheadrightarrow", "\\xtwoheadleftarrow", "\\xtofrom", // expfeil "\\xleftrightharpoons", // mathtools "\\xrightleftharpoons", // mathtools // The next 7 functions are here only to support mhchem "\\yields", "\\yieldsLeft", "\\mesomerism", "\\longrightharpoonup", "\\longleftharpoondown", "\\yieldsLeftRight", "\\chemequilibrium", // The next 3 functions are here only to support the {CD} environment. "\\\\cdrightarrow", "\\\\cdleftarrow", "\\\\cdlongequal" ], props: { numArgs: 1, numOptionalArgs: 1 }, handler({ parser, funcName }, args, optArgs) { return { type: "xArrow", mode: parser.mode, name: funcName, body: args[0], below: optArgs[0] }; }, mathmlBuilder(group, style) { // Build the arrow and its labels. const node = munderoverNode(group.name, group.body, group.below, style); // Create operator spacing for a relation. const row = [node]; row.unshift(padding(0.2778)); row.push(padding(0.2778)); return new mathMLTree.MathNode("mrow", row) } }); const arrowComponent = { "\\equilibriumRight": ["\\longrightharpoonup", "\\eqleftharpoondown"], "\\equilibriumLeft": ["\\eqrightharpoonup", "\\longleftharpoondown"] }; // Math fonts do not have a single glyph for these two mhchem functions. // So we stack a pair of single harpoons. defineFunction({ type: "stackedArrow", names: [ "\\equilibriumRight", "\\equilibriumLeft" ], props: { numArgs: 1, numOptionalArgs: 1 }, handler({ parser, funcName }, args, optArgs) { const lowerArrowBody = args[0] ? { type: "hphantom", mode: parser.mode, body: args[0] } : null; const upperArrowBelow = optArgs[0] ? { type: "hphantom", mode: parser.mode, body: optArgs[0] } : null; return { type: "stackedArrow", mode: parser.mode, name: funcName, body: args[0], upperArrowBelow, lowerArrowBody, below: optArgs[0] }; }, mathmlBuilder(group, style) { const topLabel = arrowComponent[group.name][0]; const botLabel = arrowComponent[group.name][1]; const topArrow = munderoverNode(topLabel, group.body, group.upperArrowBelow, style); const botArrow = munderoverNode(botLabel, group.lowerArrowBody, group.below, style); let wrapper; const raiseNode = new mathMLTree.MathNode("mpadded", [topArrow]); raiseNode.setAttribute("voffset", "0.3em"); raiseNode.setAttribute("height", "+0.3em"); raiseNode.setAttribute("depth", "-0.3em"); // One of the arrows is given ~zero width. so the other has the same horzontal alignment. if (group.name === "\\equilibriumLeft") { const botNode = new mathMLTree.MathNode("mpadded", [botArrow]); botNode.setAttribute("width", "0.5em"); wrapper = new mathMLTree.MathNode( "mpadded", [padding(0.2778), botNode, raiseNode, padding(0.2778)] ); } else { raiseNode.setAttribute("width", (group.name === "\\equilibriumRight" ? "0.5em" : "0")); wrapper = new mathMLTree.MathNode( "mpadded", [padding(0.2778), raiseNode, botArrow, padding(0.2778)] ); } wrapper.setAttribute("voffset", "-0.18em"); wrapper.setAttribute("height", "-0.18em"); wrapper.setAttribute("depth", "+0.18em"); return wrapper } }); /** * All registered environments. * `environments.js` exports this same dictionary again and makes it public. * `Parser.js` requires this dictionary via `environments.js`. */ const _environments = {}; function defineEnvironment({ type, names, props, handler, mathmlBuilder }) { // Set default values of environments. const data = { type, numArgs: props.numArgs || 0, allowedInText: false, numOptionalArgs: 0, handler }; for (let i = 0; i < names.length; ++i) { _environments[names[i]] = data; } if (mathmlBuilder) { _mathmlGroupBuilders[type] = mathmlBuilder; } } /** * Asserts that the node is of the given type and returns it with stricter * typing. Throws if the node's type does not match. */ function assertNodeType(node, type) { if (!node || node.type !== type) { throw new Error( `Expected node of type ${type}, but got ` + (node ? `node of type ${node.type}` : String(node)) ); } return node; } /** * Returns the node more strictly typed iff it is of the given type. Otherwise, * returns null. */ function assertSymbolNodeType(node) { const typedNode = checkSymbolNodeType(node); if (!typedNode) { throw new Error( `Expected node of symbol group type, but got ` + (node ? `node of type ${node.type}` : String(node)) ); } return typedNode; } /** * Returns the node more strictly typed iff it is of the given type. Otherwise, * returns null. */ function checkSymbolNodeType(node) { if (node && (node.type === "atom" || Object.prototype.hasOwnProperty.call(NON_ATOMS, node.type))) { return node; } return null; } const cdArrowFunctionName = { ">": "\\\\cdrightarrow", "<": "\\\\cdleftarrow", "=": "\\\\cdlongequal", A: "\\uparrow", V: "\\downarrow", "|": "\\Vert", ".": "no arrow" }; const newCell = () => { // Create an empty cell, to be filled below with parse nodes. return { type: "styling", body: [], mode: "math", scriptLevel: "display" }; }; const isStartOfArrow = (node) => { return node.type === "textord" && node.text === "@"; }; const isLabelEnd = (node, endChar) => { return (node.type === "mathord" || node.type === "atom") && node.text === endChar; }; function cdArrow(arrowChar, labels, parser) { // Return a parse tree of an arrow and its labels. // This acts in a way similar to a macro expansion. const funcName = cdArrowFunctionName[arrowChar]; switch (funcName) { case "\\\\cdrightarrow": case "\\\\cdleftarrow": return parser.callFunction(funcName, [labels[0]], [labels[1]]); case "\\uparrow": case "\\downarrow": { const leftLabel = parser.callFunction("\\\\cdleft", [labels[0]], []); const bareArrow = { type: "atom", text: funcName, mode: "math", family: "rel" }; const sizedArrow = parser.callFunction("\\Big", [bareArrow], []); const rightLabel = parser.callFunction("\\\\cdright", [labels[1]], []); const arrowGroup = { type: "ordgroup", mode: "math", body: [leftLabel, sizedArrow, rightLabel], semisimple: true }; return parser.callFunction("\\\\cdparent", [arrowGroup], []); } case "\\\\cdlongequal": return parser.callFunction("\\\\cdlongequal", [], []); case "\\Vert": { const arrow = { type: "textord", text: "\\Vert", mode: "math" }; return parser.callFunction("\\Big", [arrow], []); } default: return { type: "textord", text: " ", mode: "math" }; } } function parseCD(parser) { // Get the array's parse nodes with \\ temporarily mapped to \cr. const parsedRows = []; parser.gullet.beginGroup(); parser.gullet.macros.set("\\cr", "\\\\\\relax"); parser.gullet.beginGroup(); while (true) { // Get the parse nodes for the next row. parsedRows.push(parser.parseExpression(false, "\\\\")); parser.gullet.endGroup(); parser.gullet.beginGroup(); const next = parser.fetch().text; if (next === "&" || next === "\\\\") { parser.consume(); } else if (next === "\\end") { if (parsedRows[parsedRows.length - 1].length === 0) { parsedRows.pop(); // final row ended in \\ } break; } else { throw new ParseError("Expected \\\\ or \\cr or \\end", parser.nextToken); } } let row = []; const body = [row]; // Loop thru the parse nodes. Collect them into cells and arrows. for (let i = 0; i < parsedRows.length; i++) { // Start a new row. const rowNodes = parsedRows[i]; // Create the first cell. let cell = newCell(); for (let j = 0; j < rowNodes.length; j++) { if (!isStartOfArrow(rowNodes[j])) { // If a parseNode is not an arrow, it goes into a cell. cell.body.push(rowNodes[j]); } else { // Parse node j is an "@", the start of an arrow. // Before starting on the arrow, push the cell into `row`. row.push(cell); // Now collect parseNodes into an arrow. // The character after "@" defines the arrow type. j += 1; const arrowChar = assertSymbolNodeType(rowNodes[j]).text; // Create two empty label nodes. We may or may not use them. const labels = new Array(2); labels[0] = { type: "ordgroup", mode: "math", body: [] }; labels[1] = { type: "ordgroup", mode: "math", body: [] }; // Process the arrow. if ("=|.".indexOf(arrowChar) > -1) ; else if ("<>AV".indexOf(arrowChar) > -1) { // Four arrows, `@>>>`, `@<<<`, `@AAA`, and `@VVV`, each take // two optional labels. E.g. the right-point arrow syntax is // really: @>{optional label}>{optional label}> // Collect parseNodes into labels. for (let labelNum = 0; labelNum < 2; labelNum++) { let inLabel = true; for (let k = j + 1; k < rowNodes.length; k++) { if (isLabelEnd(rowNodes[k], arrowChar)) { inLabel = false; j = k; break; } if (isStartOfArrow(rowNodes[k])) { throw new ParseError( "Missing a " + arrowChar + " character to complete a CD arrow.", rowNodes[k] ); } labels[labelNum].body.push(rowNodes[k]); } if (inLabel) { // isLabelEnd never returned a true. throw new ParseError( "Missing a " + arrowChar + " character to complete a CD arrow.", rowNodes[j] ); } } } else { throw new ParseError(`Expected one of "<>AV=|." after @.`); } // Now join the arrow to its labels. const arrow = cdArrow(arrowChar, labels, parser); // Wrap the arrow in a styling node row.push(arrow); // In CD's syntax, cells are implicit. That is, everything that // is not an arrow gets collected into a cell. So create an empty // cell now. It will collect upcoming parseNodes. cell = newCell(); } } if (i % 2 === 0) { // Even-numbered rows consist of: cell, arrow, cell, arrow, ... cell // The last cell is not yet pushed into `row`, so: row.push(cell); } else { // Odd-numbered rows consist of: vert arrow, empty cell, ... vert arrow // Remove the empty cell that was placed at the beginning of `row`. row.shift(); } row = []; body.push(row); } body.pop(); // End row group parser.gullet.endGroup(); // End array group defining \\ parser.gullet.endGroup(); return { type: "array", mode: "math", body, tags: null, labels: new Array(body.length + 1).fill(""), envClasses: ["jot", "cd"], cols: [], hLinesBeforeRow: new Array(body.length + 1).fill([]) }; } // The functions below are not available for general use. // They are here only for internal use by the {CD} environment in placing labels // next to vertical arrows. // We don't need any such functions for horizontal arrows because we can reuse // the functionality that already exists for extensible arrows. defineFunction({ type: "cdlabel", names: ["\\\\cdleft", "\\\\cdright"], props: { numArgs: 1 }, handler({ parser, funcName }, args) { return { type: "cdlabel", mode: parser.mode, side: funcName.slice(4), label: args[0] }; }, mathmlBuilder(group, style) { if (group.label.body.length === 0) { return new mathMLTree.MathNode("mrow", style) // empty label } // Abuse an <mtable> to create vertically centered content. const mrow = buildGroup$1(group.label, style); if (group.side === "left") { mrow.classes.push("tml-shift-left"); } const mtd = new mathMLTree.MathNode("mtd", [mrow]); mtd.style.padding = "0"; const mtr = new mathMLTree.MathNode("mtr", [mtd]); const mtable = new mathMLTree.MathNode("mtable", [mtr]); const label = new mathMLTree.MathNode("mpadded", [mtable]); // Set the label width to zero so that the arrow will be centered under the corner cell. label.setAttribute("width", "0.1px"); // Don't use 0. WebKit would hide it. label.setAttribute("displaystyle", "false"); label.setAttribute("scriptlevel", "1"); return label; } }); defineFunction({ type: "cdlabelparent", names: ["\\\\cdparent"], props: { numArgs: 1 }, handler({ parser }, args) { return { type: "cdlabelparent", mode: parser.mode, fragment: args[0] }; }, mathmlBuilder(group, style) { return new mathMLTree.MathNode("mrow", [buildGroup$1(group.fragment, style)]); } }); const ordGroup = (body) => { return { "type": "ordgroup", "mode": "math", "body": body, "semisimple": true } }; const phantom = (body, type) => { return { "type": type, "mode": "math", "body": ordGroup(body) } }; /* * A helper for \bordermatrix. * parseArray() has parsed the tokens as if the environment * was \begin{matrix}. That parse tree is this function’s input. * Here, we rearrange the parse tree to get one that will * result in TeX \bordermatrix. * The final result includes a {pmatrix}, which is the bottom * half of a <mover> element. The top of the <mover> contains * the \bordermatrix headings. The top section also contains the * contents of the bottom {pmatrix}. Those elements are hidden via * \hphantom, but they ensure that column widths are the same top and * bottom. * * We also create a left {matrix} with a single column that contains * elements shifted out of the matrix. The left {matrix} also * contains \vphantom copies of the other {pmatrix} elements. * As before, this ensures consistent row heights of left and main. */ const bordermatrixParseTree = (matrix, delimiters) => { const body = matrix.body; body[0].shift(); // dispose of top left cell // Create an array for the left column const leftColumnBody = new Array(body.length - 1).fill().map(() => []); for (let i = 1; i < body.length; i++) { // The visible part of the cell leftColumnBody[i - 1].push(body[i].shift()); // A vphantom with contents from the pmatrix, to set minimum cell height const phantomBody = []; for (let j = 0; j < body[i].length; j++) { phantomBody.push(body[i][j]); } leftColumnBody[i - 1].push(phantom(phantomBody, "vphantom")); } // Create an array for the top row const topRowBody = new Array(body.length).fill().map(() => []); for (let j = 0; j < body[0].length; j++) { topRowBody[0].push(body[0][j]); } // Copy the rest of the pmatrix, but squashed via \hphantom for (let i = 1; i < body.length; i++) { for (let j = 0; j < body[0].length; j++) { topRowBody[i].push(phantom(body[i][j].body, "hphantom")); } } // Squash the top row of the main {pmatrix} for (let j = 0; j < body[0].length; j++) { body[0][j] = phantom(body[0][j].body, "hphantom"); } // Now wrap the arrays in the proper parse nodes. const leftColumn = { type: "array", mode: "math", body: leftColumnBody, cols: [{ type: "align", align: "c" }], rowGaps: new Array(leftColumnBody.length - 1).fill(null), hLinesBeforeRow: new Array(leftColumnBody.length + 1).fill().map(() => []), envClasses: [], scriptLevel: "text", arraystretch: 1, labels: new Array(leftColumnBody.length).fill(""), arraycolsep: { "number": 0.04, unit: "em" } }; const topRow = { type: "array", mode: "math", body: topRowBody, cols: new Array(topRowBody.length).fill({ type: "align", align: "c" }), rowGaps: new Array(topRowBody.length - 1).fill(null), hLinesBeforeRow: new Array(topRowBody.length + 1).fill().map(() => []), envClasses: [], scriptLevel: "text", arraystretch: 1, labels: new Array(topRowBody.length).fill(""), arraycolsep: null }; const topWrapper = { type: "styling", mode: "math", scriptLevel: "text", // Must set this explicitly. body: [topRow] // Default level is "script". }; const container = { type: "leftright", mode: "math", body: [matrix], left: delimiters ? delimiters[0] : "(", right: delimiters ? delimiters[1] : ")", rightColor: undefined }; const base = { type: "op", // The base of a TeX \overset mode: "math", limits: true, alwaysHandleSupSub: true, parentIsSupSub: true, symbol: false, suppressBaseShift: true, body: [container] }; const mover = { type: "supsub", // We're using the MathML equivalent mode: "math", // of TeX \overset. stack: true, base: base, // That keeps the {pmatrix} aligned with sup: topWrapper, // the math centerline. sub: null }; return ordGroup([leftColumn, mover]) }; /** * Lexing or parsing positional information for error reporting. * This object is immutable. */ class SourceLocation { constructor(lexer, start, end) { this.lexer = lexer; // Lexer holding the input string. this.start = start; // Start offset, zero-based inclusive. this.end = end; // End offset, zero-based exclusive. } /** * Merges two `SourceLocation`s from location providers, given they are * provided in order of appearance. * - Returns the first one's location if only the first is provided. * - Returns a merged range of the first and the last if both are provided * and their lexers match. * - Otherwise, returns null. */ static range(first, second) { if (!second) { return first && first.loc; } else if (!first || !first.loc || !second.loc || first.loc.lexer !== second.loc.lexer) { return null; } else { return new SourceLocation(first.loc.lexer, first.loc.start, second.loc.end); } } } /** * Interface required to break circular dependency between Token, Lexer, and * ParseError. */ /** * The resulting token returned from `lex`. * * It consists of the token text plus some position information. * The position information is essentially a range in an input string, * but instead of referencing the bare input string, we refer to the lexer. * That way it is possible to attach extra metadata to the input string, * like for example a file name or similar. * * The position information is optional, so it is OK to construct synthetic * tokens if appropriate. Not providing available position information may * lead to degraded error reporting, though. */ class Token { constructor( text, // the text of this token loc ) { this.text = text; this.loc = loc; } /** * Given a pair of tokens (this and endToken), compute a `Token` encompassing * the whole input range enclosed by these two. */ range( endToken, // last token of the range, inclusive text // the text of the newly constructed token ) { return new Token(text, SourceLocation.range(this, endToken)); } } // In TeX, there are actually three sets of dimensions, one for each of // textstyle, scriptstyle, and scriptscriptstyle. These are // provided in the the arrays below, in that order. // // Math style is not quite the same thing as script level. const StyleLevel = { DISPLAY: 0, TEXT: 1, SCRIPT: 2, SCRIPTSCRIPT: 3 }; /** * All registered global/built-in macros. * `macros.js` exports this same dictionary again and makes it public. * `Parser.js` requires this dictionary via `macros.js`. */ const _macros = {}; // This function might one day accept an additional argument and do more things. function defineMacro(name, body) { _macros[name] = body; } /** * Predefined macros for Temml. * This can be used to define some commands in terms of others. */ const macros = _macros; ////////////////////////////////////////////////////////////////////// // macro tools defineMacro("\\noexpand", function(context) { // The expansion is the token itself; but that token is interpreted // as if its meaning were ‘\relax’ if it is a control sequence that // would ordinarily be expanded by TeX’s expansion rules. const t = context.popToken(); if (context.isExpandable(t.text)) { t.noexpand = true; t.treatAsRelax = true; } return { tokens: [t], numArgs: 0 }; }); defineMacro("\\expandafter", function(context) { // TeX first reads the token that comes immediately after \expandafter, // without expanding it; let’s call this token t. Then TeX reads the // token that comes after t (and possibly more tokens, if that token // has an argument), replacing it by its expansion. Finally TeX puts // t back in front of that expansion. const t = context.popToken(); context.expandOnce(true); // expand only an expandable token return { tokens: [t], numArgs: 0 }; }); // LaTeX's \@firstoftwo{#1}{#2} expands to #1, skipping #2 // TeX source: \long\def\@firstoftwo#1#2{#1} defineMacro("\\@firstoftwo", function(context) { const args = context.consumeArgs(2); return { tokens: args[0], numArgs: 0 }; }); // LaTeX's \@secondoftwo{#1}{#2} expands to #2, skipping #1 // TeX source: \long\def\@secondoftwo#1#2{#2} defineMacro("\\@secondoftwo", function(context) { const args = context.consumeArgs(2); return { tokens: args[1], numArgs: 0 }; }); // LaTeX's \@ifnextchar{#1}{#2}{#3} looks ahead to the next (unexpanded) // symbol that isn't a space, consuming any spaces but not consuming the // first nonspace character. If that nonspace character matches #1, then // the macro expands to #2; otherwise, it expands to #3. defineMacro("\\@ifnextchar", function(context) { const args = context.consumeArgs(3); // symbol, if, else context.consumeSpaces(); const nextToken = context.future(); if (args[0].length === 1 && args[0][0].text === nextToken.text) { return { tokens: args[1], numArgs: 0 }; } else { return { tokens: args[2], numArgs: 0 }; } }); // LaTeX's \@ifstar{#1}{#2} looks ahead to the next (unexpanded) symbol. // If it is `*`, then it consumes the symbol, and the macro expands to #1; // otherwise, the macro expands to #2 (without consuming the symbol). // TeX source: \def\@ifstar#1{\@ifnextchar *{\@firstoftwo{#1}}} defineMacro("\\@ifstar", "\\@ifnextchar *{\\@firstoftwo{#1}}"); // LaTeX's \TextOrMath{#1}{#2} expands to #1 in text mode, #2 in math mode defineMacro("\\TextOrMath", function(context) { const args = context.consumeArgs(2); if (context.mode === "text") { return { tokens: args[0], numArgs: 0 }; } else { return { tokens: args[1], numArgs: 0 }; } }); const stringFromArg = arg => { // Reverse the order of the arg and return a string. let str = ""; for (let i = arg.length - 1; i > -1; i--) { str += arg[i].text; } return str }; // Lookup table for parsing numbers in base 8 through 16 const digitToNumber = { 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, a: 10, A: 10, b: 11, B: 11, c: 12, C: 12, d: 13, D: 13, e: 14, E: 14, f: 15, F: 15 }; const nextCharNumber = context => { const numStr = context.future().text; if (numStr === "EOF") { return [null, ""] } return [digitToNumber[numStr.charAt(0)], numStr] }; const appendCharNumbers = (number, numStr, base) => { for (let i = 1; i < numStr.length; i++) { const digit = digitToNumber[numStr.charAt(i)]; number *= base; number += digit; } return number }; // TeX \char makes a literal character (catcode 12) using the following forms: // (see The TeXBook, p. 43) // \char123 -- decimal // \char'123 -- octal // \char"123 -- hex // \char`x -- character that can be written (i.e. isn't active) // \char`\x -- character that cannot be written (e.g. %) // These all refer to characters from the font, so we turn them into special // calls to a function \@char dealt with in the Parser. defineMacro("\\char", function(context) { let token = context.popToken(); let base; let number = ""; if (token.text === "'") { base = 8; token = context.popToken(); } else if (token.text === '"') { base = 16; token = context.popToken(); } else if (token.text === "`") { token = context.popToken(); if (token.text[0] === "\\") { number = token.text.charCodeAt(1); } else if (token.text === "EOF") { throw new ParseError("\\char` missing argument"); } else { number = token.text.charCodeAt(0); } } else { base = 10; } if (base) { // Parse a number in the given base, starting with first `token`. let numStr = token.text; number = digitToNumber[numStr.charAt(0)]; if (number == null || number >= base) { throw new ParseError(`Invalid base-${base} digit ${token.text}`); } number = appendCharNumbers(number, numStr, base); let digit; [digit, numStr] = nextCharNumber(context); while (digit != null && digit < base) { number *= base; number += digit; number = appendCharNumbers(number, numStr, base); context.popToken(); [digit, numStr] = nextCharNumber(context); } } return `\\@char{${number}}`; }); function recreateArgStr(context) { // Recreate the macro's original argument string from the array of parse tokens. const tokens = context.consumeArgs(1)[0]; let str = ""; let expectedLoc = tokens[tokens.length - 1].loc.start; for (let i = tokens.length - 1; i >= 0; i--) { const actualLoc = tokens[i].loc.start; if (actualLoc > expectedLoc) { // context.consumeArgs has eaten a space. str += " "; expectedLoc = actualLoc; } str += tokens[i].text; expectedLoc += tokens[i].text.length; } return str } // The Latin Modern font renders <mi>√</mi> at the wrong vertical alignment. // This macro provides a better rendering. defineMacro("\\surd", '\\sqrt{\\vphantom{|}}'); // See comment for \oplus in symbols.js. defineMacro("\u2295", "\\oplus"); // Since Temml has no \par, ignore \long. defineMacro("\\long", ""); ////////////////////////////////////////////////////////////////////// // Grouping // \let\bgroup={ \let\egroup=} defineMacro("\\bgroup", "{"); defineMacro("\\egroup", "}"); // Symbols from latex.ltx: // \def~{\nobreakspace{}} // \def\lq{`} // \def\rq{'} // \def \aa {\r a} defineMacro("~", "\\nobreakspace"); defineMacro("\\lq", "`"); defineMacro("\\rq", "'"); defineMacro("\\aa", "\\r a"); defineMacro("\\Bbbk", "\\Bbb{k}"); // \mathstrut from the TeXbook, p 360 defineMacro("\\mathstrut", "\\vphantom{(}"); // \underbar from TeXbook p 353 defineMacro("\\underbar", "\\underline{\\text{#1}}"); ////////////////////////////////////////////////////////////////////// // LaTeX_2ε // \vdots{\vbox{\baselineskip4\p@ \lineskiplimit\z@ // \kern6\p@\hbox{.}\hbox{.}\hbox{.}}} // We'll call \varvdots, which gets a glyph from symbols.js. // The zero-width rule gets us an equivalent to the vertical 6pt kern. defineMacro("\\vdots", "{\\varvdots\\rule{0pt}{15pt}}"); defineMacro("\u22ee", "\\vdots"); // {array} environment gaps defineMacro("\\arraystretch", "1"); // line spacing factor times 12pt defineMacro("\\arraycolsep", "6pt"); // half the width separating columns ////////////////////////////////////////////////////////////////////// // amsmath.sty // http://mirrors.concertpass.com/tex-archive/macros/latex/required/amsmath/amsmath.pdf //\newcommand{\substack}[1]{\subarray{c}#1\endsubarray} defineMacro("\\substack", "\\begin{subarray}{c}#1\\end{subarray}"); // \def\iff{\DOTSB\;\Longleftrightarrow\;} // \def\implies{\DOTSB\;\Longrightarrow\;} // \def\impliedby{\DOTSB\;\Longleftarrow\;} defineMacro("\\iff", "\\DOTSB\\;\\Longleftrightarrow\\;"); defineMacro("\\implies", "\\DOTSB\\;\\Longrightarrow\\;"); defineMacro("\\impliedby", "\\DOTSB\\;\\Longleftarrow\\;"); // AMSMath's automatic \dots, based on \mdots@@ macro. const dotsByToken = { ",": "\\dotsc", "\\not": "\\dotsb", // \keybin@ checks for the following: "+": "\\dotsb", "=": "\\dotsb", "<": "\\dotsb", ">": "\\dotsb", "-": "\\dotsb", "*": "\\dotsb", ":": "\\dotsb", // Symbols whose definition starts with \DOTSB: "\\DOTSB": "\\dotsb", "\\coprod": "\\dotsb", "\\bigvee": "\\dotsb", "\\bigwedge": "\\dotsb", "\\biguplus": "\\dotsb", "\\bigcap": "\\dotsb", "\\bigcup": "\\dotsb", "\\prod": "\\dotsb", "\\sum": "\\dotsb", "\\bigotimes": "\\dotsb", "\\bigoplus": "\\dotsb", "\\bigodot": "\\dotsb", "\\bigsqcap": "\\dotsb", "\\bigsqcup": "\\dotsb", "\\bigtimes": "\\dotsb", "\\And": "\\dotsb", "\\longrightarrow": "\\dotsb", "\\Longrightarrow": "\\dotsb", "\\longleftarrow": "\\dotsb", "\\Longleftarrow": "\\dotsb", "\\longleftrightarrow": "\\dotsb", "\\Longleftrightarrow": "\\dotsb", "\\mapsto": "\\dotsb", "\\longmapsto": "\\dotsb", "\\hookrightarrow": "\\dotsb", "\\doteq": "\\dotsb", // Symbols whose definition starts with \mathbin: "\\mathbin": "\\dotsb", // Symbols whose definition starts with \mathrel: "\\mathrel": "\\dotsb", "\\relbar": "\\dotsb", "\\Relbar": "\\dotsb", "\\xrightarrow": "\\dotsb", "\\xleftarrow": "\\dotsb", // Symbols whose definition starts with \DOTSI: "\\DOTSI": "\\dotsi", "\\int": "\\dotsi", "\\oint": "\\dotsi", "\\iint": "\\dotsi", "\\iiint": "\\dotsi", "\\iiiint": "\\dotsi", "\\idotsint": "\\dotsi", // Symbols whose definition starts with \DOTSX: "\\DOTSX": "\\dotsx" }; defineMacro("\\dots", function(context) { // TODO: If used in text mode, should expand to \textellipsis. // However, in Temml, \textellipsis and \ldots behave the same // (in text mode), and it's unlikely we'd see any of the math commands // that affect the behavior of \dots when in text mode. So fine for now // (until we support \ifmmode ... \else ... \fi). let thedots = "\\dotso"; const next = context.expandAfterFuture().text; if (next in dotsByToken) { thedots = dotsByToken[next]; } else if (next.slice(0, 4) === "\\not") { thedots = "\\dotsb"; } else if (next in symbols.math) { if (["bin", "rel"].includes(symbols.math[next].group)) { thedots = "\\dotsb"; } } return thedots; }); const spaceAfterDots = { // \rightdelim@ checks for the following: ")": true, "]": true, "\\rbrack": true, "\\}": true, "\\rbrace": true, "\\rangle": true, "\\rceil": true, "\\rfloor": true, "\\rgroup": true, "\\rmoustache": true, "\\right": true, "\\bigr": true, "\\biggr": true, "\\Bigr": true, "\\Biggr": true, // \extra@ also tests for the following: $: true, // \extrap@ checks for the following: ";": true, ".": true, ",": true }; defineMacro("\\dotso", function(context) { const next = context.future().text; if (next in spaceAfterDots) { return "\\ldots\\,"; } else { return "\\ldots"; } }); defineMacro("\\dotsc", function(context) { const next = context.future().text; // \dotsc uses \extra@ but not \extrap@, instead specially checking for // ';' and '.', but doesn't check for ','. if (next in spaceAfterDots && next !== ",") { return "\\ldots\\,"; } else { return "\\ldots"; } }); defineMacro("\\cdots", function(context) { const next = context.future().text; if (next in spaceAfterDots) { return "\\@cdots\\,"; } else { return "\\@cdots"; } }); defineMacro("\\dotsb", "\\cdots"); defineMacro("\\dotsm", "\\cdots"); defineMacro("\\dotsi", "\\!\\cdots"); defineMacro("\\idotsint", "\\dotsi"); // amsmath doesn't actually define \dotsx, but \dots followed by a macro // starting with \DOTSX implies \dotso, and then \extra@ detects this case // and forces the added `\,`. defineMacro("\\dotsx", "\\ldots\\,"); // \let\DOTSI\relax // \let\DOTSB\relax // \let\DOTSX\relax defineMacro("\\DOTSI", "\\relax"); defineMacro("\\DOTSB", "\\relax"); defineMacro("\\DOTSX", "\\relax"); // Spacing, based on amsmath.sty's override of LaTeX defaults // \DeclareRobustCommand{\tmspace}[3]{% // \ifmmode\mskip#1#2\else\kern#1#3\fi\relax} defineMacro("\\tmspace", "\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax"); // \renewcommand{\,}{\tmspace+\thinmuskip{.1667em}} // TODO: math mode should use \thinmuskip defineMacro("\\,", "{\\tmspace+{3mu}{.1667em}}"); // \let\thinspace\, defineMacro("\\thinspace", "\\,"); // \def\>{\mskip\medmuskip} // \renewcommand{\:}{\tmspace+\medmuskip{.2222em}} // TODO: \> and math mode of \: should use \medmuskip = 4mu plus 2mu minus 4mu defineMacro("\\>", "\\mskip{4mu}"); defineMacro("\\:", "{\\tmspace+{4mu}{.2222em}}"); // \let\medspace\: defineMacro("\\medspace", "\\:"); // \renewcommand{\;}{\tmspace+\thickmuskip{.2777em}} // TODO: math mode should use \thickmuskip = 5mu plus 5mu defineMacro("\\;", "{\\tmspace+{5mu}{.2777em}}"); // \let\thickspace\; defineMacro("\\thickspace", "\\;"); // \renewcommand{\!}{\tmspace-\thinmuskip{.1667em}} // TODO: math mode should use \thinmuskip defineMacro("\\!", "{\\tmspace-{3mu}{.1667em}}"); // \let\negthinspace\! defineMacro("\\negthinspace", "\\!"); // \newcommand{\negmedspace}{\tmspace-\medmuskip{.2222em}} // TODO: math mode should use \medmuskip defineMacro("\\negmedspace", "{\\tmspace-{4mu}{.2222em}}"); // \newcommand{\negthickspace}{\tmspace-\thickmuskip{.2777em}} // TODO: math mode should use \thickmuskip defineMacro("\\negthickspace", "{\\tmspace-{5mu}{.277em}}"); // \def\enspace{\kern.5em } defineMacro("\\enspace", "\\kern.5em "); // \def\enskip{\hskip.5em\relax} defineMacro("\\enskip", "\\hskip.5em\\relax"); // \def\quad{\hskip1em\relax} defineMacro("\\quad", "\\hskip1em\\relax"); // \def\qquad{\hskip2em\relax} defineMacro("\\qquad", "\\hskip2em\\relax"); defineMacro("\\AA", "\\TextOrMath{\\Angstrom}{\\mathring{A}}\\relax"); // \tag@in@display form of \tag defineMacro("\\tag", "\\@ifstar\\tag@literal\\tag@paren"); defineMacro("\\tag@paren", "\\tag@literal{({#1})}"); defineMacro("\\tag@literal", (context) => { if (context.macros.get("\\df@tag")) { throw new ParseError("Multiple \\tag"); } return "\\gdef\\df@tag{\\text{#1}}"; }); defineMacro("\\notag", "\\nonumber"); defineMacro("\\nonumber", "\\gdef\\@eqnsw{0}"); // \renewcommand{\bmod}{\nonscript\mskip-\medmuskip\mkern5mu\mathbin // {\operator@font mod}\penalty900 // \mkern5mu\nonscript\mskip-\medmuskip} // \newcommand{\pod}[1]{\allowbreak // \if@display\mkern18mu\else\mkern8mu\fi(#1)} // \renewcommand{\pmod}[1]{\pod{{\operator@font mod}\mkern6mu#1}} // \newcommand{\mod}[1]{\allowbreak\if@display\mkern18mu // \else\mkern12mu\fi{\operator@font mod}\,\,#1} // TODO: math mode should use \medmuskip = 4mu plus 2mu minus 4mu defineMacro("\\bmod", "\\mathbin{\\text{mod}}"); defineMacro( "\\pod", "\\allowbreak" + "\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)" ); defineMacro("\\pmod", "\\pod{{\\rm mod}\\mkern6mu#1}"); defineMacro( "\\mod", "\\allowbreak" + "\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}" + "{\\rm mod}\\,\\,#1" ); ////////////////////////////////////////////////////////////////////// // LaTeX source2e // \expandafter\let\expandafter\@normalcr // \csname\expandafter\@gobble\string\\ \endcsname // \DeclareRobustCommand\newline{\@normalcr\relax} defineMacro("\\newline", "\\\\\\relax"); // \def\TeX{T\kern-.1667em\lower.5ex\hbox{E}\kern-.125emX\@} // TODO: Doesn't normally work in math mode because \@ fails. defineMacro("\\TeX", "\\textrm{T}\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125em\\textrm{X}"); defineMacro( "\\LaTeX", "\\textrm{L}\\kern-.35em\\raisebox{0.2em}{\\scriptstyle A}\\kern-.15em\\TeX" ); defineMacro( "\\Temml", // eslint-disable-next-line max-len "\\textrm{T}\\kern-0.2em\\lower{0.2em}{\\textrm{E}}\\kern-0.08em{\\textrm{M}\\kern-0.08em\\raise{0.2em}\\textrm{M}\\kern-0.08em\\textrm{L}}" ); // \DeclareRobustCommand\hspace{\@ifstar\@hspacer\@hspace} // \def\@hspace#1{\hskip #1\relax} // \def\@hspacer#1{\vrule \@width\z@\nobreak // \hskip #1\hskip \z@skip} defineMacro("\\hspace", "\\@ifstar\\@hspacer\\@hspace"); defineMacro("\\@hspace", "\\hskip #1\\relax"); defineMacro("\\@hspacer", "\\rule{0pt}{0pt}\\hskip #1\\relax"); defineMacro("\\colon", `\\mathpunct{\\char"3a}`); ////////////////////////////////////////////////////////////////////// // mathtools.sty defineMacro("\\prescript", "\\pres@cript{_{#1}^{#2}}{}{#3}"); //\providecommand\ordinarycolon{:} defineMacro("\\ordinarycolon", `\\char"3a`); // Raise to center on the math axis, as closely as possible. defineMacro("\\vcentcolon", "\\mathrel{\\raisebox{0.035em}{\\ordinarycolon}}"); // \providecommand*\coloneq{\vcentcolon\mathrel{\mkern-1.2mu}\mathrel{-}} defineMacro("\\coloneq", '\\mathrel{\\raisebox{0.035em}{\\ordinarycolon}\\char"2212}'); // \providecommand*\Coloneq{\dblcolon\mathrel{\mkern-1.2mu}\mathrel{-}} defineMacro("\\Coloneq", '\\mathrel{\\char"2237\\char"2212}'); // \providecommand*\Eqqcolon{=\mathrel{\mkern-1.2mu}\dblcolon} defineMacro("\\Eqqcolon", '\\mathrel{\\char"3d\\char"2237}'); // \providecommand*\Eqcolon{\mathrel{-}\mathrel{\mkern-1.2mu}\dblcolon} defineMacro("\\Eqcolon", '\\mathrel{\\char"2212\\char"2237}'); // \providecommand*\colonapprox{\vcentcolon\mathrel{\mkern-1.2mu}\approx} defineMacro("\\colonapprox", '\\mathrel{\\raisebox{0.035em}{\\ordinarycolon}\\char"2248}'); // \providecommand*\Colonapprox{\dblcolon\mathrel{\mkern-1.2mu}\approx} defineMacro("\\Colonapprox", '\\mathrel{\\char"2237\\char"2248}'); // \providecommand*\colonsim{\vcentcolon\mathrel{\mkern-1.2mu}\sim} defineMacro("\\colonsim", '\\mathrel{\\raisebox{0.035em}{\\ordinarycolon}\\char"223c}'); // \providecommand*\Colonsim{\dblcolon\mathrel{\mkern-1.2mu}\sim} defineMacro("\\Colonsim", '\\mathrel{\\raisebox{0.035em}{\\ordinarycolon}\\char"223c}'); ////////////////////////////////////////////////////////////////////// // colonequals.sty // Alternate names for mathtools's macros: defineMacro("\\ratio", "\\vcentcolon"); defineMacro("\\coloncolon", "\\dblcolon"); defineMacro("\\colonequals", "\\coloneqq"); defineMacro("\\coloncolonequals", "\\Coloneqq"); defineMacro("\\equalscolon", "\\eqqcolon"); defineMacro("\\equalscoloncolon", "\\Eqqcolon"); defineMacro("\\colonminus", "\\coloneq"); defineMacro("\\coloncolonminus", "\\Coloneq"); defineMacro("\\minuscolon", "\\eqcolon"); defineMacro("\\minuscoloncolon", "\\Eqcolon"); // \colonapprox name is same in mathtools and colonequals. defineMacro("\\coloncolonapprox", "\\Colonapprox"); // \colonsim name is same in mathtools and colonequals. defineMacro("\\coloncolonsim", "\\Colonsim"); // Present in newtxmath, pxfonts and txfonts defineMacro("\\notni", "\\mathrel{\\char`\u220C}"); defineMacro("\\limsup", "\\DOTSB\\operatorname*{lim\\,sup}"); defineMacro("\\liminf", "\\DOTSB\\operatorname*{lim\\,inf}"); ////////////////////////////////////////////////////////////////////// // From amsopn.sty defineMacro("\\injlim", "\\DOTSB\\operatorname*{inj\\,lim}"); defineMacro("\\projlim", "\\DOTSB\\operatorname*{proj\\,lim}"); defineMacro("\\varlimsup", "\\DOTSB\\operatorname*{\\overline{\\text{lim}}}"); defineMacro("\\varliminf", "\\DOTSB\\operatorname*{\\underline{\\text{lim}}}"); defineMacro("\\varinjlim", "\\DOTSB\\operatorname*{\\underrightarrow{\\text{lim}}}"); defineMacro("\\varprojlim", "\\DOTSB\\operatorname*{\\underleftarrow{\\text{lim}}}"); defineMacro("\\centerdot", "{\\medspace\\rule{0.167em}{0.189em}\\medspace}"); ////////////////////////////////////////////////////////////////////// // statmath.sty // https://ctan.math.illinois.edu/macros/latex/contrib/statmath/statmath.pdf defineMacro("\\argmin", "\\DOTSB\\operatorname*{arg\\,min}"); defineMacro("\\argmax", "\\DOTSB\\operatorname*{arg\\,max}"); defineMacro("\\plim", "\\DOTSB\\operatorname*{plim}"); ////////////////////////////////////////////////////////////////////// // MnSymbol.sty defineMacro("\\leftmodels", "\\mathop{\\reflectbox{$\\models$}}"); ////////////////////////////////////////////////////////////////////// // braket.sty // http://ctan.math.washington.edu/tex-archive/macros/latex/contrib/braket/braket.pdf defineMacro("\\bra", "\\mathinner{\\langle{#1}|}"); defineMacro("\\ket", "\\mathinner{|{#1}\\rangle}"); defineMacro("\\braket", "\\mathinner{\\langle{#1}\\rangle}"); defineMacro("\\Bra", "\\left\\langle#1\\right|"); defineMacro("\\Ket", "\\left|#1\\right\\rangle"); // A helper for \Braket and \Set const replaceVert = (argStr, match) => { const ch = match[0] === "|" ? "\\vert" : "\\Vert"; const replaceStr = `}\\,\\middle${ch}\\,{`; return argStr.slice(0, match.index) + replaceStr + argStr.slice(match.index + match[0].length) }; defineMacro("\\Braket", function(context) { let argStr = recreateArgStr(context); const regEx = /\|\||\||\\\|/g; let match; while ((match = regEx.exec(argStr)) !== null) { argStr = replaceVert(argStr, match); } return "\\left\\langle{" + argStr + "}\\right\\rangle" }); defineMacro("\\Set", function(context) { let argStr = recreateArgStr(context); const match = /\|\||\||\\\|/.exec(argStr); if (match) { argStr = replaceVert(argStr, match); } return "\\left\\{\\:{" + argStr + "}\\:\\right\\}" }); defineMacro("\\set", function(context) { const argStr = recreateArgStr(context); return "\\{{" + argStr.replace(/\|/, "}\\mid{") + "}\\}" }); ////////////////////////////////////////////////////////////////////// // actuarialangle.dtx defineMacro("\\angln", "{\\angl n}"); ////////////////////////////////////////////////////////////////////// // derivative.sty defineMacro("\\odv", "\\@ifstar\\odv@next\\odv@numerator"); defineMacro("\\odv@numerator", "\\frac{\\mathrm{d}#1}{\\mathrm{d}#2}"); defineMacro("\\odv@next", "\\frac{\\mathrm{d}}{\\mathrm{d}#2}#1"); defineMacro("\\pdv", "\\@ifstar\\pdv@next\\pdv@numerator"); const pdvHelper = args => { const numerator = args[0][0].text; const denoms = stringFromArg(args[1]).split(","); const power = String(denoms.length); const numOp = power === "1" ? "\\partial" : `\\partial^${power}`; let denominator = ""; denoms.map(e => { denominator += "\\partial " + e.trim() + "\\,";}); return [numerator, numOp, denominator.replace(/\\,$/, "")] }; defineMacro("\\pdv@numerator", function(context) { const [numerator, numOp, denominator] = pdvHelper(context.consumeArgs(2)); return `\\frac{${numOp} ${numerator}}{${denominator}}` }); defineMacro("\\pdv@next", function(context) { const [numerator, numOp, denominator] = pdvHelper(context.consumeArgs(2)); return `\\frac{${numOp}}{${denominator}} ${numerator}` }); ////////////////////////////////////////////////////////////////////// // upgreek.dtx defineMacro("\\upalpha", "\\up@greek{\\alpha}"); defineMacro("\\upbeta", "\\up@greek{\\beta}"); defineMacro("\\upgamma", "\\up@greek{\\gamma}"); defineMacro("\\updelta", "\\up@greek{\\delta}"); defineMacro("\\upepsilon", "\\up@greek{\\epsilon}"); defineMacro("\\upzeta", "\\up@greek{\\zeta}"); defineMacro("\\upeta", "\\up@greek{\\eta}"); defineMacro("\\uptheta", "\\up@greek{\\theta}"); defineMacro("\\upiota", "\\up@greek{\\iota}"); defineMacro("\\upkappa", "\\up@greek{\\kappa}"); defineMacro("\\uplambda", "\\up@greek{\\lambda}"); defineMacro("\\upmu", "\\up@greek{\\mu}"); defineMacro("\\upnu", "\\up@greek{\\nu}"); defineMacro("\\upxi", "\\up@greek{\\xi}"); defineMacro("\\upomicron", "\\up@greek{\\omicron}"); defineMacro("\\uppi", "\\up@greek{\\pi}"); defineMacro("\\upalpha", "\\up@greek{\\alpha}"); defineMacro("\\uprho", "\\up@greek{\\rho}"); defineMacro("\\upsigma", "\\up@greek{\\sigma}"); defineMacro("\\uptau", "\\up@greek{\\tau}"); defineMacro("\\upupsilon", "\\up@greek{\\upsilon}"); defineMacro("\\upphi", "\\up@greek{\\phi}"); defineMacro("\\upchi", "\\up@greek{\\chi}"); defineMacro("\\uppsi", "\\up@greek{\\psi}"); defineMacro("\\upomega", "\\up@greek{\\omega}"); ////////////////////////////////////////////////////////////////////// // cmll package defineMacro("\\invamp", '\\mathbin{\\char"214b}'); defineMacro("\\parr", '\\mathbin{\\char"214b}'); defineMacro("\\with", '\\mathbin{\\char"26}'); defineMacro("\\multimapinv", '\\mathrel{\\char"27dc}'); defineMacro("\\multimapboth", '\\mathrel{\\char"29df}'); defineMacro("\\scoh", '{\\mkern5mu\\char"2322\\mkern5mu}'); defineMacro("\\sincoh", '{\\mkern5mu\\char"2323\\mkern5mu}'); defineMacro("\\coh", `{\\mkern5mu\\rule{}{0.7em}\\mathrlap{\\smash{\\raise2mu{\\char"2322}}} {\\smash{\\lower4mu{\\char"2323}}}\\mkern5mu}`); defineMacro("\\incoh", `{\\mkern5mu\\rule{}{0.7em}\\mathrlap{\\smash{\\raise2mu{\\char"2323}}} {\\smash{\\lower4mu{\\char"2322}}}\\mkern5mu}`); ////////////////////////////////////////////////////////////////////// // chemstyle package defineMacro("\\standardstate", "\\text{\\tiny\\char`⦵}"); // Helper functions function getHLines(parser) { // Return an array. The array length = number of hlines. // Each element in the array tells if the line is dashed. const hlineInfo = []; parser.consumeSpaces(); let nxt = parser.fetch().text; if (nxt === "\\relax") { parser.consume(); parser.consumeSpaces(); nxt = parser.fetch().text; } while (nxt === "\\hline" || nxt === "\\hdashline") { parser.consume(); hlineInfo.push(nxt === "\\hdashline"); parser.consumeSpaces(); nxt = parser.fetch().text; } return hlineInfo; } const validateAmsEnvironmentContext = context => { const settings = context.parser.settings; if (!settings.displayMode) { throw new ParseError(`{${context.envName}} can be used only in display mode.`); } }; const sizeRegEx$1 = /([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/; const arrayGaps = macros => { let arraystretch = macros.get("\\arraystretch"); if (typeof arraystretch !== "string") { arraystretch = stringFromArg(arraystretch.tokens); } arraystretch = isNaN(arraystretch) ? null : Number(arraystretch); let arraycolsepStr = macros.get("\\arraycolsep"); if (typeof arraycolsepStr !== "string") { arraycolsepStr = stringFromArg(arraycolsepStr.tokens); } const match = sizeRegEx$1.exec(arraycolsepStr); const arraycolsep = match ? { number: +(match[1] + match[2]), unit: match[3] } : null; return [arraystretch, arraycolsep] }; const checkCellForLabels = cell => { // Check if the author wrote a \tag{} inside this cell. let rowLabel = ""; for (let i = 0; i < cell.length; i++) { if (cell[i].type === "label") { if (rowLabel) { throw new ParseError(("Multiple \\labels in one row")) } rowLabel = cell[i].string; } } return rowLabel }; // autoTag (an argument to parseArray) can be one of three values: // * undefined: Regular (not-top-level) array; no tags on each row // * true: Automatic equation numbering, overridable by \tag // * false: Tags allowed on each row, but no automatic numbering // This function *doesn't* work with the "split" environment name. function getAutoTag(name) { if (name.indexOf("ed") === -1) { return name.indexOf("*") === -1; } // return undefined; } /** * Parse the body of the environment, with rows delimited by \\ and * columns delimited by &, and create a nested list in row-major order * with one group per cell. If given an optional argument scriptLevel * ("text", "display", etc.), then each cell is cast into that scriptLevel. */ function parseArray( parser, { cols, // [{ type: string , align: l|c|r|null }] envClasses, // align(ed|at|edat) | array | cases | cd | small | multline autoTag, // boolean singleRow, // boolean emptySingleRow, // boolean maxNumCols, // number leqno, // boolean arraystretch, // number | null arraycolsep // size value | null }, scriptLevel ) { const endToken = envClasses && envClasses.includes("bordermatrix") ? "}" : "\\end"; parser.gullet.beginGroup(); if (!singleRow) { // \cr is equivalent to \\ without the optional size argument (see below) // TODO: provide helpful error when \cr is used outside array environment parser.gullet.macros.set("\\cr", "\\\\\\relax"); } // Start group for first cell parser.gullet.beginGroup(); let row = []; const body = [row]; const rowGaps = []; const labels = []; const hLinesBeforeRow = []; const tags = (autoTag != null ? [] : undefined); // amsmath uses \global\@eqnswtrue and \global\@eqnswfalse to represent // whether this row should have an equation number. Simulate this with // a \@eqnsw macro set to 1 or 0. function beginRow() { if (autoTag) { parser.gullet.macros.set("\\@eqnsw", "1", true); } } function endRow() { if (tags) { if (parser.gullet.macros.get("\\df@tag")) { tags.push(parser.subparse([new Token("\\df@tag")])); parser.gullet.macros.set("\\df@tag", undefined, true); } else { tags.push(Boolean(autoTag) && parser.gullet.macros.get("\\@eqnsw") === "1"); } } } beginRow(); // Test for \hline at the top of the array. hLinesBeforeRow.push(getHLines(parser)); while (true) { // Parse each cell in its own group (namespace) let cell = parser.parseExpression(false, singleRow ? "\\end" : "\\\\"); parser.gullet.endGroup(); parser.gullet.beginGroup(); cell = { type: "ordgroup", mode: parser.mode, body: cell, semisimple: true }; row.push(cell); const next = parser.fetch().text; if (next === "&") { if (maxNumCols && row.length === maxNumCols) { if (envClasses.includes("array")) { if (parser.settings.strict) { throw new ParseError("Too few columns " + "specified in the {array} column argument.", parser.nextToken) } } else if (maxNumCols === 2) { throw new ParseError("The split environment accepts no more than two columns", parser.nextToken); } else { throw new ParseError("The equation environment accepts only one column", parser.nextToken) } } parser.consume(); } else if (next === endToken) { endRow(); // Arrays terminate newlines with `\crcr` which consumes a `\cr` if // the last line is empty. However, AMS environments keep the // empty row if it's the only one. // NOTE: Currently, `cell` is the last item added into `row`. if (row.length === 1 && cell.body.length === 0 && (body.length > 1 || !emptySingleRow)) { body.pop(); } labels.push(checkCellForLabels(cell.body)); if (hLinesBeforeRow.length < body.length + 1) { hLinesBeforeRow.push([]); } break; } else if (next === "\\\\") { parser.consume(); let size; // \def\Let@{\let\\\math@cr} // \def\math@cr{...\math@cr@} // \def\math@cr@{\new@ifnextchar[\math@cr@@{\math@cr@@[\z@]}} // \def\math@cr@@[#1]{...\math@cr@@@...} // \def\math@cr@@@{\cr} if (parser.gullet.future().text !== " ") { size = parser.parseSizeGroup(true); } rowGaps.push(size ? size.value : null); endRow(); labels.push(checkCellForLabels(cell.body)); // check for \hline(s) following the row separator hLinesBeforeRow.push(getHLines(parser)); row = []; body.push(row); beginRow(); } else { throw new ParseError("Expected & or \\\\ or \\cr or " + endToken, parser.nextToken); } } // End cell group parser.gullet.endGroup(); // End array group defining \cr parser.gullet.endGroup(); return { type: "array", mode: parser.mode, body, cols, rowGaps, hLinesBeforeRow, envClasses, autoTag, scriptLevel, tags, labels, leqno, arraystretch, arraycolsep }; } // Decides on a scriptLevel for cells in an array according to whether the given // environment name starts with the letter 'd'. function dCellStyle(envName) { return envName.slice(0, 1) === "d" ? "display" : "text" } const alignMap = { c: "center ", l: "left ", r: "right " }; const glue = group => { const glueNode = new mathMLTree.MathNode("mtd", []); glueNode.style = { padding: "0", width: "50%" }; if (group.envClasses.includes("multline")) { glueNode.style.width = "7.5%"; } return glueNode }; const mathmlBuilder$9 = function(group, style) { const tbl = []; const numRows = group.body.length; const hlines = group.hLinesBeforeRow; for (let i = 0; i < numRows; i++) { const rw = group.body[i]; const row = []; const cellLevel = group.scriptLevel === "text" ? StyleLevel.TEXT : group.scriptLevel === "script" ? StyleLevel.SCRIPT : StyleLevel.DISPLAY; for (let j = 0; j < rw.length; j++) { const mtd = new mathMLTree.MathNode( "mtd", [buildGroup$1(rw[j], style.withLevel(cellLevel))] ); if (group.envClasses.includes("multline")) { const align = i === 0 ? "left" : i === numRows - 1 ? "right" : "center"; if (align !== "center") { mtd.classes.push("tml-" + align); } } row.push(mtd); } const numColumns = group.body[0].length; // Fill out a short row with empty <mtd> elements. for (let k = 0; k < numColumns - rw.length; k++) { row.push(new mathMLTree.MathNode("mtd", [], [], style)); } if (group.autoTag) { const tag = group.tags[i]; let tagElement; if (tag === true) { // automatic numbering tagElement = new mathMLTree.MathNode("mtext", [new Span(["tml-eqn"])]); } else if (tag === false) { // \nonumber/\notag or starred environment tagElement = new mathMLTree.MathNode("mtext", [], []); } else { // manual \tag tagElement = buildExpressionRow(tag[0].body, style.withLevel(cellLevel), true); tagElement = consolidateText(tagElement); tagElement.classes = ["tml-tag"]; } if (tagElement) { row.unshift(glue(group)); row.push(glue(group)); if (group.leqno) { row[0].children.push(tagElement); } else { row[row.length - 1].children.push(tagElement); } } } const mtr = new mathMLTree.MathNode("mtr", row, []); const label = group.labels.shift(); if (label && group.tags && group.tags[i]) { mtr.setAttribute("id", label); if (Array.isArray(group.tags[i])) { mtr.classes.push("tml-tageqn"); } } // Write horizontal rules if (i === 0 && hlines[0].length > 0) { if (hlines[0].length === 2) { mtr.children.forEach(cell => { cell.style.borderTop = "0.15em double"; }); } else { mtr.children.forEach(cell => { cell.style.borderTop = hlines[0][0] ? "0.06em dashed" : "0.06em solid"; }); } } if (hlines[i + 1].length > 0) { if (hlines[i + 1].length === 2) { mtr.children.forEach(cell => { cell.style.borderBottom = "0.15em double"; }); } else { mtr.children.forEach(cell => { cell.style.borderBottom = hlines[i + 1][0] ? "0.06em dashed" : "0.06em solid"; }); } } // Check for \hphantom \from \bordermatrix let mustSquashRow = true; for (let j = 0; j < mtr.children.length; j++) { const child = mtr.children[j].children[0]; if (!(child && child.type === "mpadded" && child.attributes.height === "0px")) { mustSquashRow = false; break } } if (mustSquashRow) { // All the cell contents are \hphantom. Squash the cell. for (let j = 0; j < mtr.children.length; j++) { mtr.children[j].style.display = "block"; // necessary in Firefox only mtr.children[j].style.height = "0"; // necessary in Firefox only mtr.children[j].style.paddingTop = "0"; mtr.children[j].style.paddingBottom = "0"; } } tbl.push(mtr); } if (group.arraystretch && group.arraystretch !== 1) { // In LaTeX, \arraystretch is a factor applied to a 12pt strut height. // It defines a baseline to baseline distance. // Here, we do an approximation of that approach. const pad = String(1.4 * group.arraystretch - 0.8) + "ex"; for (let i = 0; i < tbl.length; i++) { for (let j = 0; j < tbl[i].children.length; j++) { tbl[i].children[j].style.paddingTop = pad; tbl[i].children[j].style.paddingBottom = pad; } } } let sidePadding; let sidePadUnit; if (group.envClasses.length > 0) { sidePadding = group.envClasses.includes("abut") ? "0" : group.envClasses.includes("cases") ? "0" : group.envClasses.includes("small") ? "0.1389" : group.envClasses.includes("cd") ? "0.25" : "0.4"; // default side padding sidePadUnit = "em"; } if (group.arraycolsep) { const arraySidePad = calculateSize(group.arraycolsep, style); sidePadding = arraySidePad.number.toFixed(4); sidePadUnit = arraySidePad.unit; } if (sidePadding) { const numCols = tbl.length === 0 ? 0 : tbl[0].children.length; const sidePad = (j, hand) => { if (j === 0 && hand === 0) { return "0" } if (j === numCols - 1 && hand === 1) { return "0" } if (group.envClasses[0] !== "align") { return sidePadding } if (hand === 1) { return "0" } if (group.autoTag) { return (j % 2) ? "1" : "0" } else { return (j % 2) ? "0" : "1" } }; // Side padding for (let i = 0; i < tbl.length; i++) { for (let j = 0; j < tbl[i].children.length; j++) { tbl[i].children[j].style.paddingLeft = `${sidePad(j, 0)}${sidePadUnit}`; tbl[i].children[j].style.paddingRight = `${sidePad(j, 1)}${sidePadUnit}`; } } } if (group.envClasses.length === 0) { // Set zero padding on side of the matrix for (let i = 0; i < tbl.length; i++) { tbl[i].children[0].style.paddingLeft = "0em"; if (tbl[i].children.length === tbl[0].children.length) { tbl[i].children[tbl[i].children.length - 1].style.paddingRight = "0em"; } } } if (group.envClasses.length > 0) { // Justification const align = group.envClasses.includes("align") || group.envClasses.includes("alignat"); for (let i = 0; i < tbl.length; i++) { const row = tbl[i]; if (align) { for (let j = 0; j < row.children.length; j++) { // Chromium does not recognize text-align: left. Use -webkit- // TODO: Remove -webkit- when Chromium no longer needs it. row.children[j].classes = ["tml-" + (j % 2 ? "left" : "right")]; } if (group.autoTag) { const k = group.leqno ? 0 : row.children.length - 1; row.children[k].classes = []; // Default is center. } } if (row.children.length > 1 && group.envClasses.includes("cases")) { row.children[1].style.paddingLeft = "1em"; } if (group.envClasses.includes("cases") || group.envClasses.includes("subarray")) { for (const cell of row.children) { cell.classes.push("tml-left"); } } } } let table = new mathMLTree.MathNode("mtable", tbl); if (group.envClasses.length > 0) { // Top & bottom padding if (group.envClasses.includes("jot")) { table.classes.push("tml-jot"); } else if (group.envClasses.includes("small")) { table.classes.push("tml-small"); } } if (group.scriptLevel === "display") { table.setAttribute("displaystyle", "true"); } if (group.autoTag || group.envClasses.includes("multline")) { table.style.width = "100%"; } // Column separator lines and column alignment if (group.cols && group.cols.length > 0) { const cols = group.cols; let prevTypeWasAlign = false; let iStart = 0; let iEnd = cols.length; while (cols[iStart].type === "separator") { iStart += 1; } while (cols[iEnd - 1].type === "separator") { iEnd -= 1; } if (cols[0].type === "separator") { const sep = cols[1].type === "separator" ? "0.15em double" : cols[0].separator === "|" ? "0.06em solid " : "0.06em dashed "; for (const row of table.children) { row.children[0].style.borderLeft = sep; } } let iCol = group.autoTag ? 0 : -1; for (let i = iStart; i < iEnd; i++) { if (cols[i].type === "align") { const colAlign = alignMap[cols[i].align]; iCol += 1; for (const row of table.children) { if (colAlign.trim() !== "center" && iCol < row.children.length) { row.children[iCol].classes = ["tml-" + colAlign.trim()]; } } prevTypeWasAlign = true; } else if (cols[i].type === "separator") { // MathML accepts only single lines between cells. // So we read only the first of consecutive separators. if (prevTypeWasAlign) { const sep = cols[i + 1].type === "separator" ? "0.15em double" : cols[i].separator === "|" ? "0.06em solid" : "0.06em dashed"; for (const row of table.children) { if (iCol < row.children.length) { row.children[iCol].style.borderRight = sep; } } } prevTypeWasAlign = false; } } if (cols[cols.length - 1].type === "separator") { const sep = cols[cols.length - 2].type === "separator" ? "0.15em double" : cols[cols.length - 1].separator === "|" ? "0.06em solid" : "0.06em dashed"; for (const row of table.children) { row.children[row.children.length - 1].style.borderRight = sep; row.children[row.children.length - 1].style.paddingRight = "0.4em"; } } } if (group.envClasses.includes("small")) { // A small array. Wrap in scriptstyle. table = new mathMLTree.MathNode("mstyle", [table]); table.setAttribute("scriptlevel", "1"); } return table }; // Convenience function for align, align*, aligned, alignat, alignat*, alignedat, split. const alignedHandler = function(context, args) { if (context.envName.indexOf("ed") === -1) { validateAmsEnvironmentContext(context); } const isSplit = context.envName === "split"; const cols = []; const res = parseArray( context.parser, { cols, emptySingleRow: true, autoTag: isSplit ? undefined : getAutoTag(context.envName), envClasses: ["abut", "jot"], // set row spacing & provisional column spacing maxNumCols: context.envName === "split" ? 2 : undefined, leqno: context.parser.settings.leqno }, "display" ); // Determining number of columns. // 1. If the first argument is given, we use it as a number of columns, // and makes sure that each row doesn't exceed that number. // 2. Otherwise, just count number of columns = maximum number // of cells in each row ("aligned" mode -- isAligned will be true). // // At the same time, prepend empty group {} at beginning of every second // cell in each row (starting with second cell) so that operators become // binary. This behavior is implemented in amsmath's \start@aligned. let numMaths; let numCols = 0; const isAlignedAt = context.envName.indexOf("at") > -1; if (args[0] && isAlignedAt) { // alignat environment takes an argument w/ number of columns let arg0 = ""; for (let i = 0; i < args[0].body.length; i++) { const textord = assertNodeType(args[0].body[i], "textord"); arg0 += textord.text; } if (isNaN(arg0)) { throw new ParseError("The alignat enviroment requires a numeric first argument.") } numMaths = Number(arg0); numCols = numMaths * 2; } res.body.forEach(function(row) { if (isAlignedAt) { // Case 1 const curMaths = row.length / 2; if (numMaths < curMaths) { throw new ParseError( "Too many math in a row: " + `expected ${numMaths}, but got ${curMaths}`, row[0] ); } } else if (numCols < row.length) { // Case 2 numCols = row.length; } }); // Adjusting alignment. // In aligned mode, we add one \qquad between columns; // otherwise we add nothing. for (let i = 0; i < numCols; ++i) { let align = "r"; if (i % 2 === 1) { align = "l"; } cols[i] = { type: "align", align: align }; } if (context.envName === "split") ; else if (isAlignedAt) { res.envClasses.push("alignat"); // Sets justification } else { res.envClasses[0] = "align"; // Sets column spacing & justification } return res; }; // Arrays are part of LaTeX, defined in lttab.dtx so its documentation // is part of the source2e.pdf file of LaTeX2e source documentation. // {darray} is an {array} environment where cells are set in \displaystyle, // as defined in nccmath.sty. defineEnvironment({ type: "array", names: ["array", "darray"], props: { numArgs: 1 }, handler(context, args) { // Since no types are specified above, the two possibilities are // - The argument is wrapped in {} or [], in which case Parser's // parseGroup() returns an "ordgroup" wrapping some symbol node. // - The argument is a bare symbol node. const symNode = checkSymbolNodeType(args[0]); const colalign = symNode ? [args[0]] : assertNodeType(args[0], "ordgroup").body; const cols = colalign.map(function(nde) { const node = assertSymbolNodeType(nde); const ca = node.text; if ("lcr".indexOf(ca) !== -1) { return { type: "align", align: ca }; } else if (ca === "|") { return { type: "separator", separator: "|" }; } else if (ca === ":") { return { type: "separator", separator: ":" }; } throw new ParseError("Unknown column alignment: " + ca, nde); }); const [arraystretch, arraycolsep] = arrayGaps(context.parser.gullet.macros); const res = { cols, envClasses: ["array"], maxNumCols: cols.length, arraystretch, arraycolsep }; return parseArray(context.parser, res, dCellStyle(context.envName)); }, mathmlBuilder: mathmlBuilder$9 }); // The matrix environments of amsmath build on the array environment // of LaTeX, which is discussed above. // The mathtools package adds starred versions of the same environments. // These have an optional argument to choose left|center|right justification. defineEnvironment({ type: "array", names: [ "matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix", "matrix*", "pmatrix*", "bmatrix*", "Bmatrix*", "vmatrix*", "Vmatrix*" ], props: { numArgs: 0 }, handler(context) { const delimiters = { matrix: null, pmatrix: ["(", ")"], bmatrix: ["[", "]"], Bmatrix: ["\\{", "\\}"], vmatrix: ["|", "|"], Vmatrix: ["\\Vert", "\\Vert"] }[context.envName.replace("*", "")]; // \hskip -\arraycolsep in amsmath let colAlign = "c"; const payload = { envClasses: [], cols: [] }; if (context.envName.charAt(context.envName.length - 1) === "*") { // It's one of the mathtools starred functions. // Parse the optional alignment argument. const parser = context.parser; parser.consumeSpaces(); if (parser.fetch().text === "[") { parser.consume(); parser.consumeSpaces(); colAlign = parser.fetch().text; if ("lcr".indexOf(colAlign) === -1) { throw new ParseError("Expected l or c or r", parser.nextToken); } parser.consume(); parser.consumeSpaces(); parser.expect("]"); parser.consume(); payload.cols = []; } } const res = parseArray(context.parser, payload, "text"); res.cols = res.body.length > 0 ? new Array(res.body[0].length).fill({ type: "align", align: colAlign }) : []; const [arraystretch, arraycolsep] = arrayGaps(context.parser.gullet.macros); res.arraystretch = arraystretch; if (arraycolsep && !(arraycolsep === 6 && arraycolsep === "pt")) { res.arraycolsep = arraycolsep; } return delimiters ? { type: "leftright", mode: context.mode, body: [res], left: delimiters[0], right: delimiters[1], rightColor: undefined // \right uninfluenced by \color in array } : res; }, mathmlBuilder: mathmlBuilder$9 }); defineEnvironment({ type: "array", names: ["bordermatrix"], props: { numArgs: 0 }, handler(context) { const payload = { cols: [], envClasses: ["bordermatrix"] }; const res = parseArray(context.parser, payload, "text"); res.cols = res.body.length > 0 ? new Array(res.body[0].length).fill({ type: "align", align: "c" }) : []; res.envClasses = []; res.arraystretch = 1; if (context.envName === "matrix") { return res} return bordermatrixParseTree(res, context.delimiters) }, mathmlBuilder: mathmlBuilder$9 }); defineEnvironment({ type: "array", names: ["smallmatrix"], props: { numArgs: 0 }, handler(context) { const payload = { type: "small" }; const res = parseArray(context.parser, payload, "script"); res.envClasses = ["small"]; return res; }, mathmlBuilder: mathmlBuilder$9 }); defineEnvironment({ type: "array", names: ["subarray"], props: { numArgs: 1 }, handler(context, args) { // Parsing of {subarray} is similar to {array} const symNode = checkSymbolNodeType(args[0]); const colalign = symNode ? [args[0]] : assertNodeType(args[0], "ordgroup").body; const cols = colalign.map(function(nde) { const node = assertSymbolNodeType(nde); const ca = node.text; // {subarray} only recognizes "l" & "c" if ("lc".indexOf(ca) !== -1) { return { type: "align", align: ca }; } throw new ParseError("Unknown column alignment: " + ca, nde); }); if (cols.length > 1) { throw new ParseError("{subarray} can contain only one column"); } let res = { cols, envClasses: ["small"] }; res = parseArray(context.parser, res, "script"); if (res.body.length > 0 && res.body[0].length > 1) { throw new ParseError("{subarray} can contain only one column"); } return res; }, mathmlBuilder: mathmlBuilder$9 }); // A cases environment (in amsmath.sty) is almost equivalent to // \def // \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right. // {dcases} is a {cases} environment where cells are set in \displaystyle, // as defined in mathtools.sty. // {rcases} is another mathtools environment. It's brace is on the right side. defineEnvironment({ type: "array", names: ["cases", "dcases", "rcases", "drcases"], props: { numArgs: 0 }, handler(context) { const payload = { cols: [], envClasses: ["cases"] }; const res = parseArray(context.parser, payload, dCellStyle(context.envName)); return { type: "leftright", mode: context.mode, body: [res], left: context.envName.indexOf("r") > -1 ? "." : "\\{", right: context.envName.indexOf("r") > -1 ? "\\}" : ".", rightColor: undefined }; }, mathmlBuilder: mathmlBuilder$9 }); // In the align environment, one uses ampersands, &, to specify number of // columns in each row, and to locate spacing between each column. // align gets automatic numbering. align* and aligned do not. // The alignedat environment can be used in math mode. defineEnvironment({ type: "array", names: ["align", "align*", "aligned", "split"], props: { numArgs: 0 }, handler: alignedHandler, mathmlBuilder: mathmlBuilder$9 }); // alignat environment is like an align environment, but one must explicitly // specify maximum number of columns in each row, and can adjust where spacing occurs. defineEnvironment({ type: "array", names: ["alignat", "alignat*", "alignedat"], props: { numArgs: 1 }, handler: alignedHandler, mathmlBuilder: mathmlBuilder$9 }); // A gathered environment is like an array environment with one centered // column, but where rows are considered lines so get \jot line spacing // and contents are set in \displaystyle. defineEnvironment({ type: "array", names: ["gathered", "gather", "gather*"], props: { numArgs: 0 }, handler(context) { if (context.envName !== "gathered") { validateAmsEnvironmentContext(context); } const res = { cols: [], envClasses: ["abut", "jot"], autoTag: getAutoTag(context.envName), emptySingleRow: true, leqno: context.parser.settings.leqno }; return parseArray(context.parser, res, "display"); }, mathmlBuilder: mathmlBuilder$9 }); defineEnvironment({ type: "array", names: ["equation", "equation*"], props: { numArgs: 0 }, handler(context) { validateAmsEnvironmentContext(context); const res = { autoTag: getAutoTag(context.envName), emptySingleRow: true, singleRow: true, maxNumCols: 1, envClasses: ["align"], leqno: context.parser.settings.leqno }; return parseArray(context.parser, res, "display"); }, mathmlBuilder: mathmlBuilder$9 }); defineEnvironment({ type: "array", names: ["multline", "multline*"], props: { numArgs: 0 }, handler(context) { validateAmsEnvironmentContext(context); const res = { autoTag: context.envName === "multline", maxNumCols: 1, envClasses: ["jot", "multline"], leqno: context.parser.settings.leqno }; return parseArray(context.parser, res, "display"); }, mathmlBuilder: mathmlBuilder$9 }); defineEnvironment({ type: "array", names: ["CD"], props: { numArgs: 0 }, handler(context) { validateAmsEnvironmentContext(context); return parseCD(context.parser); }, mathmlBuilder: mathmlBuilder$9 }); // Catch \hline outside array environment defineFunction({ type: "text", // Doesn't matter what this is. names: ["\\hline", "\\hdashline"], props: { numArgs: 0, allowedInText: true, allowedInMath: true }, handler(context, args) { throw new ParseError(`${context.funcName} valid only within array environment`); } }); const environments = _environments; // \bordermatrix from TeXbook pp 177 & 361 // Optional argument from Herbert Voß, Math mode, p 20 // Ref: https://tug.ctan.org/obsolete/info/math/voss/mathmode/Mathmode.pdf defineFunction({ type: "bordermatrix", names: ["\\bordermatrix", "\\matrix"], props: { numArgs: 0, numOptionalArgs: 1 }, handler: ({ parser, funcName }, args, optArgs) => { // Find out if the author has defined custom delimiters let delimiters = ["(", ")"]; if (funcName === "\\bordermatrix" && optArgs[0] && optArgs[0].body) { const body = optArgs[0].body; if (body.length === 2 && body[0].type === "atom" && body[1].type === "atom") { if (body[0].family === "open" && body[1].family === "close") { delimiters = [body[0].text, body[1].text]; } } } // consume the opening brace parser.consumeSpaces(); parser.consume(); // Pass control to the environment handler in array.js. const env = environments["bordermatrix"]; const context = { mode: parser.mode, envName: funcName.slice(1), delimiters, parser }; const result = env.handler(context); parser.expect("}", true); return result } }); // \@char is an internal function that takes a grouped decimal argument like // {123} and converts into symbol with code 123. It is used by the *macro* // \char defined in macros.js. defineFunction({ type: "textord", names: ["\\@char"], props: { numArgs: 1, allowedInText: true }, handler({ parser, token }, args) { const arg = assertNodeType(args[0], "ordgroup"); const group = arg.body; let number = ""; for (let i = 0; i < group.length; i++) { const node = assertNodeType(group[i], "textord"); number += node.text; } const code = parseInt(number); if (isNaN(code)) { throw new ParseError(`\\@char has non-numeric argument ${number}`, token) } return { type: "textord", mode: parser.mode, text: String.fromCodePoint(code) } } }); // Helpers const htmlRegEx = /^(#[a-f0-9]{3}|#?[a-f0-9]{6})$/i; const htmlOrNameRegEx = /^(#[a-f0-9]{3}|#?[a-f0-9]{6}|[a-z]+)$/i; const RGBregEx = /^ *\d{1,3} *(?:, *\d{1,3} *){2}$/; const rgbRegEx = /^ *[10](?:\.\d*)? *(?:, *[10](?:\.\d*)? *){2}$/; const xcolorHtmlRegEx = /^[a-f0-9]{6}$/i; const toHex = num => { let str = num.toString(16); if (str.length === 1) { str = "0" + str; } return str }; // Colors from Tables 4.1 and 4.2 of the xcolor package. // Table 4.1 (lower case) RGB values are taken from chroma and xcolor.dtx. // Table 4.2 (Capitalizzed) values were sampled, because Chroma contains a unreliable // conversion from cmyk to RGB. See https://tex.stackexchange.com/a/537274. const xcolors = JSON.parse(`{ "Apricot": "#ffb484", "Aquamarine": "#08b4bc", "Bittersweet": "#c84c14", "blue": "#0000FF", "Blue": "#303494", "BlueGreen": "#08b4bc", "BlueViolet": "#503c94", "BrickRed": "#b8341c", "brown": "#BF8040", "Brown": "#802404", "BurntOrange": "#f8941c", "CadetBlue": "#78749c", "CarnationPink": "#f884b4", "Cerulean": "#08a4e4", "CornflowerBlue": "#40ace4", "cyan": "#00FFFF", "Cyan": "#08acec", "Dandelion": "#ffbc44", "darkgray": "#404040", "DarkOrchid": "#a8548c", "Emerald": "#08ac9c", "ForestGreen": "#089c54", "Fuchsia": "#90348c", "Goldenrod": "#ffdc44", "gray": "#808080", "Gray": "#98949c", "green": "#00FF00", "Green": "#08a44c", "GreenYellow": "#e0e474", "JungleGreen": "#08ac9c", "Lavender": "#f89cc4", "lightgray": "#c0c0c0", "lime": "#BFFF00", "LimeGreen": "#90c43c", "magenta": "#FF00FF", "Magenta": "#f0048c", "Mahogany": "#b0341c", "Maroon": "#b03434", "Melon": "#f89c7c", "MidnightBlue": "#086494", "Mulberry": "#b03c94", "NavyBlue": "#086cbc", "olive": "#7F7F00", "OliveGreen": "#407c34", "orange": "#FF8000", "Orange": "#f8843c", "OrangeRed": "#f0145c", "Orchid": "#b074ac", "Peach": "#f8945c", "Periwinkle": "#8074bc", "PineGreen": "#088c74", "pink": "#ff7f7f", "Plum": "#98248c", "ProcessBlue": "#08b4ec", "purple": "#BF0040", "Purple": "#a0449c", "RawSienna": "#983c04", "red": "#ff0000", "Red": "#f01c24", "RedOrange": "#f86434", "RedViolet": "#a0246c", "Rhodamine": "#f0549c", "Royallue": "#0874bc", "RoyalPurple": "#683c9c", "RubineRed": "#f0047c", "Salmon": "#f8948c", "SeaGreen": "#30bc9c", "Sepia": "#701404", "SkyBlue": "#48c4dc", "SpringGreen": "#c8dc64", "Tan": "#e09c74", "teal": "#007F7F", "TealBlue": "#08acb4", "Thistle": "#d884b4", "Turquoise": "#08b4cc", "violet": "#800080", "Violet": "#60449c", "VioletRed": "#f054a4", "WildStrawberry": "#f0246c", "yellow": "#FFFF00", "Yellow": "#fff404", "YellowGreen": "#98cc6c", "YellowOrange": "#ffa41c" }`); const colorFromSpec = (model, spec) => { let color = ""; if (model === "HTML") { if (!htmlRegEx.test(spec)) { throw new ParseError("Invalid HTML input.") } color = spec; } else if (model === "RGB") { if (!RGBregEx.test(spec)) { throw new ParseError("Invalid RGB input.") } spec.split(",").map(e => { color += toHex(Number(e.trim())); }); } else { if (!rgbRegEx.test(spec)) { throw new ParseError("Invalid rbg input.") } spec.split(",").map(e => { const num = Number(e.trim()); if (num > 1) { throw new ParseError("Color rgb input must be < 1.") } color += toHex(Number((num * 255).toFixed(0))); }); } if (color.charAt(0) !== "#") { color = "#" + color; } return color }; const validateColor = (color, macros, token) => { const macroName = `\\\\color@${color}`; // from \defineColor. const match = htmlOrNameRegEx.exec(color); if (!match) { throw new ParseError("Invalid color: '" + color + "'", token) } // We allow a 6-digit HTML color spec without a leading "#". // This follows the xcolor package's HTML color model. // Predefined color names are all missed by this RegEx pattern. if (xcolorHtmlRegEx.test(color)) { return "#" + color } else if (color.charAt(0) === "#") { return color } else if (macros.has(macroName)) { color = macros.get(macroName).tokens[0].text; } else if (xcolors[color]) { color = xcolors[color]; } return color }; const mathmlBuilder$8 = (group, style) => { // In LaTeX, color is not supposed to change the spacing of any node. // So instead of wrapping the group in an <mstyle>, we apply // the color individually to each node and return a document fragment. let expr = buildExpression(group.body, style.withColor(group.color)); if (expr.length === 0) { expr.push(new mathMLTree.MathNode("mrow")); } expr = expr.map(e => { e.style.color = group.color; return e }); return mathMLTree.newDocumentFragment(expr) }; defineFunction({ type: "color", names: ["\\textcolor"], props: { numArgs: 2, numOptionalArgs: 1, allowedInText: true, argTypes: ["raw", "raw", "original"] }, handler({ parser, token }, args, optArgs) { const model = optArgs[0] && assertNodeType(optArgs[0], "raw").string; let color = ""; if (model) { const spec = assertNodeType(args[0], "raw").string; color = colorFromSpec(model, spec); } else { color = validateColor(assertNodeType(args[0], "raw").string, parser.gullet.macros, token); } const body = args[1]; return { type: "color", mode: parser.mode, color, isTextColor: true, body: ordargument(body) } }, mathmlBuilder: mathmlBuilder$8 }); defineFunction({ type: "color", names: ["\\color"], props: { numArgs: 1, numOptionalArgs: 1, allowedInText: true, argTypes: ["raw", "raw"] }, handler({ parser, breakOnTokenText, token }, args, optArgs) { const model = optArgs[0] && assertNodeType(optArgs[0], "raw").string; let color = ""; if (model) { const spec = assertNodeType(args[0], "raw").string; color = colorFromSpec(model, spec); } else { color = validateColor(assertNodeType(args[0], "raw").string, parser.gullet.macros, token); } // Parse out the implicit body that should be colored. const body = parser.parseExpression(true, breakOnTokenText, true); return { type: "color", mode: parser.mode, color, isTextColor: false, body } }, mathmlBuilder: mathmlBuilder$8 }); defineFunction({ type: "color", names: ["\\definecolor"], props: { numArgs: 3, allowedInText: true, argTypes: ["raw", "raw", "raw"] }, handler({ parser, funcName, token }, args) { const name = assertNodeType(args[0], "raw").string; if (!/^[A-Za-z]+$/.test(name)) { throw new ParseError("Color name must be latin letters.", token) } const model = assertNodeType(args[1], "raw").string; if (!["HTML", "RGB", "rgb"].includes(model)) { throw new ParseError("Color model must be HTML, RGB, or rgb.", token) } const spec = assertNodeType(args[2], "raw").string; const color = colorFromSpec(model, spec); parser.gullet.macros.set(`\\\\color@${name}`, { tokens: [{ text: color }], numArgs: 0 }); return { type: "internal", mode: parser.mode } } // No mathmlBuilder. The point of \definecolor is to set a macro. }); // Row breaks within tabular environments, and line breaks at top level // \DeclareRobustCommand\\{...\@xnewline} defineFunction({ type: "cr", names: ["\\\\"], props: { numArgs: 0, numOptionalArgs: 0, allowedInText: true }, handler({ parser }, args, optArgs) { const size = parser.gullet.future().text === "[" ? parser.parseSizeGroup(true) : null; const newLine = !parser.settings.displayMode; return { type: "cr", mode: parser.mode, newLine, size: size && assertNodeType(size, "size").value } }, // The following builder is called only at the top level, // not within tabular/array environments. mathmlBuilder(group, style) { // MathML 3.0 calls for newline to occur in an <mo> or an <mspace>. // Ref: https://www.w3.org/TR/MathML3/chapter3.html#presm.linebreaking const node = new mathMLTree.MathNode("mo"); if (group.newLine) { node.setAttribute("linebreak", "newline"); if (group.size) { const size = calculateSize(group.size, style); node.setAttribute("height", size.number + size.unit); } } return node } }); const globalMap = { "\\global": "\\global", "\\long": "\\\\globallong", "\\\\globallong": "\\\\globallong", "\\def": "\\gdef", "\\gdef": "\\gdef", "\\edef": "\\xdef", "\\xdef": "\\xdef", "\\let": "\\\\globallet", "\\futurelet": "\\\\globalfuture" }; const checkControlSequence = (tok) => { const name = tok.text; if (/^(?:[\\{}$&#^_]|EOF)$/.test(name)) { throw new ParseError("Expected a control sequence", tok); } return name; }; const getRHS = (parser) => { let tok = parser.gullet.popToken(); if (tok.text === "=") { // consume optional equals tok = parser.gullet.popToken(); if (tok.text === " ") { // consume one optional space tok = parser.gullet.popToken(); } } return tok; }; const letCommand = (parser, name, tok, global) => { let macro = parser.gullet.macros.get(tok.text); if (macro == null) { // don't expand it later even if a macro with the same name is defined // e.g., \let\foo=\frac \def\frac{\relax} \frac12 tok.noexpand = true; macro = { tokens: [tok], numArgs: 0, // reproduce the same behavior in expansion unexpandable: !parser.gullet.isExpandable(tok.text) }; } parser.gullet.macros.set(name, macro, global); }; // <assignment> -> <non-macro assignment>|<macro assignment> // <non-macro assignment> -> <simple assignment>|\global<non-macro assignment> // <macro assignment> -> <definition>|<prefix><macro assignment> // <prefix> -> \global|\long|\outer defineFunction({ type: "internal", names: [ "\\global", "\\long", "\\\\globallong" // can’t be entered directly ], props: { numArgs: 0, allowedInText: true }, handler({ parser, funcName }) { parser.consumeSpaces(); const token = parser.fetch(); if (globalMap[token.text]) { // Temml doesn't have \par, so ignore \long if (funcName === "\\global" || funcName === "\\\\globallong") { token.text = globalMap[token.text]; } return assertNodeType(parser.parseFunction(), "internal"); } throw new ParseError(`Invalid token after macro prefix`, token); } }); // Basic support for macro definitions: \def, \gdef, \edef, \xdef // <definition> -> <def><control sequence><definition text> // <def> -> \def|\gdef|\edef|\xdef // <definition text> -> <parameter text><left brace><balanced text><right brace> defineFunction({ type: "internal", names: ["\\def", "\\gdef", "\\edef", "\\xdef"], props: { numArgs: 0, allowedInText: true, primitive: true }, handler({ parser, funcName }) { let tok = parser.gullet.popToken(); const name = tok.text; if (/^(?:[\\{}$&#^_]|EOF)$/.test(name)) { throw new ParseError("Expected a control sequence", tok); } let numArgs = 0; let insert; const delimiters = [[]]; // <parameter text> contains no braces while (parser.gullet.future().text !== "{") { tok = parser.gullet.popToken(); if (tok.text === "#") { // If the very last character of the <parameter text> is #, so that // this # is immediately followed by {, TeX will behave as if the { // had been inserted at the right end of both the parameter text // and the replacement text. if (parser.gullet.future().text === "{") { insert = parser.gullet.future(); delimiters[numArgs].push("{"); break; } // A parameter, the first appearance of # must be followed by 1, // the next by 2, and so on; up to nine #’s are allowed tok = parser.gullet.popToken(); if (!/^[1-9]$/.test(tok.text)) { throw new ParseError(`Invalid argument number "${tok.text}"`); } if (parseInt(tok.text) !== numArgs + 1) { throw new ParseError(`Argument number "${tok.text}" out of order`); } numArgs++; delimiters.push([]); } else if (tok.text === "EOF") { throw new ParseError("Expected a macro definition"); } else { delimiters[numArgs].push(tok.text); } } // replacement text, enclosed in '{' and '}' and properly nested let { tokens } = parser.gullet.consumeArg(); if (insert) { tokens.unshift(insert); } if (funcName === "\\edef" || funcName === "\\xdef") { tokens = parser.gullet.expandTokens(tokens); if (tokens.length > parser.gullet.settings.maxExpand) { throw new ParseError("Too many expansions in an " + funcName); } tokens.reverse(); // to fit in with stack order } // Final arg is the expansion of the macro parser.gullet.macros.set( name, { tokens, numArgs, delimiters }, funcName === globalMap[funcName] ); return { type: "internal", mode: parser.mode }; } }); // <simple assignment> -> <let assignment> // <let assignment> -> \futurelet<control sequence><token><token> // | \let<control sequence><equals><one optional space><token> // <equals> -> <optional spaces>|<optional spaces>= defineFunction({ type: "internal", names: [ "\\let", "\\\\globallet" // can’t be entered directly ], props: { numArgs: 0, allowedInText: true, primitive: true }, handler({ parser, funcName }) { const name = checkControlSequence(parser.gullet.popToken()); parser.gullet.consumeSpaces(); const tok = getRHS(parser); letCommand(parser, name, tok, funcName === "\\\\globallet"); return { type: "internal", mode: parser.mode }; } }); // ref: https://www.tug.org/TUGboat/tb09-3/tb22bechtolsheim.pdf defineFunction({ type: "internal", names: [ "\\futurelet", "\\\\globalfuture" // can’t be entered directly ], props: { numArgs: 0, allowedInText: true, primitive: true }, handler({ parser, funcName }) { const name = checkControlSequence(parser.gullet.popToken()); const middle = parser.gullet.popToken(); const tok = parser.gullet.popToken(); letCommand(parser, name, tok, funcName === "\\\\globalfuture"); parser.gullet.pushToken(tok); parser.gullet.pushToken(middle); return { type: "internal", mode: parser.mode }; } }); defineFunction({ type: "internal", names: ["\\newcommand", "\\renewcommand", "\\providecommand"], props: { numArgs: 0, allowedInText: true, primitive: true }, handler({ parser, funcName }) { let name = ""; const tok = parser.gullet.popToken(); if (tok.text === "{") { name = checkControlSequence(parser.gullet.popToken()); parser.gullet.popToken(); } else { name = checkControlSequence(tok); } const exists = parser.gullet.isDefined(name); if (exists && funcName === "\\newcommand") { throw new ParseError( `\\newcommand{${name}} attempting to redefine ${name}; use \\renewcommand` ); } if (!exists && funcName === "\\renewcommand") { throw new ParseError( `\\renewcommand{${name}} when command ${name} does not yet exist; use \\newcommand` ); } let numArgs = 0; if (parser.gullet.future().text === "[") { let tok = parser.gullet.popToken(); tok = parser.gullet.popToken(); if (!/^[0-9]$/.test(tok.text)) { throw new ParseError(`Invalid number of arguments: "${tok.text}"`); } numArgs = parseInt(tok.text); tok = parser.gullet.popToken(); if (tok.text !== "]") { throw new ParseError(`Invalid argument "${tok.text}"`); } } // replacement text, enclosed in '{' and '}' and properly nested const { tokens } = parser.gullet.consumeArg(); if (!(funcName === "\\providecommand" && parser.gullet.macros.has(name))) { // Ignore \providecommand parser.gullet.macros.set( name, { tokens, numArgs } ); } return { type: "internal", mode: parser.mode }; } }); // Extra data needed for the delimiter handler down below const delimiterSizes = { "\\bigl": { mclass: "mopen", size: 1 }, "\\Bigl": { mclass: "mopen", size: 2 }, "\\biggl": { mclass: "mopen", size: 3 }, "\\Biggl": { mclass: "mopen", size: 4 }, "\\bigr": { mclass: "mclose", size: 1 }, "\\Bigr": { mclass: "mclose", size: 2 }, "\\biggr": { mclass: "mclose", size: 3 }, "\\Biggr": { mclass: "mclose", size: 4 }, "\\bigm": { mclass: "mrel", size: 1 }, "\\Bigm": { mclass: "mrel", size: 2 }, "\\biggm": { mclass: "mrel", size: 3 }, "\\Biggm": { mclass: "mrel", size: 4 }, "\\big": { mclass: "mord", size: 1 }, "\\Big": { mclass: "mord", size: 2 }, "\\bigg": { mclass: "mord", size: 3 }, "\\Bigg": { mclass: "mord", size: 4 } }; const delimiters = [ "(", "\\lparen", ")", "\\rparen", "[", "\\lbrack", "]", "\\rbrack", "\\{", "\\lbrace", "\\}", "\\rbrace", "⦇", "\\llparenthesis", "⦈", "\\rrparenthesis", "\\lfloor", "\\rfloor", "\u230a", "\u230b", "\\lceil", "\\rceil", "\u2308", "\u2309", "<", ">", "\\langle", "\u27e8", "\\rangle", "\u27e9", "\\lAngle", "\u27ea", "\\rAngle", "\u27eb", "\\llangle", "⦉", "\\rrangle", "⦊", "\\lt", "\\gt", "\\lvert", "\\rvert", "\\lVert", "\\rVert", "\\lgroup", "\\rgroup", "\u27ee", "\u27ef", "\\lmoustache", "\\rmoustache", "\u23b0", "\u23b1", "\\llbracket", "\\rrbracket", "\u27e6", "\u27e6", "\\lBrace", "\\rBrace", "\u2983", "\u2984", "/", "\\backslash", "|", "\\vert", "\\|", "\\Vert", "\u2016", "\\uparrow", "\\Uparrow", "\\downarrow", "\\Downarrow", "\\updownarrow", "\\Updownarrow", "." ]; // Export isDelimiter for benefit of parser. const dels = ["}", "\\left", "\\middle", "\\right"]; const isDelimiter = str => str.length > 0 && (delimiters.includes(str) || delimiterSizes[str] || dels.includes(str)); // Metrics of the different sizes. Found by looking at TeX's output of // $\bigl| // \Bigl| \biggl| \Biggl| \showlists$ // Used to create stacked delimiters of appropriate sizes in makeSizedDelim. const sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0]; // Delimiter functions function checkDelimiter(delim, context) { if (delim.type === "ordgroup" && delim.body.length === 1) { delim = delim.body[0]; // Unwrap the braces } const symDelim = checkSymbolNodeType(delim); if (symDelim && delimiters.includes(symDelim.text)) { // If a character is not in the MathML operator dictionary, it will not stretch. // Replace such characters w/characters that will stretch. if (["<", "\\lt"].includes(symDelim.text)) { symDelim.text = "⟨"; } if ([">", "\\gt"].includes(symDelim.text)) { symDelim.text = "⟩"; } return symDelim; } else if (symDelim) { throw new ParseError(`Invalid delimiter '${symDelim.text}' after '${context.funcName}'`, delim); } else { throw new ParseError(`Invalid delimiter type '${delim.type}'`, delim); } } // / \ const needExplicitStretch = ["\u002F", "\u005C", "\\backslash", "\\vert", "|"]; defineFunction({ type: "delimsizing", names: [ "\\bigl", "\\Bigl", "\\biggl", "\\Biggl", "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", "\\big", "\\Big", "\\bigg", "\\Bigg" ], props: { numArgs: 1, argTypes: ["primitive"] }, handler: (context, args) => { const delim = checkDelimiter(args[0], context); const delimNode = { type: "delimsizing", mode: context.parser.mode, size: delimiterSizes[context.funcName].size, mclass: delimiterSizes[context.funcName].mclass, delim: delim.text }; const nextToken = context.parser.fetch().text; if (nextToken !== "^" && nextToken !== "_") { return delimNode } else { // Chromium mis-renders a sized delim if it is the base of a supsub. // So wrap it in a ordgroup. return { type: "ordgroup", mode: "math", body: [delimNode, { type: "ordgroup", mode: "math", body: [] }] } } }, mathmlBuilder: (group) => { const children = []; if (group.delim === ".") { group.delim = ""; } children.push(makeText(group.delim, group.mode)); const node = new mathMLTree.MathNode("mo", children); if (group.mclass === "mopen" || group.mclass === "mclose") { // Only some of the delimsizing functions act as fences, and they // return "mopen" or "mclose" mclass. node.setAttribute("fence", "true"); } else { // Explicitly disable fencing if it's not a fence, to override the // defaults. node.setAttribute("fence", "false"); } if (needExplicitStretch.includes(group.delim) || group.delim.indexOf("arrow") > -1) { // We have to explicitly set stretchy to true. node.setAttribute("stretchy", "true"); } node.setAttribute("symmetric", "true"); // Needed for tall arrows in Firefox. node.setAttribute("minsize", sizeToMaxHeight[group.size] + "em"); node.setAttribute("maxsize", sizeToMaxHeight[group.size] + "em"); return node; } }); function assertParsed(group) { if (!group.body) { throw new Error("Bug: The leftright ParseNode wasn't fully parsed."); } } defineFunction({ type: "leftright-right", names: ["\\right"], props: { numArgs: 1, argTypes: ["primitive"] }, handler: (context, args) => { return { type: "leftright-right", mode: context.parser.mode, delim: checkDelimiter(args[0], context).text }; } }); defineFunction({ type: "leftright", names: ["\\left"], props: { numArgs: 1, argTypes: ["primitive"] }, handler: (context, args) => { const delim = checkDelimiter(args[0], context); const parser = context.parser; // Parse out the implicit body ++parser.leftrightDepth; // parseExpression stops before '\\right' or `\\middle` let body = parser.parseExpression(false, null, true); let nextToken = parser.fetch(); while (nextToken.text === "\\middle") { // `\middle`, from the ε-TeX package, ends one group and starts another group. // We had to parse this expression with `breakOnMiddle` enabled in order // to get TeX-compliant parsing of \over. // But we do not want, at this point, to end on \middle, so continue // to parse until we fetch a `\right`. parser.consume(); const middle = parser.fetch().text; if (!symbols.math[middle]) { throw new ParseError(`Invalid delimiter '${middle}' after '\\middle'`); } checkDelimiter({ type: "atom", mode: "math", text: middle }, { funcName: "\\middle" }); body.push({ type: "middle", mode: "math", delim: middle }); parser.consume(); body = body.concat(parser.parseExpression(false, null, true)); nextToken = parser.fetch(); } --parser.leftrightDepth; // Check the next token parser.expect("\\right", false); const right = assertNodeType(parser.parseFunction(), "leftright-right"); return { type: "leftright", mode: parser.mode, body, left: delim.text, right: right.delim }; }, mathmlBuilder: (group, style) => { assertParsed(group); const inner = buildExpression(group.body, style); if (group.left === ".") { group.left = ""; } const leftNode = new mathMLTree.MathNode("mo", [makeText(group.left, group.mode)]); leftNode.setAttribute("fence", "true"); leftNode.setAttribute("form", "prefix"); if (group.left === "/" || group.left === "\u005C" || group.left.indexOf("arrow") > -1) { leftNode.setAttribute("stretchy", "true"); } inner.unshift(leftNode); if (group.right === ".") { group.right = ""; } const rightNode = new mathMLTree.MathNode("mo", [makeText(group.right, group.mode)]); rightNode.setAttribute("fence", "true"); rightNode.setAttribute("form", "postfix"); if (group.right === "\u2216" || group.right.indexOf("arrow") > -1) { rightNode.setAttribute("stretchy", "true"); } if (group.body.length > 0) { const lastElement = group.body[group.body.length - 1]; if (lastElement.type === "color" && !lastElement.isTextColor) { // \color is a switch. If the last element is of type "color" then // the user set the \color switch and left it on. // A \right delimiter turns the switch off, but the delimiter itself gets the color. rightNode.setAttribute("mathcolor", lastElement.color); } } inner.push(rightNode); return makeRow(inner); } }); defineFunction({ type: "middle", names: ["\\middle"], props: { numArgs: 1, argTypes: ["primitive"] }, handler: (context, args) => { const delim = checkDelimiter(args[0], context); if (!context.parser.leftrightDepth) { throw new ParseError("\\middle without preceding \\left", delim); } return { type: "middle", mode: context.parser.mode, delim: delim.text }; }, mathmlBuilder: (group, style) => { const textNode = makeText(group.delim, group.mode); const middleNode = new mathMLTree.MathNode("mo", [textNode]); middleNode.setAttribute("fence", "true"); if (group.delim.indexOf("arrow") > -1) { middleNode.setAttribute("stretchy", "true"); } // The next line is not semantically correct, but // Chromium fails to stretch if it is not there. middleNode.setAttribute("form", "prefix"); // MathML gives 5/18em spacing to each <mo> element. // \middle should get delimiter spacing instead. middleNode.setAttribute("lspace", "0.05em"); middleNode.setAttribute("rspace", "0.05em"); return middleNode; } }); const mathmlBuilder$7 = (group, style) => { const node = new mathMLTree.MathNode("menclose", [buildGroup$1(group.body, style)]); switch (group.label) { case "\\overline": node.setAttribute("notation", "top"); // for Firefox & WebKit node.classes.push("tml-overline"); // for Chromium break case "\\underline": node.setAttribute("notation", "bottom"); node.classes.push("tml-underline"); break case "\\cancel": node.setAttribute("notation", "updiagonalstrike"); node.children.push(new mathMLTree.MathNode("mrow", [], ["tml-cancel", "upstrike"])); break case "\\bcancel": node.setAttribute("notation", "downdiagonalstrike"); node.children.push(new mathMLTree.MathNode("mrow", [], ["tml-cancel", "downstrike"])); break case "\\sout": node.setAttribute("notation", "horizontalstrike"); node.children.push(new mathMLTree.MathNode("mrow", [], ["tml-cancel", "sout"])); break case "\\xcancel": node.setAttribute("notation", "updiagonalstrike downdiagonalstrike"); node.classes.push("tml-xcancel"); break case "\\longdiv": node.setAttribute("notation", "longdiv"); node.classes.push("longdiv-top"); node.children.push(new mathMLTree.MathNode("mrow", [], ["longdiv-arc"])); break case "\\phase": node.setAttribute("notation", "phasorangle"); node.classes.push("phasor-bottom"); node.children.push(new mathMLTree.MathNode("mrow", [], ["phasor-angle"])); break case "\\textcircled": node.setAttribute("notation", "circle"); node.classes.push("circle-pad"); node.children.push(new mathMLTree.MathNode("mrow", [], ["textcircle"])); break case "\\angl": node.setAttribute("notation", "actuarial"); node.classes.push("actuarial"); break case "\\boxed": // \newcommand{\boxed}[1]{\fbox{\m@th$\displaystyle#1$}} from amsmath.sty node.setAttribute("notation", "box"); node.style.padding = "3pt"; node.style.border = "1px solid"; node.setAttribute("scriptlevel", "0"); node.setAttribute("displaystyle", "true"); break case "\\fbox": node.setAttribute("notation", "box"); node.classes.push("tml-fbox"); break case "\\fcolorbox": case "\\colorbox": { // <menclose> doesn't have a good notation option for \colorbox. // So use <mpadded> instead. Set some attributes that come // included with <menclose>. //const fboxsep = 3; // 3 pt from LaTeX source2e //node.setAttribute("height", `+${2 * fboxsep}pt`) //node.setAttribute("voffset", `${fboxsep}pt`) node.style.padding = "3pt"; if (group.label === "\\fcolorbox") { node.style.border = "0.0667em solid " + String(group.borderColor); } break } } if (group.backgroundColor) { node.setAttribute("mathbackground", group.backgroundColor); } return node; }; defineFunction({ type: "enclose", names: ["\\colorbox"], props: { numArgs: 2, numOptionalArgs: 1, allowedInText: true, argTypes: ["raw", "raw", "text"] }, handler({ parser, funcName }, args, optArgs) { const model = optArgs[0] && assertNodeType(optArgs[0], "raw").string; let color = ""; if (model) { const spec = assertNodeType(args[0], "raw").string; color = colorFromSpec(model, spec); } else { color = validateColor(assertNodeType(args[0], "raw").string, parser.gullet.macros); } const body = args[1]; return { type: "enclose", mode: parser.mode, label: funcName, backgroundColor: color, body }; }, mathmlBuilder: mathmlBuilder$7 }); defineFunction({ type: "enclose", names: ["\\fcolorbox"], props: { numArgs: 3, numOptionalArgs: 1, allowedInText: true, argTypes: ["raw", "raw", "raw", "text"] }, handler({ parser, funcName }, args, optArgs) { const model = optArgs[0] && assertNodeType(optArgs[0], "raw").string; let borderColor = ""; let backgroundColor; if (model) { const borderSpec = assertNodeType(args[0], "raw").string; const backgroundSpec = assertNodeType(args[0], "raw").string; borderColor = colorFromSpec(model, borderSpec); backgroundColor = colorFromSpec(model, backgroundSpec); } else { borderColor = validateColor(assertNodeType(args[0], "raw").string, parser.gullet.macros); backgroundColor = validateColor(assertNodeType(args[1], "raw").string, parser.gullet.macros); } const body = args[2]; return { type: "enclose", mode: parser.mode, label: funcName, backgroundColor, borderColor, body }; }, mathmlBuilder: mathmlBuilder$7 }); defineFunction({ type: "enclose", names: ["\\fbox"], props: { numArgs: 1, argTypes: ["hbox"], allowedInText: true }, handler({ parser }, args) { return { type: "enclose", mode: parser.mode, label: "\\fbox", body: args[0] }; } }); defineFunction({ type: "enclose", names: ["\\angl", "\\cancel", "\\bcancel", "\\xcancel", "\\sout", "\\overline", "\\boxed", "\\longdiv", "\\phase"], props: { numArgs: 1 }, handler({ parser, funcName }, args) { const body = args[0]; return { type: "enclose", mode: parser.mode, label: funcName, body }; }, mathmlBuilder: mathmlBuilder$7 }); defineFunction({ type: "enclose", names: ["\\underline"], props: { numArgs: 1, allowedInText: true }, handler({ parser, funcName }, args) { const body = args[0]; return { type: "enclose", mode: parser.mode, label: funcName, body }; }, mathmlBuilder: mathmlBuilder$7 }); defineFunction({ type: "enclose", names: ["\\textcircled"], props: { numArgs: 1, argTypes: ["text"], allowedInArgument: true, allowedInText: true }, handler({ parser, funcName }, args) { const body = args[0]; return { type: "enclose", mode: parser.mode, label: funcName, body }; }, mathmlBuilder: mathmlBuilder$7 }); // Environment delimiters. HTML/MathML rendering is defined in the corresponding // defineEnvironment definitions. defineFunction({ type: "environment", names: ["\\begin", "\\end"], props: { numArgs: 1, argTypes: ["text"] }, handler({ parser, funcName }, args) { const nameGroup = args[0]; if (nameGroup.type !== "ordgroup") { throw new ParseError("Invalid environment name", nameGroup); } let envName = ""; for (let i = 0; i < nameGroup.body.length; ++i) { envName += assertNodeType(nameGroup.body[i], "textord").text; } if (funcName === "\\begin") { // begin...end is similar to left...right if (!Object.prototype.hasOwnProperty.call(environments, envName )) { throw new ParseError("No such environment: " + envName, nameGroup); } // Build the environment object. Arguments and other information will // be made available to the begin and end methods using properties. const env = environments[envName]; const { args, optArgs } = parser.parseArguments("\\begin{" + envName + "}", env); const context = { mode: parser.mode, envName, parser }; const result = env.handler(context, args, optArgs); parser.expect("\\end", false); const endNameToken = parser.nextToken; const end = assertNodeType(parser.parseFunction(), "environment"); if (end.name !== envName) { throw new ParseError( `Mismatch: \\begin{${envName}} matched by \\end{${end.name}}`, endNameToken ); } return result; } return { type: "environment", mode: parser.mode, name: envName, nameGroup }; } }); defineFunction({ type: "envTag", names: ["\\env@tag"], props: { numArgs: 1, argTypes: ["math"] }, handler({ parser }, args) { return { type: "envTag", mode: parser.mode, body: args[0] }; }, mathmlBuilder(group, style) { return new mathMLTree.MathNode("mrow"); } }); defineFunction({ type: "noTag", names: ["\\env@notag"], props: { numArgs: 0 }, handler({ parser }) { return { type: "noTag", mode: parser.mode }; }, mathmlBuilder(group, style) { return new mathMLTree.MathNode("mrow"); } }); const isLongVariableName = (group, font) => { if (font !== "mathrm" || group.body.type !== "ordgroup" || group.body.body.length === 1) { return false } if (group.body.body[0].type !== "mathord") { return false } for (let i = 1; i < group.body.body.length; i++) { const parseNodeType = group.body.body[i].type; if (!(parseNodeType === "mathord" || (parseNodeType === "textord" && !isNaN(group.body.body[i].text)))) { return false } } return true }; const mathmlBuilder$6 = (group, style) => { const font = group.font; const newStyle = style.withFont(font); const mathGroup = buildGroup$1(group.body, newStyle); if (mathGroup.children.length === 0) { return mathGroup } // empty group, e.g., \mathrm{} if (font === "boldsymbol" && ["mo", "mpadded", "mrow"].includes(mathGroup.type)) { mathGroup.style.fontWeight = "bold"; return mathGroup } // Check if it is possible to consolidate elements into a single <mi> element. if (isLongVariableName(group, font)) { // This is a \mathrm{…} group. It gets special treatment because symbolsOrd.js // wraps <mi> elements with <mpadded>s to work around a Firefox bug. const mi = mathGroup.children[0].children[0].children ? mathGroup.children[0].children[0] : mathGroup.children[0]; delete mi.attributes.mathvariant; for (let i = 1; i < mathGroup.children.length; i++) { mi.children[0].text += mathGroup.children[i].children[0].children ? mathGroup.children[i].children[0].children[0].text : mathGroup.children[i].children[0].text; } // Wrap in a <mpadded> to prevent the same Firefox bug. const mpadded = new mathMLTree.MathNode("mpadded", [mi]); mpadded.setAttribute("lspace", "0"); return mpadded } let canConsolidate = mathGroup.children[0].type === "mo"; for (let i = 1; i < mathGroup.children.length; i++) { if (mathGroup.children[i].type === "mo" && font === "boldsymbol") { mathGroup.children[i].style.fontWeight = "bold"; } if (mathGroup.children[i].type !== "mi") { canConsolidate = false; } const localVariant = mathGroup.children[i].attributes && mathGroup.children[i].attributes.mathvariant || ""; if (localVariant !== "normal") { canConsolidate = false; } } if (!canConsolidate) { return mathGroup } // Consolidate the <mi> elements. const mi = mathGroup.children[0]; for (let i = 1; i < mathGroup.children.length; i++) { mi.children.push(mathGroup.children[i].children[0]); } if (mi.attributes.mathvariant && mi.attributes.mathvariant === "normal") { // Workaround for a Firefox bug that renders spurious space around // a <mi mathvariant="normal"> // Ref: https://bugs.webkit.org/show_bug.cgi?id=129097 // We insert a text node that contains a zero-width space and wrap in an mrow. // TODO: Get rid of this <mi> workaround when the Firefox bug is fixed. const bogus = new mathMLTree.MathNode("mtext", new mathMLTree.TextNode("\u200b")); return new mathMLTree.MathNode("mrow", [bogus, mi]) } return mi }; const fontAliases = { "\\Bbb": "\\mathbb", "\\bold": "\\mathbf", "\\frak": "\\mathfrak", "\\bm": "\\boldsymbol" }; defineFunction({ type: "font", names: [ // styles "\\mathrm", "\\mathit", "\\mathbf", "\\mathnormal", "\\up@greek", "\\boldsymbol", // families "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf", "\\mathsfit", "\\mathtt", // aliases "\\Bbb", "\\bm", "\\bold", "\\frak" ], props: { numArgs: 1, allowedInArgument: true }, handler: ({ parser, funcName }, args) => { const body = normalizeArgument(args[0]); let func = funcName; if (func in fontAliases) { func = fontAliases[func]; } return { type: "font", mode: parser.mode, font: func.slice(1), body }; }, mathmlBuilder: mathmlBuilder$6 }); // Old font changing functions defineFunction({ type: "font", names: ["\\rm", "\\sf", "\\tt", "\\bf", "\\it", "\\cal"], props: { numArgs: 0, allowedInText: true }, handler: ({ parser, funcName, breakOnTokenText }, args) => { const { mode } = parser; const body = parser.parseExpression(true, breakOnTokenText, true); const fontStyle = `math${funcName.slice(1)}`; return { type: "font", mode: mode, font: fontStyle, body: { type: "ordgroup", mode: parser.mode, body } }; }, mathmlBuilder: mathmlBuilder$6 }); const stylArray = ["display", "text", "script", "scriptscript"]; const scriptLevel = { auto: -1, display: 0, text: 0, script: 1, scriptscript: 2 }; const mathmlBuilder$5 = (group, style) => { // Track the scriptLevel of the numerator and denominator. // We may need that info for \mathchoice or for adjusting em dimensions. const childOptions = group.scriptLevel === "auto" ? style.incrementLevel() : group.scriptLevel === "display" ? style.withLevel(StyleLevel.TEXT) : group.scriptLevel === "text" ? style.withLevel(StyleLevel.SCRIPT) : style.withLevel(StyleLevel.SCRIPTSCRIPT); // Chromium (wrongly) continues to shrink fractions beyond scriptscriptlevel. // So we check for levels that Chromium shrinks too small. // If necessary, set an explicit fraction depth. const numer = buildGroup$1(group.numer, childOptions); const denom = buildGroup$1(group.denom, childOptions); if (style.level === 3) { numer.style.mathDepth = "2"; numer.setAttribute("scriptlevel", "2"); denom.style.mathDepth = "2"; denom.setAttribute("scriptlevel", "2"); } let node = new mathMLTree.MathNode("mfrac", [numer, denom]); if (!group.hasBarLine) { node.setAttribute("linethickness", "0px"); } else if (group.barSize) { const ruleWidth = calculateSize(group.barSize, style); node.setAttribute("linethickness", ruleWidth.number + ruleWidth.unit); } if (group.leftDelim != null || group.rightDelim != null) { const withDelims = []; if (group.leftDelim != null) { const leftOp = new mathMLTree.MathNode("mo", [ new mathMLTree.TextNode(group.leftDelim.replace("\\", "")) ]); leftOp.setAttribute("fence", "true"); withDelims.push(leftOp); } withDelims.push(node); if (group.rightDelim != null) { const rightOp = new mathMLTree.MathNode("mo", [ new mathMLTree.TextNode(group.rightDelim.replace("\\", "")) ]); rightOp.setAttribute("fence", "true"); withDelims.push(rightOp); } node = makeRow(withDelims); } if (group.scriptLevel !== "auto") { node = new mathMLTree.MathNode("mstyle", [node]); node.setAttribute("displaystyle", String(group.scriptLevel === "display")); node.setAttribute("scriptlevel", scriptLevel[group.scriptLevel]); } return node; }; defineFunction({ type: "genfrac", names: [ "\\dfrac", "\\frac", "\\tfrac", "\\dbinom", "\\binom", "\\tbinom", "\\\\atopfrac", // can’t be entered directly "\\\\bracefrac", "\\\\brackfrac" // ditto ], props: { numArgs: 2, allowedInArgument: true }, handler: ({ parser, funcName }, args) => { const numer = args[0]; const denom = args[1]; let hasBarLine = false; let leftDelim = null; let rightDelim = null; let scriptLevel = "auto"; switch (funcName) { case "\\dfrac": case "\\frac": case "\\tfrac": hasBarLine = true; break; case "\\\\atopfrac": hasBarLine = false; break; case "\\dbinom": case "\\binom": case "\\tbinom": leftDelim = "("; rightDelim = ")"; break; case "\\\\bracefrac": leftDelim = "\\{"; rightDelim = "\\}"; break; case "\\\\brackfrac": leftDelim = "["; rightDelim = "]"; break; default: throw new Error("Unrecognized genfrac command"); } switch (funcName) { case "\\dfrac": case "\\dbinom": scriptLevel = "display"; break; case "\\tfrac": case "\\tbinom": scriptLevel = "text"; break; } return { type: "genfrac", mode: parser.mode, continued: false, numer, denom, hasBarLine, leftDelim, rightDelim, scriptLevel, barSize: null }; }, mathmlBuilder: mathmlBuilder$5 }); defineFunction({ type: "genfrac", names: ["\\cfrac"], props: { numArgs: 2 }, handler: ({ parser, funcName }, args) => { const numer = args[0]; const denom = args[1]; return { type: "genfrac", mode: parser.mode, continued: true, numer, denom, hasBarLine: true, leftDelim: null, rightDelim: null, scriptLevel: "display", barSize: null }; } }); // Infix generalized fractions -- these are not rendered directly, but replaced // immediately by one of the variants above. defineFunction({ type: "infix", names: ["\\over", "\\choose", "\\atop", "\\brace", "\\brack"], props: { numArgs: 0, infix: true }, handler({ parser, funcName, token }) { let replaceWith; switch (funcName) { case "\\over": replaceWith = "\\frac"; break; case "\\choose": replaceWith = "\\binom"; break; case "\\atop": replaceWith = "\\\\atopfrac"; break; case "\\brace": replaceWith = "\\\\bracefrac"; break; case "\\brack": replaceWith = "\\\\brackfrac"; break; default: throw new Error("Unrecognized infix genfrac command"); } return { type: "infix", mode: parser.mode, replaceWith, token }; } }); const delimFromValue = function(delimString) { let delim = null; if (delimString.length > 0) { delim = delimString; delim = delim === "." ? null : delim; } return delim; }; defineFunction({ type: "genfrac", names: ["\\genfrac"], props: { numArgs: 6, allowedInArgument: true, argTypes: ["math", "math", "size", "text", "math", "math"] }, handler({ parser }, args) { const numer = args[4]; const denom = args[5]; // Look into the parse nodes to get the desired delimiters. const leftNode = normalizeArgument(args[0]); const leftDelim = leftNode.type === "atom" && leftNode.family === "open" ? delimFromValue(leftNode.text) : null; const rightNode = normalizeArgument(args[1]); const rightDelim = rightNode.type === "atom" && rightNode.family === "close" ? delimFromValue(rightNode.text) : null; const barNode = assertNodeType(args[2], "size"); let hasBarLine; let barSize = null; if (barNode.isBlank) { // \genfrac acts differently than \above. // \genfrac treats an empty size group as a signal to use a // standard bar size. \above would see size = 0 and omit the bar. hasBarLine = true; } else { barSize = barNode.value; hasBarLine = barSize.number > 0; } // Find out if we want displaystyle, textstyle, etc. let scriptLevel = "auto"; let styl = args[3]; if (styl.type === "ordgroup") { if (styl.body.length > 0) { const textOrd = assertNodeType(styl.body[0], "textord"); scriptLevel = stylArray[Number(textOrd.text)]; } } else { styl = assertNodeType(styl, "textord"); scriptLevel = stylArray[Number(styl.text)]; } return { type: "genfrac", mode: parser.mode, numer, denom, continued: false, hasBarLine, barSize, leftDelim, rightDelim, scriptLevel }; }, mathmlBuilder: mathmlBuilder$5 }); // \above is an infix fraction that also defines a fraction bar size. defineFunction({ type: "infix", names: ["\\above"], props: { numArgs: 1, argTypes: ["size"], infix: true }, handler({ parser, funcName, token }, args) { return { type: "infix", mode: parser.mode, replaceWith: "\\\\abovefrac", barSize: assertNodeType(args[0], "size").value, token }; } }); defineFunction({ type: "genfrac", names: ["\\\\abovefrac"], props: { numArgs: 3, argTypes: ["math", "size", "math"] }, handler: ({ parser, funcName }, args) => { const numer = args[0]; const barSize = assert(assertNodeType(args[1], "infix").barSize); const denom = args[2]; const hasBarLine = barSize.number > 0; return { type: "genfrac", mode: parser.mode, numer, denom, continued: false, hasBarLine, barSize, leftDelim: null, rightDelim: null, scriptLevel: "auto" }; }, mathmlBuilder: mathmlBuilder$5 }); // \hbox is provided for compatibility with LaTeX functions that act on a box. // This function by itself doesn't do anything but set scriptlevel to \textstyle // and prevent a soft line break. defineFunction({ type: "hbox", names: ["\\hbox"], props: { numArgs: 1, argTypes: ["hbox"], allowedInArgument: true, allowedInText: false }, handler({ parser }, args) { return { type: "hbox", mode: parser.mode, body: ordargument(args[0]) }; }, mathmlBuilder(group, style) { const newStyle = style.withLevel(StyleLevel.TEXT); const mrow = buildExpressionRow(group.body, newStyle); return consolidateText(mrow) } }); const mathmlBuilder$4 = (group, style) => { const accentNode = stretchy.mathMLnode(group.label); accentNode.style["math-depth"] = 0; return new mathMLTree.MathNode(group.isOver ? "mover" : "munder", [ buildGroup$1(group.base, style), accentNode ]); }; // Horizontal stretchy braces defineFunction({ type: "horizBrace", names: ["\\overbrace", "\\underbrace"], props: { numArgs: 1 }, handler({ parser, funcName }, args) { return { type: "horizBrace", mode: parser.mode, label: funcName, isOver: /^\\over/.test(funcName), base: args[0] }; }, mathmlBuilder: mathmlBuilder$4 }); defineFunction({ type: "html", names: ["\\class", "\\id", "\\style", "\\data"], props: { numArgs: 2, argTypes: ["raw", "original"], allowedInText: true }, handler: ({ parser, funcName, token }, args) => { const value = assertNodeType(args[0], "raw").string; const body = args[1]; if (parser.settings.strict) { throw new ParseError(`Function "${funcName}" is disabled in strict mode`, token) } let trustContext; const attributes = {}; switch (funcName) { case "\\class": attributes.class = value; trustContext = { command: "\\class", class: value }; break; case "\\id": attributes.id = value; trustContext = { command: "\\id", id: value }; break; case "\\style": attributes.style = value; trustContext = { command: "\\style", style: value }; break; case "\\data": { const data = value.split(","); for (let i = 0; i < data.length; i++) { const keyVal = data[i].split("="); if (keyVal.length !== 2) { throw new ParseError("Error parsing key-value for \\data"); } attributes["data-" + keyVal[0].trim()] = keyVal[1].trim(); } trustContext = { command: "\\data", attributes }; break; } default: throw new Error("Unrecognized html command"); } if (!parser.settings.isTrusted(trustContext)) { throw new ParseError(`Function "${funcName}" is not trusted`, token) } return { type: "html", mode: parser.mode, attributes, body: ordargument(body) }; }, mathmlBuilder: (group, style) => { const element = buildExpressionRow(group.body, style); const classes = []; if (group.attributes.class) { classes.push(...group.attributes.class.trim().split(/\s+/)); } element.classes = classes; for (const attr in group.attributes) { if (attr !== "class" && Object.prototype.hasOwnProperty.call(group.attributes, attr)) { element.setAttribute(attr, group.attributes[attr]); } } return element; } }); const sizeData = function(str) { if (/^[-+]? *(\d+(\.\d*)?|\.\d+)$/.test(str)) { // str is a number with no unit specified. // default unit is bp, per graphix package. return { number: +str, unit: "bp" } } else { const match = /([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(str); if (!match) { throw new ParseError("Invalid size: '" + str + "' in \\includegraphics"); } const data = { number: +(match[1] + match[2]), // sign + magnitude, cast to number unit: match[3] }; if (!validUnit(data)) { throw new ParseError("Invalid unit: '" + data.unit + "' in \\includegraphics."); } return data } }; defineFunction({ type: "includegraphics", names: ["\\includegraphics"], props: { numArgs: 1, numOptionalArgs: 1, argTypes: ["raw", "url"], allowedInText: false }, handler: ({ parser, token }, args, optArgs) => { let width = { number: 0, unit: "em" }; let height = { number: 0.9, unit: "em" }; // sorta character sized. let totalheight = { number: 0, unit: "em" }; let alt = ""; if (optArgs[0]) { const attributeStr = assertNodeType(optArgs[0], "raw").string; // Parser.js does not parse key/value pairs. We get a string. const attributes = attributeStr.split(","); for (let i = 0; i < attributes.length; i++) { const keyVal = attributes[i].split("="); if (keyVal.length === 2) { const str = keyVal[1].trim(); switch (keyVal[0].trim()) { case "alt": alt = str; break case "width": width = sizeData(str); break case "height": height = sizeData(str); break case "totalheight": totalheight = sizeData(str); break default: throw new ParseError("Invalid key: '" + keyVal[0] + "' in \\includegraphics.") } } } } const src = assertNodeType(args[0], "url").url; if (alt === "") { // No alt given. Use the file name. Strip away the path. alt = src; alt = alt.replace(/^.*[\\/]/, ""); alt = alt.substring(0, alt.lastIndexOf(".")); } if ( !parser.settings.isTrusted({ command: "\\includegraphics", url: src }) ) { throw new ParseError(`Function "\\includegraphics" is not trusted`, token) } return { type: "includegraphics", mode: parser.mode, alt: alt, width: width, height: height, totalheight: totalheight, src: src } }, mathmlBuilder: (group, style) => { const height = calculateSize(group.height, style); const depth = { number: 0, unit: "em" }; if (group.totalheight.number > 0) { if (group.totalheight.unit === height.unit && group.totalheight.number > height.number) { depth.number = group.totalheight.number - height.number; depth.unit = height.unit; } } let width = 0; if (group.width.number > 0) { width = calculateSize(group.width, style); } const graphicStyle = { height: height.number + depth.number + "em" }; if (width.number > 0) { graphicStyle.width = width.number + width.unit; } if (depth.number > 0) { graphicStyle.verticalAlign = -depth.number + depth.unit; } const node = new Img(group.src, group.alt, graphicStyle); node.height = height; node.depth = depth; return new mathMLTree.MathNode("mtext", [node]) } }); // Horizontal spacing commands // TODO: \hskip and \mskip should support plus and minus in lengths defineFunction({ type: "kern", names: ["\\kern", "\\mkern", "\\hskip", "\\mskip"], props: { numArgs: 1, argTypes: ["size"], primitive: true, allowedInText: true }, handler({ parser, funcName, token }, args) { const size = assertNodeType(args[0], "size"); if (parser.settings.strict) { const mathFunction = funcName[1] === "m"; // \mkern, \mskip const muUnit = size.value.unit === "mu"; if (mathFunction) { if (!muUnit) { throw new ParseError(`LaTeX's ${funcName} supports only mu units, ` + `not ${size.value.unit} units`, token) } if (parser.mode !== "math") { throw new ParseError(`LaTeX's ${funcName} works only in math mode`, token) } } else { // !mathFunction if (muUnit) { throw new ParseError(`LaTeX's ${funcName} doesn't support mu units`, token) } } } return { type: "kern", mode: parser.mode, dimension: size.value }; }, mathmlBuilder(group, style) { const dimension = calculateSize(group.dimension, style); const ch = dimension.number > 0 && dimension.unit === "em" ? spaceCharacter(dimension.number) : ""; if (group.mode === "text" && ch.length > 0) { const character = new mathMLTree.TextNode(ch); return new mathMLTree.MathNode("mtext", [character]); } else { if (dimension.number >= 0) { const node = new mathMLTree.MathNode("mspace"); node.setAttribute("width", dimension.number + dimension.unit); return node } else { // Don't use <mspace> or <mpadded> because // WebKit recognizes negative left margin only on a <mrow> element const node = new mathMLTree.MathNode("mrow"); node.style.marginLeft = dimension.number + dimension.unit; return node } } } }); const spaceCharacter = function(width) { if (width >= 0.05555 && width <= 0.05556) { return "\u200a"; //   } else if (width >= 0.1666 && width <= 0.1667) { return "\u2009"; //   } else if (width >= 0.2222 && width <= 0.2223) { return "\u2005"; //   } else if (width >= 0.2777 && width <= 0.2778) { return "\u2005\u200a"; //    } else { return ""; } }; // Limit valid characters to a small set, for safety. const invalidIdRegEx = /[^A-Za-z_0-9-]/g; defineFunction({ type: "label", names: ["\\label"], props: { numArgs: 1, argTypes: ["raw"] }, handler({ parser }, args) { return { type: "label", mode: parser.mode, string: args[0].string.replace(invalidIdRegEx, "") }; }, mathmlBuilder(group, style) { // Return a no-width, no-ink element with an HTML id. const node = new mathMLTree.MathNode("mrow", [], ["tml-label"]); if (group.string.length > 0) { node.setLabel(group.string); } return node } }); // Horizontal overlap functions const textModeLap = ["\\clap", "\\llap", "\\rlap"]; defineFunction({ type: "lap", names: ["\\mathllap", "\\mathrlap", "\\mathclap", "\\clap", "\\llap", "\\rlap"], props: { numArgs: 1, allowedInText: true }, handler: ({ parser, funcName, token }, args) => { if (textModeLap.includes(funcName)) { if (parser.settings.strict && parser.mode !== "text") { throw new ParseError(`{${funcName}} can be used only in text mode. Try \\math${funcName.slice(1)}`, token) } funcName = funcName.slice(1); } else { funcName = funcName.slice(5); } const body = args[0]; return { type: "lap", mode: parser.mode, alignment: funcName, body } }, mathmlBuilder: (group, style) => { // mathllap, mathrlap, mathclap let strut; if (group.alignment === "llap") { // We need an invisible strut with the same depth as the group. // We can't just read the depth, so we use \vphantom methods. const phantomInner = buildExpression(ordargument(group.body), style); const phantom = new mathMLTree.MathNode("mphantom", phantomInner); strut = new mathMLTree.MathNode("mpadded", [phantom]); strut.setAttribute("width", "0.1px"); // Don't use 0. WebKit would hide it. } const inner = buildGroup$1(group.body, style); let node; if (group.alignment === "llap") { inner.style.position = "absolute"; inner.style.right = "0"; inner.style.bottom = `0`; // If we could have read the ink depth, it would go here. node = new mathMLTree.MathNode("mpadded", [strut, inner]); } else { node = new mathMLTree.MathNode("mpadded", [inner]); } if (group.alignment === "rlap") { if (group.body.body.length > 0 && group.body.body[0].type === "genfrac") { // In Firefox, a <mpadded> squashes the 3/18em padding of a child \frac. Put it back. node.setAttribute("lspace", "0.16667em"); } } else { const offset = group.alignment === "llap" ? "-1" : "-0.5"; node.setAttribute("lspace", offset + "width"); if (group.alignment === "llap") { node.style.position = "relative"; } else { node.style.display = "flex"; node.style.justifyContent = "center"; } } node.setAttribute("width", "0.1px"); // Don't use 0. WebKit would hide it. return node } }); // Switching from text mode back to math mode defineFunction({ type: "ordgroup", names: ["\\(", "$"], props: { numArgs: 0, allowedInText: true, allowedInMath: false }, handler({ funcName, parser }, args) { const outerMode = parser.mode; parser.switchMode("math"); const close = funcName === "\\(" ? "\\)" : "$"; const body = parser.parseExpression(false, close); parser.expect(close); parser.switchMode(outerMode); return { type: "ordgroup", mode: parser.mode, body }; } }); // Check for extra closing math delimiters defineFunction({ type: "text", // Doesn't matter what this is. names: ["\\)", "\\]"], props: { numArgs: 0, allowedInText: true, allowedInMath: false }, handler(context, token) { throw new ParseError(`Mismatched ${context.funcName}`, token); } }); const chooseStyle = (group, style) => { switch (style.level) { case StyleLevel.DISPLAY: // 0 return group.display; case StyleLevel.TEXT: // 1 return group.text; case StyleLevel.SCRIPT: // 2 return group.script; case StyleLevel.SCRIPTSCRIPT: // 3 return group.scriptscript; default: return group.text; } }; defineFunction({ type: "mathchoice", names: ["\\mathchoice"], props: { numArgs: 4, primitive: true }, handler: ({ parser }, args) => { return { type: "mathchoice", mode: parser.mode, display: ordargument(args[0]), text: ordargument(args[1]), script: ordargument(args[2]), scriptscript: ordargument(args[3]) }; }, mathmlBuilder: (group, style) => { const body = chooseStyle(group, style); return buildExpressionRow(body, style); } }); const textAtomTypes = ["text", "textord", "mathord", "atom"]; function mathmlBuilder$3(group, style) { let node; const inner = buildExpression(group.body, style); if (group.mclass === "minner") { node = new mathMLTree.MathNode("mpadded", inner); } else if (group.mclass === "mord") { if (group.isCharacterBox || inner[0].type === "mathord") { node = inner[0]; node.type = "mi"; if (node.children.length === 1 && node.children[0].text && node.children[0].text === "∇") { node.setAttribute("mathvariant", "normal"); } } else { node = new mathMLTree.MathNode("mi", inner); } } else { node = new mathMLTree.MathNode("mrow", inner); if (group.mustPromote) { node = inner[0]; node.type = "mo"; if (group.isCharacterBox && group.body[0].text && /[A-Za-z]/.test(group.body[0].text)) { node.setAttribute("mathvariant", "italic"); } } else { node = new mathMLTree.MathNode("mrow", inner); } // Set spacing based on what is the most likely adjacent atom type. // See TeXbook p170. const doSpacing = style.level < 2; // Operator spacing is zero inside a (sub|super)script. if (node.type === "mrow") { if (doSpacing ) { if (group.mclass === "mbin") { // medium space node.children.unshift(padding(0.2222)); node.children.push(padding(0.2222)); } else if (group.mclass === "mrel") { // thickspace node.children.unshift(padding(0.2778)); node.children.push(padding(0.2778)); } else if (group.mclass === "mpunct") { node.children.push(padding(0.1667)); } else if (group.mclass === "minner") { node.children.unshift(padding(0.0556)); // 1 mu is the most likely option node.children.push(padding(0.0556)); } } } else { if (group.mclass === "mbin") { // medium space node.attributes.lspace = (doSpacing ? "0.2222em" : "0"); node.attributes.rspace = (doSpacing ? "0.2222em" : "0"); } else if (group.mclass === "mrel") { // thickspace node.attributes.lspace = (doSpacing ? "0.2778em" : "0"); node.attributes.rspace = (doSpacing ? "0.2778em" : "0"); } else if (group.mclass === "mpunct") { node.attributes.lspace = "0em"; node.attributes.rspace = (doSpacing ? "0.1667em" : "0"); } else if (group.mclass === "mopen" || group.mclass === "mclose") { node.attributes.lspace = "0em"; node.attributes.rspace = "0em"; } else if (group.mclass === "minner" && doSpacing) { node.attributes.lspace = "0.0556em"; // 1 mu is the most likely option node.attributes.width = "+0.1111em"; } } if (!(group.mclass === "mopen" || group.mclass === "mclose")) { delete node.attributes.stretchy; delete node.attributes.form; } } return node; } // Math class commands except \mathop defineFunction({ type: "mclass", names: [ "\\mathord", "\\mathbin", "\\mathrel", "\\mathopen", "\\mathclose", "\\mathpunct", "\\mathinner" ], props: { numArgs: 1, primitive: true }, handler({ parser, funcName }, args) { const body = args[0]; const isCharacterBox = utils.isCharacterBox(body); // We should not wrap a <mo> around a <mi> or <mord>. That would be invalid MathML. // In that case, we instead promote the text contents of the body to the parent. let mustPromote = true; const mord = { type: "mathord", text: "", mode: parser.mode }; const arr = (body.body) ? body.body : [body]; for (const arg of arr) { if (textAtomTypes.includes(arg.type)) { if (symbols[parser.mode][arg.text]) { mord.text += symbols[parser.mode][arg.text].replace; } else if (arg.text) { mord.text += arg.text; } else if (arg.body) { arg.body.map(e => { mord.text += e.text; }); } } else { mustPromote = false; break } } if (mustPromote && funcName === "\\mathord" && mord.type === "mathord" && mord.text.length > 1) { return mord } else { return { type: "mclass", mode: parser.mode, mclass: "m" + funcName.slice(5), body: ordargument(mustPromote ? mord : body), isCharacterBox, mustPromote }; } }, mathmlBuilder: mathmlBuilder$3 }); const binrelClass = (arg) => { // \binrel@ spacing varies with (bin|rel|ord) of the atom in the argument. // (by rendering separately and with {}s before and after, and measuring // the change in spacing). We'll do roughly the same by detecting the // atom type directly. const atom = arg.type === "ordgroup" && arg.body.length && arg.body.length === 1 ? arg.body[0] : arg; if (atom.type === "atom" && (atom.family === "bin" || atom.family === "rel")) { return "m" + atom.family; } else { return "mord"; } }; // \@binrel{x}{y} renders like y but as mbin/mrel/mord if x is mbin/mrel/mord. // This is equivalent to \binrel@{x}\binrel@@{y} in AMSTeX. defineFunction({ type: "mclass", names: ["\\@binrel"], props: { numArgs: 2 }, handler({ parser }, args) { return { type: "mclass", mode: parser.mode, mclass: binrelClass(args[0]), body: ordargument(args[1]), isCharacterBox: utils.isCharacterBox(args[1]) }; } }); // Build a relation or stacked op by placing one symbol on top of another defineFunction({ type: "mclass", names: ["\\stackrel", "\\overset", "\\underset"], props: { numArgs: 2 }, handler({ parser, funcName }, args) { const baseArg = args[1]; const shiftedArg = args[0]; let mclass; if (funcName !== "\\stackrel") { // LaTeX applies \binrel spacing to \overset and \underset. mclass = binrelClass(baseArg); } else { mclass = "mrel"; // for \stackrel } const baseType = mclass === "mrel" || mclass === "mbin" ? "op" : "ordgroup"; const baseOp = { type: baseType, mode: baseArg.mode, limits: true, alwaysHandleSupSub: true, parentIsSupSub: false, symbol: false, suppressBaseShift: funcName !== "\\stackrel", body: ordargument(baseArg) }; return { type: "supsub", mode: shiftedArg.mode, stack: true, base: baseOp, sup: funcName === "\\underset" ? null : shiftedArg, sub: funcName === "\\underset" ? shiftedArg : null }; }, mathmlBuilder: mathmlBuilder$3 }); // Helper function const buildGroup = (el, style, noneNode) => { if (!el) { return noneNode } const node = buildGroup$1(el, style); if (node.type === "mrow" && node.children.length === 0) { return noneNode } return node }; defineFunction({ type: "multiscript", names: ["\\sideset", "\\pres@cript"], // See macros.js for \prescript props: { numArgs: 3 }, handler({ parser, funcName, token }, args) { if (args[2].body.length === 0) { throw new ParseError(funcName + `cannot parse an empty base.`) } const base = args[2].body[0]; if (parser.settings.strict && funcName === "\\sideset" && !base.symbol) { throw new ParseError(`The base of \\sideset must be a big operator. Try \\prescript.`) } if ((args[0].body.length > 0 && args[0].body[0].type !== "supsub") || (args[1].body.length > 0 && args[1].body[0].type !== "supsub")) { throw new ParseError("\\sideset can parse only subscripts and " + "superscripts in its first two arguments", token) } // The prescripts and postscripts come wrapped in a supsub. const prescripts = args[0].body.length > 0 ? args[0].body[0] : null; const postscripts = args[1].body.length > 0 ? args[1].body[0] : null; if (!prescripts && !postscripts) { return base } else if (!prescripts) { // It's not a multi-script. Get a \textstyle supsub. return { type: "styling", mode: parser.mode, scriptLevel: "text", body: [{ type: "supsub", mode: parser.mode, base, sup: postscripts.sup, sub: postscripts.sub }] } } else { return { type: "multiscript", mode: parser.mode, isSideset: funcName === "\\sideset", prescripts, postscripts, base } } }, mathmlBuilder(group, style) { const base = buildGroup$1(group.base, style); const prescriptsNode = new mathMLTree.MathNode("mprescripts"); const noneNode = new mathMLTree.MathNode("none"); let children = []; const preSub = buildGroup(group.prescripts.sub, style, noneNode); const preSup = buildGroup(group.prescripts.sup, style, noneNode); if (group.isSideset) { // This seems silly, but LaTeX does this. Firefox ignores it, which does not make me sad. preSub.setAttribute("style", "text-align: left;"); preSup.setAttribute("style", "text-align: left;"); } if (group.postscripts) { const postSub = buildGroup(group.postscripts.sub, style, noneNode); const postSup = buildGroup(group.postscripts.sup, style, noneNode); children = [base, postSub, postSup, prescriptsNode, preSub, preSup]; } else { children = [base, prescriptsNode, preSub, preSup]; } return new mathMLTree.MathNode("mmultiscripts", children); } }); defineFunction({ type: "not", names: ["\\not"], props: { numArgs: 1, primitive: true, allowedInText: false }, handler({ parser }, args) { const isCharacterBox = utils.isCharacterBox(args[0]); let body; if (isCharacterBox) { body = ordargument(args[0]); if (body[0].text.charAt(0) === "\\") { body[0].text = symbols.math[body[0].text].replace; } // \u0338 is the Unicode Combining Long Solidus Overlay body[0].text = body[0].text.slice(0, 1) + "\u0338" + body[0].text.slice(1); } else { // When the argument is not a character box, TeX does an awkward, poorly placed overlay. // We'll do the same. const notNode = { type: "textord", mode: "math", text: "\u0338" }; const kernNode = { type: "kern", mode: "math", dimension: { number: -0.6, unit: "em" } }; body = [notNode, kernNode, args[0]]; } return { type: "not", mode: parser.mode, body, isCharacterBox }; }, mathmlBuilder(group, style) { if (group.isCharacterBox) { const inner = buildExpression(group.body, style, true); return inner[0] } else { return buildExpressionRow(group.body, style) } } }); // Limits, symbols // Some helpers const ordAtomTypes = ["textord", "mathord", "atom"]; // Most operators have a large successor symbol, but these don't. const noSuccessor = ["\\smallint"]; // Math operators (e.g. \sin) need a space between these types and themselves: const ordTypes = ["textord", "mathord", "ordgroup", "close", "leftright", "font"]; // NOTE: Unlike most `builders`s, this one handles not only "op", but also // "supsub" since some of them (like \int) can affect super/subscripting. const setSpacing = node => { // The user wrote a \mathop{…} function. Change spacing from default to OP spacing. // The most likely spacing for an OP is a thin space per TeXbook p170. node.attributes.lspace = "0.1667em"; node.attributes.rspace = "0.1667em"; }; const mathmlBuilder$2 = (group, style) => { let node; if (group.symbol) { // This is a symbol. Just add the symbol. node = new MathNode("mo", [makeText(group.name, group.mode)]); if (noSuccessor.includes(group.name)) { node.setAttribute("largeop", "false"); } else { node.setAttribute("movablelimits", "false"); } if (group.fromMathOp) { setSpacing(node); } } else if (group.body) { // This is an operator with children. Add them. node = new MathNode("mo", buildExpression(group.body, style)); if (group.fromMathOp) { setSpacing(node); } } else { // This is a text operator. Add all of the characters from the operator's name. node = new MathNode("mi", [new TextNode(group.name.slice(1))]); if (!group.parentIsSupSub) { // Append an invisible <mo>⁡</mo>. // ref: https://www.w3.org/TR/REC-MathML/chap3_2.html#sec3.2.4 const operator = new MathNode("mo", [makeText("\u2061", "text")]); const row = [node, operator]; // Set spacing if (group.needsLeadingSpace) { const lead = new MathNode("mspace"); lead.setAttribute("width", "0.1667em"); // thin space. row.unshift(lead); } if (!group.isFollowedByDelimiter) { const trail = new MathNode("mspace"); trail.setAttribute("width", "0.1667em"); // thin space. row.push(trail); } node = new MathNode("mrow", row); } } return node; }; const singleCharBigOps = { "\u220F": "\\prod", "\u2210": "\\coprod", "\u2211": "\\sum", "\u22c0": "\\bigwedge", "\u22c1": "\\bigvee", "\u22c2": "\\bigcap", "\u22c3": "\\bigcup", "\u2a00": "\\bigodot", "\u2a01": "\\bigoplus", "\u2a02": "\\bigotimes", "\u2a04": "\\biguplus", "\u2a05": "\\bigsqcap", "\u2a06": "\\bigsqcup", "\u2a03": "\\bigcupdot", "\u2a07": "\\bigdoublevee", "\u2a08": "\\bigdoublewedge", "\u2a09": "\\bigtimes" }; defineFunction({ type: "op", names: [ "\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcupplus", "\\bigcupdot", "\\bigcap", "\\bigcup", "\\bigdoublevee", "\\bigdoublewedge", "\\intop", "\\prod", "\\sum", "\\bigotimes", "\\bigoplus", "\\bigodot", "\\bigsqcap", "\\bigsqcup", "\\bigtimes", "\\smallint", "\u220F", "\u2210", "\u2211", "\u22c0", "\u22c1", "\u22c2", "\u22c3", "\u2a00", "\u2a01", "\u2a02", "\u2a03", "\u2a04", "\u2a05", "\u2a06", "\u2a07", "\u2a08", "\u2a09" ], props: { numArgs: 0 }, handler: ({ parser, funcName }, args) => { let fName = funcName; if (fName.length === 1) { fName = singleCharBigOps[fName]; } return { type: "op", mode: parser.mode, limits: true, parentIsSupSub: false, symbol: true, stack: false, // This is true for \stackrel{}, not here. name: fName }; }, mathmlBuilder: mathmlBuilder$2 }); // Note: calling defineFunction with a type that's already been defined only // works because the same mathmlBuilder is being used. defineFunction({ type: "op", names: ["\\mathop"], props: { numArgs: 1, primitive: true }, handler: ({ parser }, args) => { const body = args[0]; // It would be convienient to just wrap a <mo> around the argument. // But if the argument is a <mi> or <mord>, that would be invalid MathML. // In that case, we instead promote the text contents of the body to the parent. const arr = (body.body) ? body.body : [body]; const isSymbol = arr.length === 1 && ordAtomTypes.includes(arr[0].type); return { type: "op", mode: parser.mode, limits: true, parentIsSupSub: false, symbol: isSymbol, fromMathOp: true, stack: false, name: isSymbol ? arr[0].text : null, body: isSymbol ? null : ordargument(body) }; }, mathmlBuilder: mathmlBuilder$2 }); // There are 2 flags for operators; whether they produce limits in // displaystyle, and whether they are symbols and should grow in // displaystyle. These four groups cover the four possible choices. const singleCharIntegrals = { "\u222b": "\\int", "\u222c": "\\iint", "\u222d": "\\iiint", "\u222e": "\\oint", "\u222f": "\\oiint", "\u2230": "\\oiiint", "\u2231": "\\intclockwise", "\u2232": "\\varointclockwise", "\u2a0c": "\\iiiint", "\u2a0d": "\\intbar", "\u2a0e": "\\intBar", "\u2a0f": "\\fint", "\u2a12": "\\rppolint", "\u2a13": "\\scpolint", "\u2a15": "\\pointint", "\u2a16": "\\sqint", "\u2a17": "\\intlarhk", "\u2a18": "\\intx", "\u2a19": "\\intcap", "\u2a1a": "\\intcup" }; // No limits, not symbols defineFunction({ type: "op", names: [ "\\arcsin", "\\arccos", "\\arctan", "\\arctg", "\\arcctg", "\\arg", "\\ch", "\\cos", "\\cosec", "\\cosh", "\\cot", "\\cotg", "\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", "\\dim", "\\exp", "\\hom", "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh", "\\sh", "\\sgn", "\\tan", "\\tanh", "\\tg", "\\th" ], props: { numArgs: 0 }, handler({ parser, funcName }) { const prevAtomType = parser.prevAtomType; const next = parser.gullet.future().text; return { type: "op", mode: parser.mode, limits: false, parentIsSupSub: false, symbol: false, stack: false, isFollowedByDelimiter: isDelimiter(next), needsLeadingSpace: prevAtomType.length > 0 && ordTypes.includes(prevAtomType), name: funcName }; }, mathmlBuilder: mathmlBuilder$2 }); // Limits, not symbols defineFunction({ type: "op", names: ["\\det", "\\gcd", "\\inf", "\\lim", "\\max", "\\min", "\\Pr", "\\sup"], props: { numArgs: 0 }, handler({ parser, funcName }) { const prevAtomType = parser.prevAtomType; const next = parser.gullet.future().text; return { type: "op", mode: parser.mode, limits: true, parentIsSupSub: false, symbol: false, stack: false, isFollowedByDelimiter: isDelimiter(next), needsLeadingSpace: prevAtomType.length > 0 && ordTypes.includes(prevAtomType), name: funcName }; }, mathmlBuilder: mathmlBuilder$2 }); // No limits, symbols defineFunction({ type: "op", names: [ "\\int", "\\iint", "\\iiint", "\\iiiint", "\\oint", "\\oiint", "\\oiiint", "\\intclockwise", "\\varointclockwise", "\\intbar", "\\intBar", "\\fint", "\\rppolint", "\\scpolint", "\\pointint", "\\sqint", "\\intlarhk", "\\intx", "\\intcap", "\\intcup", "\u222b", "\u222c", "\u222d", "\u222e", "\u222f", "\u2230", "\u2231", "\u2232", "\u2a0c", "\u2a0d", "\u2a0e", "\u2a0f", "\u2a12", "\u2a13", "\u2a15", "\u2a16", "\u2a17", "\u2a18", "\u2a19", "\u2a1a" ], props: { numArgs: 0 }, handler({ parser, funcName }) { let fName = funcName; if (fName.length === 1) { fName = singleCharIntegrals[fName]; } return { type: "op", mode: parser.mode, limits: false, parentIsSupSub: false, symbol: true, stack: false, name: fName }; }, mathmlBuilder: mathmlBuilder$2 }); // NOTE: Unlike most builders, this one handles not only // "operatorname", but also "supsub" since \operatorname* can // affect super/subscripting. const mathmlBuilder$1 = (group, style) => { let expression = buildExpression(group.body, style.withFont("mathrm")); // Is expression a string or has it something like a fraction? let isAllString = true; // default for (let i = 0; i < expression.length; i++) { let node = expression[i]; if (node instanceof mathMLTree.MathNode) { if ((node.type === "mrow" || node.type === "mpadded") && node.children.length === 1 && node.children[0] instanceof mathMLTree.MathNode) { node = node.children[0]; } switch (node.type) { case "mi": case "mn": case "ms": case "mtext": break; // Do nothing yet. case "mspace": { if (node.attributes.width) { const width = node.attributes.width.replace("em", ""); const ch = spaceCharacter(Number(width)); if (ch === "") { isAllString = false; } else { expression[i] = new mathMLTree.MathNode("mtext", [new mathMLTree.TextNode(ch)]); } } } break case "mo": { const child = node.children[0]; if (node.children.length === 1 && child instanceof mathMLTree.TextNode) { child.text = child.text.replace(/\u2212/, "-").replace(/\u2217/, "*"); } else { isAllString = false; } break } default: isAllString = false; } } else { isAllString = false; } } if (isAllString) { // Write a single TextNode instead of multiple nested tags. const word = expression.map((node) => node.toText()).join(""); expression = [new mathMLTree.TextNode(word)]; } else if ( expression.length === 1 && ["mover", "munder"].includes(expression[0].type) && (expression[0].children[0].type === "mi" || expression[0].children[0].type === "mtext") ) { expression[0].children[0].type = "mi"; if (group.parentIsSupSub) { return new mathMLTree.MathNode("mrow", expression) } else { const operator = new mathMLTree.MathNode("mo", [makeText("\u2061", "text")]); return mathMLTree.newDocumentFragment([expression[0], operator]) } } let wrapper; if (isAllString) { wrapper = new mathMLTree.MathNode("mi", expression); if (expression[0].text.length === 1) { wrapper.setAttribute("mathvariant", "normal"); } } else { wrapper = new mathMLTree.MathNode("mrow", expression); } if (!group.parentIsSupSub) { // Append an <mo>⁡</mo>. // ref: https://www.w3.org/TR/REC-MathML/chap3_2.html#sec3.2.4 const operator = new mathMLTree.MathNode("mo", [makeText("\u2061", "text")]); const fragment = [wrapper, operator]; if (group.needsLeadingSpace) { // LaTeX gives operator spacing, but a <mi> gets ord spacing. // So add a leading space. const space = new mathMLTree.MathNode("mspace"); space.setAttribute("width", "0.1667em"); // thin space. fragment.unshift(space); } if (!group.isFollowedByDelimiter) { const trail = new mathMLTree.MathNode("mspace"); trail.setAttribute("width", "0.1667em"); // thin space. fragment.push(trail); } return mathMLTree.newDocumentFragment(fragment) } return wrapper }; // \operatorname // amsopn.dtx: \mathop{#1\kern\z@\operator@font#3}\newmcodes@ defineFunction({ type: "operatorname", names: ["\\operatorname@", "\\operatornamewithlimits"], props: { numArgs: 1, allowedInArgument: true }, handler: ({ parser, funcName }, args) => { const body = args[0]; const prevAtomType = parser.prevAtomType; const next = parser.gullet.future().text; return { type: "operatorname", mode: parser.mode, body: ordargument(body), alwaysHandleSupSub: (funcName === "\\operatornamewithlimits"), limits: false, parentIsSupSub: false, isFollowedByDelimiter: isDelimiter(next), needsLeadingSpace: prevAtomType.length > 0 && ordTypes.includes(prevAtomType) }; }, mathmlBuilder: mathmlBuilder$1 }); defineMacro("\\operatorname", "\\@ifstar\\operatornamewithlimits\\operatorname@"); defineFunctionBuilders({ type: "ordgroup", mathmlBuilder(group, style) { return buildExpressionRow(group.body, style, group.semisimple); } }); defineFunction({ type: "phantom", names: ["\\phantom"], props: { numArgs: 1, allowedInText: true }, handler: ({ parser }, args) => { const body = args[0]; return { type: "phantom", mode: parser.mode, body: ordargument(body) }; }, mathmlBuilder: (group, style) => { const inner = buildExpression(group.body, style); return new mathMLTree.MathNode("mphantom", inner); } }); defineFunction({ type: "hphantom", names: ["\\hphantom"], props: { numArgs: 1, allowedInText: true }, handler: ({ parser }, args) => { const body = args[0]; return { type: "hphantom", mode: parser.mode, body }; }, mathmlBuilder: (group, style) => { const inner = buildExpression(ordargument(group.body), style); const phantom = new mathMLTree.MathNode("mphantom", inner); const node = new mathMLTree.MathNode("mpadded", [phantom]); node.setAttribute("height", "0px"); node.setAttribute("depth", "0px"); return node; } }); defineFunction({ type: "vphantom", names: ["\\vphantom"], props: { numArgs: 1, allowedInText: true }, handler: ({ parser }, args) => { const body = args[0]; return { type: "vphantom", mode: parser.mode, body }; }, mathmlBuilder: (group, style) => { const inner = buildExpression(ordargument(group.body), style); const phantom = new mathMLTree.MathNode("mphantom", inner); const node = new mathMLTree.MathNode("mpadded", [phantom]); node.setAttribute("width", "0px"); return node; } }); // In LaTeX, \pmb is a simulation of bold font. // The version of \pmb in ambsy.sty works by typesetting three copies of the argument // with small offsets. We use CSS font-weight:bold. defineFunction({ type: "pmb", names: ["\\pmb"], props: { numArgs: 1, allowedInText: true }, handler({ parser }, args) { return { type: "pmb", mode: parser.mode, body: ordargument(args[0]) } }, mathmlBuilder(group, style) { const inner = buildExpression(group.body, style); // Wrap with an <mstyle> element. const node = wrapWithMstyle(inner); node.setAttribute("style", "font-weight:bold"); return node } }); // \raise, \lower, and \raisebox const mathmlBuilder = (group, style) => { const newStyle = style.withLevel(StyleLevel.TEXT); const node = new mathMLTree.MathNode("mpadded", [buildGroup$1(group.body, newStyle)]); const dy = calculateSize(group.dy, style); node.setAttribute("voffset", dy.number + dy.unit); // Add padding, which acts to increase height in Chromium. // TODO: Figure out some way to change height in Firefox w/o breaking Chromium. if (dy.number > 0) { node.style.padding = dy.number + dy.unit + " 0 0 0"; } else { node.style.padding = "0 0 " + Math.abs(dy.number) + dy.unit + " 0"; } return node }; defineFunction({ type: "raise", names: ["\\raise", "\\lower"], props: { numArgs: 2, argTypes: ["size", "primitive"], primitive: true }, handler({ parser, funcName }, args) { const amount = assertNodeType(args[0], "size").value; if (funcName === "\\lower") { amount.number *= -1; } const body = args[1]; return { type: "raise", mode: parser.mode, dy: amount, body }; }, mathmlBuilder }); defineFunction({ type: "raise", names: ["\\raisebox"], props: { numArgs: 2, argTypes: ["size", "hbox"], allowedInText: true }, handler({ parser, funcName }, args) { const amount = assertNodeType(args[0], "size").value; const body = args[1]; return { type: "raise", mode: parser.mode, dy: amount, body }; }, mathmlBuilder }); defineFunction({ type: "ref", names: ["\\ref", "\\eqref"], props: { numArgs: 1, argTypes: ["raw"] }, handler({ parser, funcName }, args) { return { type: "ref", mode: parser.mode, funcName, string: args[0].string.replace(invalidIdRegEx, "") }; }, mathmlBuilder(group, style) { // Create an empty <a> node. Set a class and an href attribute. // The post-processor will populate with the target's tag or equation number. const classes = group.funcName === "\\ref" ? ["tml-ref"] : ["tml-ref", "tml-eqref"]; return new AnchorNode("#" + group.string, classes, null) } }); defineFunction({ type: "reflect", names: ["\\reflectbox"], props: { numArgs: 1, argTypes: ["hbox"], allowedInText: true }, handler({ parser }, args) { return { type: "reflect", mode: parser.mode, body: args[0] }; }, mathmlBuilder(group, style) { const node = buildGroup$1(group.body, style); node.style.transform = "scaleX(-1)"; return node } }); defineFunction({ type: "internal", names: ["\\relax"], props: { numArgs: 0, allowedInText: true, allowedInArgument: true }, handler({ parser }) { return { type: "internal", mode: parser.mode }; } }); defineFunction({ type: "rule", names: ["\\rule"], props: { numArgs: 2, numOptionalArgs: 1, allowedInText: true, allowedInMath: true, argTypes: ["size", "size", "size"] }, handler({ parser }, args, optArgs) { const shift = optArgs[0]; const width = assertNodeType(args[0], "size"); const height = assertNodeType(args[1], "size"); return { type: "rule", mode: parser.mode, shift: shift && assertNodeType(shift, "size").value, width: width.value, height: height.value }; }, mathmlBuilder(group, style) { const width = calculateSize(group.width, style); const height = calculateSize(group.height, style); const shift = group.shift ? calculateSize(group.shift, style) : { number: 0, unit: "em" }; const color = (style.color && style.getColor()) || "black"; const rule = new mathMLTree.MathNode("mspace"); if (width.number > 0 && height.number > 0) { rule.setAttribute("mathbackground", color); } rule.setAttribute("width", width.number + width.unit); rule.setAttribute("height", height.number + height.unit); if (shift.number === 0) { return rule } const wrapper = new mathMLTree.MathNode("mpadded", [rule]); if (shift.number >= 0) { wrapper.setAttribute("height", "+" + shift.number + shift.unit); } else { wrapper.setAttribute("height", shift.number + shift.unit); wrapper.setAttribute("depth", "+" + -shift.number + shift.unit); } wrapper.setAttribute("voffset", shift.number + shift.unit); return wrapper; } }); const numRegEx = /^[0-9]$/; const unicodeNumSubs = { '0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄', '5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉' }; const unicodeNumSups = { '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹' }; defineFunction({ type: "sfrac", names: ["\\sfrac"], props: { numArgs: 2, allowedInText: true, allowedInMath: true }, handler({ parser }, args) { let numerator = ""; for (const node of args[0].body) { if (node.type !== "textord" || !numRegEx.test(node.text)) { throw new ParseError("Numerator must be an integer.", node) } numerator += node.text; } let denominator = ""; for (const node of args[1].body) { if (node.type !== "textord" || !numRegEx.test(node.text)) { throw new ParseError("Denominator must be an integer.", node) } denominator += node.text; } return { type: "sfrac", mode: parser.mode, numerator, denominator }; }, mathmlBuilder(group, style) { const numerator = group.numerator.split('').map(c => unicodeNumSups[c]).join(''); const denominator = group.denominator.split('').map(c => unicodeNumSubs[c]).join(''); // Use a fraction slash. const text = new mathMLTree.TextNode(numerator + "\u2044" + denominator, group.mode, style); return new mathMLTree.MathNode("mn", [text], ["special-fraction"]) } }); // The size mappings are taken from TeX with \normalsize=10pt. // We don't have to track script level. MathML does that. const sizeMap = { "\\tiny": 0.5, "\\sixptsize": 0.6, "\\Tiny": 0.6, "\\scriptsize": 0.7, "\\footnotesize": 0.8, "\\small": 0.9, "\\normalsize": 1.0, "\\large": 1.2, "\\Large": 1.44, "\\LARGE": 1.728, "\\huge": 2.074, "\\Huge": 2.488 }; defineFunction({ type: "sizing", names: [ "\\tiny", "\\sixptsize", "\\Tiny", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge" ], props: { numArgs: 0, allowedInText: true }, handler: ({ breakOnTokenText, funcName, parser }, args) => { if (parser.settings.strict && parser.mode === "math") { // eslint-disable-next-line no-console console.log(`Temml strict-mode warning: Command ${funcName} is invalid in math mode.`); } const body = parser.parseExpression(false, breakOnTokenText, true); return { type: "sizing", mode: parser.mode, funcName, body }; }, mathmlBuilder: (group, style) => { const newStyle = style.withFontSize(sizeMap[group.funcName]); const inner = buildExpression(group.body, newStyle); // Wrap with an <mstyle> element. const node = wrapWithMstyle(inner); const factor = (sizeMap[group.funcName] / style.fontSize).toFixed(4); node.setAttribute("mathsize", factor + "em"); return node; } }); // smash, with optional [tb], as in AMS defineFunction({ type: "smash", names: ["\\smash"], props: { numArgs: 1, numOptionalArgs: 1, allowedInText: true }, handler: ({ parser }, args, optArgs) => { let smashHeight = false; let smashDepth = false; const tbArg = optArgs[0] && assertNodeType(optArgs[0], "ordgroup"); if (tbArg) { // Optional [tb] argument is engaged. // ref: amsmath: \renewcommand{\smash}[1][tb]{% // def\mb@t{\ht}\def\mb@b{\dp}\def\mb@tb{\ht\z@\z@\dp}% let letter = ""; for (let i = 0; i < tbArg.body.length; ++i) { const node = tbArg.body[i]; // TODO: Write an AssertSymbolNode letter = node.text; if (letter === "t") { smashHeight = true; } else if (letter === "b") { smashDepth = true; } else { smashHeight = false; smashDepth = false; break; } } } else { smashHeight = true; smashDepth = true; } const body = args[0]; return { type: "smash", mode: parser.mode, body, smashHeight, smashDepth }; }, mathmlBuilder: (group, style) => { const node = new mathMLTree.MathNode("mpadded", [buildGroup$1(group.body, style)]); if (group.smashHeight) { node.setAttribute("height", "0px"); } if (group.smashDepth) { node.setAttribute("depth", "0px"); } return node; } }); // Letters that are x-height w/o a descender. const xHeights = ['a', 'c', 'e', 'ı', 'm', 'n', 'o', 'r', 's', 'u', 'v', 'w', 'x', 'z', 'α', 'ε', 'ι', 'κ', 'ν', 'ο', 'π', 'σ', 'τ', 'υ', 'ω', '\\alpha', '\\epsilon', "\\iota", '\\kappa', '\\nu', '\\omega', '\\pi', '\\tau', '\\omega']; defineFunction({ type: "sqrt", names: ["\\sqrt"], props: { numArgs: 1, numOptionalArgs: 1 }, handler({ parser }, args, optArgs) { const index = optArgs[0]; const body = args[0]; // Check if the body consists entirely of an x-height letter. // TODO: Remove this check after Chromium is fixed. if (body.body && body.body.length === 1 && body.body[0].text && xHeights.includes(body.body[0].text)) { // Chromium does not put enough space above an x-height letter. // Insert a strut. body.body.push({ "type": "rule", "mode": "math", "shift": null, "width": { "number": 0, "unit": "pt" }, "height": { "number": 0.5, "unit": "em" } }); } return { type: "sqrt", mode: parser.mode, body, index }; }, mathmlBuilder(group, style) { const { body, index } = group; return index ? new mathMLTree.MathNode("mroot", [ buildGroup$1(body, style), buildGroup$1(index, style.incrementLevel()) ]) : new mathMLTree.MathNode("msqrt", [buildGroup$1(body, style)]); } }); const styleMap = { display: 0, text: 1, script: 2, scriptscript: 3 }; const styleAttributes = { display: ["0", "true"], text: ["0", "false"], script: ["1", "false"], scriptscript: ["2", "false"] }; defineFunction({ type: "styling", names: ["\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle"], props: { numArgs: 0, allowedInText: true, primitive: true }, handler({ breakOnTokenText, funcName, parser }, args) { // parse out the implicit body const body = parser.parseExpression(true, breakOnTokenText, true); const scriptLevel = funcName.slice(1, funcName.length - 5); return { type: "styling", mode: parser.mode, // Figure out what scriptLevel to use by pulling out the scriptLevel from // the function name scriptLevel, body }; }, mathmlBuilder(group, style) { // Figure out what scriptLevel we're changing to. const newStyle = style.withLevel(styleMap[group.scriptLevel]); // The style argument in the next line does NOT directly set a MathML script level. // It just tracks the style level, in case we need to know it for supsub or mathchoice. const inner = buildExpression(group.body, newStyle); // Wrap with an <mstyle> element. const node = wrapWithMstyle(inner); const attr = styleAttributes[group.scriptLevel]; // Here is where we set the MathML script level. node.setAttribute("scriptlevel", attr[0]); node.setAttribute("displaystyle", attr[1]); return node; } }); /** * Sometimes, groups perform special rules when they have superscripts or * subscripts attached to them. This function lets the `supsub` group know that * Sometimes, groups perform special rules when they have superscripts or * its inner element should handle the superscripts and subscripts instead of * handling them itself. */ // Helpers const symbolRegEx = /^m(over|under|underover)$/; // From the KaTeX font metrics, identify letters that encroach on a superscript. const smallPad = "DHKLUcegorsuvxyzΠΥΨαδηιμνοτυχϵ"; const mediumPad = "BCEFGIMNOPQRSTXZlpqtwΓΘΞΣΦΩβεζθξρςφψϑϕϱ"; const largePad = "AJdfΔΛ"; // Super scripts and subscripts, whose precise placement can depend on other // functions that precede them. defineFunctionBuilders({ type: "supsub", mathmlBuilder(group, style) { // Is the inner group a relevant horizontal brace? let isBrace = false; let isOver; let isSup; let appendApplyFunction = false; let appendSpace = false; let needsLeadingSpace = false; if (group.base && group.base.type === "horizBrace") { isSup = !!group.sup; if (isSup === group.base.isOver) { isBrace = true; isOver = group.base.isOver; } } if (group.base && !group.stack && (group.base.type === "op" || group.base.type === "operatorname")) { group.base.parentIsSupSub = true; appendApplyFunction = !group.base.symbol; appendSpace = appendApplyFunction && !group.isFollowedByDelimiter; needsLeadingSpace = group.base.needsLeadingSpace; } const children = group.stack && group.base.body.length === 1 ? [buildGroup$1(group.base.body[0], style)] : [buildGroup$1(group.base, style)]; // Note regarding scriptstyle level. // (Sub|super)scripts should not shrink beyond MathML scriptlevel 2 aka \scriptscriptstyle // Ref: https://w3c.github.io/mathml-core/#the-displaystyle-and-scriptlevel-attributes // (BTW, MathML scriptlevel 2 is equal to Temml level 3.) // But Chromium continues to shrink the (sub|super)scripts. So we explicitly set scriptlevel 2. const childStyle = style.inSubOrSup(); if (group.sub) { const sub = buildGroup$1(group.sub, childStyle); if (style.level === 3) { sub.setAttribute("scriptlevel", "2"); } children.push(sub); } if (group.sup) { const sup = buildGroup$1(group.sup, childStyle); if (style.level === 3) { sup.setAttribute("scriptlevel", "2"); } if (group.base && group.base.text && group.base.text.length === 1) { // Make an italic correction on the superscript. const text = group.base.text; if (smallPad.indexOf(text) > -1) { sup.classes.push("tml-sml-pad"); } else if (mediumPad.indexOf(text) > -1) { sup.classes.push("tml-med-pad"); } else if (largePad.indexOf(text) > -1) { sup.classes.push("tml-lrg-pad"); } } children.push(sup); } let nodeType; if (isBrace) { nodeType = isOver ? "mover" : "munder"; } else if (!group.sub) { const base = group.base; if ( base && base.type === "op" && base.limits && (style.level === StyleLevel.DISPLAY || base.alwaysHandleSupSub) ) { nodeType = "mover"; } else if ( base && base.type === "operatorname" && base.alwaysHandleSupSub && (base.limits || style.level === StyleLevel.DISPLAY) ) { nodeType = "mover"; } else { nodeType = "msup"; } } else if (!group.sup) { const base = group.base; if (group.stack) { nodeType = "munder"; } else if ( base && base.type === "op" && base.limits && (style.level === StyleLevel.DISPLAY || base.alwaysHandleSupSub) ) { nodeType = "munder"; } else if ( base && base.type === "operatorname" && base.alwaysHandleSupSub && (base.limits || style.level === StyleLevel.DISPLAY) ) { nodeType = "munder"; } else { nodeType = "msub"; } } else { const base = group.base; if (base && ((base.type === "op" && base.limits) || base.type === "multiscript") && (style.level === StyleLevel.DISPLAY || base.alwaysHandleSupSub) ) { nodeType = "munderover"; } else if ( base && base.type === "operatorname" && base.alwaysHandleSupSub && (style.level === StyleLevel.DISPLAY || base.limits) ) { nodeType = "munderover"; } else { nodeType = "msubsup"; } } let node = new mathMLTree.MathNode(nodeType, children); if (appendApplyFunction) { // Append an <mo>⁡</mo>. // ref: https://www.w3.org/TR/REC-MathML/chap3_2.html#sec3.2.4 const operator = new mathMLTree.MathNode("mo", [makeText("\u2061", "text")]); if (needsLeadingSpace) { const space = new mathMLTree.MathNode("mspace"); space.setAttribute("width", "0.1667em"); // thin space. node = mathMLTree.newDocumentFragment([space, node, operator]); } else { node = mathMLTree.newDocumentFragment([node, operator]); } if (appendSpace) { const space = new mathMLTree.MathNode("mspace"); space.setAttribute("width", "0.1667em"); // thin space. node.children.push(space); } } else if (symbolRegEx.test(nodeType)) { // Wrap in a <mrow>. Otherwise Firefox stretchy parens will not stretch to include limits. node = new mathMLTree.MathNode("mrow", [node]); } return node } }); // Operator ParseNodes created in Parser.js from symbol Groups in src/symbols.js. const short = ["\\shortmid", "\\nshortmid", "\\shortparallel", "\\nshortparallel", "\\smallsetminus"]; const arrows = ["\\Rsh", "\\Lsh", "\\restriction"]; const isArrow = str => { if (str.length === 1) { const codePoint = str.codePointAt(0); return (0x218f < codePoint && codePoint < 0x2200) } return str.indexOf("arrow") > -1 || str.indexOf("harpoon") > -1 || arrows.includes(str) }; defineFunctionBuilders({ type: "atom", mathmlBuilder(group, style) { const node = new mathMLTree.MathNode("mo", [makeText(group.text, group.mode)]); if (group.family === "punct") { node.setAttribute("separator", "true"); } else if (group.family === "open" || group.family === "close") { // Delims built here should not stretch vertically. // See delimsizing.js for stretchy delims. if (group.family === "open") { node.setAttribute("form", "prefix"); // Set an explicit attribute for stretch. Otherwise Firefox may do it wrong. node.setAttribute("stretchy", "false"); } else if (group.family === "close") { node.setAttribute("form", "postfix"); node.setAttribute("stretchy", "false"); } } else if (group.text === "\\mid") { // Firefox messes up this spacing if at the end of an <mrow>. See it explicitly. node.setAttribute("lspace", "0.22em"); // medium space node.setAttribute("rspace", "0.22em"); node.setAttribute("stretchy", "false"); } else if (group.family === "rel" && isArrow(group.text)) { node.setAttribute("stretchy", "false"); } else if (short.includes(group.text)) { node.setAttribute("mathsize", "70%"); } else if (group.text === ":") { // ":" is not in the MathML operator dictionary. Give it BIN spacing. node.attributes.lspace = "0.2222em"; node.attributes.rspace = "0.2222em"; } else if (group.needsSpacing) { // Fix a MathML bug that occurs when a <mo> is between two <mtext> elements. if (group.family === "bin") { return new mathMLTree.MathNode("mrow", [padding(0.222), node, padding(0.222)]) } else { // REL spacing return new mathMLTree.MathNode("mrow", [padding(0.2778), node, padding(0.2778)]) } } return node; } }); /** * Maps TeX font commands to "mathvariant" attribute in buildMathML.js */ const fontMap = { // styles mathbf: "bold", mathrm: "normal", textit: "italic", mathit: "italic", mathnormal: "italic", // families mathbb: "double-struck", mathcal: "script", mathfrak: "fraktur", mathscr: "script", mathsf: "sans-serif", mathtt: "monospace" }; /** * Returns the math variant as a string or null if none is required. */ const getVariant = function(group, style) { // Handle font specifiers as best we can. // Chromium does not support the MathML mathvariant attribute. // So we'll use Unicode replacement characters instead. // But first, determine the math variant. // Deal with the \textit, \textbf, etc., functions. if (style.fontFamily === "texttt") { return "monospace" } else if (style.fontFamily === "textsc") { return "normal"; // handled via character substitution in symbolsOrd.js. } else if (style.fontFamily === "textsf") { if (style.fontShape === "textit" && style.fontWeight === "textbf") { return "sans-serif-bold-italic" } else if (style.fontShape === "textit") { return "sans-serif-italic" } else if (style.fontWeight === "textbf") { return "sans-serif-bold" } else { return "sans-serif" } } else if (style.fontShape === "textit" && style.fontWeight === "textbf") { return "bold-italic" } else if (style.fontShape === "textit") { return "italic" } else if (style.fontWeight === "textbf") { return "bold" } // Deal with the \mathit, mathbf, etc, functions. const font = style.font; if (!font || font === "mathnormal") { return null } const mode = group.mode; switch (font) { case "mathit": return "italic" case "mathrm": { const codePoint = group.text.codePointAt(0); // LaTeX \mathrm returns italic for Greek characters. return (0x03ab < codePoint && codePoint < 0x03cf) ? "italic" : "normal" } case "greekItalic": return "italic" case "up@greek": return "normal" case "boldsymbol": case "mathboldsymbol": return "bold-italic" case "mathbf": return "bold" case "mathbb": return "double-struck" case "mathfrak": return "fraktur" case "mathscr": case "mathcal": return "script" case "mathsf": return "sans-serif" case "mathsfit": return "sans-serif-italic" case "mathtt": return "monospace" } let text = group.text; if (symbols[mode][text] && symbols[mode][text].replace) { text = symbols[mode][text].replace; } return Object.prototype.hasOwnProperty.call(fontMap, font) ? fontMap[font] : null }; // Chromium does not support the MathML `mathvariant` attribute. // Instead, we replace ASCII characters with Unicode characters that // are defined in the font as bold, italic, double-struck, etc. // This module identifies those Unicode code points. // First, a few helpers. const script = Object.freeze({ B: 0x20EA, // Offset from ASCII B to Unicode script B E: 0x20EB, F: 0x20EB, H: 0x20C3, I: 0x20C7, L: 0x20C6, M: 0x20E6, R: 0x20C9, e: 0x20CA, g: 0x20A3, o: 0x20C5 }); const frak = Object.freeze({ C: 0x20EA, H: 0x20C4, I: 0x20C8, R: 0x20CA, Z: 0x20CE }); const bbb = Object.freeze({ C: 0x20BF, // blackboard bold H: 0x20C5, N: 0x20C7, P: 0x20C9, Q: 0x20C9, R: 0x20CB, Z: 0x20CA }); const bold = Object.freeze({ "\u03f5": 0x1D2E7, // lunate epsilon "\u03d1": 0x1D30C, // vartheta "\u03f0": 0x1D2EE, // varkappa "\u03c6": 0x1D319, // varphi "\u03f1": 0x1D2EF, // varrho "\u03d6": 0x1D30B // varpi }); const boldItalic = Object.freeze({ "\u03f5": 0x1D35B, // lunate epsilon "\u03d1": 0x1D380, // vartheta "\u03f0": 0x1D362, // varkappa "\u03c6": 0x1D38D, // varphi "\u03f1": 0x1D363, // varrho "\u03d6": 0x1D37F // varpi }); const boldsf = Object.freeze({ "\u03f5": 0x1D395, // lunate epsilon "\u03d1": 0x1D3BA, // vartheta "\u03f0": 0x1D39C, // varkappa "\u03c6": 0x1D3C7, // varphi "\u03f1": 0x1D39D, // varrho "\u03d6": 0x1D3B9 // varpi }); const bisf = Object.freeze({ "\u03f5": 0x1D3CF, // lunate epsilon "\u03d1": 0x1D3F4, // vartheta "\u03f0": 0x1D3D6, // varkappa "\u03c6": 0x1D401, // varphi "\u03f1": 0x1D3D7, // varrho "\u03d6": 0x1D3F3 // varpi }); // Code point offsets below are derived from https://www.unicode.org/charts/PDF/U1D400.pdf const offset = Object.freeze({ upperCaseLatin: { // A-Z "normal": ch => { return 0 }, "bold": ch => { return 0x1D3BF }, "italic": ch => { return 0x1D3F3 }, "bold-italic": ch => { return 0x1D427 }, "script": ch => { return script[ch] || 0x1D45B }, "script-bold": ch => { return 0x1D48F }, "fraktur": ch => { return frak[ch] || 0x1D4C3 }, "fraktur-bold": ch => { return 0x1D52B }, "double-struck": ch => { return bbb[ch] || 0x1D4F7 }, "sans-serif": ch => { return 0x1D55F }, "sans-serif-bold": ch => { return 0x1D593 }, "sans-serif-italic": ch => { return 0x1D5C7 }, "sans-serif-bold-italic": ch => { return 0x1D63C }, "monospace": ch => { return 0x1D62F } }, lowerCaseLatin: { // a-z "normal": ch => { return 0 }, "bold": ch => { return 0x1D3B9 }, "italic": ch => { return ch === "h" ? 0x20A6 : 0x1D3ED }, "bold-italic": ch => { return 0x1D421 }, "script": ch => { return script[ch] || 0x1D455 }, "script-bold": ch => { return 0x1D489 }, "fraktur": ch => { return 0x1D4BD }, "fraktur-bold": ch => { return 0x1D525 }, "double-struck": ch => { return 0x1D4F1 }, "sans-serif": ch => { return 0x1D559 }, "sans-serif-bold": ch => { return 0x1D58D }, "sans-serif-italic": ch => { return 0x1D5C1 }, "sans-serif-bold-italic": ch => { return 0x1D5F5 }, "monospace": ch => { return 0x1D629 } }, upperCaseGreek: { // A-Ω "normal": ch => { return 0 }, "bold": ch => { return 0x1D317 }, "italic": ch => { return 0x1D351 }, // \boldsymbol actually returns upright bold for upperCaseGreek "bold-italic": ch => { return 0x1D317 }, "script": ch => { return 0 }, "script-bold": ch => { return 0 }, "fraktur": ch => { return 0 }, "fraktur-bold": ch => { return 0 }, "double-struck": ch => { return 0 }, // Unicode has no code points for regular-weight san-serif Greek. Use bold. "sans-serif": ch => { return 0x1D3C5 }, "sans-serif-bold": ch => { return 0x1D3C5 }, "sans-serif-italic": ch => { return 0 }, "sans-serif-bold-italic": ch => { return 0x1D3FF }, "monospace": ch => { return 0 } }, lowerCaseGreek: { // α-ω "normal": ch => { return 0 }, "bold": ch => { return 0x1D311 }, "italic": ch => { return 0x1D34B }, "bold-italic": ch => { return ch === "\u03d5" ? 0x1D37E : 0x1D385 }, "script": ch => { return 0 }, "script-bold": ch => { return 0 }, "fraktur": ch => { return 0 }, "fraktur-bold": ch => { return 0 }, "double-struck": ch => { return 0 }, // Unicode has no code points for regular-weight san-serif Greek. Use bold. "sans-serif": ch => { return 0x1D3BF }, "sans-serif-bold": ch => { return 0x1D3BF }, "sans-serif-italic": ch => { return 0 }, "sans-serif-bold-italic": ch => { return 0x1D3F9 }, "monospace": ch => { return 0 } }, varGreek: { // \varGamma, etc "normal": ch => { return 0 }, "bold": ch => { return bold[ch] || -51 }, "italic": ch => { return 0 }, "bold-italic": ch => { return boldItalic[ch] || 0x3A }, "script": ch => { return 0 }, "script-bold": ch => { return 0 }, "fraktur": ch => { return 0 }, "fraktur-bold": ch => { return 0 }, "double-struck": ch => { return 0 }, "sans-serif": ch => { return boldsf[ch] || 0x74 }, "sans-serif-bold": ch => { return boldsf[ch] || 0x74 }, "sans-serif-italic": ch => { return 0 }, "sans-serif-bold-italic": ch => { return bisf[ch] || 0xAE }, "monospace": ch => { return 0 } }, numeral: { // 0-9 "normal": ch => { return 0 }, "bold": ch => { return 0x1D79E }, "italic": ch => { return 0 }, "bold-italic": ch => { return 0 }, "script": ch => { return 0 }, "script-bold": ch => { return 0 }, "fraktur": ch => { return 0 }, "fraktur-bold": ch => { return 0 }, "double-struck": ch => { return 0x1D7A8 }, "sans-serif": ch => { return 0x1D7B2 }, "sans-serif-bold": ch => { return 0x1D7BC }, "sans-serif-italic": ch => { return 0 }, "sans-serif-bold-italic": ch => { return 0 }, "monospace": ch => { return 0x1D7C6 } } }); const variantChar = (ch, variant) => { const codePoint = ch.codePointAt(0); const block = 0x40 < codePoint && codePoint < 0x5b ? "upperCaseLatin" : 0x60 < codePoint && codePoint < 0x7b ? "lowerCaseLatin" : (0x390 < codePoint && codePoint < 0x3AA) ? "upperCaseGreek" : 0x3B0 < codePoint && codePoint < 0x3CA || ch === "\u03d5" ? "lowerCaseGreek" : 0x1D6E1 < codePoint && codePoint < 0x1D6FC || bold[ch] ? "varGreek" : (0x2F < codePoint && codePoint < 0x3A) ? "numeral" : "other"; return block === "other" ? ch : String.fromCodePoint(codePoint + offset[block][variant](ch)) }; const smallCaps = Object.freeze({ a: "ᴀ", b: "ʙ", c: "ᴄ", d: "ᴅ", e: "ᴇ", f: "ꜰ", g: "ɢ", h: "ʜ", i: "ɪ", j: "ᴊ", k: "ᴋ", l: "ʟ", m: "ᴍ", n: "ɴ", o: "ᴏ", p: "ᴘ", q: "ǫ", r: "ʀ", s: "s", t: "ᴛ", u: "ᴜ", v: "ᴠ", w: "ᴡ", x: "x", y: "ʏ", z: "ᴢ" }); // "mathord" and "textord" ParseNodes created in Parser.js from symbol Groups in // src/symbols.js. const numberRegEx = /^\d(?:[\d,.]*\d)?$/; const latinRegEx = /[A-Ba-z]/; const primes = new Set(["\\prime", "\\dprime", "\\trprime", "\\qprime", "\\backprime", "\\backdprime", "\\backtrprime"]); const italicNumber = (text, variant, tag) => { const mn = new mathMLTree.MathNode(tag, [text]); const wrapper = new mathMLTree.MathNode("mstyle", [mn]); wrapper.style["font-style"] = "italic"; wrapper.style["font-family"] = "Cambria, 'Times New Roman', serif"; if (variant === "bold-italic") { wrapper.style["font-weight"] = "bold"; } return wrapper }; defineFunctionBuilders({ type: "mathord", mathmlBuilder(group, style) { const text = makeText(group.text, group.mode, style); const codePoint = text.text.codePointAt(0); // Test for upper-case Greek const defaultVariant = (0x0390 < codePoint && codePoint < 0x03aa) ? "normal" : "italic"; const variant = getVariant(group, style) || defaultVariant; if (variant === "script") { text.text = variantChar(text.text, variant); return new mathMLTree.MathNode("mi", [text], [style.font]) } else if (variant !== "italic") { text.text = variantChar(text.text, variant); } let node = new mathMLTree.MathNode("mi", [text]); // TODO: Handle U+1D49C - U+1D4CF per https://www.unicode.org/charts/PDF/U1D400.pdf if (variant === "normal") { node.setAttribute("mathvariant", "normal"); if (text.text.length === 1) { // A Firefox bug will apply spacing here, but there should be none. Fix it. node = new mathMLTree.MathNode("mpadded", [node]); node.setAttribute("lspace", "0"); } } return node } }); defineFunctionBuilders({ type: "textord", mathmlBuilder(group, style) { let ch = group.text; const codePoint = ch.codePointAt(0); if (style.fontFamily === "textsc") { // Convert small latin letters to small caps. if (96 < codePoint && codePoint < 123) { ch = smallCaps[ch]; } } const text = makeText(ch, group.mode, style); const variant = getVariant(group, style) || "normal"; let node; if (numberRegEx.test(group.text)) { const tag = group.mode === "text" ? "mtext" : "mn"; if (variant === "italic" || variant === "bold-italic") { return italicNumber(text, variant, tag) } else { if (variant !== "normal") { text.text = text.text.split("").map(c => variantChar(c, variant)).join(""); } node = new mathMLTree.MathNode(tag, [text]); } } else if (group.mode === "text") { if (variant !== "normal") { text.text = variantChar(text.text, variant); } node = new mathMLTree.MathNode("mtext", [text]); } else if (primes.has(group.text)) { node = new mathMLTree.MathNode("mo", [text]); // TODO: If/when Chromium uses ssty variant for prime, remove the next line. node.classes.push("tml-prime"); } else { const origText = text.text; if (variant !== "italic") { text.text = variantChar(text.text, variant); } node = new mathMLTree.MathNode("mi", [text]); if (text.text === origText && latinRegEx.test(origText)) { node.setAttribute("mathvariant", "italic"); } } return node } }); // A map of CSS-based spacing functions to their CSS class. const cssSpace = { "\\nobreak": "nobreak", "\\allowbreak": "allowbreak" }; // A lookup table to determine whether a spacing function/symbol should be // treated like a regular space character. If a symbol or command is a key // in this table, then it should be a regular space character. Furthermore, // the associated value may have a `className` specifying an extra CSS class // to add to the created `span`. const regularSpace = { " ": {}, "\\ ": {}, "~": { className: "nobreak" }, "\\space": {}, "\\nobreakspace": { className: "nobreak" } }; // ParseNode<"spacing"> created in Parser.js from the "spacing" symbol Groups in // src/symbols.js. defineFunctionBuilders({ type: "spacing", mathmlBuilder(group, style) { let node; if (Object.prototype.hasOwnProperty.call(regularSpace, group.text)) { // Firefox does not render a space in a <mtext> </mtext>. So write a no-break space. // TODO: If Firefox fixes that bug, uncomment the next line and write ch into the node. //const ch = (regularSpace[group.text].className === "nobreak") ? "\u00a0" : " " node = new mathMLTree.MathNode("mtext", [new mathMLTree.TextNode("\u00a0")]); } else if (Object.prototype.hasOwnProperty.call(cssSpace, group.text)) { // MathML 3.0 calls for nobreak to occur in an <mo>, not an <mtext> // Ref: https://www.w3.org/Math/draft-spec/mathml.html#chapter3_presm.lbattrs node = new mathMLTree.MathNode("mo"); if (group.text === "\\nobreak") { node.setAttribute("linebreak", "nobreak"); } } else { throw new ParseError(`Unknown type of space "${group.text}"`) } return node } }); defineFunctionBuilders({ type: "tag" }); // For a \tag, the work usually done in a mathmlBuilder is instead done in buildMathML.js. // That way, a \tag can be pulled out of the parse tree and wrapped around the outer node. // Non-mathy text, possibly in a font const textFontFamilies = { "\\text": undefined, "\\textrm": "textrm", "\\textsf": "textsf", "\\texttt": "texttt", "\\textnormal": "textrm", "\\textsc": "textsc" // small caps }; const textFontWeights = { "\\textbf": "textbf", "\\textmd": "textmd" }; const textFontShapes = { "\\textit": "textit", "\\textup": "textup" }; const styleWithFont = (group, style) => { const font = group.font; // Checks if the argument is a font family or a font style. if (!font) { return style; } else if (textFontFamilies[font]) { return style.withTextFontFamily(textFontFamilies[font]); } else if (textFontWeights[font]) { return style.withTextFontWeight(textFontWeights[font]); } else if (font === "\\emph") { return style.fontShape === "textit" ? style.withTextFontShape("textup") : style.withTextFontShape("textit") } return style.withTextFontShape(textFontShapes[font]) }; defineFunction({ type: "text", names: [ // Font families "\\text", "\\textrm", "\\textsf", "\\texttt", "\\textnormal", "\\textsc", // Font weights "\\textbf", "\\textmd", // Font Shapes "\\textit", "\\textup", "\\emph" ], props: { numArgs: 1, argTypes: ["text"], allowedInArgument: true, allowedInText: true }, handler({ parser, funcName }, args) { const body = args[0]; return { type: "text", mode: parser.mode, body: ordargument(body), font: funcName }; }, mathmlBuilder(group, style) { const newStyle = styleWithFont(group, style); const mrow = buildExpressionRow(group.body, newStyle); return consolidateText(mrow) } }); // \vcenter: Vertically center the argument group on the math axis. defineFunction({ type: "vcenter", names: ["\\vcenter"], props: { numArgs: 1, argTypes: ["original"], allowedInText: false }, handler({ parser }, args) { return { type: "vcenter", mode: parser.mode, body: args[0] }; }, mathmlBuilder(group, style) { // Use a math table to create vertically centered content. const mtd = new mathMLTree.MathNode("mtd", [buildGroup$1(group.body, style)]); mtd.style.padding = "0"; const mtr = new mathMLTree.MathNode("mtr", [mtd]); return new mathMLTree.MathNode("mtable", [mtr]) } }); defineFunction({ type: "verb", names: ["\\verb"], props: { numArgs: 0, allowedInText: true }, handler(context, args, optArgs) { // \verb and \verb* are dealt with directly in Parser.js. // If we end up here, it's because of a failure to match the two delimiters // in the regex in Lexer.js. LaTeX raises the following error when \verb is // terminated by end of line (or file). throw new ParseError("\\verb ended by end of line instead of matching delimiter"); }, mathmlBuilder(group, style) { const text = new mathMLTree.TextNode(makeVerb(group)); const node = new mathMLTree.MathNode("mtext", [text]); node.setAttribute("mathvariant", "monospace"); return node; } }); /** * Converts verb group into body string. * * \verb* replaces each space with an open box \u2423 * \verb replaces each space with a no-break space \xA0 */ const makeVerb = (group) => group.body.replace(/ /g, group.star ? "\u2423" : "\xA0"); /** Include this to ensure that all functions are defined. */ const functions = _functions; /** * The Lexer class handles tokenizing the input in various ways. Since our * parser expects us to be able to backtrack, the lexer allows lexing from any * given starting point. * * Its main exposed function is the `lex` function, which takes a position to * lex from and a type of token to lex. It defers to the appropriate `_innerLex` * function. * * The various `_innerLex` functions perform the actual lexing of different * kinds. */ /* The following tokenRegex * - matches typical whitespace (but not NBSP etc.) using its first two groups * - does not match any control character \x00-\x1f except whitespace * - does not match a bare backslash * - matches any ASCII character except those just mentioned * - does not match the BMP private use area \uE000-\uF8FF * - does not match bare surrogate code units * - matches any BMP character except for those just described * - matches any valid Unicode surrogate pair * - mathches numerals * - matches a backslash followed by one or more whitespace characters * - matches a backslash followed by one or more letters then whitespace * - matches a backslash followed by any BMP character * Capturing groups: * [1] regular whitespace * [2] backslash followed by whitespace * [3] anything else, which may include: * [4] left character of \verb* * [5] left character of \verb * [6] backslash followed by word, excluding any trailing whitespace * Just because the Lexer matches something doesn't mean it's valid input: * If there is no matching function or symbol definition, the Parser will * still reject the input. */ const spaceRegexString = "[ \r\n\t]"; const controlWordRegexString = "\\\\[a-zA-Z@]+"; const controlSymbolRegexString = "\\\\[^\uD800-\uDFFF]"; const controlWordWhitespaceRegexString = `(${controlWordRegexString})${spaceRegexString}*`; const controlSpaceRegexString = "\\\\(\n|[ \r\t]+\n?)[ \r\t]*"; const combiningDiacriticalMarkString = "[\u0300-\u036f]"; const combiningDiacriticalMarksEndRegex = new RegExp(`${combiningDiacriticalMarkString}+$`); const tokenRegexString = `(${spaceRegexString}+)|` + // whitespace `${controlSpaceRegexString}|` + // whitespace "([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint `${combiningDiacriticalMarkString}*` + // ...plus accents "|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair `${combiningDiacriticalMarkString}*` + // ...plus accents "|\\\\verb\\*([^]).*?\\4" + // \verb* "|\\\\verb([^*a-zA-Z]).*?\\5" + // \verb unstarred `|${controlWordWhitespaceRegexString}` + // \macroName + spaces `|${controlSymbolRegexString})`; // \\, \', etc. /** Main Lexer class */ class Lexer { constructor(input, settings) { // Separate accents from characters this.input = input; this.settings = settings; this.tokenRegex = new RegExp(tokenRegexString, 'g'); // Category codes. The lexer only supports comment characters (14) for now. // MacroExpander additionally distinguishes active (13). this.catcodes = { "%": 14, // comment character "~": 13 // active character }; } setCatcode(char, code) { this.catcodes[char] = code; } /** * This function lexes a single token. */ lex() { const input = this.input; const pos = this.tokenRegex.lastIndex; if (pos === input.length) { return new Token("EOF", new SourceLocation(this, pos, pos)); } const match = this.tokenRegex.exec(input); if (match === null || match.index !== pos) { throw new ParseError( `Unexpected character: '${input[pos]}'`, new Token(input[pos], new SourceLocation(this, pos, pos + 1)) ); } const text = match[6] || match[3] || (match[2] ? "\\ " : " "); if (this.catcodes[text] === 14) { // comment character const nlIndex = input.indexOf("\n", this.tokenRegex.lastIndex); if (nlIndex === -1) { this.tokenRegex.lastIndex = input.length; // EOF if (this.settings.strict) { throw new ParseError("% comment has no terminating newline; LaTeX would " + "fail because of commenting the end of math mode") } } else { this.tokenRegex.lastIndex = nlIndex + 1; } return this.lex(); } return new Token(text, new SourceLocation(this, pos, this.tokenRegex.lastIndex)); } } /** * A `Namespace` refers to a space of nameable things like macros or lengths, * which can be `set` either globally or local to a nested group, using an * undo stack similar to how TeX implements this functionality. * Performance-wise, `get` and local `set` take constant time, while global * `set` takes time proportional to the depth of group nesting. */ class Namespace { /** * Both arguments are optional. The first argument is an object of * built-in mappings which never change. The second argument is an object * of initial (global-level) mappings, which will constantly change * according to any global/top-level `set`s done. */ constructor(builtins = {}, globalMacros = {}) { this.current = globalMacros; this.builtins = builtins; this.undefStack = []; } /** * Start a new nested group, affecting future local `set`s. */ beginGroup() { this.undefStack.push({}); } /** * End current nested group, restoring values before the group began. */ endGroup() { if (this.undefStack.length === 0) { throw new ParseError( "Unbalanced namespace destruction: attempt " + "to pop global namespace; please report this as a bug" ); } const undefs = this.undefStack.pop(); for (const undef in undefs) { if (Object.prototype.hasOwnProperty.call(undefs, undef )) { if (undefs[undef] === undefined) { delete this.current[undef]; } else { this.current[undef] = undefs[undef]; } } } } /** * Detect whether `name` has a definition. Equivalent to * `get(name) != null`. */ has(name) { return Object.prototype.hasOwnProperty.call(this.current, name ) || Object.prototype.hasOwnProperty.call(this.builtins, name ); } /** * Get the current value of a name, or `undefined` if there is no value. * * Note: Do not use `if (namespace.get(...))` to detect whether a macro * is defined, as the definition may be the empty string which evaluates * to `false` in JavaScript. Use `if (namespace.get(...) != null)` or * `if (namespace.has(...))`. */ get(name) { if (Object.prototype.hasOwnProperty.call(this.current, name )) { return this.current[name]; } else { return this.builtins[name]; } } /** * Set the current value of a name, and optionally set it globally too. * Local set() sets the current value and (when appropriate) adds an undo * operation to the undo stack. Global set() may change the undo * operation at every level, so takes time linear in their number. */ set(name, value, global = false) { if (global) { // Global set is equivalent to setting in all groups. Simulate this // by destroying any undos currently scheduled for this name, // and adding an undo with the *new* value (in case it later gets // locally reset within this environment). for (let i = 0; i < this.undefStack.length; i++) { delete this.undefStack[i][name]; } if (this.undefStack.length > 0) { this.undefStack[this.undefStack.length - 1][name] = value; } } else { // Undo this set at end of this group (possibly to `undefined`), // unless an undo is already in place, in which case that older // value is the correct one. const top = this.undefStack[this.undefStack.length - 1]; if (top && !Object.prototype.hasOwnProperty.call(top, name )) { top[name] = this.current[name]; } } this.current[name] = value; } } /** * This file contains the “gullet” where macros are expanded * until only non-macro tokens remain. */ // List of commands that act like macros but aren't defined as a macro, // function, or symbol. Used in `isDefined`. const implicitCommands = { "^": true, // Parser.js _: true, // Parser.js "\\limits": true, // Parser.js "\\nolimits": true // Parser.js }; class MacroExpander { constructor(input, settings, mode) { this.settings = settings; this.expansionCount = 0; this.feed(input); // Make new global namespace this.macros = new Namespace(macros, settings.macros); this.mode = mode; this.stack = []; // contains tokens in REVERSE order } /** * Feed a new input string to the same MacroExpander * (with existing macros etc.). */ feed(input) { this.lexer = new Lexer(input, this.settings); } /** * Switches between "text" and "math" modes. */ switchMode(newMode) { this.mode = newMode; } /** * Start a new group nesting within all namespaces. */ beginGroup() { this.macros.beginGroup(); } /** * End current group nesting within all namespaces. */ endGroup() { this.macros.endGroup(); } /** * Returns the topmost token on the stack, without expanding it. * Similar in behavior to TeX's `\futurelet`. */ future() { if (this.stack.length === 0) { this.pushToken(this.lexer.lex()); } return this.stack[this.stack.length - 1] } /** * Remove and return the next unexpanded token. */ popToken() { this.future(); // ensure non-empty stack return this.stack.pop(); } /** * Add a given token to the token stack. In particular, this get be used * to put back a token returned from one of the other methods. */ pushToken(token) { this.stack.push(token); } /** * Append an array of tokens to the token stack. */ pushTokens(tokens) { this.stack.push(...tokens); } /** * Find an macro argument without expanding tokens and append the array of * tokens to the token stack. Uses Token as a container for the result. */ scanArgument(isOptional) { let start; let end; let tokens; if (isOptional) { this.consumeSpaces(); // \@ifnextchar gobbles any space following it if (this.future().text !== "[") { return null; } start = this.popToken(); // don't include [ in tokens ({ tokens, end } = this.consumeArg(["]"])); } else { ({ tokens, start, end } = this.consumeArg()); } // indicate the end of an argument this.pushToken(new Token("EOF", end.loc)); this.pushTokens(tokens); return start.range(end, ""); } /** * Consume all following space tokens, without expansion. */ consumeSpaces() { for (;;) { const token = this.future(); if (token.text === " ") { this.stack.pop(); } else { break; } } } /** * Consume an argument from the token stream, and return the resulting array * of tokens and start/end token. */ consumeArg(delims) { // The argument for a delimited parameter is the shortest (possibly // empty) sequence of tokens with properly nested {...} groups that is // followed ... by this particular list of non-parameter tokens. // The argument for an undelimited parameter is the next nonblank // token, unless that token is ‘{’, when the argument will be the // entire {...} group that follows. const tokens = []; const isDelimited = delims && delims.length > 0; if (!isDelimited) { // Ignore spaces between arguments. As the TeXbook says: // "After you have said ‘\def\row#1#2{...}’, you are allowed to // put spaces between the arguments (e.g., ‘\row x n’), because // TeX doesn’t use single spaces as undelimited arguments." this.consumeSpaces(); } const start = this.future(); let tok; let depth = 0; let match = 0; do { tok = this.popToken(); tokens.push(tok); if (tok.text === "{") { ++depth; } else if (tok.text === "}") { --depth; if (depth === -1) { throw new ParseError("Extra }", tok); } } else if (tok.text === "EOF") { throw new ParseError( "Unexpected end of input in a macro argument" + ", expected '" + (delims && isDelimited ? delims[match] : "}") + "'", tok ); } if (delims && isDelimited) { if ((depth === 0 || (depth === 1 && delims[match] === "{")) && tok.text === delims[match]) { ++match; if (match === delims.length) { // don't include delims in tokens tokens.splice(-match, match); break; } } else { match = 0; } } } while (depth !== 0 || isDelimited); // If the argument found ... has the form ‘{<nested tokens>}’, // ... the outermost braces enclosing the argument are removed if (start.text === "{" && tokens[tokens.length - 1].text === "}") { tokens.pop(); tokens.shift(); } tokens.reverse(); // to fit in with stack order return { tokens, start, end: tok }; } /** * Consume the specified number of (delimited) arguments from the token * stream and return the resulting array of arguments. */ consumeArgs(numArgs, delimiters) { if (delimiters) { if (delimiters.length !== numArgs + 1) { throw new ParseError("The length of delimiters doesn't match the number of args!"); } const delims = delimiters[0]; for (let i = 0; i < delims.length; i++) { const tok = this.popToken(); if (delims[i] !== tok.text) { throw new ParseError("Use of the macro doesn't match its definition", tok); } } } const args = []; for (let i = 0; i < numArgs; i++) { args.push(this.consumeArg(delimiters && delimiters[i + 1]).tokens); } return args; } /** * Expand the next token only once if possible. * * If the token is expanded, the resulting tokens will be pushed onto * the stack in reverse order, and the number of such tokens will be * returned. This number might be zero or positive. * * If not, the return value is `false`, and the next token remains at the * top of the stack. * * In either case, the next token will be on the top of the stack, * or the stack will be empty (in case of empty expansion * and no other tokens). * * Used to implement `expandAfterFuture` and `expandNextToken`. * * If expandableOnly, only expandable tokens are expanded and * an undefined control sequence results in an error. */ expandOnce(expandableOnly) { const topToken = this.popToken(); const name = topToken.text; const expansion = !topToken.noexpand ? this._getExpansion(name) : null; if (expansion == null || (expandableOnly && expansion.unexpandable)) { if (expandableOnly && expansion == null && name[0] === "\\" && !this.isDefined(name)) { throw new ParseError("Undefined control sequence: " + name); } this.pushToken(topToken); return false; } this.expansionCount++; if (this.expansionCount > this.settings.maxExpand) { throw new ParseError( "Too many expansions: infinite loop or " + "need to increase maxExpand setting" ); } let tokens = expansion.tokens; const args = this.consumeArgs(expansion.numArgs, expansion.delimiters); if (expansion.numArgs) { // paste arguments in place of the placeholders tokens = tokens.slice(); // make a shallow copy for (let i = tokens.length - 1; i >= 0; --i) { let tok = tokens[i]; if (tok.text === "#") { if (i === 0) { throw new ParseError("Incomplete placeholder at end of macro body", tok); } tok = tokens[--i]; // next token on stack if (tok.text === "#") { // ## → # tokens.splice(i + 1, 1); // drop first # } else if (/^[1-9]$/.test(tok.text)) { // replace the placeholder with the indicated argument tokens.splice(i, 2, ...args[+tok.text - 1]); } else { throw new ParseError("Not a valid argument number", tok); } } } } // Concatenate expansion onto top of stack. this.pushTokens(tokens); return tokens.length; } /** * Expand the next token only once (if possible), and return the resulting * top token on the stack (without removing anything from the stack). * Similar in behavior to TeX's `\expandafter\futurelet`. * Equivalent to expandOnce() followed by future(). */ expandAfterFuture() { this.expandOnce(); return this.future(); } /** * Recursively expand first token, then return first non-expandable token. */ expandNextToken() { for (;;) { if (this.expandOnce() === false) { // fully expanded const token = this.stack.pop(); // The token after \noexpand is interpreted as if its meaning were ‘\relax’ if (token.treatAsRelax) { token.text = "\\relax"; } return token } } // This pathway is impossible. throw new Error(); // eslint-disable-line no-unreachable } /** * Fully expand the given macro name and return the resulting list of * tokens, or return `undefined` if no such macro is defined. */ expandMacro(name) { return this.macros.has(name) ? this.expandTokens([new Token(name)]) : undefined; } /** * Fully expand the given token stream and return the resulting list of * tokens. Note that the input tokens are in reverse order, but the * output tokens are in forward order. */ expandTokens(tokens) { const output = []; const oldStackLength = this.stack.length; this.pushTokens(tokens); while (this.stack.length > oldStackLength) { // Expand only expandable tokens if (this.expandOnce(true) === false) { // fully expanded const token = this.stack.pop(); if (token.treatAsRelax) { // the expansion of \noexpand is the token itself token.noexpand = false; token.treatAsRelax = false; } output.push(token); } } return output; } /** * Fully expand the given macro name and return the result as a string, * or return `undefined` if no such macro is defined. */ expandMacroAsText(name) { const tokens = this.expandMacro(name); if (tokens) { return tokens.map((token) => token.text).join(""); } else { return tokens; } } /** * Returns the expanded macro as a reversed array of tokens and a macro * argument count. Or returns `null` if no such macro. */ _getExpansion(name) { const definition = this.macros.get(name); if (definition == null) { // mainly checking for undefined here return definition; } // If a single character has an associated catcode other than 13 // (active character), then don't expand it. if (name.length === 1) { const catcode = this.lexer.catcodes[name]; if (catcode != null && catcode !== 13) { return } } const expansion = typeof definition === "function" ? definition(this) : definition; if (typeof expansion === "string") { let numArgs = 0; if (expansion.indexOf("#") !== -1) { const stripped = expansion.replace(/##/g, ""); while (stripped.indexOf("#" + (numArgs + 1)) !== -1) { ++numArgs; } } const bodyLexer = new Lexer(expansion, this.settings); const tokens = []; let tok = bodyLexer.lex(); while (tok.text !== "EOF") { tokens.push(tok); tok = bodyLexer.lex(); } tokens.reverse(); // to fit in with stack using push and pop const expanded = { tokens, numArgs }; return expanded; } return expansion; } /** * Determine whether a command is currently "defined" (has some * functionality), meaning that it's a macro (in the current group), * a function, a symbol, or one of the special commands listed in * `implicitCommands`. */ isDefined(name) { return ( this.macros.has(name) || Object.prototype.hasOwnProperty.call(functions, name ) || Object.prototype.hasOwnProperty.call(symbols.math, name ) || Object.prototype.hasOwnProperty.call(symbols.text, name ) || Object.prototype.hasOwnProperty.call(implicitCommands, name ) ); } /** * Determine whether a command is expandable. */ isExpandable(name) { const macro = this.macros.get(name); return macro != null ? typeof macro === "string" || typeof macro === "function" || !macro.unexpandable : Object.prototype.hasOwnProperty.call(functions, name ) && !functions[name].primitive; } } // Helpers for Parser.js handling of Unicode (sub|super)script characters. const unicodeSubRegEx = /^[₊₋₌₍₎₀₁₂₃₄₅₆₇₈₉ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓᵦᵧᵨᵩᵪ]/; const uSubsAndSups = Object.freeze({ '₊': '+', '₋': '-', '₌': '=', '₍': '(', '₎': ')', '₀': '0', '₁': '1', '₂': '2', '₃': '3', '₄': '4', '₅': '5', '₆': '6', '₇': '7', '₈': '8', '₉': '9', '\u2090': 'a', '\u2091': 'e', '\u2095': 'h', '\u1D62': 'i', '\u2C7C': 'j', '\u2096': 'k', '\u2097': 'l', '\u2098': 'm', '\u2099': 'n', '\u2092': 'o', '\u209A': 'p', '\u1D63': 'r', '\u209B': 's', '\u209C': 't', '\u1D64': 'u', '\u1D65': 'v', '\u2093': 'x', '\u1D66': 'β', '\u1D67': 'γ', '\u1D68': 'ρ', '\u1D69': '\u03d5', '\u1D6A': 'χ', '⁺': '+', '⁻': '-', '⁼': '=', '⁽': '(', '⁾': ')', '⁰': '0', '¹': '1', '²': '2', '³': '3', '⁴': '4', '⁵': '5', '⁶': '6', '⁷': '7', '⁸': '8', '⁹': '9', '\u1D2C': 'A', '\u1D2E': 'B', '\u1D30': 'D', '\u1D31': 'E', '\u1D33': 'G', '\u1D34': 'H', '\u1D35': 'I', '\u1D36': 'J', '\u1D37': 'K', '\u1D38': 'L', '\u1D39': 'M', '\u1D3A': 'N', '\u1D3C': 'O', '\u1D3E': 'P', '\u1D3F': 'R', '\u1D40': 'T', '\u1D41': 'U', '\u2C7D': 'V', '\u1D42': 'W', '\u1D43': 'a', '\u1D47': 'b', '\u1D9C': 'c', '\u1D48': 'd', '\u1D49': 'e', '\u1DA0': 'f', '\u1D4D': 'g', '\u02B0': 'h', '\u2071': 'i', '\u02B2': 'j', '\u1D4F': 'k', '\u02E1': 'l', '\u1D50': 'm', '\u207F': 'n', '\u1D52': 'o', '\u1D56': 'p', '\u02B3': 'r', '\u02E2': 's', '\u1D57': 't', '\u1D58': 'u', '\u1D5B': 'v', '\u02B7': 'w', '\u02E3': 'x', '\u02B8': 'y', '\u1DBB': 'z', '\u1D5D': 'β', '\u1D5E': 'γ', '\u1D5F': 'δ', '\u1D60': '\u03d5', '\u1D61': 'χ', '\u1DBF': 'θ' }); // Used for Unicode input of calligraphic and script letters const asciiFromScript = Object.freeze({ "\ud835\udc9c": "A", "\u212c": "B", "\ud835\udc9e": "C", "\ud835\udc9f": "D", "\u2130": "E", "\u2131": "F", "\ud835\udca2": "G", "\u210B": "H", "\u2110": "I", "\ud835\udca5": "J", "\ud835\udca6": "K", "\u2112": "L", "\u2133": "M", "\ud835\udca9": "N", "\ud835\udcaa": "O", "\ud835\udcab": "P", "\ud835\udcac": "Q", "\u211B": "R", "\ud835\udcae": "S", "\ud835\udcaf": "T", "\ud835\udcb0": "U", "\ud835\udcb1": "V", "\ud835\udcb2": "W", "\ud835\udcb3": "X", "\ud835\udcb4": "Y", "\ud835\udcb5": "Z" }); // Mapping of Unicode accent characters to their LaTeX equivalent in text and // math mode (when they exist). var unicodeAccents = { "\u0301": { text: "\\'", math: "\\acute" }, "\u0300": { text: "\\`", math: "\\grave" }, "\u0308": { text: '\\"', math: "\\ddot" }, "\u0303": { text: "\\~", math: "\\tilde" }, "\u0304": { text: "\\=", math: "\\bar" }, "\u0306": { text: "\\u", math: "\\breve" }, "\u030c": { text: "\\v", math: "\\check" }, "\u0302": { text: "\\^", math: "\\hat" }, "\u0307": { text: "\\.", math: "\\dot" }, "\u030a": { text: "\\r", math: "\\mathring" }, "\u030b": { text: "\\H" }, '\u0327': { text: '\\c' } }; var unicodeSymbols = { "á": "á", "à": "à", "ä": "ä", "ǟ": "ǟ", "ã": "ã", "ā": "ā", "ă": "ă", "ắ": "ắ", "ằ": "ằ", "ẵ": "ẵ", "ǎ": "ǎ", "â": "â", "ấ": "ấ", "ầ": "ầ", "ẫ": "ẫ", "ȧ": "ȧ", "ǡ": "ǡ", "å": "å", "ǻ": "ǻ", "ḃ": "ḃ", "ć": "ć", "č": "č", "ĉ": "ĉ", "ċ": "ċ", "ď": "ď", "ḋ": "ḋ", "é": "é", "è": "è", "ë": "ë", "ẽ": "ẽ", "ē": "ē", "ḗ": "ḗ", "ḕ": "ḕ", "ĕ": "ĕ", "ě": "ě", "ê": "ê", "ế": "ế", "ề": "ề", "ễ": "ễ", "ė": "ė", "ḟ": "ḟ", "ǵ": "ǵ", "ḡ": "ḡ", "ğ": "ğ", "ǧ": "ǧ", "ĝ": "ĝ", "ġ": "ġ", "ḧ": "ḧ", "ȟ": "ȟ", "ĥ": "ĥ", "ḣ": "ḣ", "í": "í", "ì": "ì", "ï": "ï", "ḯ": "ḯ", "ĩ": "ĩ", "ī": "ī", "ĭ": "ĭ", "ǐ": "ǐ", "î": "î", "ǰ": "ǰ", "ĵ": "ĵ", "ḱ": "ḱ", "ǩ": "ǩ", "ĺ": "ĺ", "ľ": "ľ", "ḿ": "ḿ", "ṁ": "ṁ", "ń": "ń", "ǹ": "ǹ", "ñ": "ñ", "ň": "ň", "ṅ": "ṅ", "ó": "ó", "ò": "ò", "ö": "ö", "ȫ": "ȫ", "õ": "õ", "ṍ": "ṍ", "ṏ": "ṏ", "ȭ": "ȭ", "ō": "ō", "ṓ": "ṓ", "ṑ": "ṑ", "ŏ": "ŏ", "ǒ": "ǒ", "ô": "ô", "ố": "ố", "ồ": "ồ", "ỗ": "ỗ", "ȯ": "ȯ", "ȱ": "ȱ", "ő": "ő", "ṕ": "ṕ", "ṗ": "ṗ", "ŕ": "ŕ", "ř": "ř", "ṙ": "ṙ", "ś": "ś", "ṥ": "ṥ", "š": "š", "ṧ": "ṧ", "ŝ": "ŝ", "ṡ": "ṡ", "ẗ": "ẗ", "ť": "ť", "ṫ": "ṫ", "ú": "ú", "ù": "ù", "ü": "ü", "ǘ": "ǘ", "ǜ": "ǜ", "ǖ": "ǖ", "ǚ": "ǚ", "ũ": "ũ", "ṹ": "ṹ", "ū": "ū", "ṻ": "ṻ", "ŭ": "ŭ", "ǔ": "ǔ", "û": "û", "ů": "ů", "ű": "ű", "ṽ": "ṽ", "ẃ": "ẃ", "ẁ": "ẁ", "ẅ": "ẅ", "ŵ": "ŵ", "ẇ": "ẇ", "ẘ": "ẘ", "ẍ": "ẍ", "ẋ": "ẋ", "ý": "ý", "ỳ": "ỳ", "ÿ": "ÿ", "ỹ": "ỹ", "ȳ": "ȳ", "ŷ": "ŷ", "ẏ": "ẏ", "ẙ": "ẙ", "ź": "ź", "ž": "ž", "ẑ": "ẑ", "ż": "ż", "Á": "Á", "À": "À", "Ä": "Ä", "Ǟ": "Ǟ", "Ã": "Ã", "Ā": "Ā", "Ă": "Ă", "Ắ": "Ắ", "Ằ": "Ằ", "Ẵ": "Ẵ", "Ǎ": "Ǎ", "Â": "Â", "Ấ": "Ấ", "Ầ": "Ầ", "Ẫ": "Ẫ", "Ȧ": "Ȧ", "Ǡ": "Ǡ", "Å": "Å", "Ǻ": "Ǻ", "Ḃ": "Ḃ", "Ć": "Ć", "Č": "Č", "Ĉ": "Ĉ", "Ċ": "Ċ", "Ď": "Ď", "Ḋ": "Ḋ", "É": "É", "È": "È", "Ë": "Ë", "Ẽ": "Ẽ", "Ē": "Ē", "Ḗ": "Ḗ", "Ḕ": "Ḕ", "Ĕ": "Ĕ", "Ě": "Ě", "Ê": "Ê", "Ế": "Ế", "Ề": "Ề", "Ễ": "Ễ", "Ė": "Ė", "Ḟ": "Ḟ", "Ǵ": "Ǵ", "Ḡ": "Ḡ", "Ğ": "Ğ", "Ǧ": "Ǧ", "Ĝ": "Ĝ", "Ġ": "Ġ", "Ḧ": "Ḧ", "Ȟ": "Ȟ", "Ĥ": "Ĥ", "Ḣ": "Ḣ", "Í": "Í", "Ì": "Ì", "Ï": "Ï", "Ḯ": "Ḯ", "Ĩ": "Ĩ", "Ī": "Ī", "Ĭ": "Ĭ", "Ǐ": "Ǐ", "Î": "Î", "İ": "İ", "Ĵ": "Ĵ", "Ḱ": "Ḱ", "Ǩ": "Ǩ", "Ĺ": "Ĺ", "Ľ": "Ľ", "Ḿ": "Ḿ", "Ṁ": "Ṁ", "Ń": "Ń", "Ǹ": "Ǹ", "Ñ": "Ñ", "Ň": "Ň", "Ṅ": "Ṅ", "Ó": "Ó", "Ò": "Ò", "Ö": "Ö", "Ȫ": "Ȫ", "Õ": "Õ", "Ṍ": "Ṍ", "Ṏ": "Ṏ", "Ȭ": "Ȭ", "Ō": "Ō", "Ṓ": "Ṓ", "Ṑ": "Ṑ", "Ŏ": "Ŏ", "Ǒ": "Ǒ", "Ô": "Ô", "Ố": "Ố", "Ồ": "Ồ", "Ỗ": "Ỗ", "Ȯ": "Ȯ", "Ȱ": "Ȱ", "Ő": "Ő", "Ṕ": "Ṕ", "Ṗ": "Ṗ", "Ŕ": "Ŕ", "Ř": "Ř", "Ṙ": "Ṙ", "Ś": "Ś", "Ṥ": "Ṥ", "Š": "Š", "Ṧ": "Ṧ", "Ŝ": "Ŝ", "Ṡ": "Ṡ", "Ť": "Ť", "Ṫ": "Ṫ", "Ú": "Ú", "Ù": "Ù", "Ü": "Ü", "Ǘ": "Ǘ", "Ǜ": "Ǜ", "Ǖ": "Ǖ", "Ǚ": "Ǚ", "Ũ": "Ũ", "Ṹ": "Ṹ", "Ū": "Ū", "Ṻ": "Ṻ", "Ŭ": "Ŭ", "Ǔ": "Ǔ", "Û": "Û", "Ů": "Ů", "Ű": "Ű", "Ṽ": "Ṽ", "Ẃ": "Ẃ", "Ẁ": "Ẁ", "Ẅ": "Ẅ", "Ŵ": "Ŵ", "Ẇ": "Ẇ", "Ẍ": "Ẍ", "Ẋ": "Ẋ", "Ý": "Ý", "Ỳ": "Ỳ", "Ÿ": "Ÿ", "Ỹ": "Ỹ", "Ȳ": "Ȳ", "Ŷ": "Ŷ", "Ẏ": "Ẏ", "Ź": "Ź", "Ž": "Ž", "Ẑ": "Ẑ", "Ż": "Ż", "ά": "ά", "ὰ": "ὰ", "ᾱ": "ᾱ", "ᾰ": "ᾰ", "έ": "έ", "ὲ": "ὲ", "ή": "ή", "ὴ": "ὴ", "ί": "ί", "ὶ": "ὶ", "ϊ": "ϊ", "ΐ": "ΐ", "ῒ": "ῒ", "ῑ": "ῑ", "ῐ": "ῐ", "ό": "ό", "ὸ": "ὸ", "ύ": "ύ", "ὺ": "ὺ", "ϋ": "ϋ", "ΰ": "ΰ", "ῢ": "ῢ", "ῡ": "ῡ", "ῠ": "ῠ", "ώ": "ώ", "ὼ": "ὼ", "Ύ": "Ύ", "Ὺ": "Ὺ", "Ϋ": "Ϋ", "Ῡ": "Ῡ", "Ῠ": "Ῠ", "Ώ": "Ώ", "Ὼ": "Ὼ" }; /* eslint no-constant-condition:0 */ const binLeftCancellers = ["bin", "op", "open", "punct", "rel"]; const sizeRegEx = /([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/; const textRegEx = /^ *\\text/; /** * This file contains the parser used to parse out a TeX expression from the * input. Since TeX isn't context-free, standard parsers don't work particularly * well. * * The strategy of this parser is as such: * * The main functions (the `.parse...` ones) take a position in the current * parse string to parse tokens from. The lexer (found in Lexer.js, stored at * this.gullet.lexer) also supports pulling out tokens at arbitrary places. When * individual tokens are needed at a position, the lexer is called to pull out a * token, which is then used. * * The parser has a property called "mode" indicating the mode that * the parser is currently in. Currently it has to be one of "math" or * "text", which denotes whether the current environment is a math-y * one or a text-y one (e.g. inside \text). Currently, this serves to * limit the functions which can be used in text mode. * * The main functions then return an object which contains the useful data that * was parsed at its given point, and a new position at the end of the parsed * data. The main functions can call each other and continue the parsing by * using the returned position as a new starting point. * * There are also extra `.handle...` functions, which pull out some reused * functionality into self-contained functions. * * The functions return ParseNodes. */ class Parser { constructor(input, settings, isPreamble = false) { // Start in math mode this.mode = "math"; // Create a new macro expander (gullet) and (indirectly via that) also a // new lexer (mouth) for this parser (stomach, in the language of TeX) this.gullet = new MacroExpander(input, settings, this.mode); // Store the settings for use in parsing this.settings = settings; // Are we defining a preamble? this.isPreamble = isPreamble; // Count leftright depth (for \middle errors) this.leftrightDepth = 0; this.prevAtomType = ""; } /** * Checks a result to make sure it has the right type, and throws an * appropriate error otherwise. */ expect(text, consume = true) { if (this.fetch().text !== text) { throw new ParseError(`Expected '${text}', got '${this.fetch().text}'`, this.fetch()); } if (consume) { this.consume(); } } /** * Discards the current lookahead token, considering it consumed. */ consume() { this.nextToken = null; } /** * Return the current lookahead token, or if there isn't one (at the * beginning, or if the previous lookahead token was consume()d), * fetch the next token as the new lookahead token and return it. */ fetch() { if (this.nextToken == null) { this.nextToken = this.gullet.expandNextToken(); } return this.nextToken; } /** * Switches between "text" and "math" modes. */ switchMode(newMode) { this.mode = newMode; this.gullet.switchMode(newMode); } /** * Main parsing function, which parses an entire input. */ parse() { // Create a group namespace for every $...$, $$...$$, \[...\].) // A \def is then valid only within that pair of delimiters. this.gullet.beginGroup(); if (this.settings.colorIsTextColor) { // Use old \color behavior (same as LaTeX's \textcolor) if requested. // We do this within the group for the math expression, so it doesn't // pollute settings.macros. this.gullet.macros.set("\\color", "\\textcolor"); } // Try to parse the input const parse = this.parseExpression(false); // If we succeeded, make sure there's an EOF at the end this.expect("EOF"); if (this.isPreamble) { const macros = Object.create(null); Object.entries(this.gullet.macros.current).forEach(([key, value]) => { macros[key] = value; }); this.gullet.endGroup(); return macros } // The only local macro that we want to save is from \tag. const tag = this.gullet.macros.get("\\df@tag"); // End the group namespace for the expression this.gullet.endGroup(); if (tag) { this.gullet.macros.current["\\df@tag"] = tag; } return parse; } static get endOfExpression() { return ["}", "\\endgroup", "\\end", "\\right", "\\endtoggle", "&"]; } /** * Fully parse a separate sequence of tokens as a separate job. * Tokens should be specified in reverse order, as in a MacroDefinition. */ subparse(tokens) { // Save the next token from the current job. const oldToken = this.nextToken; this.consume(); // Run the new job, terminating it with an excess '}' this.gullet.pushToken(new Token("}")); this.gullet.pushTokens(tokens); const parse = this.parseExpression(false); this.expect("}"); // Restore the next token from the current job. this.nextToken = oldToken; return parse; } /** * Parses an "expression", which is a list of atoms. * * `breakOnInfix`: Should the parsing stop when we hit infix nodes? This * happens when functions have higher precedence han infix * nodes in implicit parses. * * `breakOnTokenText`: The text of the token that the expression should end * with, or `null` if something else should end the * expression. * * `breakOnMiddle`: \color, \over, and old styling functions work on an implicit group. * These groups end just before the usual tokens, but they also * end just before `\middle`. */ parseExpression(breakOnInfix, breakOnTokenText, breakOnMiddle) { const body = []; this.prevAtomType = ""; // Keep adding atoms to the body until we can't parse any more atoms (either // we reached the end, a }, or a \right) while (true) { // Ignore spaces in math mode if (this.mode === "math") { this.consumeSpaces(); } const lex = this.fetch(); if (Parser.endOfExpression.indexOf(lex.text) !== -1) { break; } if (breakOnTokenText && lex.text === breakOnTokenText) { break; } if (breakOnMiddle && lex.text === "\\middle") { break } if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) { break; } const atom = this.parseAtom(breakOnTokenText); if (!atom) { break; } else if (atom.type === "internal") { // Internal nodes do not appear in parse tree continue; } body.push(atom); // Keep a record of the atom type, so that op.js can set correct spacing. this.prevAtomType = atom.type === "atom" ? atom.family : atom.type; } if (this.mode === "text") { this.formLigatures(body); } return this.handleInfixNodes(body); } /** * Rewrites infix operators such as \over with corresponding commands such * as \frac. * * There can only be one infix operator per group. If there's more than one * then the expression is ambiguous. This can be resolved by adding {}. */ handleInfixNodes(body) { let overIndex = -1; let funcName; for (let i = 0; i < body.length; i++) { if (body[i].type === "infix") { if (overIndex !== -1) { throw new ParseError("only one infix operator per group", body[i].token); } overIndex = i; funcName = body[i].replaceWith; } } if (overIndex !== -1 && funcName) { let numerNode; let denomNode; const numerBody = body.slice(0, overIndex); const denomBody = body.slice(overIndex + 1); if (numerBody.length === 1 && numerBody[0].type === "ordgroup") { numerNode = numerBody[0]; } else { numerNode = { type: "ordgroup", mode: this.mode, body: numerBody }; } if (denomBody.length === 1 && denomBody[0].type === "ordgroup") { denomNode = denomBody[0]; } else { denomNode = { type: "ordgroup", mode: this.mode, body: denomBody }; } let node; if (funcName === "\\\\abovefrac") { node = this.callFunction(funcName, [numerNode, body[overIndex], denomNode], []); } else { node = this.callFunction(funcName, [numerNode, denomNode], []); } return [node]; } else { return body; } } /** * Handle a subscript or superscript with nice errors. */ handleSupSubscript( name // For error reporting. ) { const symbolToken = this.fetch(); const symbol = symbolToken.text; this.consume(); this.consumeSpaces(); // ignore spaces before sup/subscript argument // Skip over allowed internal nodes such as \relax let group; do { group = this.parseGroup(name); } while (group.type && group.type === "internal") if (!group) { throw new ParseError("Expected group after '" + symbol + "'", symbolToken); } return group; } /** * Converts the textual input of an unsupported command into a text node * contained within a color node whose color is determined by errorColor */ formatUnsupportedCmd(text) { const textordArray = []; for (let i = 0; i < text.length; i++) { textordArray.push({ type: "textord", mode: "text", text: text[i] }); } const textNode = { type: "text", mode: this.mode, body: textordArray }; const colorNode = { type: "color", mode: this.mode, color: this.settings.errorColor, body: [textNode] }; return colorNode; } /** * Parses a group with optional super/subscripts. */ parseAtom(breakOnTokenText) { // The body of an atom is an implicit group, so that things like // \left(x\right)^2 work correctly. const base = this.parseGroup("atom", breakOnTokenText); // Internal nodes (e.g. \relax) cannot support super/subscripts. // Instead we will pick up super/subscripts with blank base next round. if (base && base.type === "internal") { return base } // In text mode, we don't have superscripts or subscripts if (this.mode === "text") { return base } // Note that base may be empty (i.e. null) at this point. let superscript; let subscript; while (true) { // Guaranteed in math mode, so eat any spaces first. this.consumeSpaces(); // Lex the first token const lex = this.fetch(); if (lex.text === "\\limits" || lex.text === "\\nolimits") { // We got a limit control if (base && base.type === "op") { const limits = lex.text === "\\limits"; base.limits = limits; base.alwaysHandleSupSub = true; } else if (base && base.type === "operatorname") { if (base.alwaysHandleSupSub) { base.limits = lex.text === "\\limits"; } } else { throw new ParseError("Limit controls must follow a math operator", lex); } this.consume(); } else if (lex.text === "^") { // We got a superscript start if (superscript) { throw new ParseError("Double superscript", lex); } superscript = this.handleSupSubscript("superscript"); } else if (lex.text === "_") { // We got a subscript start if (subscript) { throw new ParseError("Double subscript", lex); } subscript = this.handleSupSubscript("subscript"); } else if (lex.text === "'") { // We got a prime if (superscript) { throw new ParseError("Double superscript", lex); } const prime = { type: "textord", mode: this.mode, text: "\\prime" }; // Many primes can be grouped together, so we handle this here const primes = [prime]; this.consume(); // Keep lexing tokens until we get something that's not a prime while (this.fetch().text === "'") { // For each one, add another prime to the list primes.push(prime); this.consume(); } // If there's a superscript following the primes, combine that // superscript in with the primes. if (this.fetch().text === "^") { primes.push(this.handleSupSubscript("superscript")); } // Put everything into an ordgroup as the superscript superscript = { type: "ordgroup", mode: this.mode, body: primes }; } else if (uSubsAndSups[lex.text]) { // A Unicode subscript or superscript character. // We treat these similarly to the unicode-math package. // So we render a string of Unicode (sub|super)scripts the // same as a (sub|super)script of regular characters. const isSub = unicodeSubRegEx.test(lex.text); const subsupTokens = []; subsupTokens.push(new Token(uSubsAndSups[lex.text])); this.consume(); // Continue fetching tokens to fill out the group. while (true) { const token = this.fetch().text; if (!(uSubsAndSups[token])) { break } if (unicodeSubRegEx.test(token) !== isSub) { break } subsupTokens.unshift(new Token(uSubsAndSups[token])); this.consume(); } // Now create a (sub|super)script. const body = this.subparse(subsupTokens); if (isSub) { subscript = { type: "ordgroup", mode: "math", body }; } else { superscript = { type: "ordgroup", mode: "math", body }; } } else { // If it wasn't ^, _, a Unicode (sub|super)script, or ', stop parsing super/subscripts break; } } if (superscript || subscript) { if (base && base.type === "multiscript" && !base.postscripts) { // base is the result of a \prescript function. // Write the sub- & superscripts into the multiscript element. base.postscripts = { sup: superscript, sub: subscript }; return base } else { // We got either a superscript or subscript, create a supsub const isFollowedByDelimiter = (!base || base.type !== "op" && base.type !== "operatorname") ? undefined : isDelimiter(this.nextToken.text); return { type: "supsub", mode: this.mode, base: base, sup: superscript, sub: subscript, isFollowedByDelimiter } } } else { // Otherwise return the original body return base; } } /** * Parses an entire function, including its base and all of its arguments. */ parseFunction( breakOnTokenText, name // For determining its context ) { const token = this.fetch(); const func = token.text; const funcData = functions[func]; if (!funcData) { return null; } this.consume(); // consume command token if (name && name !== "atom" && !funcData.allowedInArgument) { throw new ParseError( "Got function '" + func + "' with no arguments" + (name ? " as " + name : ""), token ); } else if (this.mode === "text" && !funcData.allowedInText) { throw new ParseError("Can't use function '" + func + "' in text mode", token); } else if (this.mode === "math" && funcData.allowedInMath === false) { throw new ParseError("Can't use function '" + func + "' in math mode", token); } const prevAtomType = this.prevAtomType; const { args, optArgs } = this.parseArguments(func, funcData); this.prevAtomType = prevAtomType; return this.callFunction(func, args, optArgs, token, breakOnTokenText); } /** * Call a function handler with a suitable context and arguments. */ callFunction(name, args, optArgs, token, breakOnTokenText) { const context = { funcName: name, parser: this, token, breakOnTokenText }; const func = functions[name]; if (func && func.handler) { return func.handler(context, args, optArgs); } else { throw new ParseError(`No function handler for ${name}`); } } /** * Parses the arguments of a function or environment */ parseArguments( func, // Should look like "\name" or "\begin{name}". funcData ) { const totalArgs = funcData.numArgs + funcData.numOptionalArgs; if (totalArgs === 0) { return { args: [], optArgs: [] }; } const args = []; const optArgs = []; for (let i = 0; i < totalArgs; i++) { let argType = funcData.argTypes && funcData.argTypes[i]; const isOptional = i < funcData.numOptionalArgs; if ( (funcData.primitive && argType == null) || // \sqrt expands into primitive if optional argument doesn't exist (funcData.type === "sqrt" && i === 1 && optArgs[0] == null) ) { argType = "primitive"; } const arg = this.parseGroupOfType(`argument to '${func}'`, argType, isOptional); if (isOptional) { optArgs.push(arg); } else if (arg != null) { args.push(arg); } else { // should be unreachable throw new ParseError("Null argument, please report this as a bug"); } } return { args, optArgs }; } /** * Parses a group when the mode is changing. */ parseGroupOfType(name, type, optional) { switch (type) { case "size": return this.parseSizeGroup(optional); case "url": return this.parseUrlGroup(optional); case "math": case "text": return this.parseArgumentGroup(optional, type); case "hbox": { // hbox argument type wraps the argument in the equivalent of // \hbox, which is like \text but switching to \textstyle size. const group = this.parseArgumentGroup(optional, "text"); return group != null ? { type: "styling", mode: group.mode, body: [group], scriptLevel: "text" // simulate \textstyle } : null; } case "raw": { const token = this.parseStringGroup("raw", optional); return token != null ? { type: "raw", mode: "text", string: token.text } : null; } case "primitive": { if (optional) { throw new ParseError("A primitive argument cannot be optional"); } const group = this.parseGroup(name); if (group == null) { throw new ParseError("Expected group as " + name, this.fetch()); } return group; } case "original": case null: case undefined: return this.parseArgumentGroup(optional); default: throw new ParseError("Unknown group type as " + name, this.fetch()); } } /** * Discard any space tokens, fetching the next non-space token. */ consumeSpaces() { while (true) { const ch = this.fetch().text; // \ufe0e is the Unicode variation selector to supress emoji. Ignore it. if (ch === " " || ch === "\u00a0" || ch === "\ufe0e") { this.consume(); } else { break } } } /** * Parses a group, essentially returning the string formed by the * brace-enclosed tokens plus some position information. */ parseStringGroup( modeName, // Used to describe the mode in error messages. optional ) { const argToken = this.gullet.scanArgument(optional); if (argToken == null) { return null; } let str = ""; let nextToken; while ((nextToken = this.fetch()).text !== "EOF") { str += nextToken.text; this.consume(); } this.consume(); // consume the end of the argument argToken.text = str; return argToken; } /** * Parses a regex-delimited group: the largest sequence of tokens * whose concatenated strings match `regex`. Returns the string * formed by the tokens plus some position information. */ parseRegexGroup( regex, modeName // Used to describe the mode in error messages. ) { const firstToken = this.fetch(); let lastToken = firstToken; let str = ""; let nextToken; while ((nextToken = this.fetch()).text !== "EOF" && regex.test(str + nextToken.text)) { lastToken = nextToken; str += lastToken.text; this.consume(); } if (str === "") { throw new ParseError("Invalid " + modeName + ": '" + firstToken.text + "'", firstToken); } return firstToken.range(lastToken, str); } /** * Parses a size specification, consisting of magnitude and unit. */ parseSizeGroup(optional) { let res; let isBlank = false; // don't expand before parseStringGroup this.gullet.consumeSpaces(); if (!optional && this.gullet.future().text !== "{") { res = this.parseRegexGroup(/^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$/, "size"); } else { res = this.parseStringGroup("size", optional); } if (!res) { return null; } if (!optional && res.text.length === 0) { // Because we've tested for what is !optional, this block won't // affect \kern, \hspace, etc. It will capture the mandatory arguments // to \genfrac and \above. res.text = "0pt"; // Enable \above{} isBlank = true; // This is here specifically for \genfrac } const match = sizeRegEx.exec(res.text); if (!match) { throw new ParseError("Invalid size: '" + res.text + "'", res); } const data = { number: +(match[1] + match[2]), // sign + magnitude, cast to number unit: match[3] }; if (!validUnit(data)) { throw new ParseError("Invalid unit: '" + data.unit + "'", res); } return { type: "size", mode: this.mode, value: data, isBlank }; } /** * Parses an URL, checking escaped letters and allowed protocols, * and setting the catcode of % as an active character (as in \hyperref). */ parseUrlGroup(optional) { this.gullet.lexer.setCatcode("%", 13); // active character this.gullet.lexer.setCatcode("~", 12); // other character const res = this.parseStringGroup("url", optional); this.gullet.lexer.setCatcode("%", 14); // comment character this.gullet.lexer.setCatcode("~", 13); // active character if (res == null) { return null; } // hyperref package allows backslashes alone in href, but doesn't // generate valid links in such cases; we interpret this as // "undefined" behaviour, and keep them as-is. Some browser will // replace backslashes with forward slashes. let url = res.text.replace(/\\([#$%&~_^{}])/g, "$1"); url = res.text.replace(/{\u2044}/g, "/"); return { type: "url", mode: this.mode, url }; } /** * Parses an argument with the mode specified. */ parseArgumentGroup(optional, mode) { const argToken = this.gullet.scanArgument(optional); if (argToken == null) { return null; } const outerMode = this.mode; if (mode) { // Switch to specified mode this.switchMode(mode); } this.gullet.beginGroup(); const expression = this.parseExpression(false, "EOF"); // TODO: find an alternative way to denote the end this.expect("EOF"); // expect the end of the argument this.gullet.endGroup(); const result = { type: "ordgroup", mode: this.mode, loc: argToken.loc, body: expression }; if (mode) { // Switch mode back this.switchMode(outerMode); } return result; } /** * Parses an ordinary group, which is either a single nucleus (like "x") * or an expression in braces (like "{x+y}") or an implicit group, a group * that starts at the current position, and ends right before a higher explicit * group ends, or at EOF. */ parseGroup( name, // For error reporting. breakOnTokenText ) { const firstToken = this.fetch(); const text = firstToken.text; let result; // Try to parse an open brace or \begingroup if (text === "{" || text === "\\begingroup" || text === "\\toggle") { this.consume(); const groupEnd = text === "{" ? "}" : text === "\\begingroup" ? "\\endgroup" : "\\endtoggle"; this.gullet.beginGroup(); // If we get a brace, parse an expression const expression = this.parseExpression(false, groupEnd); const lastToken = this.fetch(); this.expect(groupEnd); // Check that we got a matching closing brace this.gullet.endGroup(); result = { type: (lastToken.text === "\\endtoggle" ? "toggle" : "ordgroup"), mode: this.mode, loc: SourceLocation.range(firstToken, lastToken), body: expression, // A group formed by \begingroup...\endgroup is a semi-simple group // which doesn't affect spacing in math mode, i.e., is transparent. // https://tex.stackexchange.com/questions/1930/ semisimple: text === "\\begingroup" || undefined }; } else { // If there exists a function with this name, parse the function. // Otherwise, just return a nucleus result = this.parseFunction(breakOnTokenText, name) || this.parseSymbol(); if (result == null && text[0] === "\\" && !Object.prototype.hasOwnProperty.call(implicitCommands, text )) { result = this.formatUnsupportedCmd(text); this.consume(); } } return result; } /** * Form ligature-like combinations of characters for text mode. * This includes inputs like "--", "---", "``" and "''". * The result will simply replace multiple textord nodes with a single * character in each value by a single textord node having multiple * characters in its value. The representation is still ASCII source. * The group will be modified in place. */ formLigatures(group) { let n = group.length - 1; for (let i = 0; i < n; ++i) { const a = group[i]; const v = a.text; if (v === "-" && group[i + 1].text === "-") { if (i + 1 < n && group[i + 2].text === "-") { group.splice(i, 3, { type: "textord", mode: "text", loc: SourceLocation.range(a, group[i + 2]), text: "---" }); n -= 2; } else { group.splice(i, 2, { type: "textord", mode: "text", loc: SourceLocation.range(a, group[i + 1]), text: "--" }); n -= 1; } } if ((v === "'" || v === "`") && group[i + 1].text === v) { group.splice(i, 2, { type: "textord", mode: "text", loc: SourceLocation.range(a, group[i + 1]), text: v + v }); n -= 1; } } } /** * Parse a single symbol out of the string. Here, we handle single character * symbols and special functions like \verb. */ parseSymbol() { const nucleus = this.fetch(); let text = nucleus.text; if (/^\\verb[^a-zA-Z]/.test(text)) { this.consume(); let arg = text.slice(5); const star = arg.charAt(0) === "*"; if (star) { arg = arg.slice(1); } // Lexer's tokenRegex is constructed to always have matching // first/last characters. if (arg.length < 2 || arg.charAt(0) !== arg.slice(-1)) { throw new ParseError(`\\verb assertion failed -- please report what input caused this bug`); } arg = arg.slice(1, -1); // remove first and last char return { type: "verb", mode: "text", body: arg, star }; } // At this point, we should have a symbol, possibly with accents. // First expand any accented base symbol according to unicodeSymbols. if (Object.prototype.hasOwnProperty.call(unicodeSymbols, text[0]) && this.mode === "math" && !symbols[this.mode][text[0]]) { // This behavior is not strict (XeTeX-compatible) in math mode. if (this.settings.strict && this.mode === "math") { throw new ParseError(`Accented Unicode text character "${text[0]}" used in ` + `math mode`, nucleus ); } text = unicodeSymbols[text[0]] + text.slice(1); } // Strip off any combining characters const match = this.mode === "math" ? combiningDiacriticalMarksEndRegex.exec(text) : null; if (match) { text = text.substring(0, match.index); if (text === "i") { text = "\u0131"; // dotless i, in math and text mode } else if (text === "j") { text = "\u0237"; // dotless j, in math and text mode } } // Recognize base symbol let symbol; if (symbols[this.mode][text]) { let group = symbols[this.mode][text].group; if (group === "bin" && binLeftCancellers.includes(this.prevAtomType)) { // Change from a binary operator to a unary (prefix) operator group = "open"; } const loc = SourceLocation.range(nucleus); let s; if (Object.prototype.hasOwnProperty.call(ATOMS, group )) { const family = group; s = { type: "atom", mode: this.mode, family, loc, text }; if ((family === "rel" || family === "bin") && this.prevAtomType === "text") { if (textRegEx.test(loc.lexer.input.slice(loc.end))) { s.needsSpacing = true; // Fix a MathML bug. } } } else { if (asciiFromScript[text]) { // Unicode 14 disambiguates chancery from roundhand. // See https://www.unicode.org/charts/PDF/U1D400.pdf this.consume(); const nextCode = this.fetch().text.charCodeAt(0); // mathcal is Temml default. Use mathscript if called for. const font = nextCode === 0xfe01 ? "mathscr" : "mathcal"; if (nextCode === 0xfe00 || nextCode === 0xfe01) { this.consume(); } return { type: "font", mode: "math", font, body: { type: "mathord", mode: "math", loc, text: asciiFromScript[text] } } } // Default ord character. No disambiguation necessary. s = { type: group, mode: this.mode, loc, text }; } symbol = s; } else if (text.charCodeAt(0) >= 0x80 || combiningDiacriticalMarksEndRegex.exec(text)) { // no symbol for e.g. ^ if (this.settings.strict && this.mode === "math") { throw new ParseError(`Unicode text character "${text[0]}" used in math mode`, nucleus) } // All nonmathematical Unicode characters are rendered as if they // are in text mode (wrapped in \text) because that's what it // takes to render them in LaTeX. symbol = { type: "textord", mode: "text", loc: SourceLocation.range(nucleus), text }; } else { return null; // EOF, ^, _, {, }, etc. } this.consume(); // Transform combining characters into accents if (match) { for (let i = 0; i < match[0].length; i++) { const accent = match[0][i]; if (!unicodeAccents[accent]) { throw new ParseError(`Unknown accent ' ${accent}'`, nucleus); } const command = unicodeAccents[accent][this.mode] || unicodeAccents[accent].text; if (!command) { throw new ParseError(`Accent ${accent} unsupported in ${this.mode} mode`, nucleus); } symbol = { type: "accent", mode: this.mode, loc: SourceLocation.range(nucleus), label: command, isStretchy: false, base: symbol }; } } return symbol; } } /** * Parses an expression using a Parser, then returns the parsed result. */ const parseTree = function(toParse, settings) { if (!(typeof toParse === "string" || toParse instanceof String)) { throw new TypeError("Temml can only parse string typed expression") } const parser = new Parser(toParse, settings); // Blank out any \df@tag to avoid spurious "Duplicate \tag" errors delete parser.gullet.macros.current["\\df@tag"]; let tree = parser.parse(); // LaTeX ignores a \tag placed outside an AMS environment. if (!(tree.length > 0 && tree[0].type && tree[0].type === "array" && tree[0].addEqnNum)) { // If the input used \tag, it will set the \df@tag macro to the tag. // In this case, we separately parse the tag and wrap the tree. if (parser.gullet.macros.get("\\df@tag")) { if (!settings.displayMode) { throw new ParseError("\\tag works only in display mode") } parser.gullet.feed("\\df@tag"); tree = [ { type: "tag", mode: "text", body: tree, tag: parser.parse() } ]; } } return tree }; /** * This file contains information about the style that the mathmlBuilder carries * around with it. Data is held in an `Style` object, and when * recursing, a new `Style` object can be created with the `.with*` functions. */ const subOrSupLevel = [2, 2, 3, 3]; /** * This is the main Style class. It contains the current style.level, color, and font. * * Style objects should not be modified. To create a new Style with * different properties, call a `.with*` method. */ class Style { constructor(data) { // Style.level can be 0 | 1 | 2 | 3, which correspond to // displaystyle, textstyle, scriptstyle, and scriptscriptstyle. // style.level usually does not directly set MathML's script level. MathML does that itself. // However, Chromium does not stop shrinking after scriptscriptstyle, so we do explicitly // set a scriptlevel attribute in those conditions. // We also use style.level to track math style so that we can get the correct // scriptlevel when needed in supsub.js, mathchoice.js, or for dimensions in em. this.level = data.level; this.color = data.color; // string | void // A font family applies to a group of fonts (i.e. SansSerif), while a font // represents a specific font (i.e. SansSerif Bold). // See: https://tex.stackexchange.com/questions/22350/difference-between-textrm-and-mathrm this.font = data.font || ""; // string this.fontFamily = data.fontFamily || ""; // string this.fontSize = data.fontSize || 1.0; // number this.fontWeight = data.fontWeight || ""; this.fontShape = data.fontShape || ""; this.maxSize = data.maxSize; // [number, number] } /** * Returns a new style object with the same properties as "this". Properties * from "extension" will be copied to the new style object. */ extend(extension) { const data = { level: this.level, color: this.color, font: this.font, fontFamily: this.fontFamily, fontSize: this.fontSize, fontWeight: this.fontWeight, fontShape: this.fontShape, maxSize: this.maxSize }; for (const key in extension) { if (Object.prototype.hasOwnProperty.call(extension, key)) { data[key] = extension[key]; } } return new Style(data); } withLevel(n) { return this.extend({ level: n }); } incrementLevel() { return this.extend({ level: Math.min(this.level + 1, 3) }); } inSubOrSup() { return this.extend({ level: subOrSupLevel[this.level] }) } /** * Create a new style object with the given color. */ withColor(color) { return this.extend({ color: color }); } /** * Creates a new style object with the given math font or old text font. * @type {[type]} */ withFont(font) { return this.extend({ font }); } /** * Create a new style objects with the given fontFamily. */ withTextFontFamily(fontFamily) { return this.extend({ fontFamily, font: "" }); } /** * Creates a new style object with the given font size */ withFontSize(num) { return this.extend({ fontSize: num }); } /** * Creates a new style object with the given font weight */ withTextFontWeight(fontWeight) { return this.extend({ fontWeight, font: "" }); } /** * Creates a new style object with the given font weight */ withTextFontShape(fontShape) { return this.extend({ fontShape, font: "" }); } /** * Gets the CSS color of the current style object */ getColor() { return this.color; } } /* Temml Post Process * Populate the text contents of each \ref & \eqref * * As with other Temml code, this file is released under terms of the MIT license. * https://mit-license.org/ */ const version = "0.11.11"; function postProcess(block) { const labelMap = {}; let i = 0; // Get a collection of the parents of each \tag & auto-numbered equation const amsEqns = document.getElementsByClassName('tml-eqn'); for (let parent of amsEqns) { // AMS automatically numbered equation. // Assign an id. i += 1; parent.setAttribute("id", "tml-eqn-" + String(i)); // No need to write a number into the text content of the element. // A CSS counter has done that even if this postProcess() function is not used. // Find any \label that refers to an AMS automatic eqn number. while (true) { if (parent.tagName === "mtable") { break } const labels = parent.getElementsByClassName("tml-label"); if (labels.length > 0) { const id = parent.attributes.id.value; labelMap[id] = String(i); break } else { parent = parent.parentElement; } } } // Find \labels associated with \tag const taggedEqns = document.getElementsByClassName('tml-tageqn'); for (const parent of taggedEqns) { const labels = parent.getElementsByClassName("tml-label"); if (labels.length > 0) { const tags = parent.getElementsByClassName("tml-tag"); if (tags.length > 0) { const id = parent.attributes.id.value; labelMap[id] = tags[0].textContent; } } } // Populate \ref & \eqref text content const refs = block.getElementsByClassName("tml-ref"); [...refs].forEach(ref => { const attr = ref.getAttribute("href"); let str = labelMap[attr.slice(1)]; if (ref.className.indexOf("tml-eqref") === -1) { // \ref. Omit parens. str = str.replace(/^\(/, ""); str = str.replace(/\)$/, ""); } else { // \eqref. Include parens if (str.charAt(0) !== "(") { str = "(" + str; } if (str.slice(-1) !== ")") { str = str + ")"; } } const mtext = document.createElementNS("http://www.w3.org/1998/Math/MathML", "mtext"); mtext.appendChild(document.createTextNode(str)); const math = document.createElementNS("http://www.w3.org/1998/Math/MathML", "math"); math.appendChild(mtext); ref.textContent = ''; ref.appendChild(math); }); } const findEndOfMath = function(delimiter, text, startIndex) { // Adapted from // https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx let index = startIndex; let braceLevel = 0; const delimLength = delimiter.length; while (index < text.length) { const character = text[index]; if (braceLevel <= 0 && text.slice(index, index + delimLength) === delimiter) { return index; } else if (character === "\\") { index++; } else if (character === "{") { braceLevel++; } else if (character === "}") { braceLevel--; } index++; } return -1; }; const escapeRegex = function(string) { return string.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); }; const amsRegex = /^\\(?:begin|(?:eq)?ref){/; const splitAtDelimiters = function(text, delimiters) { let index; const data = []; const regexLeft = new RegExp( "(" + delimiters.map((x) => escapeRegex(x.left)).join("|") + ")" ); while (true) { index = text.search(regexLeft); if (index === -1) { break; } if (index > 0) { data.push({ type: "text", data: text.slice(0, index) }); text = text.slice(index); // now text starts with delimiter } // ... so this always succeeds: const i = delimiters.findIndex((delim) => text.startsWith(delim.left)); index = findEndOfMath(delimiters[i].right, text, delimiters[i].left.length); if (index === -1) { break; } const rawData = text.slice(0, index + delimiters[i].right.length); const math = amsRegex.test(rawData) ? rawData : text.slice(delimiters[i].left.length, index); data.push({ type: "math", data: math, rawData, display: delimiters[i].display }); text = text.slice(index + delimiters[i].right.length); } if (text !== "") { data.push({ type: "text", data: text }); } return data; }; const defaultDelimiters = [ { left: "$$", right: "$$", display: true }, { left: "\\(", right: "\\)", display: false }, // LaTeX uses $…$, but it ruins the display of normal `$` in text: // {left: "$", right: "$", display: false}, // $ must come after $$ // Render AMS environments even if outside $$…$$ delimiters. { left: "\\begin{equation}", right: "\\end{equation}", display: true }, { left: "\\begin{equation*}", right: "\\end{equation*}", display: true }, { left: "\\begin{align}", right: "\\end{align}", display: true }, { left: "\\begin{align*}", right: "\\end{align*}", display: true }, { left: "\\begin{alignat}", right: "\\end{alignat}", display: true }, { left: "\\begin{alignat*}", right: "\\end{alignat*}", display: true }, { left: "\\begin{gather}", right: "\\end{gather}", display: true }, { left: "\\begin{gather*}", right: "\\end{gather*}", display: true }, { left: "\\begin{CD}", right: "\\end{CD}", display: true }, // Ditto \ref & \eqref { left: "\\ref{", right: "}", display: false }, { left: "\\eqref{", right: "}", display: false }, { left: "\\[", right: "\\]", display: true } ]; const firstDraftDelimiters = { "$": [ { left: "$$", right: "$$", display: true }, { left: "$`", right: "`$", display: false }, { left: "$", right: "$", display: false } ], "(": [ { left: "\\[", right: "\\]", display: true }, { left: "\\(", right: "\\)", display: false } ] }; const amsDelimiters = [ { left: "\\begin{equation}", right: "\\end{equation}", display: true }, { left: "\\begin{equation*}", right: "\\end{equation*}", display: true }, { left: "\\begin{align}", right: "\\end{align}", display: true }, { left: "\\begin{align*}", right: "\\end{align*}", display: true }, { left: "\\begin{alignat}", right: "\\end{alignat}", display: true }, { left: "\\begin{alignat*}", right: "\\end{alignat*}", display: true }, { left: "\\begin{gather}", right: "\\end{gather}", display: true }, { left: "\\begin{gather*}", right: "\\end{gather*}", display: true }, { left: "\\begin{CD}", right: "\\end{CD}", display: true }, { left: "\\ref{", right: "}", display: false }, { left: "\\eqref{", right: "}", display: false } ]; const delimitersFromKey = key => { if (key === "$" || key === "(") { return firstDraftDelimiters[key]; } else if (key === "$+" || key === "(+") { const firstDraft = firstDraftDelimiters[key.slice(0, 1)]; return firstDraft.concat(amsDelimiters) } else if (key === "ams") { return amsDelimiters } else if (key === "all") { return (firstDraftDelimiters["("]).concat(firstDraftDelimiters["$"]).concat(amsDelimiters) } else { return defaultDelimiters } }; /* Note: optionsCopy is mutated by this method. If it is ever exposed in the * API, we should copy it before mutating. */ const renderMathInText = function(text, optionsCopy) { const data = splitAtDelimiters(text, optionsCopy.delimiters); if (data.length === 1 && data[0].type === "text") { // There is no formula in the text. // Let's return null which means there is no need to replace // the current text node with a new one. return null; } const fragment = document.createDocumentFragment(); for (let i = 0; i < data.length; i++) { if (data[i].type === "text") { fragment.appendChild(document.createTextNode(data[i].data)); } else { const span = document.createElement("span"); let math = data[i].data; // Override any display mode defined in the settings with that // defined by the text itself optionsCopy.displayMode = data[i].display; try { if (optionsCopy.preProcess) { math = optionsCopy.preProcess(math); } // Importing render() from temml.js would be a circular dependency. // So call the global version. // eslint-disable-next-line no-undef temml.render(math, span, optionsCopy); } catch (e) { if (!(e instanceof ParseError)) { throw e; } optionsCopy.errorCallback( "Temml auto-render: Failed to parse `" + data[i].data + "` with ", e ); fragment.appendChild(document.createTextNode(data[i].rawData)); continue; } fragment.appendChild(span); } } return fragment; }; const renderElem = function(elem, optionsCopy) { for (let i = 0; i < elem.childNodes.length; i++) { const childNode = elem.childNodes[i]; if (childNode.nodeType === 3) { // Text node const frag = renderMathInText(childNode.textContent, optionsCopy); if (frag) { i += frag.childNodes.length - 1; elem.replaceChild(frag, childNode); } } else if (childNode.nodeType === 1) { // Element node const className = " " + childNode.className + " "; const shouldRender = optionsCopy.ignoredTags.indexOf(childNode.nodeName.toLowerCase()) === -1 && optionsCopy.ignoredClasses.every((x) => className.indexOf(" " + x + " ") === -1); if (shouldRender) { renderElem(childNode, optionsCopy); } } // Otherwise, it's something else, and ignore it. } }; const renderMathInElement = function(elem, options) { if (!elem) { throw new Error("No element provided to render"); } const optionsCopy = {}; // Object.assign(optionsCopy, option) for (const option in options) { if (Object.prototype.hasOwnProperty.call(options, option)) { optionsCopy[option] = options[option]; } } if (optionsCopy.fences) { optionsCopy.delimiters = delimitersFromKey(optionsCopy.fences); } else { optionsCopy.delimiters = optionsCopy.delimiters || defaultDelimiters; } optionsCopy.ignoredTags = optionsCopy.ignoredTags || [ "script", "noscript", "style", "textarea", "pre", "code", "option" ]; optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || []; // eslint-disable-next-line no-console optionsCopy.errorCallback = optionsCopy.errorCallback || console.error; // Enable sharing of global macros defined via `\gdef` between different // math elements within a single call to `renderMathInElement`. optionsCopy.macros = optionsCopy.macros || {}; renderElem(elem, optionsCopy); postProcess(elem); }; /* eslint no-console:0 */ /** * This is the main entry point for Temml. Here, we expose functions for * rendering expressions either to DOM nodes or to markup strings. * * We also expose the ParseError class to check if errors thrown from Temml are * errors in the expression, or errors in javascript handling. */ /** * @type {import('./temml').render} * Parse and build an expression, and place that expression in the DOM node * given. */ let render = function(expression, baseNode, options = {}) { baseNode.textContent = ""; const alreadyInMathElement = baseNode.tagName.toLowerCase() === "math"; if (alreadyInMathElement) { options.wrap = "none"; } const math = renderToMathMLTree(expression, options); if (alreadyInMathElement) { // The <math> element already exists. Populate it. baseNode.textContent = ""; math.children.forEach(e => { baseNode.appendChild(e.toNode()); }); } else if (math.children.length > 1) { baseNode.textContent = ""; math.children.forEach(e => { baseNode.appendChild(e.toNode()); }); } else { baseNode.appendChild(math.toNode()); } }; // Temml's styles don't work properly in quirks mode. Print out an error, and // disable rendering. if (typeof document !== "undefined") { if (document.compatMode !== "CSS1Compat") { typeof console !== "undefined" && console.warn( "Warning: Temml doesn't work in quirks mode. Make sure your " + "website has a suitable doctype." ); render = function() { throw new ParseError("Temml doesn't work in quirks mode."); }; } } /** * @type {import('./temml').renderToString} * Parse and build an expression, and return the markup for that. */ const renderToString = function(expression, options) { const markup = renderToMathMLTree(expression, options).toMarkup(); return markup; }; /** * @type {import('./temml').generateParseTree} * Parse an expression and return the parse tree. */ const generateParseTree = function(expression, options) { const settings = new Settings(options); return parseTree(expression, settings); }; /** * @type {import('./temml').definePreamble} * Take an expression which contains a preamble. * Parse it and return the macros. */ const definePreamble = function(expression, options) { const settings = new Settings(options); settings.macros = {}; if (!(typeof expression === "string" || expression instanceof String)) { throw new TypeError("Temml can only parse string typed expression") } const parser = new Parser(expression, settings, true); // Blank out any \df@tag to avoid spurious "Duplicate \tag" errors delete parser.gullet.macros.current["\\df@tag"]; const macros = parser.parse(); return macros }; /** * If the given error is a Temml ParseError, * renders the invalid LaTeX as a span with hover title giving the Temml * error message. Otherwise, simply throws the error. */ const renderError = function(error, expression, options) { if (options.throwOnError || !(error instanceof ParseError)) { throw error; } const node = new Span(["temml-error"], [new TextNode$1(expression + "\n\n" + error.toString())]); node.style.color = options.errorColor; node.style.whiteSpace = "pre-line"; return node; }; /** * @type {import('./temml').renderToMathMLTree} * Generates and returns the Temml build tree. This is used for advanced * use cases (like rendering to custom output). */ const renderToMathMLTree = function(expression, options) { const settings = new Settings(options); try { const tree = parseTree(expression, settings); const style = new Style({ level: settings.displayMode ? StyleLevel.DISPLAY : StyleLevel.TEXT, maxSize: settings.maxSize }); return buildMathML(tree, expression, style, settings); } catch (error) { return renderError(error, expression, settings); } }; /** @type {import('./temml').default} */ var temml$1 = { /** * Current Temml version */ version: version, /** * Renders the given LaTeX into MathML, and adds * it as a child to the specified DOM node. */ render, /** * Renders the given LaTeX into MathML string, * for sending to the client. */ renderToString, /** * Finds all the math delimiters in a given element of a running HTML document * and converts the contents of each instance into a <math> element. */ renderMathInElement, /** * Post-process an entire HTML block. * Writes AMS auto-numbers and implements \ref{}. * Typcally called once, after a loop has rendered many individual spans. */ postProcess, /** * Temml error, usually during parsing. */ ParseError, /** * Creates a set of macros with document-wide scope. */ definePreamble, /** * Parses the given LaTeX into Temml's internal parse tree structure, * without rendering to HTML or MathML. * * NOTE: This method is not currently recommended for public use. * The internal tree representation is unstable and is very likely * to change. Use at your own risk. */ __parse: generateParseTree, /** * Renders the given LaTeX into a MathML internal DOM tree * representation, without flattening that representation to a string. * * NOTE: This method is not currently recommended for public use. * The internal tree representation is unstable and is very likely * to change. Use at your own risk. */ __renderToMathMLTree: renderToMathMLTree, /** * adds a new symbol to builtin symbols table */ __defineSymbol: defineSymbol, /** * adds a new macro to builtin macro list */ __defineMacro: defineMacro }; return temml$1; })();