Skip to content

Commit 6edb291

Browse files
hepplerjclaude
andcommitted
v1.0.0: field rename, first stable release
Adds a `fieldRename` option for renaming top-level YAML field names in the frontmatter. Comma-separated `from=to` rules. The motivating case is correspondence templates: Tropy stores the recipient as `dc:audience`, which the plugin emits as `audience:` — users whose convention is `recipient:` configure `audience=recipient` and get exactly that. Rename applies to standard frontmatter fields (title, creator, publication, date, doc_type, source, archive, collection, box, folder, tags, photos) and to custom template properties that flow through the passthrough. Tag-dispatched entity fields are unaffected — those are already configurable via tagPrefixDispatch. The internal `tropy_hash:` field is also non-renamable since idempotency depends on reading it. This is also the first stable release. The release.yml workflow no longer marks tags as pre-release; v1.0.0 lands as a real GitHub release. README roadmap marks v1.0.0 done. Verified end to end: - fieldRename: creator=author, audience=recipient produces author/ recipient YAML keys with values intact - tropy_hash preserved (not renamable) - All 3 fixture exports still idempotent under default config Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 76b06b5 commit 6edb291

4 files changed

Lines changed: 99 additions & 18 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ jobs:
3434
uses: ncipollo/release-action@v1
3535
with:
3636
artifacts: ${{ steps.meta.outputs.artifact }}.zip
37-
prerelease: true
37+
prerelease: false
3838
generateReleaseNotes: true
3939
token: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,36 @@ require vault-relative paths or a specific image-loading config — if your
150150
editor doesn't render the embed, that's a path-resolution issue, not a
151151
plugin bug.
152152

153+
### Field rename
154+
155+
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).
158+
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:
162+
163+
```
164+
audience=recipient
165+
```
166+
167+
Other examples:
168+
169+
```
170+
creator=author, audience=recipient
171+
publication=published-in
172+
photos=attachments
173+
```
174+
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.
182+
153183
### Compose source fields
154184

155185
On by default. When enabled, the standard archival fields (`source`,
@@ -249,7 +279,8 @@ a re-export of an item, delete its file from the output directory.
249279
- [x] **v0.4.0** — selection-attached notes; ontology-aware labels for
250280
custom template fields; configurable filename pattern; optional photo
251281
embedding in the body.
252-
- [ ] **v1.0.0** — feature-complete, polished docs, stable API.
282+
- [x] **v1.0.0** — field rename support; documentation polish; first
283+
stable release.
253284

254285
## Development
255286

index.js

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

3-
// Tropy.md — v0.4.0
3+
// Tropy.md — v1.0.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
@@ -26,6 +26,37 @@ function parseCsvSet(s) {
2626
)
2727
}
2828

