mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
Merge pull request #36920 from overleaf/em-shared-frontend-backend
Move shared bibtex model classes to services/web/shared GitOrigin-RevId: ef768d4f7bcf7b9c4b41844450b0256ecb46fdae
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
export type BibEntryInput = {
|
||||
key: string
|
||||
label: string
|
||||
helperText: string
|
||||
}
|
||||
|
||||
export type BibEntryType = {
|
||||
key: string
|
||||
label: string
|
||||
fields: string[]
|
||||
optionalFields: string[]
|
||||
requiredFields: (string | string[])[]
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
import { BibtexFieldValue } from './bibtex-field-value.mts'
|
||||
import { getEntryType } from './entry-types.mts'
|
||||
import { BibtexNameList } from './bibtex-name.mts'
|
||||
|
||||
type Fields = ReadonlyMap<string, BibtexFieldValue>
|
||||
|
||||
type BibtexEntryOptions = {
|
||||
type?: string
|
||||
key?: string
|
||||
fields?: Fields
|
||||
id?: string
|
||||
occurrenceIndex?: number
|
||||
updatedAt?: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable BibTeX entry content. Position information lives separately on
|
||||
* `PositionedBibtexEntry` so `BibtexEntry` references can stay stable across
|
||||
* pure position shifts (e.g. when typing above an entry).
|
||||
*/
|
||||
export class BibtexEntry {
|
||||
/**
|
||||
* Entry type
|
||||
*/
|
||||
readonly type: string
|
||||
|
||||
/**
|
||||
* Citation key
|
||||
*/
|
||||
readonly key: string
|
||||
|
||||
/**
|
||||
* Fields
|
||||
*/
|
||||
readonly fields: Fields
|
||||
|
||||
/**
|
||||
* Last updated timestamp (set when loaded from the library)
|
||||
*/
|
||||
readonly updatedAt: Date | undefined
|
||||
|
||||
private readonly _id: string
|
||||
|
||||
/**
|
||||
* Position of this entry among entries that share its citation key, in
|
||||
* document order (0-based). In-project entries can share a citation key, so
|
||||
* this disambiguates them. It is positional metadata (not content), assigned
|
||||
* by `useBibtexEntries` once the full, ordered entry list is known.
|
||||
*/
|
||||
occurrenceIndex: number
|
||||
|
||||
/**
|
||||
* Stable identifier.
|
||||
* - Library entries: the database record ID.
|
||||
* - In-project entries: `${key}#${occurrenceIndex}`, so duplicate keys still
|
||||
* get distinct ids and can be located unambiguously in the document.
|
||||
*/
|
||||
get id(): string {
|
||||
if (this._id) return this._id
|
||||
return `${this.key}#${this.occurrenceIndex}`
|
||||
}
|
||||
|
||||
constructor({
|
||||
type = 'article',
|
||||
key = '',
|
||||
fields = new Map(),
|
||||
id = '',
|
||||
occurrenceIndex = 0,
|
||||
updatedAt,
|
||||
}: BibtexEntryOptions = {}) {
|
||||
this.type = type
|
||||
this.key = key
|
||||
this.fields = fields
|
||||
this._id = id
|
||||
this.occurrenceIndex = occurrenceIndex
|
||||
this.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
setType(type: string) {
|
||||
return new BibtexEntry({ ...this, type, id: this._id })
|
||||
}
|
||||
|
||||
setKey(key: string) {
|
||||
return new BibtexEntry({ ...this, key, id: this._id })
|
||||
}
|
||||
|
||||
getField(field: string) {
|
||||
return this.fields.get(field) ?? new BibtexFieldValue()
|
||||
}
|
||||
|
||||
getFields() {
|
||||
return this.fields.keys()
|
||||
}
|
||||
|
||||
getFieldsArray() {
|
||||
return Array.from(this.fields.entries()).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
}))
|
||||
}
|
||||
|
||||
/** Returns all authors (and editors, as fallback) as a parsed BibtexNameList. */
|
||||
getAuthors(): BibtexNameList {
|
||||
const authorStr =
|
||||
this.getField('author').toDisplayString() ||
|
||||
this.getField('editor').toDisplayString()
|
||||
return new BibtexNameList(authorStr)
|
||||
}
|
||||
|
||||
getTitle(): string {
|
||||
return (
|
||||
this.getField('title').toDisplayString() ||
|
||||
this.getField('subtitle').toDisplayString()
|
||||
)
|
||||
}
|
||||
|
||||
getYear(): string {
|
||||
const year = this.getField('year').toDisplayString()
|
||||
if (year) return year
|
||||
const dateStr = this.getField('date').toDisplayString()
|
||||
const match = dateStr.match(/^\d{4}/)
|
||||
return match ? match[0] : ''
|
||||
}
|
||||
|
||||
hasField(field: string) {
|
||||
return this.fields.has(field)
|
||||
}
|
||||
|
||||
hasFields() {
|
||||
return this.fields.size > 0
|
||||
}
|
||||
|
||||
setField(field: string, value: BibtexFieldValue) {
|
||||
const fields = new Map(this.fields)
|
||||
fields.set(field, value)
|
||||
return new BibtexEntry({ ...this, fields, id: this._id })
|
||||
}
|
||||
|
||||
deleteField(field: string) {
|
||||
const fields = new Map(this.fields)
|
||||
fields.delete(field)
|
||||
return new BibtexEntry({ ...this, fields, id: this._id })
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
let bibEntry = `@${this.type}{${this.key},\n`
|
||||
for (const [field, value] of this.fields) {
|
||||
bibEntry += ` ${field} = ${value.toString()},\n`
|
||||
}
|
||||
bibEntry += '}'
|
||||
return bibEntry
|
||||
}
|
||||
|
||||
getWarnings(): BibtexEntryWarning[] {
|
||||
const warnings: BibtexEntryWarning[] = []
|
||||
|
||||
const missingFieldGroups = this.getMissingFieldGroups()
|
||||
if (missingFieldGroups.length > 0) {
|
||||
warnings.push(new MissingFieldsWarning(missingFieldGroups))
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
getMissingFieldGroups(): string[][] {
|
||||
const entryType = getEntryType(this.type)
|
||||
const requiredFields = entryType.requiredFields
|
||||
const missingFields: string[][] = []
|
||||
for (const requiredField of requiredFields) {
|
||||
const group = Array.isArray(requiredField)
|
||||
? requiredField
|
||||
: [requiredField]
|
||||
if (!group.some(field => this.hasField(field))) {
|
||||
missingFields.push(group)
|
||||
}
|
||||
}
|
||||
return missingFields
|
||||
}
|
||||
}
|
||||
|
||||
export class MissingFieldsWarning {
|
||||
fieldGroups: string[][]
|
||||
|
||||
constructor(fieldGroups: string[][]) {
|
||||
this.fieldGroups = fieldGroups
|
||||
}
|
||||
}
|
||||
|
||||
export type BibtexEntryWarning = MissingFieldsWarning
|
||||
@@ -1,278 +0,0 @@
|
||||
import { latexToUnicode } from './latex-to-unicode.mts'
|
||||
import { purifySpecialChars } from './bibtex-purify.mts'
|
||||
|
||||
const IDENTIFIER_SYMBOLS = '!$&*+./:;<>?^`_|[]-'
|
||||
|
||||
class StringLiteral {
|
||||
private value: string
|
||||
|
||||
constructor(value: string) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `{${this.value}}`
|
||||
}
|
||||
|
||||
toEditableString() {
|
||||
return this.value.replaceAll('#', '##')
|
||||
}
|
||||
|
||||
toDisplayString() {
|
||||
let s = this.value
|
||||
s = latexToUnicode(s)
|
||||
s = purifySpecialChars(s)
|
||||
s = s.replaceAll(/[~\s]+/g, ' ')
|
||||
s = s.replaceAll(/[{}\\]/g, '')
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
class NumberLiteral {
|
||||
private value: string
|
||||
|
||||
constructor(value: string) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.value
|
||||
}
|
||||
|
||||
toEditableString() {
|
||||
return this.value
|
||||
}
|
||||
|
||||
toDisplayString() {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
|
||||
class NamedString {
|
||||
private name: string
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.name
|
||||
}
|
||||
|
||||
toEditableString() {
|
||||
return `#${this.name}#`
|
||||
}
|
||||
|
||||
toDisplayString() {
|
||||
return this.name
|
||||
}
|
||||
}
|
||||
|
||||
type Part = StringLiteral | NumberLiteral | NamedString
|
||||
|
||||
export class BibtexFieldValue {
|
||||
private readonly parts: ReadonlyArray<Part> = []
|
||||
private bibtexString: string | null = null
|
||||
private displayString: string | null = null
|
||||
private editableString: string | null = null
|
||||
|
||||
constructor(parts: Part[] = []) {
|
||||
this.parts = parts
|
||||
}
|
||||
|
||||
addString(value: string) {
|
||||
return new BibtexFieldValue([...this.parts, new StringLiteral(value)])
|
||||
}
|
||||
|
||||
addNumber(value: string) {
|
||||
return new BibtexFieldValue([...this.parts, new NumberLiteral(value)])
|
||||
}
|
||||
|
||||
addNamedString(name: string) {
|
||||
return new BibtexFieldValue([...this.parts, new NamedString(name)])
|
||||
}
|
||||
|
||||
private invalidateCache() {
|
||||
this.bibtexString = null
|
||||
this.editableString = null
|
||||
this.displayString = null
|
||||
}
|
||||
|
||||
toString() {
|
||||
if (this.bibtexString == null) {
|
||||
if (this.parts.length === 0) {
|
||||
return '{}'
|
||||
}
|
||||
|
||||
this.bibtexString = this.parts.map(part => part.toString()).join(' # ')
|
||||
}
|
||||
return this.bibtexString
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a BibtexFieldValue to editable field text.
|
||||
*
|
||||
* Steps:
|
||||
* 1. String literals are copied as-is, but every '#' is escaped as '##'.
|
||||
* 2. Numbers are copied as text.
|
||||
* 3. Named strings are wrapped as '#name#'.
|
||||
* 4. All parts are concatenated directly.
|
||||
*
|
||||
* Example:
|
||||
* BibTeX:
|
||||
* {This is } # author # {'s } # {#1 } # book # { from } # 1995
|
||||
* Editable:
|
||||
* This is #author#'s ##1 #book# from 1995
|
||||
*
|
||||
* Example:
|
||||
* BibTeX:
|
||||
* "The " # DS # " handbook"
|
||||
* Editable:
|
||||
* The #DS# handbook
|
||||
*/
|
||||
toEditableString() {
|
||||
if (this.editableString == null) {
|
||||
this.editableString = this.parts
|
||||
.map(part => part.toEditableString())
|
||||
.join('')
|
||||
}
|
||||
return this.editableString
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a BibtexFieldValue to a string suitable for display.
|
||||
*
|
||||
* - Replace tildes with spaces
|
||||
* - Replace successions of whitespace characters with a single space
|
||||
* - Trim leading and trailing whitespace
|
||||
* - Remove curly braces and backslashes
|
||||
*/
|
||||
toDisplayString() {
|
||||
if (this.displayString == null) {
|
||||
let s = this.parts.map(part => part.toDisplayString()).join('')
|
||||
s = s.replaceAll(/^\s+|\s+$/g, '')
|
||||
this.displayString = s
|
||||
}
|
||||
return this.displayString
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a new BibtexFieldValue from an editable string
|
||||
*
|
||||
* Scan left-to-right:
|
||||
* 1. '##' becomes a literal '#'.
|
||||
* 2. '#identifier#' becomes a named string part.
|
||||
* 3. Any other '#' is treated as a literal '#'.
|
||||
* 4. Literal runs become braced strings and parts are joined with ' # '.
|
||||
*
|
||||
* Identifier rule:
|
||||
* - First char: letter or one of ! $ & * + . / : ; < > ? ^ ` _ | [ ] -
|
||||
* - Following chars: same set plus digits.
|
||||
* - Cannot start with a digit.
|
||||
*
|
||||
* Example:
|
||||
* Editable:
|
||||
* This is #author#'s #1 book (#numberone) and here's a ##tag##
|
||||
* BibTeX:
|
||||
* {This is } # author # {'s #1 book (#numberone) and here's a #tag#}
|
||||
*
|
||||
* Example:
|
||||
* Editable:
|
||||
* This is from 1995
|
||||
* BibTeX:
|
||||
* {This is from 1995}
|
||||
*/
|
||||
static fromEditableString(editable: string) {
|
||||
let bibtexValue = new BibtexFieldValue()
|
||||
let currentString = ''
|
||||
let index = 0
|
||||
|
||||
while (index < editable.length) {
|
||||
if (editable[index] !== '#') {
|
||||
currentString += editable[index]
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
const nextIndex = index + 1
|
||||
const nextChar = editable[nextIndex]
|
||||
|
||||
if (nextChar === '#') {
|
||||
currentString += '#'
|
||||
index += 2
|
||||
continue
|
||||
}
|
||||
|
||||
const identifierEnd = findBibtexIdentifierEndExclusive(
|
||||
editable,
|
||||
nextIndex
|
||||
)
|
||||
if (identifierEnd !== null && editable[identifierEnd] === '#') {
|
||||
if (currentString !== '') {
|
||||
bibtexValue = bibtexValue.addString(currentString)
|
||||
currentString = ''
|
||||
}
|
||||
|
||||
bibtexValue = bibtexValue.addNamedString(
|
||||
editable.slice(nextIndex, identifierEnd)
|
||||
)
|
||||
index = identifierEnd + 1
|
||||
continue
|
||||
}
|
||||
|
||||
currentString += '#'
|
||||
index += 1
|
||||
}
|
||||
|
||||
if (currentString !== '') {
|
||||
bibtexValue = bibtexValue.addString(currentString)
|
||||
}
|
||||
|
||||
return bibtexValue
|
||||
}
|
||||
}
|
||||
|
||||
function findBibtexIdentifierEndExclusive(
|
||||
text: string,
|
||||
startIndex: number
|
||||
): number | null {
|
||||
if (startIndex > text.length) {
|
||||
throw new Error('startIndex must not be greater than text length')
|
||||
}
|
||||
|
||||
if (
|
||||
startIndex === text.length ||
|
||||
!isBibtexIdentifierStartChar(text[startIndex])
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
let index = startIndex + 1
|
||||
while (
|
||||
index < text.length &&
|
||||
isBibtexIdentifierContinuationChar(text[index])
|
||||
) {
|
||||
index += 1
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
function isBibtexIdentifierStartChar(char: string): boolean {
|
||||
return isAsciiLetter(char) || IDENTIFIER_SYMBOLS.includes(char)
|
||||
}
|
||||
|
||||
function isBibtexIdentifierContinuationChar(char: string): boolean {
|
||||
return isBibtexIdentifierStartChar(char) || isDigit(char)
|
||||
}
|
||||
|
||||
function isAsciiLetter(char: string): boolean {
|
||||
return /^[A-Za-z]$/.test(char)
|
||||
}
|
||||
|
||||
function isDigit(char: string): boolean {
|
||||
return /^[0-9]$/.test(char)
|
||||
}
|
||||
|
||||
export const __test = {
|
||||
findBibtexIdentifierEndExclusive,
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
type VonLast = { von: string; last: string }
|
||||
type FirstVonLast = { first: string; von: string; last: string }
|
||||
|
||||
/**
|
||||
* Splits "von Last" (the left-hand segment in BibTeX comma notation) into its
|
||||
* von particle and Last components. The final word is always part of Last;
|
||||
* preceding lowercase-initial words are the von particle.
|
||||
*/
|
||||
function parseVonLast(vonLast: string): VonLast {
|
||||
const words = vonLast.split(/\s+/).filter(Boolean)
|
||||
if (words.length === 0) return { von: '', last: '' }
|
||||
if (words.length === 1) return { von: '', last: words[0] }
|
||||
const mandatory = words[words.length - 1]
|
||||
const preceding = words.slice(0, -1)
|
||||
let lastVonIdx = -1
|
||||
for (let i = preceding.length - 1; i >= 0; i--) {
|
||||
if (/^[a-z]/.test(preceding[i])) {
|
||||
lastVonIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (lastVonIdx === -1) return { von: '', last: words.join(' ') }
|
||||
return {
|
||||
von: preceding.slice(0, lastVonIdx + 1).join(' '),
|
||||
last: [...preceding.slice(lastVonIdx + 1), mandatory].join(' '),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a "First von Last" name (no commas) into its three components. The
|
||||
* final word is always part of Last; uppercase words following the last
|
||||
* lowercase (von) word extend Last; lowercase words form the von particle.
|
||||
*/
|
||||
function parseFirstVonLast(name: string): FirstVonLast {
|
||||
const words = name.split(/\s+/).filter(Boolean)
|
||||
if (words.length === 0) return { first: '', von: '', last: '' }
|
||||
if (words.length === 1) return { first: '', von: '', last: words[0] }
|
||||
const mandatory = words[words.length - 1]
|
||||
const preceding = words.slice(0, -1)
|
||||
let firstVonIdx = -1
|
||||
let lastVonIdx = -1
|
||||
for (let i = 0; i < preceding.length; i++) {
|
||||
if (/^[a-z]/.test(preceding[i])) {
|
||||
if (firstVonIdx === -1) firstVonIdx = i
|
||||
lastVonIdx = i
|
||||
}
|
||||
}
|
||||
if (lastVonIdx === -1) {
|
||||
return { first: preceding.join(' '), von: '', last: mandatory }
|
||||
}
|
||||
return {
|
||||
first: preceding.slice(0, firstVonIdx).join(' '),
|
||||
von: preceding.slice(firstVonIdx, lastVonIdx + 1).join(' '),
|
||||
last: [...preceding.slice(lastVonIdx + 1), mandatory].join(' '),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single BibTeX author name, parsed into its constituents (First, von, Last,
|
||||
* Suffix) from any of the three standard BibTeX name notations:
|
||||
*
|
||||
* "First von Last"
|
||||
* "von Last, First"
|
||||
* "von Last, Suffix, First"
|
||||
*/
|
||||
export class BibtexName {
|
||||
readonly first: string
|
||||
readonly von: string
|
||||
readonly last: string
|
||||
readonly suffix: string
|
||||
|
||||
constructor(raw: string) {
|
||||
const trimmed = raw.trim()
|
||||
const segments = trimmed.split(',').map(s => s.trim())
|
||||
if (segments.length >= 3) {
|
||||
// "von Last, Suffix, First"
|
||||
const { von, last } = parseVonLast(segments[0])
|
||||
this.von = von
|
||||
this.last = last
|
||||
this.suffix = segments[1]
|
||||
this.first = segments.slice(2).join(' ').trim()
|
||||
} else if (segments.length === 2) {
|
||||
// "von Last, First"
|
||||
const { von, last } = parseVonLast(segments[0])
|
||||
this.von = von
|
||||
this.last = last
|
||||
this.suffix = ''
|
||||
this.first = segments[1]
|
||||
} else {
|
||||
// "First von Last"
|
||||
const { first, von, last } = parseFirstVonLast(trimmed)
|
||||
this.first = first
|
||||
this.von = von
|
||||
this.last = last
|
||||
this.suffix = ''
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the name in "First von Last Suffix" display order. */
|
||||
toFirstLast(): string {
|
||||
return [this.first, this.von, this.last, this.suffix]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
/** Returns the "von Last" component (without First or Suffix). */
|
||||
toLast(): string {
|
||||
return [this.von, this.last].filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
/** Returns the name in "von Last, First" display order (BibTeX comma notation). */
|
||||
toLastFirst(): string {
|
||||
const vonLast = [this.von, this.last].filter(Boolean).join(' ')
|
||||
return this.first ? `${vonLast}, ${this.first}` : vonLast
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parsed BibTeX author/editor field. Names are split on " and "
|
||||
* (case-insensitive). If the last token is "others" (case-insensitive), it is
|
||||
* not included in `names` and `hasOthers` is set to `true` instead.
|
||||
*
|
||||
* Examples:
|
||||
* "John Smith and Jane Doe" → names=[Smith, Doe], hasOthers=false
|
||||
* "John Smith and others" → names=[Smith], hasOthers=true
|
||||
* "others" → names=[others], hasOthers=false
|
||||
* "others and John Smith" → names=[others, Smith], hasOthers=false
|
||||
*/
|
||||
export class BibtexNameList {
|
||||
readonly names: BibtexName[]
|
||||
readonly hasOthers: boolean
|
||||
|
||||
constructor(raw: string) {
|
||||
if (!raw.trim()) {
|
||||
this.names = []
|
||||
this.hasOthers = false
|
||||
return
|
||||
}
|
||||
const parts = raw
|
||||
.split(/\s+and\s+/i)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
if (
|
||||
parts.length > 1 &&
|
||||
parts[parts.length - 1].toLowerCase() === 'others'
|
||||
) {
|
||||
this.hasOthers = true
|
||||
this.names = parts.slice(0, -1).map(s => new BibtexName(s))
|
||||
} else {
|
||||
this.hasOthers = false
|
||||
this.names = parts.map(s => new BibtexName(s))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the names into a formatted display string using the Oxford comma.
|
||||
* When `hasOthers` is true, renders "et al." in place of the omitted token.
|
||||
*
|
||||
* Examples with `format = n => n.toFirstLast()`:
|
||||
* [Smith] hasOthers=false → "Jane Smith"
|
||||
* [Smith, Doe] hasOthers=false → "Jane Smith and John Doe"
|
||||
* [Smith, Doe, Jones] → "Jane Smith, John Doe, and Bob Jones"
|
||||
* [Smith] hasOthers=true → "Jane Smith et al."
|
||||
*/
|
||||
join(format: (name: BibtexName) => string = n => n.toFirstLast()): string {
|
||||
const { names, hasOthers } = this
|
||||
if (names.length === 0) return ''
|
||||
const formatted = names.map(format)
|
||||
if (hasOthers) {
|
||||
if (formatted.length === 1) return `${formatted[0]} et al.`
|
||||
return `${formatted.join(', ')}, et al.`
|
||||
}
|
||||
if (formatted.length === 1) return formatted[0]
|
||||
if (formatted.length === 2) return `${formatted[0]} and ${formatted[1]}`
|
||||
return `${formatted.slice(0, -1).join(', ')}, and ${formatted[formatted.length - 1]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* All known authors, comma-joined (no "et al." truncation). Comma-only on
|
||||
* purpose: the result is fed through HighlightedText, so a fabricated "and"
|
||||
* would get <mark>ed for queries like "a"/"an" — never synthesise words.
|
||||
*/
|
||||
joinPlain(
|
||||
format: (name: BibtexName) => string = n => n.toFirstLast()
|
||||
): string {
|
||||
return this.names.map(format).join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a short author string using family names only. For 3+ names or
|
||||
* when `hasOthers` is true, returns the first surname followed by "et al.".
|
||||
* For exactly 2 names, joins their surnames with " & ".
|
||||
*/
|
||||
summarize(): string {
|
||||
const { names, hasOthers } = this
|
||||
if (names.length === 0) return ''
|
||||
const surnames = names.map(n => [n.von, n.last].filter(Boolean).join(' '))
|
||||
if (hasOthers || surnames.length > 2) return `${surnames[0]} et al.`
|
||||
return surnames.join(' & ')
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
StringName,
|
||||
} from '@/features/source-editor/lezer-bibtex/bibtex.terms.mjs'
|
||||
import { parser as lezerParser } from '@/features/source-editor/lezer-bibtex/bibtex.mjs'
|
||||
import { BibtexEntry } from './bibtex-entry.mts'
|
||||
import { BibtexEntry } from '@shared/bibtex/bibtex-entry.mts'
|
||||
import { PositionedBibtexEntry } from './positioned-bibtex-entry'
|
||||
import { BibtexFieldValue } from './bibtex-field-value.mts'
|
||||
import { BibtexFieldValue } from '@shared/bibtex/bibtex-field-value.mts'
|
||||
|
||||
type GetText = (from: number, to: number) => string
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Implements the BibTeX purify$ function for special characters
|
||||
*
|
||||
* Special characters are first level groupings of curly braces that start with
|
||||
* a backslash (e.g. "This is a special character: {\textbf{hello}}"). Inside
|
||||
* special characters, LaTeX commands are removed.
|
||||
*
|
||||
* For more details, see Tame the BeaST, section 10
|
||||
* (https://tug.ctan.org/info/bibtex/tamethebeast/ttb_en.pdf)
|
||||
*/
|
||||
export function purifySpecialChars(s: string) {
|
||||
let result = ''
|
||||
let currSpecialChar: string | null = null
|
||||
let openBraces = 0
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const c = s[i]
|
||||
if (c === '{') {
|
||||
openBraces += 1
|
||||
} else if (c === '}') {
|
||||
openBraces -= 1
|
||||
} else if (c === '\\' && openBraces === 1) {
|
||||
// Open a special character
|
||||
currSpecialChar = ''
|
||||
}
|
||||
|
||||
if (currSpecialChar != null && openBraces === 0) {
|
||||
// We just closed a special char: remove all LaTeX command and add to the
|
||||
// result
|
||||
result += currSpecialChar.replaceAll(/\\([A-Za-z]+|[^A-Za-z])\s*/g, '')
|
||||
currSpecialChar = null
|
||||
}
|
||||
|
||||
// Accumulate the character either in the current special character or in
|
||||
// the final result
|
||||
if (currSpecialChar != null) {
|
||||
currSpecialChar += c
|
||||
} else {
|
||||
result += c
|
||||
}
|
||||
}
|
||||
|
||||
// If the string is malformed, just append the unprocessed special character
|
||||
// as-is
|
||||
if (currSpecialChar != null) {
|
||||
result += currSpecialChar
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { BibtexNameList } from './bibtex-name.mts'
|
||||
import { BibtexNameList } from '@shared/bibtex/bibtex-name.mts'
|
||||
|
||||
const YEAR_REGEX = /^\d{4}$/
|
||||
|
||||
@@ -1,479 +0,0 @@
|
||||
import type { BibEntryType } from './bibtex-entry-schema.mts'
|
||||
|
||||
const entryTypes: Map<string, BibEntryType> = new Map()
|
||||
|
||||
entryTypes.set('article', {
|
||||
key: 'article',
|
||||
label: 'Article',
|
||||
fields: ['author', 'title', 'journal', 'journaltitle', 'year', 'date'],
|
||||
requiredFields: [
|
||||
'author',
|
||||
'title',
|
||||
['journal', 'journaltitle'],
|
||||
['year', 'date'],
|
||||
],
|
||||
optionalFields: [
|
||||
'translator',
|
||||
'annotator',
|
||||
'commentator',
|
||||
'subtitle',
|
||||
'titleaddon',
|
||||
'editor',
|
||||
'editora',
|
||||
'editorb',
|
||||
'editorc',
|
||||
'journalsubtitle',
|
||||
'journaltitleaddon',
|
||||
'issuetitle',
|
||||
'issuesubtitle',
|
||||
'issuetitleaddon',
|
||||
'language',
|
||||
'origlanguage',
|
||||
'series',
|
||||
'volume',
|
||||
'number',
|
||||
'eid',
|
||||
'issue',
|
||||
'month',
|
||||
'pages',
|
||||
'version',
|
||||
'note',
|
||||
'issn',
|
||||
'addendum',
|
||||
'pubstate',
|
||||
'doi',
|
||||
'eprint',
|
||||
'eprintclass',
|
||||
'eprinttype',
|
||||
'url',
|
||||
'urldate',
|
||||
],
|
||||
})
|
||||
entryTypes.set('artwork', {
|
||||
key: 'artwork',
|
||||
label: 'Artwork',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('audio', {
|
||||
key: 'audio',
|
||||
label: 'Audio',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('book', {
|
||||
key: 'book',
|
||||
label: 'Book',
|
||||
fields: ['author', 'editor', 'title', 'publisher', 'year', 'date'],
|
||||
requiredFields: [['author', 'editor'], 'title', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('bookinbook', {
|
||||
key: 'bookinbook',
|
||||
label: 'Book in book',
|
||||
fields: [
|
||||
'author',
|
||||
'editor',
|
||||
'title',
|
||||
'booktitle',
|
||||
'chapter',
|
||||
'pages',
|
||||
'publisher',
|
||||
'year',
|
||||
'date',
|
||||
],
|
||||
requiredFields: ['author', 'title', 'booktitle', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('booklet', {
|
||||
key: 'booklet',
|
||||
label: 'Booklet',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: ['title'],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('commentary', {
|
||||
key: 'commentary',
|
||||
label: 'Commentary',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('conference', {
|
||||
key: 'conference',
|
||||
label: 'Conference',
|
||||
fields: ['author', 'title', 'booktitle', 'year', 'date'],
|
||||
requiredFields: ['author', 'title', 'booktitle', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('collection', {
|
||||
key: 'collection',
|
||||
label: 'Collection',
|
||||
fields: ['editor', 'title', 'year', 'date'],
|
||||
requiredFields: ['editor', 'title', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('dataset', {
|
||||
key: 'dataset',
|
||||
label: 'Dataset',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [['author', 'editor'], 'title', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('electronic', {
|
||||
key: 'electronic',
|
||||
label: 'Electronic resource',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date', 'doi', 'eprint', 'url'],
|
||||
requiredFields: [
|
||||
['author', 'editor'],
|
||||
'title',
|
||||
['year', 'date'],
|
||||
['doi', 'eprint', 'url'],
|
||||
],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('image', {
|
||||
key: 'image',
|
||||
label: 'Image',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('inproceedings', {
|
||||
key: 'inproceedings',
|
||||
label: 'In proceedings',
|
||||
fields: ['author', 'title', 'booktitle', 'year', 'date'],
|
||||
requiredFields: ['author', 'title', 'booktitle', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('inbook', {
|
||||
key: 'inbook',
|
||||
label: 'In book',
|
||||
fields: [
|
||||
'author',
|
||||
'editor',
|
||||
'title',
|
||||
'booktitle',
|
||||
'chapter',
|
||||
'pages',
|
||||
'publisher',
|
||||
'year',
|
||||
'date',
|
||||
],
|
||||
requiredFields: [['author', 'editor'], 'title', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('incollection', {
|
||||
key: 'incollection',
|
||||
label: 'In collection',
|
||||
fields: ['author', 'title', 'booktitle', 'publisher', 'year', 'date'],
|
||||
requiredFields: ['author', 'title', 'booktitle', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('inreference', {
|
||||
key: 'inreference',
|
||||
label: 'In reference',
|
||||
fields: ['author', 'editor', 'title', 'booktitle', 'year', 'date'],
|
||||
requiredFields: ['author', 'title', 'editor', 'booktitle', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('jurisdiction', {
|
||||
key: 'jurisdiction',
|
||||
label: 'Jurisdiction',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('manual', {
|
||||
key: 'manual',
|
||||
label: 'Manual',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: ['title'],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('mastersthesis', {
|
||||
key: 'mastersthesis',
|
||||
label: "Master's thesis",
|
||||
fields: ['author', 'title', 'institution', 'school', 'year', 'date'],
|
||||
requiredFields: [
|
||||
'author',
|
||||
'title',
|
||||
['school', 'institution'],
|
||||
['year', 'date'],
|
||||
],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('misc', {
|
||||
key: 'misc',
|
||||
label: 'Miscellaneous',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('movie', {
|
||||
key: 'movie',
|
||||
label: 'Movie',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('music', {
|
||||
key: 'music',
|
||||
label: 'Music',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('mvbook', {
|
||||
key: 'mvbook',
|
||||
label: 'Multi-volume book',
|
||||
fields: ['author', 'title', 'year', 'date'],
|
||||
requiredFields: ['author', 'title', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('mvcollection', {
|
||||
key: 'mvcollection',
|
||||
label: 'Multi-volume collection',
|
||||
fields: ['editor', 'title', 'year', 'date'],
|
||||
requiredFields: ['editor', 'title', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('mvproceedings', {
|
||||
key: 'mvproceedings',
|
||||
label: 'Multi-volume proceedings',
|
||||
fields: ['title', 'year', 'date'],
|
||||
requiredFields: ['title', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('mvreference', {
|
||||
key: 'mvreference',
|
||||
label: 'Multi-volume reference',
|
||||
fields: ['editor', 'title', 'year', 'date'],
|
||||
requiredFields: ['editor', 'title', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('legal', {
|
||||
key: 'legal',
|
||||
label: 'Legal',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('legislation', {
|
||||
key: 'legislation',
|
||||
label: 'Legislation',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('letter', {
|
||||
key: 'letter',
|
||||
label: 'Letter',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('online', {
|
||||
key: 'online',
|
||||
label: 'Online resource',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date', 'doi', 'eprint', 'url'],
|
||||
requiredFields: [
|
||||
['author', 'editor'],
|
||||
'title',
|
||||
['year', 'date'],
|
||||
['doi', 'eprint', 'url'],
|
||||
],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('patent', {
|
||||
key: 'patent',
|
||||
label: 'Patent',
|
||||
fields: ['author', 'title', 'number', 'year', 'date'],
|
||||
requiredFields: ['author', 'title', 'number', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('performance', {
|
||||
key: 'performance',
|
||||
label: 'Performance',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('periodical', {
|
||||
key: 'periodical',
|
||||
label: 'Periodical',
|
||||
fields: ['editor', 'title', 'year', 'date'],
|
||||
requiredFields: ['editor', 'title', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('phdthesis', {
|
||||
key: 'phdthesis',
|
||||
label: 'PhD thesis',
|
||||
fields: ['author', 'title', 'institution', 'school', 'year', 'date'],
|
||||
requiredFields: [
|
||||
'author',
|
||||
'title',
|
||||
['school', 'institution'],
|
||||
['year', 'date'],
|
||||
],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('proceedings', {
|
||||
key: 'proceedings',
|
||||
label: 'Proceedings',
|
||||
fields: ['title', 'year', 'date'],
|
||||
requiredFields: ['title', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('reference', {
|
||||
key: 'reference',
|
||||
label: 'Reference',
|
||||
fields: ['editor', 'title', 'year', 'date'],
|
||||
requiredFields: ['editor', 'title', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('report', {
|
||||
key: 'report',
|
||||
label: 'Report',
|
||||
fields: ['author', 'title', 'type', 'institution', 'year', 'date'],
|
||||
requiredFields: ['author', 'title', 'type', 'institution', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('review', {
|
||||
key: 'review',
|
||||
label: 'Review',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('software', {
|
||||
key: 'software',
|
||||
label: 'Software',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('standard', {
|
||||
key: 'standard',
|
||||
label: 'Standard',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('suppbook', {
|
||||
key: 'suppbook',
|
||||
label: 'Supplemental material in book',
|
||||
fields: [
|
||||
'author',
|
||||
'editor',
|
||||
'title',
|
||||
'booktitle',
|
||||
'chapter',
|
||||
'pages',
|
||||
'publisher',
|
||||
'year',
|
||||
'date',
|
||||
],
|
||||
requiredFields: ['author', 'title', 'booktitle', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('suppcollection', {
|
||||
key: 'suppcollection',
|
||||
label: 'Supplemental material in collection',
|
||||
fields: ['author', 'editor', 'title', 'booktitle', 'year', 'date'],
|
||||
requiredFields: ['author', 'title', 'editor', 'booktitle', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('suppperiodical', {
|
||||
key: 'suppperiodical',
|
||||
label: 'Supplemental material in periodical',
|
||||
fields: ['author', 'title', 'journaltitle', 'year', 'date'],
|
||||
optionalFields: [],
|
||||
requiredFields: ['author', 'title', 'journaltitle', ['year', 'date']],
|
||||
})
|
||||
entryTypes.set('techreport', {
|
||||
key: 'techreport',
|
||||
label: 'Tech report',
|
||||
fields: ['author', 'title', 'institution', 'year', 'date'],
|
||||
requiredFields: ['author', 'title', 'institution', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('thesis', {
|
||||
key: 'thesis',
|
||||
label: 'Thesis',
|
||||
fields: ['author', 'title', 'type', 'institution', 'year', 'date'],
|
||||
requiredFields: ['author', 'title', 'type', 'institution', ['year', 'date']],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('unpublished', {
|
||||
key: 'unpublished',
|
||||
label: 'Unpublished',
|
||||
fields: ['author', 'title', 'year', 'date', 'note'],
|
||||
requiredFields: ['author', 'title'],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('video', {
|
||||
key: 'video',
|
||||
label: 'Video',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date'],
|
||||
requiredFields: [],
|
||||
optionalFields: [],
|
||||
})
|
||||
entryTypes.set('www', {
|
||||
key: 'www',
|
||||
label: 'WWW',
|
||||
fields: ['author', 'editor', 'title', 'year', 'date', 'doi', 'eprint', 'url'],
|
||||
requiredFields: [
|
||||
['author', 'editor'],
|
||||
'title',
|
||||
['year', 'date'],
|
||||
['doi', 'eprint', 'url'],
|
||||
],
|
||||
optionalFields: [],
|
||||
})
|
||||
|
||||
function getMiscEntryType(): BibEntryType | undefined {
|
||||
return entryTypes.get('misc')
|
||||
}
|
||||
|
||||
export function isEntryType(key: string): boolean {
|
||||
return entryTypes.has(key)
|
||||
}
|
||||
|
||||
export function getEntryType(key: string): BibEntryType {
|
||||
const entryType = entryTypes.get(key)
|
||||
if (entryType != null) {
|
||||
return entryType
|
||||
} else {
|
||||
return generateCustomEntryType(key)
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllEntryTypes(): BibEntryType[] {
|
||||
return Array.from(entryTypes.values())
|
||||
}
|
||||
|
||||
export function getAllEntryKeys(): string[] {
|
||||
return Array.from(entryTypes.keys()) ?? []
|
||||
}
|
||||
|
||||
export function generateCustomEntryType(key: string): BibEntryType {
|
||||
const miscEntryType = getMiscEntryType()
|
||||
return {
|
||||
key,
|
||||
label: key,
|
||||
fields: miscEntryType?.fields || [],
|
||||
optionalFields: miscEntryType?.optionalFields || [],
|
||||
requiredFields: miscEntryType?.requiredFields || [],
|
||||
}
|
||||
}
|
||||
|
||||
export function getTypeFields(type: string): string[] {
|
||||
return (
|
||||
(!isEntryType(type)
|
||||
? getMiscEntryType()?.fields
|
||||
: getEntryType(type)?.fields) || []
|
||||
)
|
||||
}
|
||||
@@ -1,876 +0,0 @@
|
||||
export function latexToUnicode(value: string) {
|
||||
let s = value
|
||||
|
||||
// First, replace the LaTeX commands we know about that take no argument
|
||||
//
|
||||
// A LaTeX command is one of:
|
||||
//
|
||||
// - a backslash followed by letters (\textbf) and optional spaces
|
||||
// - a backslash followed by a single non-letter (\^) and optional spaces
|
||||
s = s.replaceAll(/\\([a-zA-Z]+|[^a-zA-Z])\s*/g, noArgsReplacer)
|
||||
|
||||
// Next, replace the LaTeX commands we know about that take one argument
|
||||
//
|
||||
// This first regex identifies arguments that are not inside braces. These
|
||||
// can be:
|
||||
//
|
||||
// - a single character (\'e)
|
||||
// - another LaTeX command (\^\alpha) - we don't support args for that command
|
||||
s = s.replaceAll(
|
||||
/\\([a-zA-Z]+|[^a-zA-Z])\s*([^{\\]|\\[^a-zA-Z]|[a-zA-Z]+)/g,
|
||||
singleArgReplacer
|
||||
)
|
||||
|
||||
// This second regex identifies arguments inside braces. We support two levels
|
||||
// of braces. For example:
|
||||
//
|
||||
// - \'{e}
|
||||
// - \mkbibquote{\'{e}cole}
|
||||
//
|
||||
// Because we support two levels of nested braces, we also need to run this
|
||||
// twice.
|
||||
for (let i = 0; i < 2; i++) {
|
||||
s = s.replaceAll(
|
||||
/\\([a-zA-Z]+|[^a-zA-Z])\s*{([^{}]*(?:{[^{}]*}[^{}]*)*)}/g,
|
||||
singleArgReplacer
|
||||
)
|
||||
}
|
||||
return s.normalize('NFC')
|
||||
}
|
||||
|
||||
function noArgsReplacer(match: string, command: string) {
|
||||
return NO_ARGS_REPLACEMENTS[command] ?? match
|
||||
}
|
||||
|
||||
function singleArgReplacer(match: string, command: string, arg: string) {
|
||||
if (command === 'mkbibquote') {
|
||||
return `"${arg}"`
|
||||
}
|
||||
|
||||
const replacement = SINGLE_ARG_REPLACEMENTS[command]?.[arg]
|
||||
if (replacement != null) {
|
||||
return replacement
|
||||
}
|
||||
|
||||
if (arg.length === 1) {
|
||||
const combiningChar = COMBINING_CHAR_REPLACEMENTS[command]
|
||||
if (combiningChar != null) {
|
||||
return arg + combiningChar
|
||||
}
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
/**
|
||||
* List of replacements from bib2json (https://github.com/mayanklahiri/bib2json)
|
||||
*
|
||||
* Copyright 2012-13 Mayank Lahiri
|
||||
* mlahiri@gmail.com
|
||||
* Released under the BSD License.
|
||||
*
|
||||
* Modifications 2016 Sharelatex
|
||||
* Modifications 2017-2020 Overleaf
|
||||
*/
|
||||
const NO_ARGS_REPLACEMENTS: Record<string, string> = {
|
||||
aa: 'å',
|
||||
AA: 'Å',
|
||||
AE: 'Æ',
|
||||
ae: 'æ',
|
||||
aleph: 'ℵ',
|
||||
allequal: '≌',
|
||||
Alpha: 'Α',
|
||||
alpha: 'α',
|
||||
amalg: '⨿',
|
||||
angle: '∠',
|
||||
Angle: '⦜',
|
||||
approxeq: '≊',
|
||||
approxnotequal: '≆',
|
||||
approx: '≈',
|
||||
aquarius: '♒',
|
||||
aries: '♈',
|
||||
arrowwaveleft: '↜',
|
||||
arrowwaveright: '↝',
|
||||
ast: '*',
|
||||
asymp: '≍',
|
||||
backepsilon: '϶',
|
||||
backprime: '‵',
|
||||
backsimeq: '⋍',
|
||||
backsim: '∽',
|
||||
barwedge: '⌅',
|
||||
because: '∵',
|
||||
Beta: 'Β',
|
||||
beta: 'β',
|
||||
beth: 'ℶ',
|
||||
between: '≬',
|
||||
bigcap: '⋂',
|
||||
bigcirc: '◯',
|
||||
bigcup: '⋃',
|
||||
bigtriangledown: '▽',
|
||||
bigtriangleup: '△',
|
||||
blacklozenge: '⧫',
|
||||
blacksquare: '▪',
|
||||
blacktriangledown: '▾',
|
||||
blacktriangleleft: '◂',
|
||||
blacktriangleright: '▸',
|
||||
blacktriangle: '▴',
|
||||
bowtie: '⋈',
|
||||
boxdot: '⊡',
|
||||
boxminus: '⊟',
|
||||
boxplus: '⊞',
|
||||
boxtimes: '⊠',
|
||||
bullet: '∙',
|
||||
Bumpeq: '≎',
|
||||
bumpeq: '≏',
|
||||
cancer: '♋',
|
||||
capricornus: '♑',
|
||||
cap: '∩',
|
||||
Cap: '⋒',
|
||||
cdots: '⋯',
|
||||
cdot: '·',
|
||||
Chi: 'Χ',
|
||||
chi: 'χ',
|
||||
circeq: '≗',
|
||||
circlearrowleft: '↺',
|
||||
circlearrowright: '↻',
|
||||
circledast: '⊛',
|
||||
circledcirc: '⊚',
|
||||
circleddash: '⊝',
|
||||
circledS: 'Ⓢ',
|
||||
circ: '∘',
|
||||
clockoint: '⨏',
|
||||
clwintegral: '∱',
|
||||
Colon: '∷',
|
||||
complement: '∁',
|
||||
cong: '≅',
|
||||
coprod: '∐',
|
||||
cup: '∪',
|
||||
Cup: '⋓',
|
||||
curlyeqprec: '⋞',
|
||||
curlyeqsucc: '⋟',
|
||||
curlyvee: '⋎',
|
||||
curlywedge: '⋏',
|
||||
curvearrowleft: '↶',
|
||||
curvearrowright: '↷',
|
||||
daleth: 'ℸ',
|
||||
dashv: '⊣',
|
||||
dblarrowupdown: '⇅',
|
||||
ddddot: '⃜',
|
||||
dddot: '⃛',
|
||||
Delta: 'Δ',
|
||||
delta: 'δ',
|
||||
DH: 'Ð',
|
||||
dh: 'ð',
|
||||
diagup: '╱',
|
||||
diamond: '⋄',
|
||||
Digamma: 'Ϝ',
|
||||
digamma: 'ϝ',
|
||||
divideontimes: '⋇',
|
||||
div: '÷',
|
||||
DJ: 'Đ',
|
||||
dj: 'đ',
|
||||
doteqdot: '≑',
|
||||
doteq: '≐',
|
||||
dotplus: '∔',
|
||||
DownArrowBar: '⤓',
|
||||
downarrow: '↓',
|
||||
Downarrow: '⇓',
|
||||
DownArrowUpArrow: '⇵',
|
||||
downdownarrows: '⇊',
|
||||
downharpoonleft: '⇃',
|
||||
downharpoonright: '⇂',
|
||||
DownLeftRightVector: '⥐',
|
||||
DownLeftTeeVector: '⥞',
|
||||
DownLeftVectorBar: '⥖',
|
||||
DownRightTeeVector: '⥟',
|
||||
DownRightVectorBar: '⥗',
|
||||
downslopeellipsis: '⋱',
|
||||
eighthnote: '♪',
|
||||
Elolarr: '⥀',
|
||||
Elorarr: '⥁',
|
||||
ElOr: '⩖',
|
||||
Elroang: '⦆',
|
||||
Elxsqcup: '⨆',
|
||||
Elxuplus: '⨄',
|
||||
ElzAnd: '⩓',
|
||||
Elzbar: '̶',
|
||||
Elzbtdl: 'ɬ',
|
||||
ElzCint: '⨍',
|
||||
Elzcirfb: '◒',
|
||||
Elzcirfl: '◐',
|
||||
Elzcirfr: '◑',
|
||||
Elzclomeg: 'ɷ',
|
||||
Elzddfnc: '⦙',
|
||||
Elzdefas: '⧋',
|
||||
Elzdlcorn: '⎣',
|
||||
Elzdshfnc: '┆',
|
||||
Elzdyogh: 'ʤ',
|
||||
Elzesh: 'ʃ',
|
||||
Elzfhr: 'ɾ',
|
||||
Elzglst: 'ʔ',
|
||||
Elzhlmrk: 'ˑ',
|
||||
ElzInf: '⨇',
|
||||
Elzinglst: 'ʖ',
|
||||
Elzinvv: 'ʌ',
|
||||
Elzinvw: 'ʍ',
|
||||
ElzLap: '⧊',
|
||||
Elzlmrk: 'ː',
|
||||
Elzlow: '˕',
|
||||
Elzlpargt: '⦠',
|
||||
Elzltlmr: 'ɱ',
|
||||
Elzltln: 'ɲ',
|
||||
Elzminhat: '⩟',
|
||||
Elzopeno: 'ɔ',
|
||||
ElzOr: '⩔',
|
||||
Elzpalh: '̡',
|
||||
Elzpbgam: 'ɤ',
|
||||
Elzpgamma: 'ɣ',
|
||||
Elzpscrv: 'ʋ',
|
||||
Elzpupsil: 'ʊ',
|
||||
Elzrais: '˔',
|
||||
Elzrarrx: '⥇',
|
||||
Elzreapos: '‛',
|
||||
Elzreglst: 'ʕ',
|
||||
Elzrh: '̢',
|
||||
ElzRlarr: '⥂',
|
||||
ElzrLarr: '⥄',
|
||||
Elzrl: 'ɼ',
|
||||
Elzrtld: 'ɖ',
|
||||
Elzrtll: 'ɭ',
|
||||
Elzrtln: 'ɳ',
|
||||
Elzrtlr: 'ɽ',
|
||||
Elzrtls: 'ʂ',
|
||||
Elzrtlt: 'ʈ',
|
||||
Elzrtlz: 'ʐ',
|
||||
Elzrttrnr: 'ɻ',
|
||||
Elzrvbull: '◘',
|
||||
Elzsbbrg: '̪',
|
||||
Elzsblhr: '˓',
|
||||
Elzsbrhr: '˒',
|
||||
Elzschwa: 'ə',
|
||||
Elzsqfl: '◧',
|
||||
Elzsqfnw: '┙',
|
||||
Elzsqfr: '◨',
|
||||
Elzsqfse: '◪',
|
||||
Elzsqspne: '⋥',
|
||||
ElzSup: '⨈',
|
||||
Elztdcol: '⫶',
|
||||
Elztesh: 'ʧ',
|
||||
Elztfnc: '⦀',
|
||||
ElzThr: '⨅',
|
||||
ElzTimes: '⨯',
|
||||
Elztrna: 'ɐ',
|
||||
Elztrnh: 'ɥ',
|
||||
Elztrnmlr: 'ɰ',
|
||||
Elztrnm: 'ɯ',
|
||||
Elztrnrl: 'ɺ',
|
||||
Elztrnr: 'ɹ',
|
||||
Elztrnsa: 'ɒ',
|
||||
Elztrnt: 'ʇ',
|
||||
Elztrny: 'ʎ',
|
||||
Elzverti: 'ˌ',
|
||||
Elzverts: 'ˈ',
|
||||
Elzvrecto: '▯',
|
||||
Elzxh: 'ħ',
|
||||
Elzxl: '̵',
|
||||
Elzxrat: '℞',
|
||||
Elzyogh: 'ʒ',
|
||||
Epsilon: 'Ε',
|
||||
epsilon: 'ε',
|
||||
eqcirc: '≖',
|
||||
eqslantgtr: '⪖',
|
||||
eqslantless: '⪕',
|
||||
Equal: '⩵',
|
||||
equiv: '≡',
|
||||
estimates: '≙',
|
||||
Eta: 'Η',
|
||||
eta: 'η',
|
||||
eth: 'ƪ',
|
||||
exists: '∃',
|
||||
fallingdotseq: '≒',
|
||||
flat: '♭',
|
||||
forall: '∀',
|
||||
forcesextra: '⊨',
|
||||
frown: '⌢',
|
||||
Gamma: 'Γ',
|
||||
gamma: 'γ',
|
||||
gemini: '♊',
|
||||
geqq: '≧',
|
||||
geqslant: '⩾',
|
||||
geq: '≥',
|
||||
gg: '≫',
|
||||
gimel: 'ℷ',
|
||||
gnapprox: '⪊',
|
||||
gneqq: '≩',
|
||||
gneq: '⪈',
|
||||
gnsim: '⋧',
|
||||
greaterequivlnt: '≳',
|
||||
gtrapprox: '⪆',
|
||||
gtrdot: '⋗',
|
||||
gtreqless: '⋛',
|
||||
gtreqqless: '⪌',
|
||||
gtrless: '≷',
|
||||
guillemotleft: '«',
|
||||
guillemotright: '»',
|
||||
guilsinglleft: '‹',
|
||||
guilsinglright: '›',
|
||||
hermitconjmatrix: '⊹',
|
||||
homothetic: '∻',
|
||||
hookleftarrow: '↩',
|
||||
hookrightarrow: '↪',
|
||||
hslash: 'ℏ',
|
||||
image: '⊷',
|
||||
infty: '∞',
|
||||
intercal: '⊺',
|
||||
int: '∫',
|
||||
in: '∈',
|
||||
Iota: 'Ι',
|
||||
iota: 'ι',
|
||||
i: 'ı',
|
||||
jupiter: '♃',
|
||||
Kappa: 'Κ',
|
||||
kappa: 'κ',
|
||||
Koppa: 'Ϟ',
|
||||
Lambda: 'Λ',
|
||||
lambda: 'λ',
|
||||
langle: '〈',
|
||||
lazysinv: '∾',
|
||||
lbrace: '{',
|
||||
lceil: '⌈',
|
||||
ldots: '…',
|
||||
leftarrowtail: '↢',
|
||||
leftarrow: '←',
|
||||
Leftarrow: '⇐',
|
||||
LeftDownTeeVector: '⥡',
|
||||
LeftDownVectorBar: '⥙',
|
||||
leftharpoondown: '↽',
|
||||
leftharpoonup: '↼',
|
||||
leftleftarrows: '⇇',
|
||||
leftrightarrows: '⇆',
|
||||
leftrightarrow: '↔',
|
||||
Leftrightarrow: '⇔',
|
||||
leftrightharpoons: '⇋',
|
||||
leftrightsquigarrow: '↭',
|
||||
LeftRightVector: '⥎',
|
||||
LeftTeeVector: '⥚',
|
||||
leftthreetimes: '⋋',
|
||||
LeftTriangleBar: '⧏',
|
||||
LeftUpDownVector: '⥑',
|
||||
LeftUpTeeVector: '⥠',
|
||||
LeftUpVectorBar: '⥘',
|
||||
LeftVectorBar: '⥒',
|
||||
leo: '♌',
|
||||
leqq: '≦',
|
||||
leqslant: '⩽',
|
||||
leq: '≤',
|
||||
lessapprox: '⪅',
|
||||
lessdot: '⋖',
|
||||
lesseqgtr: '⋚',
|
||||
lesseqqgtr: '⪋',
|
||||
lessequivlnt: '≲',
|
||||
lessgtr: '≶',
|
||||
lfloor: '⌊',
|
||||
libra: '♎',
|
||||
llcorner: '⌞',
|
||||
Lleftarrow: '⇚',
|
||||
ll: '≪',
|
||||
lmoustache: '⎰',
|
||||
lnapprox: '⪉',
|
||||
lneqq: '≨',
|
||||
lneq: '⪇',
|
||||
lnot: '¬',
|
||||
lnsim: '⋦',
|
||||
longleftarrow: '⟵',
|
||||
Longleftarrow: '⟸',
|
||||
longleftrightarrow: '⟷',
|
||||
Longleftrightarrow: '⟺',
|
||||
longmapsto: '⟼',
|
||||
longrightarrow: '⟶',
|
||||
Longrightarrow: '⟹',
|
||||
looparrowleft: '↫',
|
||||
looparrowright: '↬',
|
||||
lozenge: '◊',
|
||||
lrcorner: '⌟',
|
||||
Lsh: '↰',
|
||||
ltimes: '⋉',
|
||||
L: 'Ł',
|
||||
l: 'ł',
|
||||
male: '♂',
|
||||
mapsto: '↦',
|
||||
measuredangle: '∡',
|
||||
mercury: '☿',
|
||||
mho: '℧',
|
||||
mid: '∣',
|
||||
mp: '∓',
|
||||
multimap: '⊸',
|
||||
mu: 'μ',
|
||||
nabla: '∇',
|
||||
natural: '♮',
|
||||
nearrow: '↗',
|
||||
neptune: '♆',
|
||||
NestedGreaterGreater: '⪢',
|
||||
NestedLessLess: '⪡',
|
||||
nexists: '∄',
|
||||
NG: 'Ŋ',
|
||||
ng: 'ŋ',
|
||||
ni: '∋',
|
||||
nleftarrow: '↚',
|
||||
nLeftarrow: '⇍',
|
||||
nleftrightarrow: '↮',
|
||||
nLeftrightarrow: '⇎',
|
||||
nmid: '∤',
|
||||
nolinebreak: '',
|
||||
notgreaterless: '≹',
|
||||
notlessgreater: '≸',
|
||||
nparallel: '∦',
|
||||
nrightarrow: '↛',
|
||||
nRightarrow: '⇏',
|
||||
ntrianglelefteq: '⋬',
|
||||
ntriangleleft: '⋪',
|
||||
ntrianglerighteq: '⋭',
|
||||
ntriangleright: '⋫',
|
||||
nu: 'ν',
|
||||
nvdash: '⊬',
|
||||
nvDash: '⊭',
|
||||
nVdash: '⊮',
|
||||
nVDash: '⊯',
|
||||
nwarrow: '↖',
|
||||
odot: '⊙',
|
||||
OE: 'Œ',
|
||||
oe: 'œ',
|
||||
oint: '∮',
|
||||
Omega: 'Ω',
|
||||
omega: 'ω',
|
||||
ominus: '⊖',
|
||||
openbracketleft: '〚',
|
||||
openbracketright: '〛',
|
||||
oplus: '⊕',
|
||||
original: '⊶',
|
||||
oslash: '⊘',
|
||||
otimes: '⊗',
|
||||
O: 'Ø',
|
||||
o: 'ø',
|
||||
parallel: '∥',
|
||||
partial: '∂',
|
||||
perp: '⊥',
|
||||
perspcorrespond: '⩞',
|
||||
Phi: 'Φ',
|
||||
phi: 'ϕ',
|
||||
pisces: '♓',
|
||||
pitchfork: '⋔',
|
||||
Pi: 'Π',
|
||||
pi: 'π',
|
||||
pluto: '♇',
|
||||
pm: '±',
|
||||
precapprox: '⪷',
|
||||
preccurlyeq: '≼',
|
||||
precedesnotsimilar: '⋨',
|
||||
preceq: '⪯',
|
||||
precnapprox: '⪹',
|
||||
precneqq: '⪵',
|
||||
prec: '≺',
|
||||
prod: '∏',
|
||||
propto: '∝',
|
||||
Psi: 'Ψ',
|
||||
psi: 'ψ',
|
||||
quarternote: '♩',
|
||||
rangle: '〉',
|
||||
rbrace: '}',
|
||||
rceil: '⌉',
|
||||
recorder: '⌕',
|
||||
ReverseUpEquilibrium: '⥯',
|
||||
rfloor: '⌋',
|
||||
Rho: 'Ρ',
|
||||
rho: 'ρ',
|
||||
rightanglearc: '⊾',
|
||||
rightangle: '∟',
|
||||
rightarrowtail: '↣',
|
||||
rightarrow: '→',
|
||||
Rightarrow: '⇒',
|
||||
RightDownTeeVector: '⥝',
|
||||
RightDownVectorBar: '⥕',
|
||||
rightharpoondown: '⇁',
|
||||
rightharpoonup: '⇀',
|
||||
rightleftarrows: '⇄',
|
||||
rightleftharpoons: '⇌',
|
||||
rightmoon: '☾',
|
||||
rightrightarrows: '⇉',
|
||||
rightsquigarrow: '⇝',
|
||||
RightTeeVector: '⥛',
|
||||
rightthreetimes: '⋌',
|
||||
RightTriangleBar: '⧐',
|
||||
RightUpDownVector: '⥏',
|
||||
RightUpTeeVector: '⥜',
|
||||
RightUpVectorBar: '⥔',
|
||||
RightVectorBar: '⥓',
|
||||
risingdotseq: '≓',
|
||||
rmoustache: '⎱',
|
||||
RoundImplies: '⥰',
|
||||
Rrightarrow: '⇛',
|
||||
Rsh: '↱',
|
||||
rtimes: '⋊',
|
||||
RuleDelayed: '⧴',
|
||||
sagittarius: '♐',
|
||||
Sampi: 'Ϡ',
|
||||
saturn: '♄',
|
||||
scorpio: '♏',
|
||||
searrow: '↘',
|
||||
setminus: '∖',
|
||||
sharp: '♯',
|
||||
Sigma: 'Σ',
|
||||
sigma: 'σ',
|
||||
simeq: '≃',
|
||||
sim: '∼',
|
||||
smile: '⌣',
|
||||
space: ' ',
|
||||
sphericalangle: '∢',
|
||||
sqcap: '⊓',
|
||||
sqcup: '⊔',
|
||||
sqrint: '⨖',
|
||||
sqsubseteq: '⊑',
|
||||
sqsubset: '⊏',
|
||||
sqsupseteq: '⊒',
|
||||
sqsupset: '⊐',
|
||||
square: '□',
|
||||
ss: 'ß',
|
||||
starequal: '≛',
|
||||
star: '⋆',
|
||||
Stigma: 'Ϛ',
|
||||
subseteqq: '⫅',
|
||||
subseteq: '⊆',
|
||||
subsetneqq: '⫋',
|
||||
subsetneq: '⊊',
|
||||
subset: '⊂',
|
||||
Subset: '⋐',
|
||||
succapprox: '≿',
|
||||
succcurlyeq: '≽',
|
||||
succeq: '⪰',
|
||||
succnapprox: '⪺',
|
||||
succneqq: '⪶',
|
||||
succnsim: '⋩',
|
||||
succ: '≻',
|
||||
sum: '∑',
|
||||
supseteqq: '⫆',
|
||||
supseteq: '⊇',
|
||||
supsetneqq: '⫌',
|
||||
supsetneq: '⊋',
|
||||
supset: '⊃',
|
||||
Supset: '⋑',
|
||||
surd: '√',
|
||||
surfintegral: '∯',
|
||||
swarrow: '↙',
|
||||
taurus: '♉',
|
||||
Tau: 'Τ',
|
||||
tau: 'τ',
|
||||
textasciiacute: '´',
|
||||
textasciibreve: '˘',
|
||||
textasciicaron: 'ˇ',
|
||||
textasciidieresis: '¨',
|
||||
textasciigrave: '`',
|
||||
textasciimacron: '¯',
|
||||
textasciitilde: '~',
|
||||
textbackslash: '\\',
|
||||
textbrokenbar: '¦',
|
||||
textbullet: '•',
|
||||
textcent: '¢',
|
||||
textcopyright: '©',
|
||||
textcurrency: '¤',
|
||||
textdaggerdbl: '‡',
|
||||
textdagger: '†',
|
||||
textdegree: '°',
|
||||
textdollar: '$',
|
||||
textdoublepipe: 'ǂ',
|
||||
textemdash: '—',
|
||||
textendash: '–',
|
||||
texteuro: '€',
|
||||
textexclamdown: '¡',
|
||||
texthvlig: 'ƕ',
|
||||
textnrleg: 'ƞ',
|
||||
textonehalf: '½',
|
||||
textonequarter: '¼',
|
||||
textordfeminine: 'ª',
|
||||
textordmasculine: 'º',
|
||||
textparagraph: '¶',
|
||||
textperiodcentered: '˙',
|
||||
textpertenthousand: '‱',
|
||||
textperthousand: '‰',
|
||||
textphi: 'ɸ',
|
||||
textquestiondown: '¿',
|
||||
textquotedblleft: '“',
|
||||
textquotedblright: '”',
|
||||
textquotesingle: "'",
|
||||
textregistered: '®',
|
||||
textsection: '§',
|
||||
textsterling: '£',
|
||||
texttheta: 'θ',
|
||||
textTheta: 'ϴ',
|
||||
textthreequarters: '¾',
|
||||
texttildelow: '˜',
|
||||
texttimes: '×',
|
||||
texttrademark: '™',
|
||||
textturnk: 'ʞ',
|
||||
textvartheta: 'ϑ',
|
||||
textvisiblespace: '␣',
|
||||
textyen: '¥',
|
||||
therefore: '∴',
|
||||
Theta: 'Θ',
|
||||
TH: 'Þ',
|
||||
th: 'þ',
|
||||
tildetrpl: '≋',
|
||||
top: '⊤',
|
||||
triangledown: '▿',
|
||||
trianglelefteq: '⊴',
|
||||
triangleleft: '◃',
|
||||
triangleq: '≜',
|
||||
trianglerighteq: '⊵',
|
||||
triangleright: '▹',
|
||||
truestate: '⊧',
|
||||
twoheadleftarrow: '↞',
|
||||
twoheadrightarrow: '↠',
|
||||
ulcorner: '⌜',
|
||||
UpArrowBar: '⤒',
|
||||
uparrow: '↑',
|
||||
Uparrow: '⇑',
|
||||
updownarrow: '↕',
|
||||
Updownarrow: '⇕',
|
||||
UpEquilibrium: '⥮',
|
||||
upharpoonleft: '↿',
|
||||
upharpoonright: '↾',
|
||||
uplus: '⊎',
|
||||
upsilon: 'υ',
|
||||
Upsilon: 'ϒ',
|
||||
upslopeellipsis: '⋰',
|
||||
upuparrows: '⇈',
|
||||
uranus: '♅',
|
||||
urcorner: '⌝',
|
||||
varepsilon: 'ɛ',
|
||||
varkappa: 'ϰ',
|
||||
varnothing: '∅',
|
||||
varphi: 'φ',
|
||||
varpi: 'ϖ',
|
||||
varrho: 'ϱ',
|
||||
varsigma: 'ς',
|
||||
vartriangleleft: '⊲',
|
||||
vartriangleright: '⊳',
|
||||
vartriangle: '▵',
|
||||
vdash: '⊢',
|
||||
Vdash: '⊩',
|
||||
VDash: '⊫',
|
||||
vdots: '⋮',
|
||||
veebar: '⊻',
|
||||
vee: '∨',
|
||||
venus: '♀',
|
||||
vert: '|',
|
||||
Vert: '‖',
|
||||
verymuchgreater: '⋙',
|
||||
verymuchless: '⋘',
|
||||
virgo: '♍',
|
||||
volintegral: '∰',
|
||||
Vvdash: '⊪',
|
||||
wedge: '∧',
|
||||
wp: '℘',
|
||||
wr: '≀',
|
||||
Xi: 'Ξ',
|
||||
xi: 'ξ',
|
||||
Zeta: 'Ζ',
|
||||
zeta: 'ζ',
|
||||
}
|
||||
|
||||
const SINGLE_ARG_REPLACEMENTS: Record<string, Record<string, string>> = {
|
||||
mathbb: {
|
||||
A: '𝔸',
|
||||
B: '𝔹',
|
||||
C: 'ℂ',
|
||||
D: '𝔻',
|
||||
E: '𝔼',
|
||||
F: '𝔽',
|
||||
G: '𝔾',
|
||||
H: 'ℍ',
|
||||
I: '𝕀',
|
||||
J: '𝕁',
|
||||
K: '𝕂',
|
||||
L: '𝕃',
|
||||
M: '𝕄',
|
||||
N: 'ℕ',
|
||||
O: '𝕆',
|
||||
P: 'ℙ',
|
||||
Q: 'ℚ',
|
||||
R: 'ℝ',
|
||||
S: '𝕊',
|
||||
T: '𝕋',
|
||||
U: '𝕌',
|
||||
V: '𝕍',
|
||||
W: '𝕎',
|
||||
X: '𝕏',
|
||||
Y: '𝕐',
|
||||
Z: 'ℤ',
|
||||
a: '𝕒',
|
||||
b: '𝕓',
|
||||
c: '𝕔',
|
||||
d: '𝕕',
|
||||
e: '𝕖',
|
||||
f: '𝕗',
|
||||
g: '𝕘',
|
||||
h: '𝕙',
|
||||
i: '𝕚',
|
||||
j: '𝕛',
|
||||
k: '𝕜',
|
||||
l: '𝕝',
|
||||
m: '𝕞',
|
||||
n: '𝕟',
|
||||
o: '𝕠',
|
||||
p: '𝕡',
|
||||
q: '𝕢',
|
||||
r: '𝕣',
|
||||
s: '𝕤',
|
||||
t: '𝕥',
|
||||
u: '𝕦',
|
||||
v: '𝕧',
|
||||
w: '𝕨',
|
||||
x: '𝕩',
|
||||
y: '𝕪',
|
||||
z: '𝕫',
|
||||
},
|
||||
mathfrak: {
|
||||
A: '𝔄',
|
||||
B: '𝔅',
|
||||
C: 'ℭ',
|
||||
D: '𝔇',
|
||||
E: '𝔈',
|
||||
F: '𝔉',
|
||||
G: '𝔊',
|
||||
H: 'ℌ',
|
||||
I: 'ℑ',
|
||||
J: '𝔍',
|
||||
K: '𝔎',
|
||||
L: '𝔏',
|
||||
M: '𝔐',
|
||||
N: '𝔑',
|
||||
O: '𝔒',
|
||||
P: '𝔓',
|
||||
Q: '𝔔',
|
||||
R: 'ℜ',
|
||||
S: '𝔖',
|
||||
T: '𝔗',
|
||||
U: '𝔘',
|
||||
V: '𝔙',
|
||||
W: '𝔚',
|
||||
X: '𝔛',
|
||||
Y: '𝔜',
|
||||
Z: 'ℨ',
|
||||
a: '𝔞',
|
||||
b: '𝔟',
|
||||
c: '𝔠',
|
||||
d: '𝔡',
|
||||
e: '𝔢',
|
||||
f: '𝔣',
|
||||
g: '𝔤',
|
||||
h: '𝔥',
|
||||
i: '𝔦',
|
||||
j: '𝔧',
|
||||
k: '𝔨',
|
||||
l: '𝔩',
|
||||
m: '𝔪',
|
||||
n: '𝔫',
|
||||
o: '𝔬',
|
||||
p: '𝔭',
|
||||
q: '𝔮',
|
||||
r: '𝔯',
|
||||
s: '𝔰',
|
||||
t: '𝔱',
|
||||
u: '𝔲',
|
||||
v: '𝔳',
|
||||
w: '𝔴',
|
||||
x: '𝔵',
|
||||
y: '𝔶',
|
||||
z: '𝔷',
|
||||
},
|
||||
mathscr: {
|
||||
A: '𝒜',
|
||||
B: 'ℬ',
|
||||
C: '𝒞',
|
||||
D: '𝒟',
|
||||
E: 'ℰ',
|
||||
F: 'ℱ',
|
||||
G: '𝒢',
|
||||
H: 'ℋ',
|
||||
I: 'ℐ',
|
||||
J: '𝒥',
|
||||
K: '𝒦',
|
||||
L: 'ℒ',
|
||||
M: 'ℳ',
|
||||
N: '𝒩',
|
||||
O: '𝒪',
|
||||
P: '𝒫',
|
||||
Q: '𝒬',
|
||||
R: 'ℛ',
|
||||
S: '𝒮',
|
||||
T: '𝒯',
|
||||
U: '𝒰',
|
||||
V: '𝒱',
|
||||
W: '𝒲',
|
||||
X: '𝒳',
|
||||
Y: '𝒴',
|
||||
Z: '𝒵',
|
||||
a: '𝒶',
|
||||
b: '𝒷',
|
||||
c: '𝒸',
|
||||
d: '𝒹',
|
||||
e: 'ℯ',
|
||||
f: '𝒻',
|
||||
g: 'ℊ',
|
||||
h: '𝒽',
|
||||
i: '𝒾',
|
||||
j: '𝒿',
|
||||
k: '𝓀',
|
||||
l: 'ℓ',
|
||||
m: '𝓂',
|
||||
n: '𝓃',
|
||||
o: 'ℴ',
|
||||
p: '𝓅',
|
||||
q: '𝓆',
|
||||
r: '𝓇',
|
||||
s: '𝓈',
|
||||
t: '𝓉',
|
||||
u: '𝓊',
|
||||
v: '𝓋',
|
||||
w: '𝓌',
|
||||
x: '𝓍',
|
||||
y: '𝓎',
|
||||
z: '𝓏',
|
||||
},
|
||||
}
|
||||
|
||||
const COMBINING_CHAR_REPLACEMENTS: Record<string, string> = {
|
||||
"'": '\u0301',
|
||||
'"': '\u0308',
|
||||
'.': '\u0307',
|
||||
'=': '\u0304',
|
||||
'^': '\u0302',
|
||||
'`': '\u0300',
|
||||
'~': '\u0303',
|
||||
H: '\u030B',
|
||||
acute: '\u0301',
|
||||
b: '\u0331',
|
||||
bar: '\u0305',
|
||||
breve: '\u0306',
|
||||
c: '\u0327',
|
||||
check: '\u030C',
|
||||
d: '\u0323',
|
||||
dot: '\u0307',
|
||||
ddot: '\u0308',
|
||||
doubleunderline: '\u0333',
|
||||
grave: '\u0300',
|
||||
hat: '\u0302',
|
||||
mathring: '\u030A',
|
||||
overline: '\u0305',
|
||||
r: '\u030A',
|
||||
not: '\u0338',
|
||||
slash: '\u0338',
|
||||
strikethrough: '\u0335',
|
||||
tilde: '\u0303',
|
||||
u: '\u0306',
|
||||
underline: '\u0332',
|
||||
v: '\u030C',
|
||||
vec: '\u20d7',
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ProjectionItem } from '@/features/source-editor/utils/tree-operations/projection'
|
||||
import { BibtexEntry } from './bibtex-entry.mts'
|
||||
import { BibtexEntry } from '@shared/bibtex/bibtex-entry.mts'
|
||||
|
||||
/**
|
||||
* A BibtexEntry placed in a CodeMirror document. The `entry` reference is
|
||||
|
||||
Reference in New Issue
Block a user