Skip to content

Commit 1d4fa59

Browse files
arbronFyorl
andauthored
[#46] Extract adventure embedded documents into folders by type (#68)
Co-authored-by: fyorl <kim.mantas@gmail.com>
1 parent 9bc89c9 commit 1d4fa59

2 files changed

Lines changed: 52 additions & 23 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ Extract the contents of a compendium pack into individual source files for each
213213
* **transformEntry:** *(entry: object): Promise<false|void>* A function that is called on every entry. Returning *false* indicates that the entry should be discarded.
214214
* **transformName:** *(entry: object): Promise<string|void>* A function that is called on every entry. The value returned from this will be used as the entry's filename and must include the appropriate file extension. If nothing is returned, an auto-generated name will be used instead.
215215
* **transformFolderName:** *(entry: object): Promise<string|void>* A function used to generate a directory name for an extracted Folder document when the `folders` option is used.
216-
* **expandAdventures:** *boolean* Write documents emebdded in Adventures to their own files. If the `folders` option is also supplied, the Adventure is treated like a folder, and written to `_Adventure.{yml|json}` instead of `_Folder.{yml|json}`.
216+
* **expandAdventures:** *boolean* Write documents embedded in Adventures to their own files. If the `folders` option is also supplied, the Adventure is treated like a folder, and written to `_Adventure.{yml|json}` instead of `_Folder.{yml|json}`. Additionally, all its entries are grouped into sub-folders by Document type.
217217
* **jsonOptions:** *object*
218218
* **replacer:** *(key: string, value: any): any|Array<string|number>* A replacer function or an array of property names in the object to include in the resulting string.
219219
* **space:** *string|number* A number of spaces or a string to use as indentation.

lib/package.mjs

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ import { ClassicLevel } from "classic-level";
4848
* @property {boolean} [folders] Create a directory structure that matches the compendium folders.
4949
* @property {boolean} [expandAdventures] Write documents embedded in Adventures to their own files. If the
5050
* folders option is also supplied, the Adventure is treated like a
51-
* folder.
51+
* folder, and all its entries are grouped into sub-folders by
52+
* Document type.
5253
* @property {DocumentCollection} [collection] Required only for NeDB packs in order to generate a correct key.
5354
* Can be used instead of documentType if known.
5455
* @property {NameTransformer} [transformName] A function that is used to generate a filename for the extracted
@@ -486,22 +487,10 @@ async function extractClassicLevel(pack, dest, {
486487
const db = new ClassicLevel(pack, { keyEncoding: "utf8", valueEncoding: "json", createIfMissing: false });
487488

488489
// Build up the folder structure
489-
const folderMap = new Map();
490+
let folderMap = new Map();
490491
if ( folders ) {
491-
for await ( const [key, doc] of db.iterator() ) {
492-
if ( !key.startsWith("!folders") ) continue;
493-
let name = await transformFolderName?.(doc);
494-
if ( !name ) name = doc.name ? `${getSafeFilename(doc.name)}_${doc._id}` : key;
495-
folderMap.set(doc._id, { name, folder: doc.folder });
496-
}
497-
for ( const folder of folderMap.values() ) {
498-
let parent = folderMap.get(folder.folder);
499-
folder.path = folder.name;
500-
while ( parent ) {
501-
folder.path = path.join(parent.name, folder.path);
502-
parent = folderMap.get(parent.folder);
503-
}
504-
}
492+
const keys = (await db.keys().all()).filter(k => k.startsWith("!folders"));
493+
folderMap = await buildFolderMap(await db.getMany(keys), { transformFolderName });
505494
}
506495

507496
const unpackDoc = applyHierarchy(async (doc, collection, { sublevelPrefix, idPrefix }={}) => {
@@ -556,7 +545,7 @@ async function extractClassicLevel(pack, dest, {
556545
* @param {Partial<ExtractOptions>} [extractOptions] Options to configure serialization behavior.
557546
*/
558547
async function extractAdventure(doc, dest, { folderMap }={}, {
559-
yaml, yamlOptions, jsonOptions, log, folders, transformEntry, transformName
548+
yaml, yamlOptions, jsonOptions, log, folders, transformEntry, transformName, transformFolderName
560549
}={}) {
561550
let adventureFolder;
562551

@@ -572,21 +561,32 @@ async function extractAdventure(doc, dest, { folderMap }={}, {
572561
if ( folder ) name = path.join(folder, name);
573562
}
574563
}
564+
const context = { adventure: { doc, path: name } };
565+
566+
// Build up the folder structure
567+
const embeddedFolderMap = folders
568+
? await buildFolderMap(doc.folders ?? [], { groupByType: true, transformFolderName })
569+
: new Map();
575570

576571
// Write all documents contained in the adventure
577-
const context = { adventure: { doc, path: name } };
578572
for ( const embeddedCollectionName of ADVENTURE_DOCS ) {
579573
const paths = [];
580574
for ( const embeddedDoc of doc[embeddedCollectionName] ?? [] ) {
581575
if ( await transformEntry?.(embeddedDoc, context) === false ) continue;
582-
let embeddedName = await transformName?.(embeddedDoc, context);
576+
let embeddedFolder = path.join(adventureFolder ?? "", embeddedFolderMap.get(embeddedDoc.folder)?.path ?? "");
577+
let embeddedName = await transformName?.(embeddedDoc, { ...context, folder: embeddedFolder });
583578
if ( !embeddedName ) {
584579
const { name, _id: id } = embeddedDoc;
585-
embeddedName = `${name ? `${getSafeFilename(name)}_${id}` : doc._id}.${yaml ? "yml" : "json"}`;
586-
if ( adventureFolder ) embeddedName = path.join(adventureFolder, embeddedName);
580+
if ( (embeddedCollectionName === "folders") && embeddedFolderMap.has(embeddedDoc._id) ) {
581+
embeddedFolder = adventureFolder;
582+
embeddedName = path.join(embeddedFolderMap.get(embeddedDoc._id).path, `_Folder.${yaml ? "yml" : "json"}`);
583+
} else {
584+
embeddedName = `${name ? `${getSafeFilename(name)}_${id}` : doc._id}.${yaml ? "yml" : "json"}`;
585+
}
586+
if ( embeddedFolder ) embeddedName = path.join(embeddedFolder, embeddedName);
587587
}
588588
const filename = path.join(dest, embeddedName);
589-
paths.push(path.basename(embeddedName));
589+
paths.push(adventureFolder ? path.relative(adventureFolder, embeddedName) : path.basename(embeddedName));
590590
serializeDocument(embeddedDoc, filename, { yaml, yamlOptions, jsonOptions });
591591
if ( log ) console.log(`Wrote ${chalk.blue(embeddedName)}`);
592592
}
@@ -624,6 +624,35 @@ function applyHierarchy(fn) {
624624

625625
/* -------------------------------------------- */
626626

627+
/**
628+
* Build up the folder structure used to extract files.
629+
* @param {object[]} folders Folders to process.
630+
* @param {object} [options={}]
631+
* @param {boolean} [options.groupByType=false] Should folders be in sub-folders based on document type?
632+
* @param {NameTransformer} [options.transformFolderName] Name transformer.
633+
* @returns {Map<string, string>} Mapping of folder IDs to paths.
634+
*/
635+
async function buildFolderMap(folders, { groupByType=false, transformFolderName }={}) {
636+
const folderMap = new Map();
637+
for ( const doc of folders ) {
638+
let name = await transformFolderName?.(doc);
639+
if ( !name ) name = doc.name ? `${getSafeFilename(doc.name)}_${doc._id}` : doc._id;
640+
folderMap.set(doc._id, { name, folder: doc.folder, type: doc.type });
641+
}
642+
for ( const folder of folderMap.values() ) {
643+
let parent = folderMap.get(folder.folder);
644+
folder.path = folder.name;
645+
while ( parent ) {
646+
folder.path = path.join(parent.name, folder.path);
647+
parent = folderMap.get(parent.folder);
648+
}
649+
if ( groupByType ) folder.path = path.join(folder.type, folder.path);
650+
}
651+
return folderMap;
652+
}
653+
654+
/* -------------------------------------------- */
655+
627656
/**
628657
* Transform a Document's embedded collections by applying a function to them.
629658
* @param {object} doc The Document being operated on.

0 commit comments

Comments
 (0)