Skip to content

Commit a655078

Browse files
committed
Initaial commit
0 parents  commit a655078

21 files changed

+357
-0
lines changed

.eslintignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
dist*
3+
*.config.js
4+
*.config.ts

.eslintrc

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"root": true,
3+
"parser": "@typescript-eslint/parser",
4+
"plugins": [
5+
"@typescript-eslint"
6+
],
7+
"extends": [
8+
"eslint:recommended",
9+
"plugin:@typescript-eslint/eslint-recommended",
10+
"plugin:@typescript-eslint/recommended",
11+
"prettier"
12+
]
13+
}

.gitignore

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
**/.DS_Store
2+
*-debug.log
3+
*-error.log
4+
/.idea
5+
/.tsimp
6+
/.nyc_output
7+
/lib
8+
/package-lock.json
9+
/tmp
10+
/yarn.lock
11+
node_modules
12+
/*.env
13+
/input
14+
/output
15+
/test/fixtures/*.epub

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Dancing Queen, the ABA Book Parser
2+
3+
Given an EPUB file from A Book Apart, extract the table of contents, files, and other sundries for use in an 11ty site.

ava.config.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
files: ['test/**/*', '!test/**/fixtures/**/*', '!test/**/*.md'],
3+
extensions: {
4+
ts: 'module',
5+
},
6+
nodeArguments: ['--import=tsimp'],
7+
};

dist/index.cjs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"use strict";var p=Object.defineProperty;var o=(t,e)=>p(t,"name",{value:e,configurable:!0});var s=require("jszip"),a=require("fs-jetpack"),h=require("micromatch"),y=require("path"),w=require("utimes");const v={output:"./output",preserveDates:!0,preservePath:!0};async function E(t,e={}){const n={...v,...e},c=await a.readAsync(t,"buffer").then(r=>{if(r)return s.loadAsync(r)});if(c===void 0)throw new Error("EBook file could not be read");let u=Object.keys(c.files);n.matching!==void 0&&(u=u.filter(r=>h.isMatch(r,n.matching)));const f=a.dir(n.output??".");for(const r of u){const i=c.file(r);if(i){const l=n.preservePath?r:y.parse(r).base,d=await i.async("nodebuffer");f.write(l,d),n.preserveDates&&await w.utimes(f.path(l),{btime:i.date,mtime:i.date})}}}o(E,"copyFiles");async function b(t){return await a.readAsync(t,"buffer").then(e=>{if(e)return s.loadAsync(e);throw new Error("EBook file could not be read")}).then(e=>Object.keys(e.files))}o(b,"listContents");async function m(t){return await a.readAsync(t,"buffer").then(e=>{if(e)return s.loadAsync(e);throw new Error("EBook file could not be read")}).then(e=>e.files["OEBPS/content.opf"]?.async("string"))}o(m,"parseMeta");async function A(t){return await a.readAsync(t,"buffer").then(e=>{if(e)return s.loadAsync(e);throw new Error("EBook file could not be read")}).then(e=>e.files["OEBPS/toc.ncx"]?.async("string"))}o(A,"parseToc"),exports.copyFiles=E,exports.listContents=b,exports.parseMeta=m,exports.parseToc=A;

dist/index.d.cts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
interface CopyFileOptions {
2+
matching?: string | string[];
3+
output?: string;
4+
preserveDates?: boolean;
5+
preservePath?: boolean;
6+
}
7+
/**
8+
* Copies matching items from the EPUB to real honest-to-god files.
9+
*/
10+
declare function copyFiles(path: string, options?: CopyFileOptions): Promise<void>;
11+
12+
/**
13+
* Returns a list of all the EPUB's internal files.
14+
*/
15+
declare function listContents(path: string): Promise<string[]>;
16+
17+
declare function parseMeta(path: string): Promise<string>;
18+
19+
declare function parseToc(path: string): Promise<string>;
20+
21+
export { type CopyFileOptions, copyFiles, listContents, parseMeta, parseToc };

