Source code for zensols.zotsite.screate

"""Generates the the static HTML pages that make up the Zotero exported website.

"""
__author__ = 'Paul Landes'

from typing import Dict
from dataclasses import dataclass, field
import logging
import sys
import json
from pathlib import Path
from io import TextIOBase
import shutil
from zensols.config import Settings, ConfigFactory
from zensols.persist import persisted
from zensols.zotsite import (
    ZoteroApplicationError, DatabaseReader, RegexItemMapper, IdItemMapper,
    Library, Walker,
    NavCreateVisitor, FileSystemCopyVisitor, PruneVisitor, PrintVisitor,
    BetterBibtexVisitor,
)

logger = logging.getLogger(__name__)


[docs] @dataclass class SiteCreator(object): """Creates the Zotero content web site. """ config_factory: ConfigFactory = field() """The configuration factory used to create the :class:`.Walker` instance. """ package: Settings = field() """Containes this Python package information used to create the site metadata. """ site_resource: Path = field() """The (resource) path the static site files.""" db: DatabaseReader = field() """The database access object.""" prune_visitor: PruneVisitor = field() """A visitor that prunes collections based on a regular expression.""" sort_walkers: Settings = field() """A mapping of name to a :class:`.Walker` instance definition configuration section. """ sort: str = field(default='none') """whether or not to sort items, either: ``none`` or ``case`` (non-case might be added later). """ id_mapping: bool = field(default='none') """How to generate unique identifiers for URLS, either ``none``, or `betterbib``. """ file_mapping: str = field(default='item') """Whether to use unique item IDs for the file names or the full PDF file name; either: ``item`` or ``long`` """ out_dir: Path = field(default=None) """The default output directory to store the collection.""" robust_fs: bool = field(default=False) """Whether to raise an exception on file system errors.""" @property @persisted('_walker') def walker(self) -> Walker: walker_class_name = self.sort_walkers.get(self.sort) if logger.isEnabledFor(logging.DEBUG): logger.debug(f'using walker: {walker_class_name}') if walker_class_name is None: raise ZoteroApplicationError( f'Configuration error: no such walker: {self.sort}') return self.config_factory(walker_class_name) @property @persisted('_library') def library(self) -> Library: lib: Library = self.db.get_library() if self.prune_visitor.should_walk: self.walker.walk(lib, self.prune_visitor) if self.id_mapping == 'none': pass elif self.id_mapping == 'betterbib': visitor = BetterBibtexVisitor(lib) self.walker.walk(lib, visitor) else: raise ZoteroApplicationError( f'Unknown ID mapping: {self.id_mapping}') return lib @property @persisted('_item_mapper') def item_mapper(self): if self.file_mapping == 'long': mapper = RegexItemMapper(self.library, r'.*\.pdf$', '[ ]') elif self.file_mapping == 'item': mapper = IdItemMapper(self.library) else: raise ZoteroApplicationError( f'Unknown file mapping: {self.file_mapping}') return mapper
[docs] def print_structure(self, writer: TextIOBase = sys.stdout): """Print (sub)collections and papers in those collections as a tree.""" self.walker.walk(self.library, PrintVisitor(writer))
def _write_meta(self, path: Path): """Write version and other metadata to the website, which is used during rending of the site. """ meta: Dict[str, str] = {'version': self.package.version or '<none>', 'project_name': self.package.name or '<none>'} js: str = f'var zoteroMeta = {json.dumps(meta)};' with open(path, 'w') as f: f.write(js) def _create_tree_data(self): """Create the table of contents/tree info used by the navigation widget. """ js_dir: Path = self.out_dir / 'js' nav_file: Path = js_dir / 'zotero-tree-data.js' if logger.isEnabledFor(logging.INFO): logger.info(f'creating js nav tree: {nav_file}') visitor = NavCreateVisitor(self.library, self.item_mapper) self.walker.walk(self.library, visitor) with open(nav_file, 'w') as f: f.write("var tree =\n") f.write(json.dumps(visitor.primary_roots, indent=2)) meta_file = Path(js_dir, 'zotero-meta.js') if logger.isEnabledFor(logging.INFO): logger.info(f'creating js metadata: {meta_file}') self._write_meta(meta_file) def _copy_storage(self): """Copy the storage contents, which is the location of the PDF (and other) documents that will be rendered in the site GUI. """ dst: Path = self.out_dir fsvisitor = FileSystemCopyVisitor( self.library, dst, self.robust_fs, self.item_mapper) if logger.isEnabledFor(logging.INFO): logger.info(f'copying storage to {dst}') self.walker.walk(self.library, fsvisitor) def _copy_static_res(self, src: Path, dst: Path): """Copy static resources from the distribution package. :param src: the source package directory :param dst: the destination on the file system """ if logger.isEnabledFor(logging.DEBUG): logger.debug(f'copy: {src} -> {dst}') dst.mkdir(parents=True, exist_ok=True) for res in src.iterdir(): res = res.name src_file = src / res dst_file = dst / res if src_file.is_dir(): self._copy_static_res(src_file, dst_file) else: if logger.isEnabledFor(logging.DEBUG): logger.debug(f'copy: {src_file} -> {dst_file}') shutil.copyfile(src_file, dst_file) def _copy_static(self): if logger.isEnabledFor(logging.INFO): logger.info(f'copying static data -> {self.out_dir}') res: Path = self.site_resource if not res.exists(): raise ZoteroApplicationError( f'Missing resource directory {res}') for rpath in res.iterdir(): self._copy_static_res(rpath, self.out_dir)
[docs] def export(self): """Entry point method to export (create) the website. """ self._copy_static() self._create_tree_data() self._copy_storage()
[docs] def tmp(self): print(self.package)