biblatex-csl-converter
Version:
a set of converters: biblatex => json, CSL => json, json => biblatex, json => CSL
390 lines (368 loc) • 14.7 kB
text/typescript
import { TexSpecialChars } from "./const"
import { BibTypes, BibFieldTypes, NodeArray, NodeObject } from "../const"
/** Export a list of bibliography items to bibLateX and serve the file to the user as a ZIP-file.
* @class BibLatexExporter
* @param pks A list of pks of the bibliography items that are to be exported.
*/
export interface TagEntry {
open: string
close: string
verbatim: boolean
}
export type Tags = Record<string, TagEntry>
const TAGS: Tags = {
strong: { open: "\\mkbibbold{", close: "}", verbatim: false },
em: { open: "\\mkbibitalic{", close: "}", verbatim: false },
smallcaps: { open: "\\textsc{", close: "}", verbatim: false },
enquote: { open: "\\enquote{", close: "}", verbatim: false },
nocase: { open: "{{", close: "}}", verbatim: false },
sub: { open: "_{", close: "}", verbatim: false },
sup: { open: "^{", close: "}", verbatim: false },
math: { open: "$", close: "$", verbatim: false },
url: { open: "\\url{", close: "}", verbatim: true },
}
import type { BibDB } from "../import/biblatex"
type ConfigObject = {
traditionalNames?: boolean
exportUnexpectedFields?: boolean
}
type BibObject = {
type: string
key: string
values?: Record<string, unknown>
}
type WarningObject = {
type: string
variable: string
}
export class BibLatexExporter {
bibDB: BibDB
pks: string[]
config: ConfigObject
warnings: WarningObject[]
bibtexStr: string
bibtexArray: BibObject[]
constructor(
bibDB: BibDB,
pks: string[] | false = false,
config: ConfigObject = {}
) {
this.bibDB = bibDB // The bibliography database to export from.
if (pks) {
this.pks = pks // A list of pk values of the bibliography items to be exported.
} else {
this.pks = Object.keys(bibDB) // If none are selected, all keys are exporter
}
this.config = config
this.warnings = []
this.bibtexArray = []
this.bibtexStr = ""
}
parse(): string {
this.pks.forEach((pk) => {
let bib = this.bibDB[pk as unknown as number]
let bibEntry: BibObject = {
type: BibTypes[bib["bib_type"]]["biblatex"],
key: bib["entry_key"].length ? bib["entry_key"] : "Undefined",
}
let fValues: Record<string, unknown> = {}
if (BibTypes[bib["bib_type"]]["biblatex-subtype"]) {
fValues["entrysubtype"] =
BibTypes[bib["bib_type"]]["biblatex-subtype"]
}
const fields = this.config.exportUnexpectedFields
? { ...bib.fields, ...bib.unexpected_fields }
: bib.fields
for (let fKey in fields) {
if (!(fKey in BibFieldTypes)) {
continue
}
let fValue = fields[fKey]
let fType: string = BibFieldTypes[fKey]["type"]
let key: string = BibFieldTypes[fKey]["biblatex"]
switch (fType) {
case "f_date":
fValues[key] = fValue // EDTF 1.0 level 0/1 compliant string.
break
case "f_integer":
fValues[key] = this._reformText(fValue)
break
case "f_key":
fValues[key] = this._reformKey(fValue, fKey)
break
case "f_literal":
case "f_long_literal":
fValues[key] = this._reformText(fValue)
break
case "l_range":
fValues[key] = this._reformRange(fValue)
break
case "f_title":
fValues[key] = this._reformText(fValue)
break
case "f_uri":
case "f_verbatim":
fValues[key] = (fValue as string).replace(/{|}/g, "") // TODO: balanced braces should probably be ok here.
break
case "l_key":
fValues[key] = this._escapeTeX(
(fValue as (string | NodeArray)[])
.map((key: string | NodeArray) => {
return this._reformKey(key, fKey)
})
.join(" and ")
)
break
case "l_literal":
fValues[key] = (fValue as NodeArray[])
.map((text: NodeArray) => {
return this._reformText(text)
})
.join(" and ")
break
case "l_name":
fValues[key] = this._reformName(fValue)
break
case "l_tag":
fValues[key] = this._escapeTeX(
(fValue as string[]).join(", ")
)
break
default:
console.warn(`Unrecognized type: ${fType}!`)
}
}
bibEntry.values = fValues
this.bibtexArray[this.bibtexArray.length] = bibEntry
})
this.bibtexStr = this._getBibtexString(this.bibtexArray)
return this.bibtexStr
}
_reformKey(theValue: string | unknown, fKey: string): string {
if (typeof theValue === "string") {
let fieldType = BibFieldTypes[fKey]
if (Array.isArray(fieldType["options"])) {
return this._escapeTeX(theValue)
} else {
return this._escapeTeX(
fieldType.options?.[theValue]["biblatex"] ?? ""
)
}
} else {
return this._reformText(theValue)
}
}
_reformRange(theValue: unknown): string {
if (!Array.isArray(theValue)) {
console.warn(`Wrong format for reformRange`, theValue)
return ""
}
return theValue
.map((range) => this._reformInterval(range))
.filter((interval) => interval.length)
.join(",")
}
_reformInterval(theValue: unknown): string {
if (!Array.isArray(theValue)) {
console.warn(`Wrong format for reformInterval`, theValue)
return ""
}
return theValue.map((text) => this._reformText(text)).join("--")
}
_reformName(theValue: unknown): string {
if (!Array.isArray(theValue)) {
console.warn(`Wrong format for reformName`, theValue)
return ""
}
let names: string[] = []
theValue.forEach((name) => {
if (name.literal) {
let literal = this._reformText(name.literal)
if (literal.length) {
names.push(`{${literal}}`)
}
} else {
let family = name.family ? this._reformText(name.family) : ""
let given = name.given ? this._reformText(name.given) : ""
let suffix = name.suffix ? this._reformText(name.suffix) : false
let prefix = name.prefix ? this._reformText(name.prefix) : false
let useprefix = name.useprefix ? name.useprefix : false
if (this.config.traditionalNames) {
if (suffix && prefix) {
names.push(
`{${prefix} ${family}}, {${suffix}}, {${given}}`
)
} else if (suffix) {
names.push(`{${family}}, {${suffix}}, {${given}}`)
} else if (prefix) {
names.push(`{${prefix} ${family}}, {${given}}`)
} else {
names.push(`{${family}}, {${given}}`)
}
} else {
let nameParts = []
if (given.length) {
nameParts.push(
this._protectNamePart(`given={${given}}`)
)
}
if (family.length) {
nameParts.push(
this._protectNamePart(`family={${family}}`)
)
}
if (suffix) {
nameParts.push(
this._protectNamePart(`suffix={${suffix}}`)
)
}
if (prefix) {
nameParts.push(
this._protectNamePart(`prefix={${prefix}}`)
)
nameParts.push(`useprefix=${String(useprefix)}`)
}
names.push(nameParts.join(", "))
}
}
})
return names.join(" and ")
}
_protectNamePart(namePart: string): string {
if (namePart.includes(",")) {
return `"${namePart}"`
} else {
return namePart
}
}
_escapeTeX(theValue: unknown): string {
if (typeof theValue !== "string") {
console.warn(`Wrong format for escapeTeX`, theValue)
return ""
}
let len = TexSpecialChars.length
for (let i = 0; i < len; i++) {
theValue = (theValue as string).replace(
TexSpecialChars[i][0],
TexSpecialChars[i][1]
)
}
return theValue as string
}
_reformText(theValue: unknown): string {
let latex = "",
lastMarks: string[] = []
if (!Array.isArray(theValue)) {
console.warn(`Wrong format for reformText`, theValue)
return latex
}
// Add one extra empty node to theValue to close all still open tags for last node.
theValue
.concat({ type: "text", text: "" })
.forEach((node: NodeObject) => {
if (node.type === "variable") {
// This is an undefined variable
// This should usually not happen, as CSL doesn't know what to
// do with these. We'll put them into an unsupported tag.
latex += `} # ${node.attrs!.variable} # {`
this.warnings.push({
type: "undefined_variable",
variable: node.attrs!.variable as string,
})
return
}
let newMarks: string[] = []
if (node.marks) {
let mathMode = false
node.marks.forEach((mark) => {
// We need to activate mathmode for the lowest level sub/sup node.
if (
(mark.type === "sup" || mark.type === "sub") &&
!mathMode
) {
newMarks.push("math")
newMarks.push(mark.type)
mathMode = true
} else if (mark.type === "nocase") {
// No case has to be applied at the top level to be effective.
newMarks.unshift(mark.type)
} else {
newMarks.push(mark.type)
}
})
}
// close all tags that are not present in current text node.
let closing = false,
closeTags: string[] = []
lastMarks.forEach((mark, index) => {
if (mark != newMarks[index]) {
closing = true
}
if (closing) {
let closeTag = TAGS[mark].close
// If not inside of a nocase, add a protective brace around tag.
if (
lastMarks[0] !== "nocase" &&
TAGS[mark].open[0] === "\\"
) {
closeTag += "}"
}
closeTags.push(closeTag)
}
})
// Add close tags to latex in reverse order to close innermost tags
// first.
closeTags.reverse()
latex += closeTags.join("")
// open all new tags that were not present in the last text node.
let opening = false,
verbatim = false
newMarks.forEach((mark, index) => {
if (mark != lastMarks[index]) {
opening = true
}
if (opening) {
// If not inside of a nocase, add a protective brace around tag.
if (
newMarks[0] !== "nocase" &&
TAGS[mark].open[0] === "\\"
) {
latex += "{"
}
latex += TAGS[mark].open
if (TAGS[mark].verbatim) {
verbatim = true
}
}
})
if ("text" in node) {
if (verbatim) {
latex += node.text
} else {
latex += this._escapeTeX(node.text)
}
}
lastMarks = newMarks
})
return latex
}
_getBibtexString(biblist: BibObject[]): string {
const len = biblist.length
let str = ""
for (let i = 0; i < len; i++) {
if (0 < i) {
str += "\n\n"
}
const data = biblist[i]
str += `@${data.type}{${data.key}`
for (let vKey in data.values) {
let value = `{${data.values[vKey]}}`
.replace(/\{\} # /g, "")
.replace(/# \{\}/g, "")
str += `,\n${vKey} = ${value}`
}
str += "\n}"
}
return str
}
}