dist/index.d.mts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
interface CopyFileOptions {
2+
matching?: string | string[];
3+
output?: string;
4+
preserveDates?: boolean;
5+
preservePath?: boolean;
6+
}
7+
/**
8+
* Copies matching items from the EPUB to real honest-to-god files.
9+
*/
10+
declare function copyFiles(path: string, options?: CopyFileOptions): Promise<void>;
11+
12+
/**
13+
* Returns a list of all the EPUB's internal files.
14+
*/
15+
declare function listContents(path: string): Promise<string[]>;
16+
17+
declare function parseMeta(path: string): Promise<string>;
18+
19+
declare function parseToc(path: string): Promise<string>;
20+
21+
export { type CopyFileOptions, copyFiles, listContents, parseMeta, parseToc };

dist/index.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
var d=Object.defineProperty;var n=(e,t)=>d(e,"name",{value:t,configurable:!0});import s from"jszip";import i from"fs-jetpack";import h from"micromatch";import{parse as m}from"path";import{utimes as y}from"utimes";const w={output:"./output",preserveDates:!0,preservePath:!0};async function E(e,t={}){const o={...w,...t},c=await i.readAsync(e,"buffer").then(r=>{if(r)return s.loadAsync(r)});if(c===void 0)throw new Error("EBook file could not be read");let f=Object.keys(c.files);o.matching!==void 0&&(f=f.filter(r=>h.isMatch(r,o.matching)));const u=i.dir(o.output??".");for(const r of f){const a=c.file(r);if(a){const l=o.preservePath?r:m(r).base,p=await a.async("nodebuffer");u.write(l,p),o.preserveDates&&await y(u.path(l),{btime:a.date,mtime:a.date})}}}n(E,"copyFiles");async function b(e){return await i.readAsync(e,"buffer").then(t=>{if(t)return s.loadAsync(t);throw new Error("EBook file could not be read")}).then(t=>Object.keys(t.files))}n(b,"listContents");async function A(e){return await i.readAsync(e,"buffer").then(t=>{if(t)return s.loadAsync(t);throw new Error("EBook file could not be read")}).then(t=>t.files["OEBPS/content.opf"]?.async("string"))}n(A,"parseMeta");async function k(e){return await i.readAsync(e,"buffer").then(t=>{if(t)return s.loadAsync(t);throw new Error("EBook file could not be read")}).then(t=>t.files["OEBPS/toc.ncx"]?.async("string"))}n(k,"parseToc");export{E as copyFiles,b as listContents,A as parseMeta,k as parseToc};

package.json

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"name": "@eatonfyi/dq",
3+
"version": "1.0.0",
4+
"description": "Dancing Queen, the ABA book parser.",
5+
"type": "module",
6+
"main": "./dist/index.cjs",
7+
"module": "./dist/index.mjs",
8+
"types": "./dist/index.d.cts",
9+
"exports": {
10+
"require": {
11+
"types": "./dist/index.d.cts",
12+
"default": "./dist/index.cjs"
13+
},
14+
"import": {
15+
"types": "./dist/index.d.mts",
16+
"default": "./dist/index.mjs"
17+
}
18+
},
19+
"files": [
20+
"/dist",
21+
"README.md"
22+
],
23+
"scripts": {
24+
"build": "shx rm -rf dist; shx rm -rf .tsimp; pkgroll --minify",
25+
"format": "prettier --config prettier.config.js 'src/**/*.ts' --write",
26+
"lint": "eslint .",
27+
"lint-and-fix": "eslint . --fix",
28+
"prepare": "npm run build",
29+
"test": "ava"
30+
},
31+
"author": "eaton",
32+
"license": "MIT",
33+
"repository": {
34+
"type": "git",
35+
"url": "https://github.com/eaton/cover-me.git"
36+
},
37+
"devDependencies": {
38+
"@eslint/js": "^8.57.0",
39+
"@types/jszip": "^3.4.1",
40+
"@types/micromatch": "^4.0.7",
41+
"@types/node": "^20.11.30",
42+
"@types/xml2js": "^0.4.14",
43+
"ava": "^6.1.2",
44+
"eslint": "^8.57.0",
45+
"eslint-config-prettier": "^8.10.0",
46+
"pkgroll": "^2.0.2",
47+
"prettier": "^3.2.5",
48+
"prettier-plugin-organize-imports": "^3.2.4",
49+
"shx": "^0.3.4",
50+
"tsimp": "^2.0.11",
51+
"typescript": "^5.4.5",
52+
"typescript-eslint": "^7.9.0"
53+
},
54+
"dependencies": {
55+
"fs-jetpack": "^5.1.0",
56+
"jszip": "^3.10.1",
57+
"micromatch": "^4.0.7",
58+
"utimes": "^5.2.1",
59+
"xml2js": "^0.6.2",
60+
"zod": "^3.23.8"
61+
}
62+
}

