refkit 0.0.7__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {refkit-0.0.7 → refkit-0.1.0}/Cargo.lock +2 -2
  2. {refkit-0.0.7 → refkit-0.1.0}/Cargo.toml +2 -2
  3. {refkit-0.0.7 → refkit-0.1.0}/PKG-INFO +1 -1
  4. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/document.rs +3 -3
  5. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/library/guard.rs +14 -14
  6. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/raw/parse.rs +15 -13
  7. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/raw/tests.rs +9 -0
  8. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/raw.rs +32 -44
  9. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/render/bibliography.rs +9 -4
  10. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/render/html.rs +14 -9
  11. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/render/mod.rs +8 -1
  12. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/render_tree.rs +2 -2
  13. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/tidy/keys.rs +22 -24
  14. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/tidy/latex.rs +2 -2
  15. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/tidy/mod.rs +8 -3
  16. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/tidy/render/value.rs +3 -0
  17. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/conversion.rs +2 -2
  18. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/entry.rs +12 -12
  19. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/library.rs +8 -8
  20. {refkit-0.0.7 → refkit-0.1.0}/pyproject.toml +1 -1
  21. {refkit-0.0.7 → refkit-0.1.0}/.agent-plugin/plugin.json +0 -0
  22. {refkit-0.0.7 → refkit-0.1.0}/.agent-plugin/skills/refkit/SKILL.md +0 -0
  23. {refkit-0.0.7 → refkit-0.1.0}/.agent-plugin/skills/refkit/agents/openai.yaml +0 -0
  24. {refkit-0.0.7 → refkit-0.1.0}/.agent-plugin/skills/refkit/references/contracts.md +0 -0
  25. {refkit-0.0.7 → refkit-0.1.0}/.agent-plugin/skills/refkit/references/edit.md +0 -0
  26. {refkit-0.0.7 → refkit-0.1.0}/.agent-plugin/skills/refkit/references/inspect.md +0 -0
  27. {refkit-0.0.7 → refkit-0.1.0}/.agent-plugin/skills/refkit/references/render.md +0 -0
  28. {refkit-0.0.7 → refkit-0.1.0}/.agent-plugin/skills/refkit/references/tidy.md +0 -0
  29. {refkit-0.0.7 → refkit-0.1.0}/LICENSE +0 -0
  30. {refkit-0.0.7 → refkit-0.1.0}/NOTICE +0 -0
  31. {refkit-0.0.7 → refkit-0.1.0}/README.md +0 -0
  32. {refkit-0.0.7 → refkit-0.1.0}/build_backend.py +0 -0
  33. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/Cargo.toml +0 -0
  34. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/lib.rs +0 -0
  35. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/library/diagnostic.rs +0 -0
  36. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/library/mod.rs +0 -0
  37. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/library/parse.rs +0 -0
  38. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/library/recovery.rs +0 -0
  39. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/library/source.rs +0 -0
  40. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/raw/edit.rs +0 -0
  41. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/raw/sanitize.rs +0 -0
  42. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/render/citation.rs +0 -0
  43. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/render/text.rs +0 -0
  44. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/source.rs +0 -0
  45. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/strings.rs +0 -0
  46. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/style/validate.rs +0 -0
  47. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/style.rs +0 -0
  48. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/tidy/duplicates.rs +0 -0
  49. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/tidy/options.rs +0 -0
  50. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/tidy/references.rs +0 -0
  51. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/tidy/render/sort.rs +0 -0
  52. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/tidy/render.rs +0 -0
  53. {refkit-0.0.7 → refkit-0.1.0}/crates/refkit-core/src/tidy/unicode.rs +0 -0
  54. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/Cargo.toml +0 -0
  55. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/citation.rs +0 -0
  56. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/document.rs +0 -0
  57. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/errors.rs +0 -0
  58. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/filesystem.rs +0 -0
  59. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/lib.rs +0 -0
  60. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/module.rs +0 -0
  61. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/raw.rs +0 -0
  62. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/rendered.rs +0 -0
  63. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/repr.rs +0 -0
  64. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/style.rs +0 -0
  65. {refkit-0.0.7 → refkit-0.1.0}/packages/refkit/rust/src/tidy.rs +0 -0
  66. {refkit-0.0.7 → refkit-0.1.0}/src/refkit/__init__.py +0 -0
  67. {refkit-0.0.7 → refkit-0.1.0}/src/refkit/__init__.pyi +0 -0
  68. {refkit-0.0.7 → refkit-0.1.0}/src/refkit/_native.pyi +0 -0
  69. {refkit-0.0.7 → refkit-0.1.0}/src/refkit/agent.py +0 -0
  70. {refkit-0.0.7 → refkit-0.1.0}/src/refkit/py.typed +0 -0
  71. {refkit-0.0.7 → refkit-0.1.0}/src/refkit/types.py +0 -0