29+
function parseFieldRename(s) {
30+
// Parses the fieldRename config into a Map<from, to>.
31+
//
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:
35+
//
36+
// creator=author, audience=recipient, publication=published-in
37+
//
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).
42+
const map = new Map()
43+
for (const entry of String(s || '').split(',')) {
44+
const trimmed = entry.trim()
45+
if (!trimmed) continue
46+
const eq = trimmed.indexOf('=')
47+
if (eq < 0) continue
48+
const from = trimmed.slice(0, eq).trim()
49+
const to = trimmed.slice(eq + 1).trim()
50+
if (from && to) map.set(from, to)
51+
}
52+
return map
53+
}
54+
55+
function renameYamlKey(opts, key) {
56+
if (!opts.fieldRename) return key
57+
return opts.fieldRename.get(key) || key
58+
}
59+
2960
function parseDispatch(s) {
3061
// Parses the tagPrefixDispatch config into an ordered list of
3162
// { prefix, field }
@@ -310,28 +341,36 @@ function buildFrontmatter(item, hash, opts) {
310341

311342
const { fields, fieldOrder, leftover } = dispatchTags(allTags, opts.dispatch)
312343

344+
// Helper to keep emit sites readable. fieldRename is consulted at every
345+
// 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)
350+
313351
const lines = ['---']
314-
lines.push(`title: ${yamlScalar(item.title)}`)
315-
if (item.creator) lines.push(`creator: ${yamlScalar(item.creator)}`)
316-
if (item.publisher) lines.push(`publication: ${yamlScalar(item.publisher)}`)
317-
if (item.date) lines.push(`date: ${yamlScalar(item.date)}`)
318-
if (item.type) lines.push(`doc_type: ${yamlScalar(item.type)}`)
352+
lines.push(`${k('title')}: ${yamlScalar(item.title)}`)
353+
if (item.creator) lines.push(`${k('creator')}: ${yamlScalar(item.creator)}`)
354+
if (item.publisher) lines.push(`${k('publication')}: ${yamlScalar(item.publisher)}`)
355+
if (item.date) lines.push(`${k('date')}: ${yamlScalar(item.date)}`)
356+
if (item.type) lines.push(`${k('doc_type')}: ${yamlScalar(item.type)}`)
319357

320358
// Source: either composed into one string, or emitted as separate fields.
321359
if (opts.composeSource) {
322360
const source = composeSource(item)
323-
if (source) lines.push(`source: ${yamlScalar(source)}`)
361+
if (source) lines.push(`${k('source')}: ${yamlScalar(source)}`)
324362
} else {
325363
for (const part of SOURCE_PARTS) {
326-
if (item[part]) lines.push(`${part}: ${yamlScalar(item[part])}`)
364+
if (item[part]) lines.push(`${k(part)}: ${yamlScalar(item[part])}`)
327365
}
328366
}
329367

330368
// Custom template properties — anything else on the item that isn't
331369
// structural or already rendered. Lets users with custom Tropy templates
332370
// see their data without us needing to know each field in advance.
333371
// URI-shaped keys are preferentially resolved through Tropy's ontology
334-
// for human-readable labels; falls back to the URI's local name.
372+
// for human-readable labels; falls back to the URI's local name. The
373+
// fieldRename map applies on top so users can rename whatever they like.
335374
const handled = new Set([
336375
...STRUCTURAL_KEYS,
337376
...RENDERED_EXPLICITLY,
@@ -343,7 +382,7 @@ function buildFrontmatter(item, hash, opts) {
343382
if (looksLikeUri(key)) {
344383
yamlKey = ontologyLabel(opts.ontology, key) || localName(key)
345384
}
346-
emitYamlValue(lines, yamlKey, item[key])
385+
emitYamlValue(lines, k(yamlKey), item[key])
347386
}
348387

349388
// Dispatched entity fields, in declaration order.
@@ -362,18 +401,18 @@ function buildFrontmatter(item, hash, opts) {
362401

363402
// Leftover (unmatched) tags as a flat list.
364403
if (leftover.length === 0) {
365-
lines.push('tags: []')
404+
lines.push(`${k('tags')}: []`)
366405
} else {
367-
lines.push('tags:')
406+
lines.push(`${k('tags')}:`)
368407
for (const tag of leftover) lines.push(` - ${yamlScalar(tag)}`)
369408
}
370409

371410
if (opts.includePhotoPaths) {
372411
const paths = extractPhotoPaths(item)
373412
if (paths.length === 0) {
374-
lines.push('photos: []')
413+
lines.push(`${k('photos')}: []`)
375414
} else {
376-
lines.push('photos:')
415+
lines.push(`${k('photos')}:`)
377416
for (const p of paths) lines.push(` - ${yamlScalar(p)}`)
378417
}
379418
}
@@ -526,6 +565,7 @@ class MarkdownPlugin {
526565
composeSource: this.options.composeSource !== false,
527566
filenamePattern: this.options.filenamePattern || 'tropy-{hash}-{slug}',
528567
embedPhotos: this.options.embedPhotos === true,
568+
fieldRename: parseFieldRename(this.options.fieldRename),
529569
ontology
530570
}
531571
}
@@ -603,7 +643,8 @@ MarkdownPlugin.defaults = {
603643
wikiLinkEntities: false,
604644
composeSource: true,
605645
filenamePattern: 'tropy-{hash}-{slug}',
606-
embedPhotos: false
646+
embedPhotos: false,
647+
fieldRename: ''
607648
}
608649

609650
module.exports = MarkdownPlugin
@@ -612,6 +653,8 @@ module.exports = MarkdownPlugin
612653
module.exports._internals = {
613654
parseCsvSet,
614655
parseDispatch,
656+
parseFieldRename,
657+
renameYamlKey,
615658
dispatchTags,
616659
slugify,
617660
shortHash,

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "tropymd",
33
"productName": "Tropy.md",
4-
"version": "0.4.1",
4+
"version": "1.0.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",
@@ -87,6 +87,13 @@
8787
"label": "Embed photos in the body",
8888
"default": false,
8989
"hint": "When enabled, each page's notes are preceded by an `![](photo path)` line that embeds the scan image. Useful for editors that render image links inline. Single-photo items skip the page marker; multi-photo items get both the marker and the embed."
90+
},
91+
{
92+
"field": "fieldRename",
93+
"type": "string",
94+
"label": "Field rename (frontmatter keys)",
95+
"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."
9097
}
9198
]
9299
}

0 commit comments

Comments
 (0)