epubavocado
Version:
I am an EPUB object model aspiring to be standards compliant.
600 lines (495 loc) • 14.9 kB
text/typescript
import { Constructor } from './mixins/constructor.js'
import { Entity, EntityConstructor } from './mixins/entity.js'
import { I18n } from './package/mixins/i18n.js'
import { ID } from './package/mixins/id.js'
import { Properties } from './package/mixins/properties.js'
import { Resource } from './mixins/resource.js'
import { Value } from './package/mixins/value.js'
import { Maybe, splitRelAttribute, toArray } from '../util.js'
import { prefixMap, select } from '../xpath.js'
import { ManifestItem } from './package/manifest-item.js'
import { Manifest } from './package/manifest.js'
import { SpineItem } from './package/spine-item.js'
import { Spine } from './package/spine.js'
import {
idFilter,
attributeFilter,
anyPropertiesFilter,
allPropertiesFilter,
anyRelFilter,
allRelFilter,
} from './package/util.js'
export { ManifestItem, Manifest, SpineItem, Spine }
const nodeTypeMap: () => {
[prefix: string]: Constructor<Entity>
} = () => ({
'opf:package': Package,
'opf:metadata': Metadata,
'opf:manifest': Manifest,
'opf:spine': Spine,
'opf:meta': Meta,
'opf:item': ManifestItem,
'opf:itemref': SpineItem,
'opf:link': Link,
// 'opf:collection': Collection,
'dc:identifier': Identifier,
'dc:title': Title,
'dc:language': Language,
'dc:contributor': Contributor,
'dc:coverage': Coverage,
'dc:creator': Creator,
'dc:date': Date,
'dc:description': Description,
'dc:format': Format,
'dc:publisher': Publisher,
'dc:relation': Relation,
'dc:rights': Rights,
'dc:source': Source,
'dc:subject': Subject,
'dc:type': Type,
})
function MetaProperties<TBase extends EntityConstructor>(Base: TBase) {
return class MetaProperties extends Base {
_resolveMetaProperty(property: string, constructor = Meta) {
return toArray(this._resolveMetaPropertyList(property, constructor))[0]
}
_resolveMetaPropertyList(property: string, constructor = Meta) {
const id = ((this as unknown) as Meta).id()
if (!id) {
return []
}
const propertyMap = (this._context as Package).metadata()
?._metaPropertyMap[id]
if (!propertyMap) {
return []
}
const metaNodes = propertyMap[property]
if (!metaNodes) {
return []
}
return metaNodes.map((node: Node) => new constructor(node, this._context))
}
alternateScript() {
return this.alternateScripts()[0]
}
alternateScripts() {
return this._resolveMetaPropertyList('alternate-script')
}
displaySeq() {
return this._resolveMetaProperty('display-seq')
}
fileAs() {
return this._resolveMetaProperty('file-as')
}
groupPosition() {
return this._resolveMetaProperty('group-position')
}
metaAuth() {
return this._resolveMetaProperty('meta-auth')
}
}
}
function MetaAttributes<TBase extends EntityConstructor>(Base: TBase) {
return class MetaAttributes extends Base {
property() {
return this._resolve('./@property')
}
scheme() {
return this._resolve('./@scheme')
}
}
}
function Refines<TBase extends EntityConstructor>(Base: TBase) {
return class Refines extends Base {
refines() {
const refines = this._resolve('./@refines') as string
if (!refines) {
return null
}
// drop the # prefix
const idRefined = refines[0] === '#' ? refines.substr(1) : refines
const node = this._context._select(`//*[@id='${idRefined}']`) as Attr
if (!node) {
return null
}
const name = node.localName
const namespace = node.namespaceURI
if (!namespace) {
return null
}
const prefix = prefixMap[namespace]
const typeConstructor = nodeTypeMap()[`${prefix}:${name}`]
if (!typeConstructor) {
return null
}
return new typeConstructor(node, this._context)
}
}
}
export class Meta extends MetaProperties(
MetaAttributes(Refines(I18n(Value(ID(Entity))))),
) {}
export class Identifier extends Value(MetaProperties(ID(Entity))) {
identifierType(): Maybe<Meta> {
return this._resolveMetaProperty('identifier-type')
}
}
export class Title extends Value(I18n(MetaProperties(ID(Entity)))) {
titleType(): Maybe<Meta> {
return this._resolveMetaProperty('title-type')
}
}
export class Language extends Value(MetaProperties(ID(Entity))) {}
export class Contributor extends Value(I18n(MetaProperties(ID(Entity)))) {
role(): Maybe<Meta> {
return this._resolveMetaProperty('role')
}
}
export class Coverage extends Value(I18n(MetaProperties(ID(Entity)))) {}
export class Creator extends Contributor {}
export class Date extends Value(MetaProperties(ID(Entity))) {}
export class Description extends Value(I18n(MetaProperties(ID(Entity)))) {}
export class Format extends Value(MetaProperties(ID(Entity))) {}
export class Publisher extends Value(I18n(MetaProperties(ID(Entity)))) {}
export class Relation extends Value(I18n(MetaProperties(ID(Entity)))) {}
export class Rights extends Value(I18n(MetaProperties(ID(Entity)))) {}
export class Source extends Identifier {
sourceOf(): Maybe<Meta> {
return this._resolveMetaProperty('source-of')
}
}
export class Subject extends Value(I18n(MetaProperties(ID(Entity)))) {
authority(): Maybe<Meta> {
return this._resolveMetaProperty('authority')
}
term(): Maybe<Meta> {
return this._resolveMetaProperty('term')
}
}
export class Type extends Value(MetaProperties(ID(Entity))) {}
export class BelongsToCollection extends Value(
I18n(Refines(MetaAttributes(MetaProperties(ID(Entity))))),
) {
identifier(): Maybe<Meta> {
return this._resolveMetaProperty('dcterms:identifier')
}
collectionType(): Maybe<Meta> {
return this._resolveMetaProperty('collection-type')
}
belongsToCollection(): Meta {
return this.belongsToCollections()[0]
}
belongsToCollections(): Meta[] {
return this._resolveMetaPropertyList(
'belongs-to-collection',
BelongsToCollection,
)
}
}
export class Link extends Resource(Properties(Refines(ID(Entity)))) {
rel(): Maybe<string> {
return this.rels()[0]
}
rels(): string[] {
const rel = this._resolve('./@rel') as string
if (rel) {
return splitRelAttribute(rel)
}
return []
}
}
export class Metadata extends ID(Entity) {
_metaPropertyMap: {
[idRefined: string]: {
[property: string]: Node[]
}
}
constructor(node: Node, context: Entity) {
super(node, context)
this._metaPropertyMap = {}
const metaRefiningSelected = toArray(
this._selectAll('./opf:meta[@refines and @property]'),
)
metaRefiningSelected.forEach((selectedValue) => {
const node = selectedValue as Node
if (!node) {
return
}
const refinesAttr = select('./@refines', node)
if (!refinesAttr) {
return
}
const refinesValue = (refinesAttr as Attr).value
// drop the # prefix
const idRefined =
refinesValue[0] === '#' ? refinesValue.substr(1) : refinesValue
const propertyAttr = select('./@property', node)
if (!propertyAttr) {
return
}
const property = (propertyAttr as Attr).value
if (!this._metaPropertyMap[idRefined]) {
this._metaPropertyMap[idRefined] = {}
}
if (!this._metaPropertyMap[idRefined][property]) {
this._metaPropertyMap[idRefined][property] = []
}
this._metaPropertyMap[idRefined][property].push(node)
})
}
identifier({ id }: { id?: string }): Maybe<Identifier> {
return this._resolve(`./dc:identifier${idFilter(id)}`, Identifier)
}
modified(): Maybe<Meta> {
const node = this._select(
"./opf:meta[@property='dcterms:modified' and not(@refines)]",
) as Node
if (node) {
return new Meta(node, this._context)
}
}
title({ id }: { id?: string }): Title {
return this.titles({ ids: id ? [id] : [] })[0]
}
titles({ ids }: { ids?: string[] }): Title[] {
return this._resolveAll(`./dc:title${idFilter(ids)}`, Title)
}
language({ id }: { id?: string }): Language {
return this.languages({ ids: id ? [id] : [] })[0]
}
languages({ ids }: { ids?: string[] }): Language[] {
return this._resolveAll(`./dc:language${idFilter(ids)}`, Language)
}
contributor({ id }: { id?: string }): Contributor {
return this.contributors({ ids: id ? [id] : [] })[0]
}
contributors({ ids }: { ids?: string[] }): Contributor[] {
return this._resolveAll(`./dc:contributor${idFilter(ids)}`, Contributor)
}
coverage({ id }: { id?: string }): Coverage {
return this.coverages({ ids: id ? [id] : [] })[0]
}
coverages({ ids }: { ids?: string[] }): Coverage[] {
return this._resolveAll(`./dc:coverage${idFilter(ids)}`, Coverage)
}
creator({ id }: { id?: string }): Creator {
return this.creators({ ids: id ? [id] : [] })[0]
}
creators({ ids }: { ids?: string[] }): Creator[] {
return this._resolveAll(`./dc:creator${idFilter(ids)}`, Creator)
}
date({ id }: { id?: string }): Maybe<Date> {
return this._resolve(`./dc:date${idFilter(id)}`, Date)
}
description({ id }: { id?: string }): Description {
return this.descriptions({ ids: id ? [id] : [] })[0]
}
descriptions({ ids }: { ids?: string[] }): Description[] {
return this._resolveAll(`./dc:description${idFilter(ids)}`, Description)
}
format({ id }: { id?: string }): Format {
return this.formats({ ids: id ? [id] : [] })[0]
}
formats({ ids }: { ids?: string[] }): Format[] {
return this._resolveAll(`./dc:format${idFilter(ids)}`, Format)
}
publisher({ id }: { id?: string }): Publisher {
return this.publishers({ ids: id ? [id] : [] })[0]
}
publishers({ ids }: { ids?: string[] }): Publisher[] {
return this._resolveAll(`./dc:publisher${idFilter(ids)}`, Publisher)
}
relation({ id }: { id?: string }): Relation {
return this.relations({ ids: id ? [id] : [] })[0]
}
relations({ ids }: { ids?: string[] }): Relation[] {
return this._resolveAll(`./dc:relation${idFilter(ids)}`, Relation)
}
rights({ id }: { id?: string }): Rights {
return this.rightses({ ids: id ? [id] : [] })[0]
}
rightses({ ids }: { ids?: string[] }): Rights[] {
return this._resolveAll(`./dc:rights${idFilter(ids)}`, Rights)
}
source({ id }: { id?: string }): Source {
return this.sources({ ids: id ? [id] : [] })[0]
}
sources({ ids }: { ids?: string[] }): Source[] {
return this._resolveAll(`./dc:source${idFilter(ids)}`, Source)
}
subject({ id }: { id?: string }): Subject {
return this.subjects({ ids: id ? [id] : [] })[0]
}
subjects({ ids }: { ids?: string[] }): Subject[] {
return this._resolveAll(`./dc:subject${idFilter(ids)}`, Subject)
}
type({ id }: { id?: string }): Type {
return this.types({ ids: id ? [id] : [] })[0]
}
types({ ids }: { ids?: string[] }): Type[] {
return this._resolveAll(`./dc:type${idFilter(ids)}`, Type)
}
belongsToCollection({ id }: { id?: string }): BelongsToCollection {
return this.belongsToCollections({ ids: id ? [id] : [] })[0]
}
belongsToCollections({
ids,
}: {
ids?: string[]
} = {}): BelongsToCollection[] {
return this._resolveAll(
`./opf:meta${idFilter(
ids,
)}[='belongs-to-collection' and not()]`,
BelongsToCollection,
)
}
meta({
id,
property,
refines,
}: {
id?: string
property?: string
refines?: string
} = {}): Maybe<Meta> {
return this.metas({
ids: id ? [id] : [],
property,
refines,
})[0]
}
metas({
ids,
property,
refines,
}: {
ids?: string[]
property?: string
refines?: string
} = {}): Meta[] {
return this._resolveAll(
`./opf:meta${idFilter(ids)}${attributeFilter(
'@property',
property,
'and',
)}${attributeFilter(
'@refines',
refines ? `#${refines}` : undefined,
'or',
)}`,
Meta,
)
}
link({
id,
href,
anyProperties,
allProperties,
onlyProperties,
anyRel,
allRel,
onlyRel,
}: {
id?: string
href?: string
anyProperties?: string[]
allProperties?: string[]
onlyProperties?: string[]
anyRel?: string[]
allRel?: string[]
onlyRel?: string[]
} = {}): Maybe<Link> {
return this.links({
ids: id ? [id] : [],
href,
anyProperties,
allProperties,
onlyProperties,
anyRel,
allRel,
onlyRel,
})[0]
}
links(
args: {
ids?: string[]
href?: string
anyProperties?: string[]
allProperties?: string[]
onlyProperties?: string[]
anyRel?: string[]
allRel?: string[]
onlyRel?: string[]
} = {},
): Link[] {
const {
ids,
href,
anyProperties,
allProperties,
onlyProperties,
anyRel,
allRel,
onlyRel,
} = args
if (onlyProperties) {
return this.links({
...args,
allProperties: onlyProperties,
onlyProperties: undefined,
}).filter(
(item) => toArray(item.properties()).length === onlyProperties.length,
)
}
if (onlyRel) {
return this.links({
...args,
allRel: onlyRel,
onlyRel: undefined,
}).filter((item) => toArray(item.rels()).length === onlyRel.length)
}
const expression = `./opf:link${idFilter(ids)}${attributeFilter(
'@href',
href,
)}${anyPropertiesFilter(anyProperties)}${allPropertiesFilter(
allProperties,
)}${anyRelFilter(anyRel)}${allRelFilter(allRel)}`
return this._resolveAll(expression, Link)
}
}
export class Package extends I18n(ID(Entity)) {
private _metadata: Maybe<Metadata>
private _spine: Maybe<Spine>
private _manifest: Maybe<Manifest>
constructor(doc: Node) {
super(select('/opf:package', doc) as Node)
}
version(): Maybe<string> {
return this._resolve('./@version')
}
uniqueIdentifier(): Maybe<Identifier> {
const uniqueIdentifierIDRef = this._resolve('./@unique-identifier')
if (uniqueIdentifierIDRef) {
return this.metadata()?.identifier({ id: uniqueIdentifierIDRef })
}
}
releaseIdentifier(): Maybe<string> {
const uniqueIdentifier = this.uniqueIdentifier()
const modified = this.metadata()?.modified()
if (uniqueIdentifier && modified) {
return `${uniqueIdentifier.value()}@${modified.value()}`
}
}
metadata(): Maybe<Metadata> {
return (this._metadata =
this._metadata || this._resolve('./opf:metadata', Metadata))
}
spine(): Maybe<Spine> {
return (this._spine = this._spine || this._resolve('./opf:spine', Spine))
}
manifest(): Maybe<Manifest> {
return (this._manifest =
this._manifest || this._resolve('./opf:manifest', Manifest))
}
}