prettier.config.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/** @type {import("prettier").Config} */
2+
3+
const config = {
4+
semi: true,
5+
singleQuote: true,
6+
arrowParens: "avoid",
7+
trailingComma: "all",
8+
plugins: [
9+
"prettier-plugin-organize-imports"
10+
]
11+
}
12+
13+
export default config;

src/copy-files.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import JSZip from 'jszip';
2+
import jetpack from 'fs-jetpack';
3+
import micromatch from 'micromatch';
4+
import { parse as parsePath } from 'path';
5+
import { utimes } from 'utimes';
6+
7+
export interface CopyFileOptions {
8+
matching?: string | string[];
9+
output?: string;
10+
preserveDates?: boolean;
11+
preservePath?: boolean;
12+
}
13+
14+
const defaults: CopyFileOptions = {
15+
output: './output',
16+
preserveDates: true,
17+
preservePath: true,
18+
};
19+
20+
/**
21+
* Copies matching items from the EPUB to real honest-to-god files.
22+
*/
23+
export async function copyFiles(path: string, options: CopyFileOptions = {}) {
24+
const opt = { ...defaults, ...options };
25+
26+
const zip = await jetpack.readAsync(path, 'buffer')
27+
.then(buffer => {
28+
if (buffer) return JSZip.loadAsync(buffer);
29+
});
30+
31+
if (zip === undefined) throw new Error('EBook file could not be read');
32+
33+
let filesToExtract = Object.keys(zip.files);
34+
if (opt.matching !== undefined) {
35+
filesToExtract = filesToExtract.filter(f => micromatch.isMatch(f, opt.matching!));
36+
}
37+
38+
const output = jetpack.dir(opt.output ?? '.');
39+
for (const f of filesToExtract) {
40+
const file = zip.file(f);
41+
if (file) {
42+
const outFile = opt.preservePath ? f : parsePath(f).base;
43+
const buffer = await file.async('nodebuffer');
44+
output.write(outFile, buffer);
45+
if (opt.preserveDates) {
46+
await utimes(output.path(outFile), { btime: file.date, mtime: file.date })
47+
}
48+
}
49+
}
50+
}

src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './copy-files.js';
2+
export * from './list-contents.js';
3+
export * from './parse-meta.js';
4+
export * from './parse-toc.js';

src/list-contents.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import JSZip from 'jszip';
2+
import jetpack from 'fs-jetpack';
3+
4+
/**
5+
* Returns a list of all the EPUB's internal files.
6+
*/
7+
export async function listContents(path: string) {
8+
return await jetpack.readAsync(path, 'buffer')
9+
.then(buffer => {
10+
if (buffer) return JSZip.loadAsync(buffer);
11+
throw new Error('EBook file could not be read');
12+
})
13+
.then(zip => Object.keys(zip.files));
14+
}

src/parse-meta.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import JSZip from 'jszip';
2+
import jetpack from 'fs-jetpack';
3+
4+
export async function parseMeta(path: string) {
5+
return await jetpack.readAsync(path, 'buffer')
6+
.then(buffer => {
7+
if (buffer) return JSZip.loadAsync(buffer);
8+
throw new Error('EBook file could not be read');
9+
})
10+
.then(zip => zip.files['OEBPS/content.opf']?.async('string'));
11+
}

src/parse-toc.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import JSZip from 'jszip';
2+
import jetpack from 'fs-jetpack';
3+
4+
export async function parseToc(path: string) {
5+
return await jetpack.readAsync(path, 'buffer')
6+
.then(buffer => {
7+
if (buffer) return JSZip.loadAsync(buffer);
8+
throw new Error('EBook file could not be read');
9+
})
10+
.then(zip => zip.files['OEBPS/toc.ncx']?.async('string'));
11+
}

test/copy.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import test from 'ava';
2+
import jetpack from 'fs-jetpack';
3+
import { CopyFileOptions, copyFiles } from '../src/copy-files.js';
4+
5+
test('copy files from ebook', async t => {
6+
const file = './test/fixtures/content-strategy.epub';
7+
const output = jetpack.dir('./test/fixtures/copy');
8+
9+
const options: CopyFileOptions = {
10+
output: output.path(),
11+
preserveDates: false,
12+
preservePath: false,
13+
matching: '**/*.jpg'
14+
};
15+
16+
await copyFiles(file, options);
17+
t.not(output.list()?.length, 0);
18+
19+
output.remove();
20+
});
21+
22+
23+
test('copy with path', async t => {
24+
const file = './test/fixtures/content-strategy.epub';
25+
const output = jetpack.dir('./test/fixtures/copy-with-path');
26+
27+
const options: CopyFileOptions = {
28+
output: output.path(),
29+
preserveDates: false,
30+
preservePath: true,
31+
matching: 'OEBPS/toc.ncx'
32+
};
33+
34+
await copyFiles(file, options);
35+
t.assert(output.exists('OEBPS/toc.ncx'));
36+
37+
output.remove();
38+
});
39+
40+
test('copy with date', async t => {
41+
const file = './test/fixtures/content-strategy.epub';
42+
const output = jetpack.dir('./test/fixtures/copy-with-date');
43+
44+
const options: CopyFileOptions = {
45+
output: output.path(),
46+
preserveDates: true,
47+
preservePath: false,
48+
matching: 'OEBPS/toc.ncx'
49+
};
50+
51+
await copyFiles(file, options);
52+
t.assert(output.exists('toc.ncx'));
53+
54+
const fileYear = output.inspect('toc.ncx', { times: true })?.birthTime?.getFullYear();
55+
t.is(fileYear, 2014)
56+
57+
output.remove();
58+
});

test/list.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import test from 'ava';
2+
import { listContents } from '../src/list-contents.js';
3+
4+
test('list ebook contents', async t => {
5+
const file = './test/fixtures/content-strategy.epub';
6+
7+
const fileList = await listContents(file);
8+
t.not(fileList, undefined);
9+
t.is(fileList.length, 102);
10+
t.is(fileList[0], 'mimetype');
11+
t.is(fileList[101], 'OEBPS/toc.xhtml');
12+
});

test/meta.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import test from 'ava';
2+
import { parseMeta } from '../src/parse-meta.js';
3+
4+
test('list ebook contents', async t => {
5+
const file = './test/fixtures/content-strategy.epub';
6+
7+
const metadata = await parseMeta(file);
8+
t.not(metadata, undefined);
9+
10+
t.log(metadata);
11+
});

test/toc.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import test from 'ava';
2+
import { parseToc } from '../src/parse-toc.js';
3+
4+
test('list ebook contents', async t => {
5+
const file = './test/fixtures/content-strategy.epub';
6+
7+
const toc = await parseToc(file);
8+
t.not(toc, undefined);
9+
t.log(toc);
10+
});

tsconfig.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"declaration": true,
4+
"module": "ESNext",
5+
"outDir": "dist",
6+
"rootDir": "src",
7+
"strict": true,
8+
"target": "ESNext",
9+
"lib": ["ESNext"],
10+
"moduleResolution": "Bundler",
11+
"skipLibCheck": true,
12+
"typeRoots": ["./types", "./node_modules/@types"]
13+
},
14+
"include": ["./src/**/*"]
15+
}

0 commit comments

Comments
 (0)