@@ -455,7 +455,7 @@ dependencies = [
455
455
 
456
456
  [[package]]
457
457
  name = "refkit-core"
458
- version = "0.0.7"
458
+ version = "0.1.0"
459
459
  dependencies = [
460
460
  "biblatex",
461
461
  "hayagriva",
@@ -468,7 +468,7 @@ dependencies = [
468
468
 
469
469
  [[package]]
470
470
  name = "refkit-native"
471
- version = "0.0.7"
471
+ version = "0.1.0"
472
472
  dependencies = [
473
473
  "pyo3",
474
474
  "refkit-core",
@@ -3,7 +3,7 @@ members = ["crates/refkit-core", "packages/refkit/rust"]
3
3
  resolver = "3"
4
4
 
5
5
  [workspace.package]
6
- version = "0.0.7"
6
+ version = "0.1.0"
7
7
  edition = "2024"
8
8
  rust-version = "1.88"
9
9
  license = "Apache-2.0"
@@ -11,7 +11,7 @@ repository = "https://github.com/peter-gy/refkit"
11
11
 
12
12
  [workspace.dependencies]
13
13
  pyo3 = "0.29.0"
14
- refkit-core = { version = "0.0.7", path = "crates/refkit-core" }
14
+ refkit-core = { version = "0.1.0", path = "crates/refkit-core" }
15
15
  serde = { version = "1.0.228", features = ["derive"] }
16
16
  serde_json = "1.0.150"
17
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refkit
3
- Version: 0.0.7
3
+ Version: 0.1.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: Apache Software License
@@ -97,12 +97,12 @@ impl Document {
97
97
  )?;
98
98
  let citations = rendered
99
99
  .citations
100
- .iter()
101
- .zip(&requests)
100
+ .into_iter()
101
+ .zip(requests)
102
102
  .map(|(citation, request)| {
103
103
  rendered_record_from_citation(
104
104
  citation,
105
- request.items.iter().map(|item| item.key.clone()).collect(),
105
+ request.items.into_iter().map(|item| item.key).collect(),
106
106
  )
107
107
  })
108
108
  .collect::<Result<Vec<_>, _>>()
@@ -165,23 +165,22 @@ pub(crate) fn validate_raw(raw: &RawBibliography<'_>) -> Result<(), Diagnostic>
165
165
  .iter()
166
166
  .map(|pair| (pair.key.v, &pair.value.v))
167
167
  .collect();
168
- let entry_index: HashMap<_, _> = raw
168
+ let has_references = raw
169
169
  .entries
170
170
  .iter()
171
- .enumerate()
172
- .map(|(index, entry)| (entry.v.key.v, index))
173
- .collect();
174
- let mut graph = vec![Vec::new(); raw.entries.len()];
171
+ .any(|entry| entry.v.fields.iter().any(|field| is_reference(field.key.v)));
175
172
  let mut weights = vec![1usize; raw.entries.len()];
176
173
  let mut bytes = 0usize;
177
174
  let mut expansion_steps = 0usize;
175
+ let mut value = String::new();
176
+ let mut ancestors = Vec::new();
178
177
  for (index, entry) in raw.entries.iter().enumerate() {
179
178
  for field in &entry.v.fields {
180
- let mut value = String::new();
179
+ value.clear();
181
180
  expand(
182
181
  &field.value.v,
183
182
  &abbreviations,
184
- &mut Vec::new(),
183
+ &mut ancestors,
185
184
  &mut expansion_steps,
186
185
  &mut value,
187
186
  )
@@ -200,15 +199,16 @@ pub(crate) fn validate_raw(raw: &RawBibliography<'_>) -> Result<(), Diagnostic>
200
199
  weights[index] = weights[index].saturating_add(value.len());
201
200
  }
202
201
  }
203
- if raw.entries.iter().all(|entry| {
204
- entry
205
- .v
206
- .fields
207
- .iter()
208
- .all(|field| !is_reference(field.key.v))
209
- }) {
202
+ if !has_references {
210
203
  return Ok(());
211
204
  }
205
+ let entry_index: HashMap<_, _> = raw
206
+ .entries
207
+ .iter()
208
+ .enumerate()
209
+ .map(|(index, entry)| (entry.v.key.v, index))
210
+ .collect();
211
+ let mut graph = vec![Vec::new(); raw.entries.len()];
212
212
  let mut detached = raw.clone();
213
213
  for entry in &mut detached.entries {
214
214
  entry.v.fields.retain(|field| {
@@ -346,6 +346,11 @@ fn parse_entry_key(body: &str) -> Result<(String, usize), String> {
346
346
 
347
347
  fn find_at_block_end(source: &str, start: usize) -> Result<usize, (usize, String)> {
348
348
  let escape_aware = find_at_block_end_with_escape_mode(source, start, true);
349
+ if let Ok(end) = escape_aware
350
+ && !source[start..end].contains('\\')
351
+ {
352
+ return Ok(end);
353
+ }
349
354
  let permissive = find_at_block_end_with_escape_mode(source, start, false);
350
355
  match (escape_aware, permissive) {
351
356
  (Ok(escaped_end), Ok(permissive_end))
@@ -381,7 +386,6 @@ fn find_at_block_end_with_escape_mode(
381
386
  start: usize,
382
387
  escape_aware: bool,
383
388
  ) -> Result<usize, (usize, String)> {
384
- let next_block = find_recovery_block_start(source, start);
385
389
  if is_line_style_comment(source, start) {
386
390
  return Err((
387
391
  take_line(source, start),
@@ -390,14 +394,12 @@ fn find_at_block_end_with_escape_mode(
390
394
  }
391
395
  let Some(open_rel) = source[start..].find(['{', '(']) else {
392
396
  return Err((
393
- next_block.unwrap_or_else(|| take_line(source, start)),
397
+ find_recovery_block_start(source, start).unwrap_or_else(|| take_line(source, start)),
394
398
  "entry opener is missing".to_string(),
395
399
  ));
396
400
  };
397
401
  let open = start + open_rel;
398
- if let Some(next) = next_block
399
- && next < open
400
- {
402
+ if let Some(next) = find_recovery_block_start(&source[..open], start) {
401
403
  return Err((next, "entry opener is missing".to_string()));
402
404
  }
403
405
  let opener = source[open..].chars().next().unwrap();
@@ -464,7 +466,7 @@ fn find_at_block_end_with_escape_mode(
464
466
  }
465
467
 
466
468
  Err((
467
- next_block.unwrap_or(source.len()),
469
+ find_recovery_block_start(source, start).unwrap_or(source.len()),
468
470
  "entry ended before closing delimiter".to_string(),
469
471
  ))
470
472
  }
@@ -539,17 +541,17 @@ fn is_parseable_at_start(source: &str, start: usize) -> bool {
539
541
  }
540
542
 
541
543
  fn is_line_style_comment(source: &str, start: usize) -> bool {
544
+ if !source[start..]
545
+ .get(.."@comment".len())
546
+ .is_some_and(|prefix| prefix.eq_ignore_ascii_case("@comment"))
547
+ {
548
+ return false;
549
+ }
542
550
  let line_start = source[..start].rfind('\n').map_or(0, |index| index + 1);
543
551
  if !source[line_start..start].chars().all(char::is_whitespace) {
544
552
  return false;
545
553
  }
546
554
  let line = &source[start..take_line(source, start)];
547
- let Some(prefix) = line.get(.."@comment".len()) else {
548
- return false;
549
- };
550
- if !prefix.eq_ignore_ascii_case("@comment") {
551
- return false;
552
- }
553
555
  let rest = line.get("@comment".len()..).unwrap_or_default();
554
556
  if !rest
555
557
  .chars()
@@ -568,12 +570,12 @@ pub(super) fn parse_value(
568
570
  ) -> Result<ParsedValue, String> {
569
571
  let first = parse_value_atom(body, start, body_offset)?;
570
572
  let mut cursor = first.1;
571
- let mut atoms = first.4.clone();
572
573
  let mut expression_cursor = cursor;
573
574
  skip_field_space(body, &mut expression_cursor);
574
575
  if !body[expression_cursor..].starts_with('#') {
575
576
  return Ok(first);
576
577
  }
578
+ let mut atoms = first.4;
577
579
 
578
580
  while expression_cursor < body.len() && body[expression_cursor..].starts_with('#') {
579
581
  expression_cursor += 1;
@@ -556,3 +556,12 @@ fn raw_document_rejects_unsafe_field_edits() {
556
556
  "jcs # \" Extra\""
557
557
  );
558
558
  }
559
+
560
+ #[test]
561
+ fn adjacent_entries_preserve_unicode_values_and_source_writeback() {
562
+ let source = "@book{a,title={Été}}@book{b,title={研究}}@book{c,title={🙂}}";
563
+ let document = RawDocument::parse(source);
564
+ assert_eq!(document.entry_keys(), ["a", "b", "c"]);
565
+ assert_eq!(raw_field_value(&document, "b", "title"), "研究");
566
+ assert_eq!(document.render().unwrap(), source);
567
+ }
@@ -435,13 +435,18 @@ impl RawDocument {
435
435
  self.data.blocks.iter().map(raw_block_info).collect()
436
436
  }
437
437
 
438
- pub(crate) fn syntax(&self) -> RawSyntaxDocument {
438
+ #[cfg(test)]
439
+ fn syntax(&self) -> RawSyntaxDocument {
440
+ self.clone().into_syntax()
441
+ }
442
+
443
+ pub(crate) fn into_syntax(self) -> RawSyntaxDocument {
439
444
  RawSyntaxDocument {
440
- blocks: self.data.blocks.iter().map(raw_syntax_block).collect(),
445
+ blocks: self.data.blocks.into_iter().map(raw_syntax_block).collect(),
441
446
  entries: self
442
447
  .data
443
448
  .entry_blocks
444
- .iter()
449
+ .into_iter()
445
450
  .enumerate()
446
451
  .map(|(entry_id, entry)| raw_syntax_entry(RawEntryId(entry_id), entry))
447
452
  .collect(),
@@ -541,70 +546,53 @@ fn raw_block_info(block: &RawBlock) -> RawBlockInfo {
541
546
  }
542
547
  }
543
548
 
544
- fn raw_syntax_block(block: &RawBlock) -> RawSyntaxBlock {
549
+ fn raw_syntax_block(block: RawBlock) -> RawSyntaxBlock {
545
550
  match block {
546
- RawBlock::Whitespace { raw, span } => RawSyntaxBlock::Whitespace {
547
- raw: raw.clone(),
548
- span: span.clone(),
549
- },
550
- RawBlock::Comment { raw, span } => RawSyntaxBlock::Comment {
551
- raw: raw.clone(),
552
- span: span.clone(),
553
- },
554
- RawBlock::Preamble { raw, value, span } => RawSyntaxBlock::Preamble {
555
- raw: raw.clone(),
556
- value: value.clone(),
557
- span: span.clone(),
558
- },
551
+ RawBlock::Whitespace { raw, span } => RawSyntaxBlock::Whitespace { raw, span },
552
+ RawBlock::Comment { raw, span } => RawSyntaxBlock::Comment { raw, span },
553
+ RawBlock::Preamble { raw, value, span } => RawSyntaxBlock::Preamble { raw, value, span },
559
554
  RawBlock::StringDef {
560
555
  raw,
561
556
  key,
562
557
  value,
563
558
  span,
564
559
  } => RawSyntaxBlock::StringDef {
565
- raw: raw.clone(),
566
- key: key.clone(),
567
- value: value.clone(),
568
- span: span.clone(),
560
+ raw,
561
+ key,
562
+ value,
563
+ span,
569
564
  },
570
565
  RawBlock::Entry { id, key, span } => RawSyntaxBlock::Entry {
571
- id: RawEntryId(*id),
572
- key: key.clone(),
573
- span: span.clone(),
574
- },
575
- RawBlock::Failed { raw, error, span } => RawSyntaxBlock::Failed {
576
- raw: raw.clone(),
577
- error: error.clone(),
578
- span: span.clone(),
579
- },
580
- RawBlock::Other { raw, span } => RawSyntaxBlock::Other {
581
- raw: raw.clone(),
582
- span: span.clone(),
566
+ id: RawEntryId(id),
567
+ key,
568
+ span,
583
569
  },
570
+ RawBlock::Failed { raw, error, span } => RawSyntaxBlock::Failed { raw, error, span },
571
+ RawBlock::Other { raw, span } => RawSyntaxBlock::Other { raw, span },
584
572
  }
585
573
  }
586
574
 
587
- fn raw_syntax_entry(id: RawEntryId, entry: &RawEntryData) -> RawSyntaxEntry {
575
+ fn raw_syntax_entry(id: RawEntryId, entry: RawEntryData) -> RawSyntaxEntry {
588
576
  RawSyntaxEntry {
589
577
  id,
590
- key: entry.key.clone(),
591
- kind: entry.kind.clone(),
578
+ key: entry.key,
579
+ kind: entry.kind,
592
580
  fields: entry
593
581
  .field_blocks
594
- .iter()
582
+ .into_iter()
595
583
  .enumerate()
596
584
  .map(|(field_id, field)| RawSyntaxField {
597
585
  id: RawFieldId(field_id),
598
- name: field.name.clone(),
599
- value: field.value.clone(),
586
+ name: field.name,
587
+ value: field.value,
600
588
  value_mode: field.value_mode,
601
- value_atoms: field.value_atoms.clone(),
602
- span: field.span.clone(),
603
- patch_span: field.patch_span.clone(),
589
+ value_atoms: field.value_atoms,
590
+ span: field.span,
591
+ patch_span: field.patch_span,
604
592
  })
605
593
  .collect(),
606
- span: entry.span.clone(),
607
- raw: entry.raw.clone(),
594
+ span: entry.span,
595
+ raw: entry.raw,
608
596
  }
609
597
  }
610
598
 
@@ -3,7 +3,6 @@ use std::fmt::{self, Write as _};
3
3
  use hayagriva::{BibliographyItem, BufWriteFormat, RenderedBibliography};
4
4
 
5
5
  use super::html::{render_child_html, render_children_html, write_html_escaped};
6
- use super::text::elem_children_to_string;
7
6
 
8
7
  pub(crate) fn bibliography_to_text_html(
9
8
  bibliography: &RenderedBibliography,
@@ -57,11 +56,17 @@ fn write_bibliography_item_text(
57
56
  .map_err(|err| err.to_string())?;
58
57
  }
59
58
 
60
- let content = elem_children_to_string(&item.content, BufWriteFormat::Plain)?;
61
- if output.len() > item_start && !content.is_empty() {
59
+ let label_end = output.len();
60
+ if label_end > item_start {
62
61
  output.push(' ');
63
62
  }
64
- output.push_str(&content);
63
+ let content_start = output.len();
64
+ item.content
65
+ .write_buf(output, BufWriteFormat::Plain)
66
+ .map_err(|err| err.to_string())?;
67
+ if output.len() == content_start {
68
+ output.truncate(label_end);
69
+ }
65
70
  Ok(())
66
71
  }
67
72
 
@@ -141,14 +141,19 @@ fn push_html_wrapper(output: &mut String, suffix: &mut String, start: &str, end:
141
141
  }
142
142
 
143
143
  pub(crate) fn write_html_escaped(output: &mut String, value: &str) {
144
- for ch in value.chars() {
145
- match ch {
146
- '&' => output.push_str("&amp;"),
147
- '<' => output.push_str("&lt;"),
148
- '>' => output.push_str("&gt;"),
149
- '"' => output.push_str("&quot;"),
150
- '\'' => output.push_str("&#39;"),
151
- _ => output.push(ch),
152
- }
144
+ let mut start = 0;
145
+ for (index, byte) in value.bytes().enumerate() {
146
+ let escaped = match byte {
147
+ b'&' => "&amp;",
148
+ b'<' => "&lt;",
149
+ b'>' => "&gt;",
150
+ b'"' => "&quot;",
151
+ b'\'' => "&#39;",
152
+ _ => continue,
153
+ };
154
+ output.push_str(&value[start..index]);
155
+ output.push_str(escaped);
156
+ start = index + 1;
153
157
  }
158
+ output.push_str(&value[start..]);
154
159
  }
@@ -25,7 +25,14 @@ pub struct RenderedOutput {
25
25
 
26
26
  pub(crate) fn bundled_locales() -> &'static [CslLocale] {
27
27
  static LOCALES: OnceLock<Vec<CslLocale>> = OnceLock::new();
28
- LOCALES.get_or_init(archive::locales).as_slice()
28
+ LOCALES
29
+ .get_or_init(|| {
30
+ let mut locales = archive::locales();
31
+ // The engine searches these resources for every term, including its en-US fallback.
32
+ locales.sort_by_key(|locale| locale.lang.as_ref().is_none_or(|lang| lang.0 != "en-US"));
33
+ locales
34
+ })
35
+ .as_slice()
29
36
  }
30
37
 
31
38
  pub fn is_bundled_locale(code: &str) -> bool {
@@ -61,7 +61,7 @@ impl RenderedRecord {
61
61
  }
62
62
 
63
63
  pub(crate) fn rendered_record_from_citation(
64
- citation: &RenderedCitation,
64
+ citation: RenderedCitation,
65
65
  keys: Vec<String>,
66
66
  ) -> Result<RenderedRecord, String> {
67
67
  Ok(RenderedRecord::new(
@@ -69,7 +69,7 @@ pub(crate) fn rendered_record_from_citation(
69
69
  elem_children_to_html(&citation.citation)?,
70
70
  None,
71
71
  RenderedTree::Citation {
72
- children: citation.citation.clone(),
72
+ children: citation.citation,
73
73
  keys,
74
74
  },
75
75
  ))
@@ -1,3 +1,4 @@
1
+ use std::borrow::Cow;
1
2
  use std::collections::{HashMap, HashSet};
2
3
 
3
4
  use indexmap::IndexMap;
@@ -40,7 +41,7 @@ pub(crate) fn generated_keys<'a>(
40
41
  let mut groups: IndexMap<String, Vec<&RawSyntaxEntry>> = IndexMap::new();
41
42
  let mut reserved = HashSet::new();
42
43
  for entry in entries {
43
- if let Some(key) = generate_key(&entry_values(entry), &template, None)? {
44
+ if let Some(key) = generate_key(entry, &template, None)? {
44
45
  groups.entry(key).or_default().push(entry);
45
46
  } else {
46
47
  reserved.insert(entry.key.clone());
@@ -54,11 +55,10 @@ pub(crate) fn generated_keys<'a>(
54
55
  let duplicate = entries.len() > 1 || retained.contains(&base);
55
56
  let mut suffix = 1;
56
57
  for entry in entries {
57
- let values = entry_values(entry);
58
58
  let candidate = if duplicate {
59
59
  let mut attempted = HashSet::new();
60
60
  loop {
61
- let mut candidate = generate_key(&values, &template, Some(suffix))?
61
+ let mut candidate = generate_key(entry, &template, Some(suffix))?
62
62
  .unwrap_or_else(|| base.clone());
63
63
  if !attempted.insert(candidate.clone()) {
64
64
  candidate = format!("{base}{}", num_to_letter(suffix));
@@ -136,7 +136,7 @@ fn marker_parameter(marker: &str) -> (String, Option<usize>) {
136
136
  }
137
137
 
138
138
  fn generate_key(
139
- values: &HashMap<String, String>,
139
+ values: &RawSyntaxEntry,
140
140
  template: &[TemplateToken],
141
141
  duplicate: Option<usize>,
142
142
  ) -> Result<Option<String>, String> {
@@ -171,13 +171,13 @@ fn generate_key(
171
171
  }
172
172
 
173
173
  fn marker_words(
174
- values: &HashMap<String, String>,
174
+ values: &RawSyntaxEntry,
175
175
  marker: &str,
176
176
  parameter: Option<usize>,
177
177
  duplicate: Option<usize>,
178
178
  ) -> Result<Vec<String>, String> {
179
179
  match marker {
180
- "auth" => Ok(parse_name_list(value(values, "author"))
180
+ "auth" => Ok(parse_name_list(&value(values, "author"))
181
181
  .into_iter()
182
182
  .filter(|name| !name.last.is_empty())
183
183
  .collect::<Vec<_>>()
@@ -185,7 +185,7 @@ fn marker_words(
185
185
  .map(|name| vec![name.last.clone()])
186
186
  .unwrap_or_default()),
187
187
  "authEtAl" => {
188
- let authors = parse_name_list(value(values, "author"))
188
+ let authors = parse_name_list(&value(values, "author"))
189
189
  .into_iter()
190
190
  .filter(|name| !name.last.is_empty())
191
191
  .collect::<Vec<_>>();
@@ -199,14 +199,14 @@ fn marker_words(
199
199
  }
200
200
  Ok(words)
201
201
  }
202
- "authors" => Ok(parse_name_list(value(values, "author"))
202
+ "authors" => Ok(parse_name_list(&value(values, "author"))
203
203
  .into_iter()
204
204
  .filter(|name| !name.last.is_empty())
205
205
  .map(|name| name.last)
206
206
  .collect()),
207
207
  "authorsN" => {
208
208
  let limit = parameter.unwrap_or(0);
209
- let authors = parse_name_list(value(values, "author"))
209
+ let authors = parse_name_list(&value(values, "author"))
210
210
  .into_iter()
211
211
  .filter(|name| !name.last.is_empty())
212
212
  .collect::<Vec<_>>();
@@ -248,7 +248,7 @@ fn marker_words(
248
248
  duplicate.map(|value| value.to_string()).unwrap_or_default(),
249
249
  ]),
250
250
  marker if marker == marker.to_uppercase() => {
251
- Ok(words(value(values, &marker.to_lowercase())))
251
+ Ok(words(&value(values, &marker.to_lowercase())))
252
252
  }
253
253
  _ => Err(format!("Invalid citation key token {marker}")),
254
254
  }
@@ -270,21 +270,13 @@ fn apply_modifier(words: Vec<String>, modifier: &str) -> Result<Vec<String>, Str
270
270
  }
271
271
  }
272
272
 
273
- fn entry_values(entry: &RawSyntaxEntry) -> HashMap<String, String> {
274
- entry
275
- .fields
276
- .iter()
277
- .map(|field| (field.name.to_lowercase(), rendered_field_value(field)))
278
- .collect()
279
- }
280
-
281
- fn rendered_field_value(field: &RawSyntaxField) -> String {
273
+ fn rendered_field_value(field: &RawSyntaxField) -> Cow<'_, str> {
282
274
  match field.value_mode {
283
- RawValueMode::Expression => expression_text(&field.value_atoms),
275
+ RawValueMode::Expression => Cow::Owned(expression_text(&field.value_atoms)),
284
276
  RawValueMode::Bare
285
277
  | RawValueMode::Braced
286
278
  | RawValueMode::Missing
287
- | RawValueMode::Quoted => field.value.clone(),
279
+ | RawValueMode::Quoted => Cow::Borrowed(&field.value),
288
280
  }
289
281
  }
290
282
 
@@ -371,7 +363,7 @@ fn is_prefix_token(token: &str) -> bool {
371
363
  .is_some_and(|ch| ch.is_ascii_lowercase())
372
364
  }
373
365
 
374
- fn title(values: &HashMap<String, String>) -> String {
366
+ fn title(values: &RawSyntaxEntry) -> String {
375
367
  let title = value(values, "title");
376
368
  if title.is_empty() {
377
369
  value(values, "booktitle").to_string()
@@ -380,8 +372,14 @@ fn title(values: &HashMap<String, String>) -> String {
380
372
  }
381
373
  }
382
374
 
383
- fn value<'a>(values: &'a HashMap<String, String>, key: &str) -> &'a str {
384
- values.get(key).map(String::as_str).unwrap_or("")
375
+ fn value<'a>(entry: &'a RawSyntaxEntry, key: &str) -> Cow<'a, str> {
376
+ entry
377
+ .fields
378
+ .iter()
379
+ .rev()
380
+ .find(|field| field.name.to_lowercase() == key)
381
+ .map(rendered_field_value)
382
+ .unwrap_or(Cow::Borrowed(""))
385
383
  }
386
384
 
387
385
  fn non_function_words(value: &str) -> Vec<String> {
@@ -61,8 +61,8 @@ fn read_command(chars: &[char], index: &mut usize) -> String {
61
61
  let mut output = String::from("\\");
62
62
  *index += 1;
63
63
  let symbol = chars.get(*index).copied().filter(|character| {
64
- !character.is_alphabetic()
65
- && !(*character == '\\'
64
+ !(character.is_alphabetic()
65
+ || *character == '\\'
66
66
  && chars
67
67
  .get(*index + 1)
68
68
  .is_some_and(|next| next.is_alphabetic()))
@@ -6,6 +6,7 @@ mod references;
6
6
  mod render;
7
7
  mod unicode;
8
8
 
9
+ use std::borrow::Cow;
9
10
  use std::fmt;
10
11
 
11
12
  use crate::raw::{RawDocument, RawEntryId, RawSyntaxBlock};
@@ -103,7 +104,7 @@ pub fn tidy_bibtex(input: &str, options: TidyOptions) -> Result<TidyResult, Tidy
103
104
  })?;
104
105
  let input = normalize_newlines(input);
105
106
  let doc = RawDocument::parse(&input);
106
- let mut syntax = doc.syntax();
107
+ let mut syntax = doc.into_syntax();
107
108
 
108
109
  if let Some((raw, error, byte)) = syntax.blocks.iter().find_map(first_failed_block) {
109
110
  return Err(syntax_error(&input, raw, error, byte));
@@ -222,8 +223,12 @@ fn line_column(input: &str, byte: usize) -> (usize, usize) {
222
223
  (line, column)
223
224
  }
224
225
 
225
- fn normalize_newlines(input: &str) -> String {
226
- input.replace("\r\n", "\n").replace('\r', "\n")
226
+ fn normalize_newlines(input: &str) -> Cow<'_, str> {
227
+ if input.contains('\r') {
228
+ Cow::Owned(input.replace("\r\n", "\n").replace('\r', "\n"))
229
+ } else {
230
+ Cow::Borrowed(input)
231
+ }
227
232
  }
228
233
 
229
234
  #[cfg(test)]
@@ -192,6 +192,9 @@ fn unwrap_text(value: &str) -> String {
192
192
  }
193
193
 
194
194
  fn wrap_braced_value(field: &RawSyntaxField, value: &str, options: &TidyOptions) -> String {
195
+ if options.wrap.is_none() && !value.contains("\n\n") {
196
+ return value.to_string();
197
+ }
195
198
  let indent = if options.tab {
196
199
  "\t".to_string()
197
200
  } else {
@@ -92,10 +92,10 @@ pub(crate) fn parse_projection_keys(
92
92
  Ok(Some(parsed))
93
93
  }
94
94
 
95
- pub(crate) fn project_rows_to_py(
95
+ pub(crate) fn project_rows_to_py<'a>(
96
96
  py: Python<'_>,
97
97
  fields: &[ProjectionField],
98
- records: &[EntryRecord],
98
+ records: impl IntoIterator<Item = &'a EntryRecord>,
99
99
  ) -> PyResult<Py<PyAny>> {
100
100
  let rows = PyList::empty(py);
101
101
  for record in records {
@@ -23,23 +23,23 @@ impl Entry {
23
23
  #[pymethods]
24
24
  impl Entry {
25
25
  #[getter]
26
- fn key(&self) -> String {
27
- self.data.key.clone()
26
+ fn key(&self) -> &str {
27
+ &self.data.key
28
28
  }
29
29
 
30
30
  #[getter]
31
- fn entry_type(&self) -> String {
32
- self.data.entry_type.clone()
31
+ fn entry_type(&self) -> &str {
32
+ &self.data.entry_type
33
33
  }
34
34
 
35
35
  #[getter]
36
- fn title(&self) -> Option<String> {
37
- self.data.title.clone()
36
+ fn title(&self) -> Option<&str> {
37
+ self.data.title.as_deref()
38
38
  }
39
39
 
40
40
  #[getter]
41
- fn date(&self) -> Option<String> {
42
- self.data.date.clone()
41
+ fn date(&self) -> Option<&str> {
42
+ self.data.date.as_deref()
43
43
  }
44
44
 
45
45
  #[getter]
@@ -53,13 +53,13 @@ impl Entry {
53
53
  }
54
54
 
55
55
  #[getter]
56
- fn volume(&self) -> Option<String> {
57
- self.data.volume.clone()
56
+ fn volume(&self) -> Option<&str> {
57
+ self.data.volume.as_deref()
58
58
  }
59
59
 
60
60
  #[getter]
61
- fn doi(&self) -> Option<String> {
62
- self.data.doi.clone()
61
+ fn doi(&self) -> Option<&str> {
62
+ self.data.doi.as_deref()
63
63
  }
64
64
 
65
65
  fn __repr__(&self) -> String {
@@ -147,14 +147,14 @@ impl Library {
147
147
  ) -> PyResult<Py<PyAny>> {
148
148
  let fields = parse_project_fields_arg(fields)?;
149
149
  let keys = parse_projection_keys(self.inner.as_ref(), keys)?;
150
- let records = match keys {
151
- Some(keys) => keys
152
- .iter()
153
- .filter_map(|key| self.inner.get_record(key).cloned())
154
- .collect(),
155
- None => self.inner.records().to_vec(),
156
- };
157
- project_rows_to_py(py, &fields, &records)
150
+ match keys {
151
+ Some(keys) => project_rows_to_py(
152
+ py,
153
+ &fields,
154
+ keys.iter().filter_map(|key| self.inner.get_record(key)),
155
+ ),
156
+ None => project_rows_to_py(py, &fields, self.inner.records()),
157
+ }
158
158
  }
159
159
 
160
160
  fn __len__(&self) -> usize {
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "refkit"
3
- version = "0.0.7"
3
+ version = "0.1.0"
4
4
  description = "Fast Python citation parsing, rendering, and BibTeX editing backed by Rust"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes