Skip to content

Commit 3ffaa6c

Browse files
hepplerjclaude
andcommitted
v1.1.0: doc-type-scoped fieldRename rules
v1.0.0 shipped fieldRename as a universal rename. That over-applied for mixed-template projects: a user who wants `creator=author` on letters also gets it on newspaper articles, where dc:creator semantically still means "creator," not "author." This release adds an optional `@<types>` suffix per rule. Pipe-separated types after the `@` restrict the rule to items whose doc_type matches. Rules without a scope still apply universally (so v1.0.0 configs keep working unchanged): unscoped: publication=published-in scoped: creator=author@letter|memorandum|telegram Verified end to end: - Newspaper item (type=newspaper): scoped rules don't match, unscoped do - Letter item (type=letter): all rules apply - Same input data, different doc_type, different output Code change: parseFieldRename now returns Map<from, {to, types}> instead of Map<from, to>; renameYamlKey takes the item to evaluate scope; the buildFrontmatter `k` helper closes over `item` so doc-type checking works at every emit site. README's Field rename section rewritten with worked examples. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6edb291 commit 3ffaa6c

3 files changed

Lines changed: 76 additions & 36 deletions

File tree

README.md

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -153,32 +153,47 @@ plugin bug.
153153
### Field rename
154154

155155
Comma-separated `from=to` rules that rename top-level YAML field names.
156-
Useful for matching the conventions of whatever Markdown vault you're
157-
exporting into. Default: empty (no renames).
156+
Default: empty (no renames). Each rule can optionally be scoped to
157+
specific doc types with an `@type|type|...` suffix; rules without a
158+
scope apply to every exported item.
158159

159-
The most common case: Tropy's correspondence template stores the
160-
recipient as `dc:audience`, which the plugin emits as `audience:` by
161-
default. If your convention is `recipient:`, configure:
160+
**The motivating case — correspondence.** Tropy's correspondence
161+
template stores the recipient as `dc:audience` (which the plugin emits
162+
as `audience:`) and the author as `dc:creator` (emitted as `creator:`).
163+
Your Obsidian convention might be `author:` and `recipient:` — but only
164+
*for letters and similar correspondence*. On a newspaper article,
165+
`creator:` should stay as `creator:`. Scoping handles this:
162166

163167
```
164-
audience=recipient
168+
creator=author@letter|memorandum|telegram,
169+
audience=recipient@letter|memorandum|telegram
165170
```
166171

167-
Other examples:
172+
Now `creator → author` only applies when `doc_type` is `letter`,
173+
`memorandum`, or `telegram`; on newspaper or generic-document items,
174+
`creator:` stays as `creator:`.
175+
176+
**Unscoped rules — apply everywhere.** If you want `publication:` renamed
177+
to `published-in:` regardless of doc type, drop the scope:
168178

169179
```
170-
creator=author, audience=recipient
171180
publication=published-in
172-
photos=attachments
173181
```
174182

175-
The rule applies to standard frontmatter fields (`title`, `creator`,
176-
`publication`, `date`, `doc_type`, `source`, `archive`, `collection`,
177-
`box`, `folder`, `tags`, `photos`) and to custom template properties
178-
that flow through the passthrough. It does **not** apply to
179-
tag-dispatched entity fields — use the Tag prefix dispatch setting to
180-
name those. The internal `tropy_hash:` field is also non-renamable, since
181-
idempotency depends on it.
183+
**Combining the two.** A single config line can mix scoped and unscoped
184+
rules:
185+
186+
```
187+
creator=author@letter|memorandum|telegram, audience=recipient@letter|memorandum|telegram, publication=published-in
188+
```
189+
190+
**What the rule applies to:** standard frontmatter fields (`title`,
191+
`creator`, `publication`, `date`, `doc_type`, `source`, `archive`,
192+
`collection`, `box`, `folder`, `tags`, `photos`) and any custom
193+
template properties that flow through the passthrough. It does **not**
194+
apply to tag-dispatched entity fields — use the Tag prefix dispatch
195+
setting to name those. The internal `tropy_hash:` field is also
196+
non-renamable, since idempotency depends on it.
182197

183198
### Compose source fields
184199

@@ -281,6 +296,8 @@ a re-export of an item, delete its file from the output directory.
281296
embedding in the body.
282297
- [x] **v1.0.0** — field rename support; documentation polish; first
283298
stable release.
299+
- [x] **v1.1.0** — doc-type-scoped field rename rules
300+
(e.g. `creator=author@letter|memorandum|telegram`).
284301

285302
## Development
286303

index.js

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22

3-
// Tropy.md — v1.0.0
3+
// Tropy.md — v1.1.0
44
//
55
// Exports each selected Tropy item to its own Markdown file in a chosen
66
// directory. Markdown-editor neutral by default — no wiki-links, no opinionated
@@ -27,34 +27,55 @@ function parseCsvSet(s) {
2727
}
2828

2929
function parseFieldRename(s) {
30-
// Parses the fieldRename config into a Map<from, to>.
30+
// Parses the fieldRename config into a Map<from, { to, types }>.
3131
//
32-
// Format: comma-separated `from=to` pairs. The keys are the YAML field
33-
// names the plugin would have written by default; the values are the
34-
// names to use instead. Example:
32+
// Format: comma-separated `from=to` pairs, with an optional doc-type
33+
// scope per rule introduced by `@`. Multiple types in the scope are
34+
// separated by `|` (since comma is the rule separator). Example:
3535
//
36-
// creator=author, audience=recipient, publication=published-in
36+
// creator=author@letter|memorandum|telegram,
37+
// audience=recipient@letter|memorandum|telegram,
38+
// publication=published-in
3739
//
38-
// Useful for matching downstream conventions (e.g. Tropy stores
39-
// correspondence "recipient" as dc:audience, which the plugin emits
40-
// through its passthrough as `audience:` — users who want `recipient:`
41-
// map it explicitly here).
40+
// A rule with no `@` applies to every item; a rule with `@` applies
41+
// only when the item's `doc_type` matches one of the listed types.
42+
// The motivating case is correspondence: dc:creator semantically means
43+
// "author" only on letters/memos/telegrams, not on newspaper articles
44+
// — the scope keeps a rename from leaking across doc types.
4245
const map = new Map()
4346
for (const entry of String(s || '').split(',')) {
4447
const trimmed = entry.trim()
4548
if (!trimmed) continue
4649
const eq = trimmed.indexOf('=')
4750
if (eq < 0) continue
4851
const from = trimmed.slice(0, eq).trim()
49-
const to = trimmed.slice(eq + 1).trim()
50-
if (from && to) map.set(from, to)
52+
let value = trimmed.slice(eq + 1).trim()
53+
let types = null
54+
const at = value.lastIndexOf('@')
55+
if (at >= 0) {
56+
const list = value.slice(at + 1)
57+
value = value.slice(0, at).trim()
58+
const parts = list.split('|')
59+
.map(t => t.trim().toLowerCase())
60+
.filter(Boolean)
61+
if (parts.length > 0) types = new Set(parts)
62+
}
63+
if (from && value) map.set(from, { to: value, types })
5164
}
5265
return map
5366
}
5467

55-
function renameYamlKey(opts, key) {
68+
function renameYamlKey(opts, key, item) {
69+
// Returns the renamed YAML key, or the original key if no rule applies.
70+
// `item` is consulted to satisfy any doc-type scope on the matching rule.
5671
if (!opts.fieldRename) return key
57-
return opts.fieldRename.get(key) || key
72+
const rule = opts.fieldRename.get(key)
73+
if (!rule) return key
74+
if (rule.types) {
75+
const docType = String((item && item.type) || '').toLowerCase()
76+
if (!rule.types.has(docType)) return key
77+
}
78+
return rule.to
5879
}
5980

6081
function parseDispatch(s) {
@@ -343,10 +364,12 @@ function buildFrontmatter(item, hash, opts) {
343364

344365
// Helper to keep emit sites readable. fieldRename is consulted at every
345366
// YAML-key emission so users can match any downstream convention. The
346-
// internal `tropy_hash:` and dispatched entity fields are intentionally
347-
// not renamable — the former because idempotency depends on it, the
348-
// latter because tagPrefixDispatch already names those fields directly.
349-
const k = name => renameYamlKey(opts, name)
367+
// helper closes over `item` so doc-type-scoped rules (e.g. only rename
368+
// `creator` for letters) can do the right thing. The internal
369+
// `tropy_hash:` and dispatched entity fields are intentionally not
370+
// renamable — the former because idempotency depends on it, the latter
371+
// because tagPrefixDispatch already names those fields directly.
372+
const k = name => renameYamlKey(opts, name, item)
350373

351374
const lines = ['---']
352375
lines.push(`${k('title')}: ${yamlScalar(item.title)}`)

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "tropymd",
33
"productName": "Tropy.md",
4-
"version": "1.0.0",
4+
"version": "1.1.0",
55
"description": "Export Tropy items as Markdown files (one per item) with YAML frontmatter, suitable for Obsidian and other Markdown editors.",
66
"icon": "icon.svg",
77
"main": "index.js",
@@ -93,7 +93,7 @@
9393
"type": "string",
9494
"label": "Field rename (frontmatter keys)",
9595
"default": "",
96-
"hint": "Comma-separated `from=to` rules that rename top-level YAML field names. Useful for matching downstream conventions — e.g. correspondence items use `dc:audience` for the recipient, which the plugin emits as `audience:` by default; configure `audience=recipient` to land it in your conventional `recipient:` field. Apply equally to the standard fields (creator, publication, date, doc_type, source, archive, collection, box, folder, tags, photos) and any custom template properties that pass through. Tag-dispatched entity fields are unaffected — use the Tag prefix dispatch setting to name those."
96+
"hint": "Comma-separated `from=to` rules that rename top-level YAML field names. Optionally scope a rule to specific doc types with `@type|type` (pipe-separated). Without a scope, the rule applies to every item. Example: `creator=author@letter|memorandum|telegram, audience=recipient@letter|memorandum|telegram, publication=published-in` renames creator → author and audience → recipient only on correspondence-type items, but always renames publication → published-in. Applies to the standard fields (creator, publication, date, doc_type, source, archive, collection, box, folder, tags, photos) and any custom template properties that pass through. Tag-dispatched entity fields are unaffected — use the Tag prefix dispatch setting to name those."
9797
}
9898
]
9999
}

0 commit comments

Comments
 (0)