Edit Raw BibTeX
BibDocument preserves source-order BibTeX blocks. Apply a patch to create an edited snapshot while retaining comments, preambles, strings, malformed blocks, duplicate occurrences, delimiters, and unrelated layout.
Update a field value
import refkit as rk
source = "@article{doe2024, title={Fast Citations}, year={2024}}"
document = rk.BibDocument.parse(source)
title_field = document.entries["doe2024"].fields["title"]
patch_result = document.apply_patch(
[
{
"kind": "set_field",
"entry_id": title_field.entry_id,
"field_id": title_field.id,
"value": "Corrected title",
}
]
)
document = patch_result["document"]
print(document.to_bibtex())import * as rk from "refkit-js";
const source = "@article{doe2024, title={Fast Citations}, year={2024}}";
let document = rk.BibDocument.parse(source);
const titleField = document.entries.getUnique("doe2024")!.fields.getUnique("title")!;
const patchResult = document.applyPatch([{
kind: "set_field", entryId: titleField.entryId,
fieldId: titleField.id, value: "Corrected title",
}]);
document = patchResult.document;
console.log(document.toBibtex());@article{doe2024, title={Corrected title}, year={2024}}The result contains a new document, byte changes, occurrence mappings, and warnings. The original title_field / titleField still reads Fast Citations. Serializing the returned document preserves every byte outside its reported changes. TypeScript examples run in Node.js. In a browser, initialize RefKit before parsing. The remaining examples continue with the returned document.
By default, set_field validates the replacement against the original delimiter mode. An unsafe replacement raises PatchError and leaves the input snapshot unchanged. Default values must have balanced braces and safe quote boundaries, with no newlines, unescaped percent delimiters, or trailing escape. Bare fields preserve safe bare values. Concatenated expressions become a single braced value.
Set expression=True / expression: true when value is one complete BibTeX expression, such as press # { Supplement} or {https://example.org/a%2Fb}. This mode preserves macros, concatenations, delimiters, and multiline braced content. Extra assignments or trailing source are rejected. It is available on set_field, add_field, and each field supplied to add_entry.
Apply structural changes atomically
A patch is an ordered list of edit records. Entry and field IDs refer to occurrences in the input snapshot. Use the returned mappings to target a subsequent snapshot.
kind | Required fields | Behavior |
|---|---|---|
set_field | Entry ID, field ID, value | Replace one existing raw value. |
add_field | Entry ID, name, value | Append a braced field before the entry's closing delimiter. |
remove_field | Entry ID, field ID | Remove the assignment and its following separator, retaining surrounding comments. |
add_entry | key, entry type | Insert a new entry with optional ordered fields. Optional before names an input entry ID, otherwise append. |
remove_entry | Entry ID | Remove one entry block. |
rename_entry | Entry ID, key | Rename a key and rewrite unambiguous crossref, xdata, and xref references. |
set_entry_type | Entry ID, entry type | Replace the type spelling. |
Python uses entry_id, field_id, and entry_type. TypeScript uses entryId, fieldId, and entryType. New fields use {name, value} records with an optional expression flag. New values are braced by default, and new entry/field layout uses two-space indentation. Existing unrelated layout is retained.
Multiple changes to the same existing field, key, or type are rejected. Removing an entry conflicts with edits inside it. Insertions before the same anchor retain input order and can precede an anchor removed in the same patch. Duplicate fields and keys remain representable and produce warnings in the result.
Automatic reference rewriting requires unique source and final targets. Ambiguity raises PatchError. Reference fields explicitly set or removed in the patch are excluded from automatic rewriting, so supply their final values. Macro definitions remain unchanged, while rewritten reference expressions become braced values.
Patches accept at most 100,000 operations and 16 MiB of authored text. Output is limited to 16 MiB. An existing larger raw snapshot can be reduced within that bound. See patch reports for byte changes and occurrence mappings.
Resolve fields for inspection
resolve() returns detached entry records with string macros and # concatenations expanded in every field. Custom field names are included. Literal TeX grouping and escapes remain available for downstream processing.
resolved_document = rk.BibDocument.parse(r"""
@string{topic = {Visual {Data}}}
@article{guide, title = {A } # topic # { Guide \& Examples}, year = 2024}
""")
print(resolved_document.resolve()[0]["fields"]["title"])const resolvedDocument = rk.BibDocument.parse(String.raw`
@string{topic = {Visual {Data}}}
@article{guide, title = {A } # topic # { Guide \& Examples}, year = 2024}
`);
console.log(resolvedDocument.resolve()[0]!.fields.title);A Visual {Data} Guide \& ExamplesEach call reads its document snapshot and leaves it unchanged. Entry keys retain their case. Entry types and field names are lowercase. Macro names are case-insensitive. Definitions can appear after their uses, and the last definition wins. Built-in month names such as jan expand to January unless the document defines that macro. crossref and xdata values are resolved as field text. Use Library when you need inherited citation metadata and normalized names or dates.
Undefined or cyclic macros reached from entry fields, duplicate entry keys or fields, malformed entry or string-definition syntax, and expansion-limit violations raise ParseError with diagnostics. Resolution diagnostic spans refer to the current serialized source. See resolved entry records for the result shape and its Polars representation.
Inspect source-order blocks
for block in document.blocks:
print(block["kind"], block["span"])for (const block of document.blocks) {
console.log(block.kind, block.span);
}Block kinds are whitespace, comment, preamble, string, entry, failed, and other. Every span is a half-open pair of UTF-8 byte offsets into that snapshot's source. Original handles retain original spans. The result document exposes updated spans.
Use comments, preamble, strings, and failed_blocks / failedBlocks for focused views. blocks remains the complete source-order view.
Address duplicate entries
A key can identify several source occurrences. Parse a document with duplicate keys to inspect and update each occurrence separately:
duplicates = rk.BibDocument.parse("""
@article{doe2024, title={First title}}
@article{doe2024, title={Second title}}
""")
matches = duplicates.entries.get_all("doe2024")
second = matches[1]
second_field = second.fields["title"]
duplicate_result = duplicates.apply_patch(
[
{
"kind": "set_field",
"entry_id": second.id,
"field_id": second_field.id,
"value": "Revised second title",
}
]
)
print(matches[0].fields["title"].value)
print(duplicate_result["document"].entries.get_all("doe2024")[1].fields["title"].value)const duplicates = rk.BibDocument.parse(`
@article{doe2024, title={First title}}
@article{doe2024, title={Second title}}
`);
const matches = duplicates.entries.getAll("doe2024");
const second = matches[1]!;
const secondField = second.fields.getUnique("title")!;
const duplicateResult = duplicates.applyPatch([{
kind: "set_field", entryId: second.id,
fieldId: secondField.id, value: "Revised second title",
}]);
console.log(matches[0]!.fields.getUnique("title")!.value);
console.log(duplicateResult.document.entries.getAll("doe2024")[1]!.fields.getUnique("title")!.value);The first occurrence still contains First title. The entry map exposes these lookups:
| Python | TypeScript | Result |
|---|---|---|
unique_keys() | uniqueKeys() | Each distinct key once. |
occurrence_keys() | occurrenceKeys() | One key per source occurrence. |
occurrences() | occurrences() | Source-order entry handles. |
get_all(key) | getAll(key) | Every exact entry-key match. |
get_unique(key) | getUnique(key) | One match, or None / null when missing. Duplicate matches raise RefkitError. |
BibFieldMap provides the same operations for fields. Entry keys are case-sensitive. Field-name matching is case-insensitive, and unique field keys are normalized to lowercase.
Inspect entry and field identity
entry = document.entries["doe2024"]
print(entry.key, entry.kind, entry.span)
field = entry.fields["title"]
print(field.name, field.value, field.span)const entry = document.entries.getUnique("doe2024")!;
console.log(entry.key, entry.kind, entry.span);
const field = entry.fields.getUnique("title")!;
console.log(field.name, field.value, field.span);BibEntry.kind retains the raw entry type spelling. A normalized Library exposes Entry.entry_type / Entry.entryType. Bibliography Models explains when to choose each object.
Handle malformed blocks
damaged = rk.BibDocument.parse(source + "\n@book{broken")
for block in damaged.failed_blocks:
print(block["error"])
print(block["raw"])const damaged = rk.BibDocument.parse(source + "\n@book{broken");
for (const block of damaged.failedBlocks) {
console.log(block.error);
console.log(block.raw);
}Malformed blocks stay in the document and its serialized output.
Percent-comment boundary
The raw parser recognizes a complete @... block that begins later on a percent-comment line as a live block. Inspect BibDocument.blocks before tidying source that embeds complete BibTeX entries inside % comments. Normalized report recovery suppresses those embedded blocks, while the formatter can render them as entries.
Read and write a file
Read an existing references.bib, update a field, and save a separate file:
file_document = rk.BibDocument.read("references.bib")
file_field = file_document.entries["doe2024"].fields["title"]
file_result = file_document.apply_patch(
[
{
"kind": "set_field",
"entry_id": file_field.entry_id,
"field_id": file_field.id,
"value": "Corrected title",
}
]
)
file_result["document"].write("references.edited.bib")import { readBibDocument } from "refkit-js/node";
import { writeFile } from "node:fs/promises";
const fileDocument = await readBibDocument("references.bib");
const fileField = fileDocument.entries.getUnique("doe2024")!.fields.getUnique("title")!;
const fileResult = fileDocument.applyPatch([{
kind: "set_field", entryId: fileField.entryId,
fieldId: fileField.id, value: "Corrected title",
}]);
await writeFile("references.edited.bib", fileResult.document.toBibtex(), "utf8");File readers try UTF-8, then a Windows-1252-compatible fallback and record that choice in diagnostics. Original file bytes and decoded-text offsets can differ. Both examples write UTF-8.
Use Format BibTeX for whole-document normalization according to formatting options.