diff --git a/.gitignore b/.gitignore index e3a95437d..09047e5dc 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ pandora/gunicorn_config.py .DS_Store .env overlay/ +pandora/encoding.conf +pandora/tasks.conf diff --git a/Dockerfile b/Dockerfile index a1b287ee5..24e6d63c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM 0x2620/pandora-base:latest +FROM code.0x2620.org/0x2620/pandora-base:latest LABEL maintainer="0x2620@0x2620.org" diff --git a/README.md b/README.md index 044a3cb8d..40cf23f0f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ We recommend to run pan.do/ra inside of LXD or LXC or dedicated VM or server. You will need at least 2GB of free disk space - pan.do/ra is known to work with Ubuntu 18.04, 20.04 and Debian/10 (buster), + pan.do/ra is known to work with Debian/12 (bookworm) and Ubuntu 22.04, other distributions might also work, let us know if it works for you. Use the following commands as root to install pan.do/ra and all dependencies: @@ -16,7 +16,7 @@ cd /root curl -sL https://pan.do/ra-install > pandora_install.sh chmod +x pandora_install.sh -export BRANCH=stable # change to 'master' to get current developement version +export BRANCH=master # change to 'stable' to get the latest release (sometimes outdated) ./pandora_install.sh 2>&1 | tee pandora_install.log ``` @@ -50,4 +50,9 @@ export BRANCH=stable # change to 'master' to get current developement version More info at https://code.0x2620.org/0x2620/pandora/wiki/Customization +## Update + + To update your existing instlalation run + + pandoractl update diff --git a/ctl b/ctl index b78183ba1..6e16fb871 100755 --- a/ctl +++ b/ctl @@ -27,25 +27,30 @@ if [ "$action" = "init" ]; then $SUDO bin/python3 -m pip install -U --ignore-installed "pip<9" fi if [ ! -d static/oxjs ]; then - $SUDO git clone --depth 1 -b $branch https://git.0x2620.org/oxjs.git static/oxjs + $SUDO git clone -b $branch https://code.0x2620.org/0x2620/oxjs.git static/oxjs fi $SUDO mkdir -p src - if [ ! -d src/oxtimelines ]; then - $SUDO git clone --depth 1 -b $branch https://git.0x2620.org/oxtimelines.git src/oxtimelines - fi for package in oxtimelines python-ox; do cd ${BASE} if [ ! -d src/${package} ]; then - $SUDO git clone --depth 1 -b $branch https://git.0x2620.org/${package}.git src/${package} + $SUDO git clone -b $branch https://code.0x2620.org/0x2620/${package}.git src/${package} fi cd ${BASE}/src/${package} - $SUDO ${BASE}/bin/python setup.py develop + + $SUDO ${BASE}/bin/pip install -e . + done cd ${BASE} $SUDO ./bin/pip install -r requirements.txt - if [ ! -e pandora/gunicorn_config.py ]; then - $SUDO cp pandora/gunicorn_config.py.in pandora/gunicorn_config.py - fi + for template in gunicorn_config.py encoding.conf tasks.conf; do + if [ ! -e pandora/$template ]; then + $SUDO cp pandora/${template}.in pandora/$template + fi + done + exit 0 +fi +if [ "$action" = "version" ]; then + git rev-list HEAD --count exit 0 fi @@ -73,10 +78,15 @@ if [ `whoami` != 'root' ]; then exit 1 fi if [ "$action" = "install" ]; then - cd "`dirname "$0"`" + cd "`dirname "$self"`" BASE=`pwd` if [ -x /bin/systemctl ]; then if [ -d /etc/systemd/system/ ]; then + for template in gunicorn_config.py encoding.conf tasks.conf; do + if [ ! -e pandora/$template ]; then + $SUDO cp pandora/${template}.in pandora/$template + fi + done for service in $SERVICES; do if [ -e /lib/systemd/system/${service}.service ]; then rm -f /lib/systemd/system/${service}.service \ diff --git a/docker-compose.yml b/docker-compose.yml index 1360247ca..532a1fda7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,6 @@ services: - "127.0.0.1:2620:80" networks: - backend - - default links: - pandora - websocketd @@ -28,7 +27,7 @@ services: restart: unless-stopped db: - image: postgres:latest + image: postgres:15 networks: - backend env_file: .env diff --git a/docker/base/Dockerfile b/docker/base/Dockerfile index 727891e81..199af6421 100644 --- a/docker/base/Dockerfile +++ b/docker/base/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:buster +FROM debian:12 LABEL maintainer="0x2620@0x2620.org" diff --git a/docker/base/install.sh b/docker/base/install.sh index 2d016eba4..c344d74ca 100755 --- a/docker/base/install.sh +++ b/docker/base/install.sh @@ -1,9 +1,17 @@ #!/bin/bash -UBUNTU_CODENAME=bionic if [ -e /etc/os-release ]; then . /etc/os-release fi +if [ -z "$UBUNTU_CODENAME" ]; then + UBUNTU_CODENAME=bionic +fi +if [ "$VERSION_CODENAME" = "bullseye" ]; then + UBUNTU_CODENAME=focal +fi +if [ "$VERSION_CODENAME" = "bookworm" ]; then + UBUNTU_CODENAME=lunar +fi export DEBIAN_FRONTEND=noninteractive echo 'Acquire::Languages "none";' > /etc/apt/apt.conf.d/99languages @@ -44,7 +52,6 @@ apt-get install -y \ python3-numpy \ python3-psycopg2 \ python3-pyinotify \ - python3-simplejson \ python3-lxml \ python3-cssselect \ python3-html5lib \ @@ -53,7 +60,6 @@ apt-get install -y \ oxframe \ ffmpeg \ mkvtoolnix \ - gpac \ imagemagick \ poppler-utils \ ipython3 \ diff --git a/docker/build.sh b/docker/build.sh index dd661ba1e..fc33b7c8f 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -11,7 +11,7 @@ else proxy= fi -docker build $proxy -t 0x2620/pandora-base base -docker build -t 0x2620/pandora-nginx nginx +docker build $proxy -t code.0x2620.org/0x2620/pandora-base base +docker build -t code.0x2620.org/0x2620/pandora-nginx nginx cd .. -docker build -t 0x2620/pandora . +docker build -t code.0x2620.org/0x2620/pandora . diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 123883f8d..4813b00ab 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -6,7 +6,7 @@ user=pandora export LANG=en_US.UTF-8 mkdir -p /run/pandora -chown -R ${user}.${user} /run/pandora +chown -R ${user}:${user} /run/pandora update="/usr/bin/sudo -u $user -E -H /srv/pandora/update.py" @@ -32,7 +32,7 @@ if [ "$action" = "pandora" ]; then /srv/pandora/pandora/manage.py init_db $update db echo "Generating static files..." - chown -R ${user}.${user} /srv/pandora/ + chown -R ${user}:${user} /srv/pandora/ $update static touch /srv/pandora/initialized fi @@ -52,7 +52,7 @@ if [ "$action" = "encoding" ]; then -A app worker \ -Q encoding -n ${name} \ --pidfile /run/pandora/encoding.pid \ - --maxtasksperchild 500 \ + --max-tasks-per-child 500 \ -c 1 \ -l INFO fi @@ -66,7 +66,7 @@ if [ "$action" = "tasks" ]; then -A app worker \ -Q default,celery -n ${name} \ --pidfile /run/pandora/tasks.pid \ - --maxtasksperchild 1000 \ + --max-tasks-per-child 1000 \ -l INFO fi if [ "$action" = "cron" ]; then @@ -103,9 +103,9 @@ fi # pan.do/ra setup hooks if [ "$action" = "docker-compose.yml" ]; then cat /srv/pandora_base/docker-compose.yml | \ - sed "s#build: \.#image: 0x2620/pandora:latest#g" | \ + sed "s#build: \.#image: code.0x2620.org/0x2620/pandora:latest#g" | \ sed "s#\./overlay:#.:#g" | \ - sed "s#build: docker/nginx#image: 0x2620/pandora-nginx:latest#g" + sed "s#build: docker/nginx#image: code.0x2620.org/0x2620/pandora-nginx:latest#g" exit fi if [ "$action" = ".env" ]; then @@ -131,5 +131,5 @@ echo " docker run 0x2620/pandora setup | sh" echo echo adjust created files to match your needs and run: echo -echo " docker-compose up" +echo " docker compose up" echo diff --git a/docker/publish.sh b/docker/publish.sh index f4ef0193b..2766a4742 100644 --- a/docker/publish.sh +++ b/docker/publish.sh @@ -1,5 +1,5 @@ #!/bin/bash -# push new version of pan.do/ra to docker hub +# push new version of pan.do/ra to code.0x2620.org set -e cd /tmp @@ -7,6 +7,6 @@ git clone https://code.0x2620.org/0x2620/pandora cd pandora ./docker/build.sh -docker push 0x2620/pandora-base:latest -docker push 0x2620/pandora-nginx:latest -docker push 0x2620/pandora:latest +docker push code.0x2620.org/0x2620/pandora-base:latest +docker push code.0x2620.org/0x2620/pandora-nginx:latest +docker push code.0x2620.org/0x2620/pandora:latest diff --git a/docker/setup-docker-compose.sh b/docker/setup-docker-compose.sh index 2f48179f9..fc25efded 100755 --- a/docker/setup-docker-compose.sh +++ b/docker/setup-docker-compose.sh @@ -1,18 +1,18 @@ #!/bin/sh -docker run 0x2620/pandora docker-compose.yml > docker-compose.yml +docker run --rm code.0x2620.org/0x2620/pandora docker-compose.yml > docker-compose.yml if [ ! -e .env ]; then - docker run 0x2620/pandora .env > .env + docker run --rm code.0x2620.org/0x2620/pandora .env > .env echo .env >> .gitignore fi if [ ! -e config.jsonc ]; then - docker run 0x2620/pandora config.jsonc > config.jsonc + docker run --rm code.0x2620.org/0x2620/pandora config.jsonc > config.jsonc fi cat > README.md << EOF pan.do/ra docker instance this folder was created with - docker run 0x2620/pandora setup | sh + docker run --rm code.0x2620.org/0x2620/pandora setup | sh To start pan.do/ra adjust the files in this folder: @@ -22,11 +22,14 @@ To start pan.do/ra adjust the files in this folder: and to get started run this: - docker-compose up -d + docker compose up -d To update pan.do/ra run: - docker-compose run pandora ctl update + docker compose run --rm pandora ctl update +To run pan.do/ra manage shell: + + docker compose run --rm pandora ctl manage shell EOF touch __init__.py diff --git a/etc/nginx/pandora b/etc/nginx/pandora index 419120aaf..db3838a3a 100644 --- a/etc/nginx/pandora +++ b/etc/nginx/pandora @@ -17,10 +17,16 @@ server { #server_name pandora.YOURDOMAIN.COM; listen 80 default; + listen [::]:80 default; access_log /var/log/nginx/pandora.access.log; error_log /var/log/nginx/pandora.error.log; + location /.well-known/acme-challenge/ { + root /var/www/html/; + autoindex off; + } + location /favicon.ico { root /srv/pandora/static; } diff --git a/etc/sudoers.d/pandora b/etc/sudoers.d/pandora new file mode 100644 index 000000000..d05bab77f --- /dev/null +++ b/etc/sudoers.d/pandora @@ -0,0 +1 @@ +pandora ALL=(ALL:ALL) NOPASSWD:/usr/local/bin/pandoractl diff --git a/etc/systemd/system/pandora-cron.service b/etc/systemd/system/pandora-cron.service index 9f1ef157b..7c6b0028d 100644 --- a/etc/systemd/system/pandora-cron.service +++ b/etc/systemd/system/pandora-cron.service @@ -11,7 +11,7 @@ PIDFile=/run/pandora/cron.pid WorkingDirectory=/srv/pandora/pandora ExecStart=/srv/pandora/bin/celery \ -A app beat \ - -s /run/pandora/celerybeat-schedule \ + --scheduler django_celery_beat.schedulers:DatabaseScheduler \ --pidfile /run/pandora/cron.pid \ -l INFO ExecReload=/bin/kill -HUP $MAINPID diff --git a/etc/systemd/system/pandora-encoding.service b/etc/systemd/system/pandora-encoding.service index b2a486262..ee8725ce5 100644 --- a/etc/systemd/system/pandora-encoding.service +++ b/etc/systemd/system/pandora-encoding.service @@ -7,14 +7,16 @@ Type=simple Restart=always User=pandora Group=pandora +EnvironmentFile=/srv/pandora/pandora/encoding.conf PIDFile=/run/pandora/encoding.pid WorkingDirectory=/srv/pandora/pandora ExecStart=/srv/pandora/bin/celery \ -A app worker \ -Q encoding -n pandora-encoding \ --pidfile /run/pandora/encoding.pid \ - --maxtasksperchild 500 \ - -l INFO + -c $CONCURRENCY \ + --max-tasks-per-child $MAX_TASKS_PER_CHILD \ + -l $LOGLEVEL ExecReload=/bin/kill -TERM $MAINPID [Install] diff --git a/etc/systemd/system/pandora-tasks.service b/etc/systemd/system/pandora-tasks.service index 19cf04afa..d1d494207 100644 --- a/etc/systemd/system/pandora-tasks.service +++ b/etc/systemd/system/pandora-tasks.service @@ -7,14 +7,16 @@ Type=simple Restart=always User=pandora Group=pandora +EnvironmentFile=/srv/pandora/pandora/tasks.conf PIDFile=/run/pandora/tasks.pid WorkingDirectory=/srv/pandora/pandora ExecStart=/srv/pandora/bin/celery \ -A app worker \ -Q default,celery -n pandora-default \ --pidfile /run/pandora/tasks.pid \ - --maxtasksperchild 1000 \ - -l INFO + -c $CONCURRENCY \ + --max-tasks-per-child $MAX_TASKS_PER_CHILD \ + -l $LOGLEVEL ExecReload=/bin/kill -TERM $MAINPID [Install] diff --git a/pandora/annotation/apps.py b/pandora/annotation/apps.py new file mode 100644 index 000000000..8b0d41a8d --- /dev/null +++ b/pandora/annotation/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AnnotationConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'annotation' diff --git a/pandora/annotation/management/commands/import_srt.py b/pandora/annotation/management/commands/import_srt.py index 881aa186e..ef309f815 100644 --- a/pandora/annotation/management/commands/import_srt.py +++ b/pandora/annotation/management/commands/import_srt.py @@ -27,6 +27,7 @@ class Command(BaseCommand): parser.add_argument('username', help='username') parser.add_argument('item', help='item') parser.add_argument('layer', help='layer') + parser.add_argument('language', help='language', default="") parser.add_argument('filename', help='filename.srt') def handle(self, *args, **options): @@ -34,6 +35,7 @@ class Command(BaseCommand): public_id = options['item'] layer_id = options['layer'] filename = options['filename'] + language = options.get("language") user = User.objects.get(username=username) item = Item.objects.get(public_id=public_id) @@ -47,6 +49,9 @@ class Command(BaseCommand): for i in range(len(annotations)-1): if annotations[i]['out'] == annotations[i+1]['in']: annotations[i]['out'] = annotations[i]['out'] - 0.001 + if language: + for annotation in annotations: + annotation["value"] = '%s' % (language, annotation["value"]) tasks.add_annotations.delay({ 'item': item.public_id, 'layer': layer_id, diff --git a/pandora/annotation/migrations/0004_alter_annotation_findvalue_alter_annotation_id.py b/pandora/annotation/migrations/0004_alter_annotation_findvalue_alter_annotation_id.py new file mode 100644 index 000000000..b767efee5 --- /dev/null +++ b/pandora/annotation/migrations/0004_alter_annotation_findvalue_alter_annotation_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('annotation', '0003_auto_20160219_1537'), + ] + + operations = [ + migrations.AlterField( + model_name='annotation', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/annotation/models.py b/pandora/annotation/models.py index e1e9d5537..452df4e01 100644 --- a/pandora/annotation/models.py +++ b/pandora/annotation/models.py @@ -74,7 +74,7 @@ def get_matches(obj, model, layer_type, qs=None): name = name.lower() name = ox.decode_html(name) name = unicodedata.normalize('NFKD', name).lower() - if name in value and (exact or re.compile('((^|\s)%s([\.,;:!?\'"\)\]\-\/\s]|$))' % re.escape(name)).findall(value)): + if name in value and (exact or re.compile(r'((^|\s)%s([\.,;:!?\'"\)\]\-\/\s]|$))' % re.escape(name)).findall(value)): matches.append(a.id) break if not matches: @@ -155,7 +155,7 @@ class Annotation(models.Model): self.sortvalue = sortvalue[:900] else: self.sortvalue = None - self.languages = ','.join(re.compile('lang="(.*?)"').findall(self.value)) + self.languages = ','.join(re.compile(r'lang="(.*?)"').findall(self.value)) if not self.languages: self.languages = None else: @@ -163,28 +163,25 @@ class Annotation(models.Model): self.sortvalue = None self.languages = None - with transaction.atomic(): - if not self.clip or self.start != self.clip.start or self.end != self.clip.end: - self.clip, created = Clip.get_or_create(self.item, self.start, self.end) + if not self.clip or self.start != self.clip.start or self.end != self.clip.end: + self.clip, created = Clip.get_or_create(self.item, self.start, self.end) + with transaction.atomic(): if set_public_id: self.set_public_id() super(Annotation, self).save(*args, **kwargs) - if self.clip: - Clip.objects.filter(**{ - 'id': self.clip.id, - self.layer: False - }).update(**{self.layer: True}) - # update clip.findvalue - self.clip.save() + if self.clip: + self.clip.update_findvalue() + setattr(self.clip, self.layer, True) + self.clip.save(update_fields=[self.layer, 'sortvalue', 'findvalue']) - # update matches in bulk if called from load_subtitles - if not delay_matches: - self.update_matches() - self.update_documents() - self.update_translations() + # update matches in bulk if called from load_subtitles + if not delay_matches: + self.update_matches() + self.update_documents() + self.update_translations() def update_matches(self): from place.models import Place @@ -267,7 +264,10 @@ class Annotation(models.Model): from translation.models import Translation layer = self.get_layer() if layer.get('translate'): - Translation.objects.get_or_create(lang=lang, key=self.value, defaults={'type': Translation.CONTENT}) + for lang in settings.CONFIG['languages']: + if lang == settings.CONFIG['language']: + continue + Translation.objects.get_or_create(lang=lang, key=self.value, defaults={'type': Translation.CONTENT}) def delete(self, *args, **kwargs): with transaction.atomic(): diff --git a/pandora/annotation/tasks.py b/pandora/annotation/tasks.py index 8054c6e3e..298695c3c 100644 --- a/pandora/annotation/tasks.py +++ b/pandora/annotation/tasks.py @@ -5,12 +5,12 @@ from django.contrib.auth import get_user_model from django.db import transaction import ox -from celery.task import task +from app.celery import app from .models import Annotation -@task(ignore_results=False, queue='default') +@app.task(ignore_results=False, queue='default') def add_annotations(data): from item.models import Item from entity.models import Entity @@ -51,7 +51,7 @@ def add_annotations(data): annotation.item.update_facets() return True -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_item(id, force=False): from item.models import Item from clip.models import Clip @@ -72,7 +72,7 @@ def update_item(id, force=False): a.item.save() -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_annotations(layers, value): items = {} diff --git a/pandora/annotation/views.py b/pandora/annotation/views.py index 8bfe70060..7db159dfd 100644 --- a/pandora/annotation/views.py +++ b/pandora/annotation/views.py @@ -180,10 +180,10 @@ def addAnnotation(request, data): text='invalid data')) item = get_object_or_404_json(Item, public_id=data['item']) - + layer_id = data['layer'] layer = get_by_id(settings.CONFIG['layers'], layer_id) - if layer['canAddAnnotations'].get(request.user.profile.get_level()): + if layer['canAddAnnotations'].get(request.user.profile.get_level()) or item.editable(request.user): if layer['type'] == 'entity': try: value = Entity.get_by_name(ox.decode_html(data['value']), layer['entity']).get_id() @@ -241,8 +241,7 @@ def addAnnotations(request, data): layer_id = data['layer'] layer = get_by_id(settings.CONFIG['layers'], layer_id) - if item.editable(request.user) \ - and layer['canAddAnnotations'].get(request.user.profile.get_level()): + if item.editable(request.user): response = json_response() data['user'] = request.user.username t = add_annotations.delay(data) diff --git a/pandora/app/apps.py b/pandora/app/apps.py new file mode 100644 index 000000000..792933989 --- /dev/null +++ b/pandora/app/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class AppConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'app' + diff --git a/pandora/app/celery.py b/pandora/app/celery.py index 710d0d0e3..e8cf17fea 100644 --- a/pandora/app/celery.py +++ b/pandora/app/celery.py @@ -6,16 +6,8 @@ root_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__))) root_dir = os.path.dirname(root_dir) os.chdir(root_dir) -# set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') -app = Celery('pandora') - -# Using a string here means the worker doesn't have to serialize -# the configuration object to child processes. -# - namespace='CELERY' means all celery-related configuration keys -# should have a `CELERY_` prefix. +app = Celery('pandora', broker_connection_retry_on_startup=True) app.config_from_object('django.conf:settings', namespace='CELERY') - -# Load task modules from all registered Django app configs. app.autodiscover_tasks() diff --git a/pandora/app/config.py b/pandora/app/config.py index 3e96ac276..672c4a3bc 100644 --- a/pandora/app/config.py +++ b/pandora/app/config.py @@ -133,7 +133,13 @@ def load_config(init=False): added = [] for key in sorted(d): if key not in c: - added.append("\"%s\": %s," % (key, json.dumps(d[key]))) + if key not in ( + 'hidden', + 'find', + 'findDocuments', + 'videoPoints', + ): + added.append("\"%s\": %s," % (key, json.dumps(d[key]))) c[key] = d[key] if added: sys.stderr.write("adding default %s:\n\t" % section) @@ -321,7 +327,11 @@ def update_static(): #locale for f in sorted(glob(os.path.join(settings.STATIC_ROOT, 'json/locale.pandora.*.json'))): with open(f) as fd: - locale = json.load(fd) + try: + locale = json.load(fd) + except: + print("failed to parse %s" % f) + raise site_locale = f.replace('locale.pandora', 'locale.' + settings.CONFIG['site']['id']) locale_file = f.replace('locale.pandora', 'locale') print('write', locale_file) @@ -365,13 +375,3 @@ def update_geoip(force=False): def init(): load_config(True) - -def shutdown(): - if settings.RELOADER_RUNNING: - RUN_RELOADER = False - settings.RELOADER_RUNNING = False - if NOTIFIER: - NOTIFIER.stop() - - - diff --git a/pandora/app/migrations/0002_alter_page_id_alter_settings_id.py b/pandora/app/migrations/0002_alter_page_id_alter_settings_id.py new file mode 100644 index 000000000..985d04c8e --- /dev/null +++ b/pandora/app/migrations/0002_alter_page_id_alter_settings_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='page', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='settings', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/app/oidc.py b/pandora/app/oidc.py new file mode 100644 index 000000000..be3973749 --- /dev/null +++ b/pandora/app/oidc.py @@ -0,0 +1,36 @@ +import unicodedata + +from django.contrib.auth import get_user_model + +import mozilla_django_oidc.auth + +from user.utils import prepare_user + +User = get_user_model() + + +class OIDCAuthenticationBackend(mozilla_django_oidc.auth.OIDCAuthenticationBackend): + def create_user(self, claims): + user = super(OIDCAuthenticationBackend, self).create_user(claims) + username = None + for key in ('preferred_username', 'name'): + if claims.get(key): + username = claims[key] + break + n = 1 + if username and username != user.username: + uname = username + while User.objects.filter(username=uname).exclude(id=user.id).exists(): + n += 1 + uname = '%s (%s)' % (username, n) + user.username = uname + user.save() + prepare_user(user) + return user + + def update_user(self, user, claims): + return user + + +def generate_username(email): + return unicodedata.normalize('NFKC', email)[:150] diff --git a/pandora/app/tasks.py b/pandora/app/tasks.py index 03b267783..51a286e4e 100644 --- a/pandora/app/tasks.py +++ b/pandora/app/tasks.py @@ -2,13 +2,16 @@ import datetime -from celery.task import periodic_task +from app.celery import app from celery.schedules import crontab - -@periodic_task(run_every=crontab(hour=6, minute=0), queue='encoding') +@app.task(queue='encoding') def cron(**kwargs): from django.db import transaction from django.contrib.sessions.models import Session Session.objects.filter(expire_date__lt=datetime.datetime.now()).delete() transaction.commit() + +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(crontab(hour=6, minute=0), cron.s()) diff --git a/pandora/app/views.py b/pandora/app/views.py index 5c76b8ec4..734a53057 100644 --- a/pandora/app/views.py +++ b/pandora/app/views.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- - -import copy from datetime import datetime +import base64 +import copy from django.shortcuts import render, redirect from django.conf import settings @@ -53,17 +53,18 @@ def embed(request, id): }) def redirect_url(request, url): - if request.META['QUERY_STRING']: - url += "?" + request.META['QUERY_STRING'] - + try: + url = base64.decodebytes(url.encode()).decode() + except: + pass if settings.CONFIG['site'].get('sendReferrer', False): return redirect(url) else: - return HttpResponse(''%json.dumps(url)) + return HttpResponse('' % json.dumps(url)) def opensearch_xml(request): osd = ET.Element('OpenSearchDescription') - osd.attrib['xmlns']="http://a9.com/-/spec/opensearch/1.1/" + osd.attrib['xmlns'] = "http://a9.com/-/spec/opensearch/1.1/" e = ET.SubElement(osd, 'ShortName') e.text = settings.SITENAME e = ET.SubElement(osd, 'Description') @@ -162,7 +163,7 @@ def init(request, data): del config['keys'] if 'HTTP_ACCEPT_LANGUAGE' in request.META: - response['data']['locale'] = request.META['HTTP_ACCEPT_LANGUAGE'].split(';')[0].split('-')[0] + response['data']['locale'] = request.META['HTTP_ACCEPT_LANGUAGE'].split(';')[0].split('-')[0].split(',')[0] if request.META.get('HTTP_X_PREFIX') == 'NO': config['site']['videoprefix'] = '' @@ -183,6 +184,7 @@ def init(request, data): except: pass + config['site']['oidc'] = bool(getattr(settings, 'OIDC_RP_CLIENT_ID', False)) response['data']['site'] = config response['data']['user'] = init_user(request.user, request) request.session['last_init'] = str(datetime.now()) @@ -245,7 +247,7 @@ def getEmbedDefaults(request, data): i = qs[0].cache response['data']['item'] = i['id'] response['data']['itemDuration'] = i['duration'] - response['data']['itemRatio'] = i['videoRatio'] + response['data']['itemRatio'] = i.get('videoRatio', settings.CONFIG['video']['previewRatio']) qs = List.objects.exclude(status='private').order_by('name') if qs.exists(): i = qs[0].json() diff --git a/pandora/archive/apps.py b/pandora/archive/apps.py new file mode 100644 index 000000000..1679d490f --- /dev/null +++ b/pandora/archive/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ArchiveConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'archive' + diff --git a/pandora/archive/external.py b/pandora/archive/external.py index cb0e7ef12..7ec598156 100644 --- a/pandora/archive/external.py +++ b/pandora/archive/external.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- import json -import subprocess -import shutil -import tempfile +import logging import os +import shutil +import subprocess +import tempfile import ox from django.conf import settings @@ -14,6 +15,9 @@ from item.tasks import load_subtitles from . import models +logger = logging.getLogger('pandora.' + __name__) + + info_keys = [ 'title', 'description', @@ -36,8 +40,12 @@ info_key_map = { 'display_id': 'id', } +YT_DLP = ['yt-dlp'] +if settings.YT_DLP_EXTRA: + YT_DLP += settings.YT_DLP_EXTRA + def get_info(url, referer=None): - cmd = ['youtube-dl', '-j', '--all-subs', url] + cmd = YT_DLP + ['-j', '--all-subs', url] if referer: cmd += ['--referer', referer] p = subprocess.Popen(cmd, @@ -88,6 +96,15 @@ def add_subtitles(item, media, tmp): sub.selected = True sub.save() +def load_formats(url): + cmd = YT_DLP + ['-q', url, '-j', '-F'] + p = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + formats = stdout.decode().strip().split('\n')[-1] + return json.loads(formats) + def download(item_id, url, referer=None): item = Item.objects.get(public_id=item_id) info = get_info(url, referer) @@ -99,18 +116,19 @@ def download(item_id, url, referer=None): if isinstance(tmp, bytes): tmp = tmp.decode('utf-8') os.chdir(tmp) - cmd = ['youtube-dl', '-q', media['url']] + cmd = YT_DLP + ['-q', media['url']] if referer: cmd += ['--referer', referer] elif 'referer' in media: cmd += ['--referer', media['referer']] + cmd += ['-o', '%(title)80s.%(ext)s'] if settings.CONFIG['video'].get('reuseUpload', False): max_resolution = max(settings.CONFIG['video']['resolutions']) format = settings.CONFIG['video']['formats'][0] if format == 'mp4': cmd += [ - '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio', + '-f', 'bestvideo[height<=%s][ext=mp4]+bestaudio[ext=m4a]' % max_resolution, '--merge-output-format', 'mp4' ] elif format == 'webm': @@ -120,6 +138,50 @@ def download(item_id, url, referer=None): stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) stdout, stderr = p.communicate() + if stderr and b'Requested format is not available.' in stderr: + formats = load_formats(url) + has_audio = bool([fmt for fmt in formats['formats'] if fmt['resolution'] == 'audio only']) + has_video = bool([fmt for fmt in formats['formats'] if 'x' in fmt['resolution']]) + + cmd = YT_DLP + [ + '-q', url, + '-o', '%(title)80s.%(ext)s' + ] + if referer: + cmd += ['--referer', referer] + elif 'referer' in media: + cmd += ['--referer', media['referer']] + if has_video and not has_audio: + cmd += [ + '-f', 'bestvideo[height<=%s][ext=mp4]' % max_resolution, + ] + elif not has_video and has_audio: + cmd += [ + 'bestaudio[ext=m4a]' + ] + else: + cmd = [] + if cmd: + p = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + if stderr and b'Requested format is not available.' in stderr: + cmd = YT_DLP + [ + '-q', url, + '-o', '%(title)80s.%(ext)s' + ] + if referer: + cmd += ['--referer', referer] + elif 'referer' in media: + cmd += ['--referer', media['referer']] + if cmd: + p = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) + stdout, stderr = p.communicate() + if stdout or stderr: + logger.error("import failed:\n%s\n%s\n%s", " ".join(cmd), stdout.decode(), stderr.decode()) parts = list(os.listdir(tmp)) if parts: part = 1 @@ -147,6 +209,7 @@ def download(item_id, url, referer=None): f.extract_stream() status = True else: + logger.error("failed to import %s file already exists %s", url, oshash) status = 'file exists' if len(parts) == 1: add_subtitles(f.item, media, tmp) diff --git a/pandora/archive/extract.py b/pandora/archive/extract.py index ebfd2ae76..7b9f24eaf 100644 --- a/pandora/archive/extract.py +++ b/pandora/archive/extract.py @@ -1,26 +1,33 @@ # -*- coding: utf-8 -*- -import os +from distutils.spawn import find_executable +from glob import glob from os.path import exists import fractions +import logging import math +import os import re import shutil import subprocess import tempfile import time -from distutils.spawn import find_executable -from glob import glob import numpy as np import ox import ox.image from ox.utils import json from django.conf import settings -from PIL import Image +from PIL import Image, ImageOps +import pillow_avif +from pillow_heif import register_heif_opener from .chop import Chop, make_keyframe_index + +register_heif_opener() +logger = logging.getLogger('pandora.' + __name__) + img_extension = 'jpg' MAX_DISTANCE = math.sqrt(3 * pow(255, 2)) @@ -57,14 +64,15 @@ def supported_formats(): stdout = stdout.decode('utf-8') stderr = stderr.decode('utf-8') version = stderr.split('\n')[0].split(' ')[2] + mp4 = 'libx264' in stdout and bool(re.compile('DEA.L. aac').findall(stdout)) return { 'version': version.split('.'), 'ogg': 'libtheora' in stdout and 'libvorbis' in stdout, 'webm': 'libvpx' in stdout and 'libvorbis' in stdout, 'vp8': 'libvpx' in stdout and 'libvorbis' in stdout, 'vp9': 'libvpx-vp9' in stdout and 'libopus' in stdout, - 'mp4': 'libx264' in stdout and bool(re.compile('DEA.L. aac').findall(stdout)), - 'h264': 'libx264' in stdout and bool(re.compile('DEA.L. aac').findall(stdout)), + 'mp4': mp4, + 'h264': mp4, } def stream(video, target, profile, info, audio_track=0, flags={}): @@ -155,7 +163,7 @@ def stream(video, target, profile, info, audio_track=0, flags={}): else: height = 96 - if settings.FFMPEG_SUPPORTS_VP9: + if settings.USE_VP9 and settings.FFMPEG_SUPPORTS_VP9: audio_codec = 'libopus' video_codec = 'libvpx-vp9' @@ -218,7 +226,7 @@ def stream(video, target, profile, info, audio_track=0, flags={}): bitrate = height*width*fps*bpp/1000 video_settings = trim + [ - '-vb', '%dk' % bitrate, + '-b:v', '%dk' % bitrate, '-aspect', aspect, # '-vf', 'yadif', '-max_muxing_queue_size', '512', @@ -246,6 +254,8 @@ def stream(video, target, profile, info, audio_track=0, flags={}): '-level', '4.0', '-pix_fmt', 'yuv420p', ] + if info['video'][0].get("force_framerate"): + video_settings += ['-r:v', str(fps)] video_settings += ['-map', '0:%s,0:0' % info['video'][0]['id']] audio_only = False else: @@ -285,7 +295,7 @@ def stream(video, target, profile, info, audio_track=0, flags={}): ac = min(ac, audiochannels) audio_settings += ['-ac', str(ac)] if audiobitrate: - audio_settings += ['-ab', audiobitrate] + audio_settings += ['-b:a', audiobitrate] if format == 'mp4': audio_settings += ['-c:a', 'aac', '-strict', '-2'] elif audio_codec == 'libopus': @@ -318,14 +328,15 @@ def stream(video, target, profile, info, audio_track=0, flags={}): pass1_post = post[:] pass1_post[-1] = '/dev/null' if format == 'webm': - pass1_post = ['-speed', '4'] + pass1_post + if video_codec != 'libvpx-vp9': + pass1_post = ['-speed', '4'] + pass1_post post = ['-speed', '1'] + post - cmds.append(base + ['-an', '-pass', '1', '-passlogfile', '%s.log' % target] - + video_settings + pass1_post) + cmds.append(base + ['-pass', '1', '-passlogfile', '%s.log' % target] + + video_settings + ['-an'] + pass1_post) cmds.append(base + ['-pass', '2', '-passlogfile', '%s.log' % target] - + audio_settings + video_settings + post) + + video_settings + audio_settings + post) else: - cmds.append(base + audio_settings + video_settings + post) + cmds.append(base + video_settings + audio_settings + post) if settings.FFMPEG_DEBUG: print('\n'.join([' '.join(cmd) for cmd in cmds])) @@ -433,10 +444,15 @@ def frame_direct(video, target, position): r = run_command(cmd) return r == 0 +def open_image_rgb(image_source): + source = Image.open(image_source) + source = ImageOps.exif_transpose(source) + source = source.convert('RGB') + return source def resize_image(image_source, image_output, width=None, size=None): if exists(image_source): - source = Image.open(image_source).convert('RGB') + source = open_image_rgb(image_source) source_width = source.size[0] source_height = source.size[1] if size: @@ -457,7 +473,7 @@ def resize_image(image_source, image_output, width=None, size=None): height = max(height, 1) if width < source_width: - resize_method = Image.ANTIALIAS + resize_method = Image.LANCZOS else: resize_method = Image.BICUBIC output = source.resize((width, height), resize_method) @@ -471,7 +487,7 @@ def timeline(video, prefix, modes=None, size=None): size = [64, 16] if isinstance(video, str): video = [video] - cmd = ['../bin/oxtimelines', + cmd = [os.path.normpath(os.path.join(settings.BASE_DIR, '../bin/oxtimelines')), '-s', ','.join(map(str, reversed(sorted(size)))), '-m', ','.join(modes), '-o', prefix, @@ -603,7 +619,7 @@ def timeline_strip(item, cuts, info, prefix): print(frame, 'cut', c, 'frame', s, frame, 'width', widths[s], box) # FIXME: why does this have to be frame+1? frame_image = Image.open(item.frame((frame+1)/fps)) - frame_image = frame_image.crop(box).resize((widths[s], timeline_height), Image.ANTIALIAS) + frame_image = frame_image.crop(box).resize((widths[s], timeline_height), Image.LANCZOS) for x_ in range(widths[s]): line_image.append(frame_image.crop((x_, 0, x_ + 1, timeline_height))) frame += widths[s] @@ -731,19 +747,24 @@ def remux_stream(src, dst): cmd = [ settings.FFMPEG, '-nostats', '-loglevel', 'error', - '-map_metadata', '-1', '-sn', '-i', src, + '-map_metadata', '-1', '-sn', ] + video + [ ] + audio + [ '-movflags', '+faststart', dst ] + print(cmd) p = subprocess.Popen(cmd, stdin=subprocess.PIPE, - stdout=open('/dev/null', 'w'), - stderr=open('/dev/null', 'w'), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, close_fds=True) - p.wait() - return True, None + stdout, stderr = p.communicate() + if stderr: + logger.error("failed to remux %s %s", cmd, stderr) + return False, stderr + else: + return True, None def ffprobe(path, *args): diff --git a/pandora/archive/migrations/0006_alter_file_extension_alter_file_id_alter_file_info_and_more.py b/pandora/archive/migrations/0006_alter_file_extension_alter_file_id_alter_file_info_and_more.py new file mode 100644 index 000000000..a890a3b9a --- /dev/null +++ b/pandora/archive/migrations/0006_alter_file_extension_alter_file_id_alter_file_info_and_more.py @@ -0,0 +1,100 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0005_auto_20180804_1554'), + ] + + operations = [ + migrations.AlterField( + model_name='file', + name='extension', + field=models.CharField(default='', max_length=255, null=True), + ), + migrations.AlterField( + model_name='file', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='file', + name='info', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='file', + name='language', + field=models.CharField(default='', max_length=255, null=True), + ), + migrations.AlterField( + model_name='file', + name='part', + field=models.CharField(default='', max_length=255, null=True), + ), + migrations.AlterField( + model_name='file', + name='part_title', + field=models.CharField(default='', max_length=255, null=True), + ), + migrations.AlterField( + model_name='file', + name='path', + field=models.CharField(default='', max_length=2048), + ), + migrations.AlterField( + model_name='file', + name='sort_path', + field=models.CharField(default='', max_length=2048), + ), + migrations.AlterField( + model_name='file', + name='type', + field=models.CharField(default='', max_length=255), + ), + migrations.AlterField( + model_name='file', + name='version', + field=models.CharField(default='', max_length=255, null=True), + ), + migrations.AlterField( + model_name='frame', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='instance', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='stream', + name='error', + field=models.TextField(blank=True, default=''), + ), + migrations.AlterField( + model_name='stream', + name='format', + field=models.CharField(default='webm', max_length=255), + ), + migrations.AlterField( + model_name='stream', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='stream', + name='info', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='volume', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/archive/migrations/0007_stream_archive_str_file_id_69a542_idx.py b/pandora/archive/migrations/0007_stream_archive_str_file_id_69a542_idx.py new file mode 100644 index 000000000..4a8bb7e2f --- /dev/null +++ b/pandora/archive/migrations/0007_stream_archive_str_file_id_69a542_idx.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.3 on 2023-08-18 12:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0006_alter_file_extension_alter_file_id_alter_file_info_and_more'), + ] + + operations = [ + migrations.AddIndex( + model_name='stream', + index=models.Index(fields=['file', 'source', 'available'], name='archive_str_file_id_69a542_idx'), + ), + ] diff --git a/pandora/archive/migrations/0008_file_filename_file_folder.py b/pandora/archive/migrations/0008_file_filename_file_folder.py new file mode 100644 index 000000000..5c8b34daf --- /dev/null +++ b/pandora/archive/migrations/0008_file_filename_file_folder.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.7 on 2025-01-23 10:48 + +from django.db import migrations, models + +def update_path(apps, schema_editor): + File = apps.get_model("archive", "File") + for file in File.objects.all(): + if file.path: + parts = file.path.split('/') + file.filename = parts.pop() + file.folder = '/'.join(parts) + file.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('archive', '0007_stream_archive_str_file_id_69a542_idx'), + ] + + operations = [ + migrations.AddField( + model_name='file', + name='filename', + field=models.CharField(default='', max_length=2048), + ), + migrations.AddField( + model_name='file', + name='folder', + field=models.CharField(default='', max_length=2048), + ), + migrations.RunPython(update_path), + ] diff --git a/pandora/archive/models.py b/pandora/archive/models.py index 56080815d..476de4c45 100644 --- a/pandora/archive/models.py +++ b/pandora/archive/models.py @@ -53,6 +53,9 @@ class File(models.Model): path = models.CharField(max_length=2048, default="") # canoncial path/file sort_path = models.CharField(max_length=2048, default="") # sort name + folder = models.CharField(max_length=2048, default="") + filename = models.CharField(max_length=2048, default="") + type = models.CharField(default="", max_length=255) # editable @@ -151,8 +154,10 @@ class File(models.Model): self.sampleate = 0 self.channels = 0 - if self.framerate: + if self.framerate and self.duration > 0: self.pixels = int(self.width * self.height * float(utils.parse_decimal(self.framerate)) * self.duration) + else: + self.pixels = 0 def get_path_info(self): data = {} @@ -181,10 +186,24 @@ class File(models.Model): for type in ox.movie.EXTENSIONS: if data['extension'] in ox.movie.EXTENSIONS[type]: data['type'] = type + if data['extension'] == 'ogg' and self.info.get('video'): + data['type'] = 'video' + if data['type'] == 'unknown': + if self.info.get('video'): + data['type'] = 'video' + elif self.info.get('audio'): + data['type'] = 'audio' if 'part' in data and isinstance(data['part'], int): data['part'] = str(data['part']) return data + def update_path(self): + path = self.normalize_path() + parts = path.split('/') + self.filename = parts.pop() + self.folder = '/'.join(parts) + return path + def normalize_path(self): # FIXME: always use format_path if settings.CONFIG['site']['folderdepth'] == 4: @@ -248,7 +267,7 @@ class File(models.Model): update_path = False if self.info: if self.id: - self.path = self.normalize_path() + self.path = self.update_path() else: update_path = True if self.item: @@ -268,7 +287,7 @@ class File(models.Model): if self.type not in ('audio', 'video'): self.duration = None - else: + elif self.id: duration = sum([s.info.get('duration', 0) for s in self.streams.filter(source=None)]) if duration: @@ -276,12 +295,12 @@ class File(models.Model): if self.is_subtitle: self.available = self.data and True or False - else: + elif self.id: self.available = not self.uploading and \ self.streams.filter(source=None, available=True).count() super(File, self).save(*args, **kwargs) if update_path: - self.path = self.normalize_path() + self.path = self.update_path() super(File, self).save(*args, **kwargs) def get_path(self, name): @@ -365,8 +384,8 @@ class File(models.Model): self.info.update(stream.info) self.parse_info() self.save() - if stream.info.get('video'): - extract.make_keyframe_index(stream.media.path) + #if stream.info.get('video'): + # extract.make_keyframe_index(stream.media.path) return True, stream.media.size return save_chunk(stream, stream.media, chunk, offset, name, done_cb) return False, 0 @@ -395,7 +414,7 @@ class File(models.Model): config = settings.CONFIG['video'] height = self.info['video'][0]['height'] if self.info.get('video') else None max_resolution = max(config['resolutions']) - if height <= max_resolution and self.extension in ('mov', 'mkv', 'mp4', 'm4v'): + if height and height <= max_resolution and self.extension in ('mov', 'mkv', 'mp4', 'm4v'): vcodec = self.get_codec('video') acodec = self.get_codec('audio') if vcodec in self.MP4_VCODECS and acodec in self.MP4_ACODECS: @@ -406,7 +425,7 @@ class File(models.Model): config = settings.CONFIG['video'] height = self.info['video'][0]['height'] if self.info.get('video') else None max_resolution = max(config['resolutions']) - if height <= max_resolution and config['formats'][0] == self.extension: + if height and height <= max_resolution and config['formats'][0] == self.extension: vcodec = self.get_codec('video') acodec = self.get_codec('audio') if self.extension in ['mp4', 'm4v'] and vcodec in self.MP4_VCODECS and acodec in self.MP4_ACODECS: @@ -465,6 +484,9 @@ class File(models.Model): 'videoCodec': self.video_codec, 'wanted': self.wanted, } + for key in ('folder', 'filename'): + if keys and key in keys: + data[key] = getattr(self, key) if error: data['error'] = error for key in self.PATH_INFO: @@ -485,7 +507,7 @@ class File(models.Model): self.item.groups.filter(id__in=user.groups.all()).count() > 0 if 'instances' in data and 'filename' in self.info and self.data: data['instances'].append({ - 'ignore': False, + 'ignore': not self.selected, 'path': self.info['filename'], 'user': self.item.user.username if self.item and self.item.user else 'system', 'volume': 'Direct Upload' @@ -531,7 +553,7 @@ class File(models.Model): def process_stream(self): ''' - extract derivatives from webm upload + extract derivatives from stream upload ''' from . import tasks return tasks.process_stream.delay(self.id) @@ -725,6 +747,9 @@ class Stream(models.Model): class Meta: unique_together = ("file", "resolution", "format") + indexes = [ + models.Index(fields=['file', 'source', 'available']) + ] file = models.ForeignKey(File, related_name='streams', on_delete=models.CASCADE) resolution = models.IntegerField(default=96) @@ -804,9 +829,15 @@ class Stream(models.Model): shutil.move(self.file.data.path, target) self.file.data.name = '' self.file.save() + self.available = True + self.save() + done = True elif self.file.can_remux(): ok, error = extract.remux_stream(media, target) - done = True + if ok: + self.available = True + self.save() + done = True if not done: ok, error = extract.stream(media, target, self.name(), info, flags=self.flags) @@ -814,7 +845,7 @@ class Stream(models.Model): # get current version from db and update try: self.refresh_from_db() - except archive.models.DoesNotExist: + except Stream.DoesNotExist: pass else: self.update_status(ok, error) diff --git a/pandora/archive/queue.py b/pandora/archive/queue.py index 8a8ff4ceb..7cb768b2b 100644 --- a/pandora/archive/queue.py +++ b/pandora/archive/queue.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- from datetime import datetime -from time import time - -import celery.task.control -import kombu.five +from time import time, monotonic +from app.celery import app from .models import File @@ -18,7 +16,7 @@ def parse_job(job): 'file': f.oshash } if job['time_start']: - start_time = datetime.fromtimestamp(time() - (kombu.five.monotonic() - job['time_start'])) + start_time = datetime.fromtimestamp(time() - (monotonic() - job['time_start'])) r.update({ 'started': start_time, 'running': (datetime.now() - start_time).total_seconds() @@ -30,7 +28,7 @@ def parse_job(job): def status(): status = [] encoding_jobs = ('archive.tasks.extract_stream', 'archive.tasks.process_stream') - c = celery.task.control.inspect() + c = app.control.inspect() for job in c.active(safe=True).get('celery@pandora-encoding', []): if job['name'] in encoding_jobs: status.append(parse_job(job)) @@ -67,7 +65,7 @@ def fill_queue(): def get_celery_worker_status(): ERROR_KEY = "ERROR" try: - insp = celery.task.control.inspect() + insp = app.control.inspect() d = insp.stats() if not d: d = {ERROR_KEY: 'No running Celery workers were found.'} diff --git a/pandora/archive/tasks.py b/pandora/archive/tasks.py index a2aa3d8ea..9e061b385 100644 --- a/pandora/archive/tasks.py +++ b/pandora/archive/tasks.py @@ -2,13 +2,14 @@ from glob import glob -from celery.task import task from django.conf import settings +from django.db import transaction from django.db.models import Q from item.models import Item from item.tasks import update_poster, update_timeline from taskqueue.models import Task +from app.celery import app from . import models from . import extract @@ -68,7 +69,7 @@ def update_or_create_instance(volume, f): instance.file.item.update_wanted() return instance -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_files(user, volume, files): user = models.User.objects.get(username=user) volume, created = models.Volume.objects.get_or_create(user=user, name=volume) @@ -100,7 +101,7 @@ def update_files(user, volume, files): Task.start(i, user) update_timeline.delay(i.public_id) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_info(user, info): user = models.User.objects.get(username=user) files = models.File.objects.filter(oshash__in=list(info)) @@ -114,7 +115,7 @@ def update_info(user, info): Task.start(i, user) update_timeline.delay(i.public_id) -@task(queue="encoding") +@app.task(queue="encoding") def process_stream(fileId): ''' process uploaded stream @@ -140,7 +141,7 @@ def process_stream(fileId): Task.finish(file.item) return True -@task(queue="encoding") +@app.task(queue="encoding") def extract_stream(fileId): ''' extract stream from direct upload @@ -169,7 +170,7 @@ def extract_stream(fileId): models.File.objects.filter(id=fileId).update(encoding=False) Task.finish(file.item) -@task(queue="encoding") +@app.task(queue="encoding") def extract_derivatives(fileId, rebuild=False): file = models.File.objects.get(id=fileId) streams = file.streams.filter(source=None) @@ -177,7 +178,7 @@ def extract_derivatives(fileId, rebuild=False): streams[0].extract_derivatives(rebuild) return True -@task(queue="encoding") +@app.task(queue="encoding") def update_stream(id): s = models.Stream.objects.get(pk=id) if not glob("%s*" % s.timeline_prefix): @@ -199,11 +200,11 @@ def update_stream(id): c.update_calculated_values() c.save() -@task(queue="encoding") +@app.task(queue="encoding") def download_media(item_id, url, referer=None): return external.download(item_id, url, referer) -@task(queue='default') +@app.task(queue='default') def move_media(data, user): from changelog.models import add_changelog from item.models import get_item, Item, ItemSort @@ -248,7 +249,8 @@ def move_media(data, user): if old_item and old_item.files.count() == 0 and i.files.count() == len(data['ids']): for a in old_item.annotations.all().order_by('id'): a.item = i - a.set_public_id() + with transaction.atomic(): + a.set_public_id() Annotation.objects.filter(id=a.id).update(item=i, public_id=a.public_id) old_item.clips.all().update(item=i, sort=i.sort) diff --git a/pandora/archive/views.py b/pandora/archive/views.py index 308e7c109..615a2db82 100644 --- a/pandora/archive/views.py +++ b/pandora/archive/views.py @@ -103,7 +103,7 @@ def update(request, data): file__available=False, file__wanted=True)] - if list(filter(lambda l: l['id'] == 'subtitles', settings.CONFIG['layers'])): + if utils.get_by_key(settings.CONFIG['layers'], 'isSubtitles', True): qs = files.filter( file__is_subtitle=True, file__available=False diff --git a/pandora/changelog/apps.py b/pandora/changelog/apps.py new file mode 100644 index 000000000..554ba51d4 --- /dev/null +++ b/pandora/changelog/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ChangelogConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'changelog' + diff --git a/pandora/changelog/migrations/0003_alter_changelog_id_alter_changelog_value_and_more.py b/pandora/changelog/migrations/0003_alter_changelog_id_alter_changelog_value_and_more.py new file mode 100644 index 000000000..e8aacfaa5 --- /dev/null +++ b/pandora/changelog/migrations/0003_alter_changelog_id_alter_changelog_value_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('changelog', '0002_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='changelog', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='changelog', + name='value', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='log', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='log', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/clip/apps.py b/pandora/clip/apps.py new file mode 100644 index 000000000..c957657fb --- /dev/null +++ b/pandora/clip/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ClipConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'clip' + diff --git a/pandora/clip/managers.py b/pandora/clip/managers.py index eb7c6ca92..45e492b75 100644 --- a/pandora/clip/managers.py +++ b/pandora/clip/managers.py @@ -17,6 +17,7 @@ keymap = { 'place': 'annotations__places__id', 'text': 'findvalue', 'annotations': 'findvalue', + 'layer': 'annotations__layer', 'user': 'annotations__user__username', } case_insensitive_keys = ('annotations__user__username', ) diff --git a/pandora/clip/migrations/0004_id_bigint.py b/pandora/clip/migrations/0004_id_bigint.py new file mode 100644 index 000000000..b0415b967 --- /dev/null +++ b/pandora/clip/migrations/0004_id_bigint.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('clip', '0003_auto_20160219_1805'), + ] + + operations = [ + migrations.AlterField( + model_name='clip', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/clip/models.py b/pandora/clip/models.py index 9736adb10..406f13dff 100644 --- a/pandora/clip/models.py +++ b/pandora/clip/models.py @@ -8,6 +8,7 @@ import ox from archive import extract from . import managers +from .utils import add_cuts def get_layers(item, interval=None, user=None): @@ -59,9 +60,7 @@ class MetaClip(object): self.hue = self.saturation = self.lightness = 0 self.volume = 0 - def save(self, *args, **kwargs): - if self.duration != self.end - self.start: - self.update_calculated_values() + def update_findvalue(self): if not self.aspect_ratio and self.item: streams = self.item.streams() if streams: @@ -89,6 +88,11 @@ class MetaClip(object): self.findvalue = '\n'.join(list(filter(None, [a.findvalue for a in anns]))) for l in [k['id'] for k in settings.CONFIG['layers']]: setattr(self, l, l in anns_by_layer and bool(len(anns_by_layer[l]))) + + def save(self, *args, **kwargs): + if self.duration != self.end - self.start: + self.update_calculated_values() + self.update_findvalue() models.Model.save(self, *args, **kwargs) clip_keys = ('id', 'in', 'out', 'position', 'created', 'modified', @@ -111,8 +115,7 @@ class MetaClip(object): del j[key] #needed here to make item find with clips work if 'annotations' in keys: - #annotations = self.annotations.filter(layer__in=settings.CONFIG['clipLayers']) - annotations = self.annotations.all() + annotations = self.annotations.all().exclude(value='') if qs: for q in qs: annotations = annotations.filter(q) @@ -150,12 +153,12 @@ class MetaClip(object): data['annotation'] = qs[0].public_id data['parts'] = self.item.cache['parts'] data['durations'] = self.item.cache['durations'] - for key in ('title', 'director', 'year', 'videoRatio'): + for key in settings.CONFIG['itemTitleKeys'] + ['videoRatio']: value = self.item.cache.get(key) if value: data[key] = value data['duration'] = data['out'] - data['in'] - data['cuts'] = tuple([c for c in self.item.get('cuts', []) if c > self.start and c < self.end]) + add_cuts(data, self.item, self.start, self.end) data['layers'] = self.get_layers(user) data['streams'] = [s.file.oshash for s in self.item.streams()] return data @@ -186,6 +189,7 @@ class MetaClip(object): def __str__(self): return self.public_id + class Meta: unique_together = ("item", "start", "end") diff --git a/pandora/clip/utils.py b/pandora/clip/utils.py new file mode 100644 index 000000000..7d6d03fe5 --- /dev/null +++ b/pandora/clip/utils.py @@ -0,0 +1,22 @@ + + +def add_cuts(data, item, start, end): + cuts = [] + last = False + outer = [] + first = 0 + for cut in item.get('cuts', []): + if cut > start and cut < end: + if not cuts: + outer.append(first) + cuts.append(cut) + last = True + elif cut <= start: + first = cut + elif cut >= end: + if not len(outer): + outer.append(first) + if len(outer) == 1: + outer.append(cut) + data['cuts'] = tuple(cuts) + data['outerCuts'] = tuple(outer) diff --git a/pandora/config.0xdb.jsonc b/pandora/config.0xdb.jsonc index 8440de9a3..0d4b1aaea 100644 --- a/pandora/config.0xdb.jsonc +++ b/pandora/config.0xdb.jsonc @@ -56,6 +56,7 @@ "canExportAnnotations": {"friend": true, "staff": true, "admin": true}, "canImportAnnotations": {"staff": true, "admin": true}, "canImportItems": {}, + "canTranscribeAudio": {}, "canManageDocuments": {"staff": true, "admin": true}, "canManageEntities": {"staff": true, "admin": true}, "canManageHome": {}, @@ -1009,7 +1010,7 @@ { "id": "tags", "title": "Tags", - "canAddAnnotations": {"member": true, "staff": true, "admin": true}, + "canAddAnnotations": {"member": true, "friend": true, "staff": true, "admin": true}, "item": "Tag", "autocomplete": true, "overlap": true, @@ -1399,11 +1400,9 @@ corner of the screen "resolutions": List of video resolutions. Supported values are 96, 144, 240, 288, 360, 432, 480, 720 and 1080. - "torrent": If true, video downloads are offered via BitTorrent */ "video": { - "torrent": false, - "formats": ["webm", "mp4"], + "formats": ["mp4"], // fixme: this should be named "ratio" or "defaultRatio", // as it also applies to clip lists (on load) "previewRatio": 1.7777777778, diff --git a/pandora/config.indiancinema.jsonc b/pandora/config.indiancinema.jsonc index 786a53d46..518d55054 100644 --- a/pandora/config.indiancinema.jsonc +++ b/pandora/config.indiancinema.jsonc @@ -38,7 +38,7 @@ "capabilities": { "canAddItems": {"researcher": true, "staff": true, "admin": true}, "canAddDocuments": {"researcher": true, "staff": true, "admin": true}, - "canDownloadVideo": {"guest": -1, "member": -1, "researcher": 3, "staff": 3, "admin": 3}, + "canDownloadVideo": {"guest": -1, "member": 1, "researcher": 3, "staff": 3, "admin": 3}, "canDownloadSource": {"guest": -1, "member": -1, "researcher": -1, "staff": -1, "admin": -1}, "canEditAnnotations": {"staff": true, "admin": true}, "canEditDocuments": {"researcher": true, "staff": true, "admin": true}, @@ -58,6 +58,7 @@ "canImportAnnotations": {"researcher": true, "staff": true, "admin": true}, // import needs to handle itemRequiresVideo=false first "canImportItems": {}, + "canTranscribeAudio": {}, "canManageDocuments": {"member": true, "researcher": true, "staff": true, "admin": true}, "canManageEntities": {"member": true, "researcher": true, "staff": true, "admin": true}, "canManageHome": {"staff": true, "admin": true}, @@ -73,13 +74,14 @@ "canSeeAccessed": {"researcher": true, "staff": true, "admin": true}, "canSeeAllTasks": {"staff": true, "admin": true}, "canSeeDebugMenu": {"researcher": true, "staff": true, "admin": true}, - "canSeeExtraItemViews": {"researcher": true, "staff": true, "admin": true}, - "canSeeMedia": {"researcher": true, "staff": true, "admin": true}, "canSeeDocument": {"guest": 1, "member": 1, "researcher": 2, "staff": 3, "admin": 3}, + "canSeeExtraItemViews": {"researcher": true, "staff": true, "admin": true}, "canSeeItem": {"guest": 2, "member": 2, "researcher": 2, "staff": 3, "admin": 3}, + "canSeeMedia": {"researcher": true, "staff": true, "admin": true}, "canSeeSize": {"researcher": true, "staff": true, "admin": true}, "canSeeSoftwareVersion": {"researcher": true, "staff": true, "admin": true}, - "canSendMail": {"staff": true, "admin": true} + "canSendMail": {"staff": true, "admin": true}, + "canShare": {"staff": true, "admin": true} }, /* "clipKeys" are the properties that clips can be sorted by (the values are @@ -312,6 +314,14 @@ "autocomplete": true, "columnWidth": 128 }, + { + "id": "fulltext", + "operator": "+", + "title": "Fulltext", + "type": "text", + "fulltext": true, + "find": true + }, { "id": "created", "operator": "-", @@ -1494,6 +1504,7 @@ "hasEvents": true, "hasPlaces": true, "item": "Keyword", + "autocomplete": true, "overlap": true, "type": "string" }, @@ -1875,11 +1886,9 @@ corner of the screen "resolutions": List of video resolutions. Supported values are 96, 144, 240, 288, 360, 432, 480, 720 and 1080. - "torrent": If true, video downloads are offered via BitTorrent */ "video": { - "torrent": false, - "formats": ["webm", "mp4"], + "formats": ["mp4"], "previewRatio": 1.375, "resolutions": [240, 480] } diff --git a/pandora/config.padma.jsonc b/pandora/config.padma.jsonc index c78740a90..a3e419881 100644 --- a/pandora/config.padma.jsonc +++ b/pandora/config.padma.jsonc @@ -56,6 +56,7 @@ "canExportAnnotations": {"member": true, "staff": true, "admin": true}, "canImportAnnotations": {"member": true, "staff": true, "admin": true}, "canImportItems": {"member": true, "staff": true, "admin": true}, + "canTranscribeAudio": {"staff": true, "admin": true}, "canManageDocuments": {"member": true, "staff": true, "admin": true}, "canManageEntities": {"member": true, "staff": true, "admin": true}, "canManageHome": {"staff": true, "admin": true}, @@ -71,13 +72,14 @@ "canSeeAccessed": {"staff": true, "admin": true}, "canSeeAllTasks": {"staff": true, "admin": true}, "canSeeDebugMenu": {"staff": true, "admin": true}, - "canSeeExtraItemViews": {"staff": true, "admin": true}, - "canSeeMedia": {"staff": true, "admin": true}, "canSeeDocument": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, + "canSeeExtraItemViews": {"staff": true, "admin": true}, "canSeeItem": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, + "canSeeMedia": {"staff": true, "admin": true}, "canSeeSize": {"staff": true, "admin": true}, "canSeeSoftwareVersion": {"staff": true, "admin": true}, - "canSendMail": {"staff": true, "admin": true} + "canSendMail": {"staff": true, "admin": true}, + "canShare": {"staff": true, "admin": true} }, /* "clipKeys" are the properties that clips can be sorted by (the values are @@ -246,6 +248,28 @@ "filter": true, "find": true }, + { + "id": "source", + "title": "Source", + "type": "string", + "autocomplete": true, + "description": true, + "columnWidth": 180, + "filter": true, + "find": true, + "sort": true + }, + { + "id": "project", + "title": "Project", + "type": "string", + "autocomplete": true, + "description": true, + "columnWidth": 120, + "filter": true, + "find": true, + "sort": true + }, { "id": "id", "operator": "+", @@ -291,6 +315,24 @@ "sort": true, "columnWidth": 256 }, + { + "id": "content", + "operator": "+", + "title": "Content", + "type": "text", + "find": true, + "sort": true, + "columnWidth": 256 + }, + { + "id": "translation", + "operator": "+", + "title": "Translation", + "type": "text", + "find": true, + "sort": true, + "columnWidth": 256 + }, { "id": "matches", "operator": "-", @@ -310,6 +352,20 @@ "autocomplete": true, "columnWidth": 128 }, + { + "id": "notes", + "title": "Notes", + "type": "text", + "capability": "canEditMetadata" + }, + { + "id": "fulltext", + "operator": "+", + "title": "Fulltext", + "type": "text", + "fulltext": true, + "find": true + }, { "id": "created", "operator": "-", @@ -545,7 +601,6 @@ "title": "Director", "type": ["string"], "autocomplete": true, - "columnRequired": true, "columnWidth": 180, "sort": true, "sortType": "person" @@ -564,7 +619,6 @@ "title": "Featuring", "type": ["string"], "autocomplete": true, - "columnRequired": true, "columnWidth": 180, "filter": true, "sort": true, @@ -620,7 +674,7 @@ { "id": "annotations", "title": "Annotations", - "type": "string", // fixme: not the best type for this magic key + "type": "text", // fixme: not the best type for this magic key "find": true }, { @@ -658,7 +712,7 @@ }, { "id": "numberofannotations", - "title": "Annotations", + "title": "Number of Annotations", "type": "integer", "columnWidth": 60, "sort": true @@ -794,12 +848,16 @@ "id": "user", "title": "User", "type": "string", + "columnWidth": 90, "capability": "canSeeMedia", + "sort": true, "find": true }, { "id": "groups", "title": "Group", + "columnWidth": 90, + "sort": true, "type": ["string"] }, { @@ -1332,11 +1390,9 @@ corner of the screen "resolutions": List of video resolutions. Supported values are 96, 144, 240, 288, 360, 432, 480, 720 and 1080. - "torrent": If true, video downloads are offered via BitTorrent */ "video": { - "torrent": true, - "formats": ["webm", "mp4"], + "formats": ["mp4"], "previewRatio": 1.3333333333, //supported resolutions are //1080, 720, 480, 432, 360, 288, 240, 144, 96 diff --git a/pandora/config.pandora.jsonc b/pandora/config.pandora.jsonc index bd76faa28..b0a64ab47 100644 --- a/pandora/config.pandora.jsonc +++ b/pandora/config.pandora.jsonc @@ -29,7 +29,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "text": Text shown on mouseover */ "cantPlay": { - "icon": "noCopyright", + "icon": "NoCopyright", "link": "", "text": "" }, @@ -63,11 +63,12 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "canExportAnnotations": {"member": true, "staff": true, "admin": true}, "canImportAnnotations": {"member": true, "staff": true, "admin": true}, "canImportItems": {"member": true, "staff": true, "admin": true}, + "canTranscribeAudio": {}, "canManageDocuments": {"member": true, "staff": true, "admin": true}, "canManageEntities": {"member": true, "staff": true, "admin": true}, "canManageHome": {"staff": true, "admin": true}, "canManagePlacesAndEvents": {"member": true, "staff": true, "admin": true}, - "canManageTitlesAndNames": {"member": true, "staff": true, "admin": true}, + "canManageTitlesAndNames": {"member": false, "staff": true, "admin": true}, "canManageTranslations": {"admin": true}, "canManageUsers": {"staff": true, "admin": true}, "canPlayClips": {"guest": 1, "member": 1, "staff": 4, "admin": 4}, @@ -102,8 +103,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. ], /* "clipLayers" is the ordered list of public layers that will appear as the - text of clips (in grid view, below the icon). Excluding a layer from this - list means it will not be included in find annotations. + text of clips (in grid view, below the icon). */ "clipLayers": ["publicnotes", "keywords", "subtitles"], /* @@ -351,11 +351,11 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "type": "enum", "columnWidth": 90, "format": {"type": "ColorLevel", "args": [ - ["Public", "Out of Copyright", "Under Copyright", "Private"] + ["Public", "Restricted", "Private"] ]}, "sort": true, "sortOperator": "+", - "values": ["Public", "Out of Copyright", "Under Copyright", "Private", "Unknown"] + "values": ["Public", "Restricted", "Private", "Unknown"] } ], /* @@ -753,6 +753,13 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "capability": "canSeeMedia", "find": true }, + { + "id": "filename", + "title": "Filename", + "type": ["string"], + "capability": "canSeeMedia", + "find": true + }, { "id": "created", "title": "Date Created", @@ -1159,6 +1166,11 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. "findDocuments": {"conditions": [], "operator": "&"}, "followPlayer": true, "help": "", + "hidden": { + "collections": [], + "edits": [], + "lists": [] + }, "icons": "posters", "infoIconSize": 256, "item": "", @@ -1267,13 +1279,11 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution. corner of the screen "resolutions": List of video resolutions. Supported values are 96, 144, 240, 288, 360, 432, 480, 720 and 1080. - "torrent": If true, video downloads are offered via BitTorrent */ "video": { - "downloadFormat": "webm", - "formats": ["webm", "mp4"], + "downloadFormat": "mp4", + "formats": ["mp4"], "previewRatio": 1.3333333333, - "resolutions": [240, 480], - "torrent": false + "resolutions": [240, 480] } } diff --git a/pandora/document/apps.py b/pandora/document/apps.py new file mode 100644 index 000000000..88a5f0b47 --- /dev/null +++ b/pandora/document/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class DocumentConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'document' + diff --git a/pandora/document/cbr.py b/pandora/document/cbr.py new file mode 100644 index 000000000..f895e9340 --- /dev/null +++ b/pandora/document/cbr.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +import logging +import os +import zipfile + +import ox + +logger = logging.getLogger(__name__) +IMAGE_EXTENSIONS = ['.jpg', '.png', '.gif'] + + +def filter_images(files): + return [f for f in files if os.path.splitext(f)[-1].lower() in IMAGE_EXTENSIONS] + +def filter_folders(files): + out = [] + for path in files: + if not [f for f in files if f.startswith(path + '/')]: + out.append(path) + return out + +def detect_format(path): + with open(path, 'rb') as fd: + head = fd.read(10) + if head[:2] == b'PK': + return 'cbz' + if head[:3] == b'Rar': + return 'cbr' + logger.debug('unknown cbr/cbz file %s - %s', head, path) + return 'unknown' + +def cover(path): + format = detect_format(path) + if format == 'cbz': + cover = cover_cbz(path) + elif format == 'cbr': + cover = cover_cbr(path) + else: + cover = None + return cover + +def cover_cbr(path): + data = None + try: + from unrardll import names, extract_member + except: + logger.error('to extract covers from cbr files you have to install python3-unrardll: apt install python3-unrardll') + return data + try: + files = list(names(path)) + files = filter_folders(files) + files = filter_images(files) + if files: + cover = ox.sorted_strings(files)[0] + filename, data = extract_member(path, lambda h: h['filename'] == cover) + except: + logger.debug('invalid cbr file %s', path) + data = None + return data + +def cover_cbz(path): + data = None + logger.debug('cover %s', path) + data = None + try: + z = zipfile.ZipFile(path) + except zipfile.BadZipFile: + logger.debug('invalid cbz file %s', path) + return data + files = [f.filename for f in z.filelist] + files = filter_images(files) + if files: + cover = ox.sorted_strings(files)[0] + try: + data = z.read(cover) + except: + data = None + return data + +def get_pages(path): + files = [] + format = detect_format(path) + if format == 'cbz': + try: + z = zipfile.ZipFile(path) + except zipfile.BadZipFile: + logger.debug('invalid cbz file %s', path) + return data + files = [f.filename for f in z.filelist] + elif format == 'cbr': + try: + from unrar import rarfile + rar = rarfile.RarFile(path) + files = rar.namelist() + except: + pass + files = filter_images(files) + return len(files) + + +def info(path): + data = {} + data['title'] = os.path.splitext(os.path.basename(path))[0] + data['pages'] = get_pages(path) + return data + diff --git a/pandora/document/epub.py b/pandora/document/epub.py new file mode 100644 index 000000000..d4656619f --- /dev/null +++ b/pandora/document/epub.py @@ -0,0 +1,189 @@ +import os +import xml.etree.ElementTree as ET +import zipfile +import re +from urllib.parse import unquote +import lxml.html +from io import BytesIO + +from PIL import Image + +from ox import strip_tags, decode_html, normalize_name + +import logging +logging.getLogger('PIL').setLevel(logging.ERROR) +logger = logging.getLogger(__name__) + + +def get_ratio(data): + try: + img = Image.open(BytesIO(data)) + return img.size[0]/img.size[1] + except: + return -1 + + +def normpath(path): + return '/'.join(os.path.normpath(path).split(os.sep)) + + +def cover(path): + logger.debug('cover %s', path) + data = None + try: + z = zipfile.ZipFile(path) + except zipfile.BadZipFile: + logger.debug('invalid epub file %s', path) + return data + + def use(filename): + logger.debug('using %s', filename) + try: + data = z.read(filename) + except: + return None + r = get_ratio(data) + if r < 0.3 or r > 2: + return None + return data + + files = [] + for f in z.filelist: + if f.filename == 'calibre-logo.png': + continue + if 'cover' in f.filename.lower() and f.filename.split('.')[-1] in ('jpg', 'jpeg', 'png'): + return use(f.filename) + files.append(f.filename) + opf = [f for f in files if f.endswith('opf')] + if opf: + #logger.debug('opf: %s', z.read(opf[0]).decode()) + info = ET.fromstring(z.read(opf[0])) + metadata = info.findall('{http://www.idpf.org/2007/opf}metadata') + if metadata: + metadata = metadata[0] + manifest = info.findall('{http://www.idpf.org/2007/opf}manifest') + if manifest: + manifest = manifest[0] + if metadata and manifest: + for e in list(metadata): + if e.tag == '{http://www.idpf.org/2007/opf}meta' and e.attrib.get('name') == 'cover': + cover_id = e.attrib['content'] + for e in list(manifest): + if e.attrib['id'] == cover_id: + filename = unquote(e.attrib['href']) + filename = normpath(os.path.join(os.path.dirname(opf[0]), filename)) + if filename in files: + return use(filename) + if manifest: + images = [e for e in list(manifest) if 'image' in e.attrib['media-type']] + if images: + image_data = [] + for e in images: + filename = unquote(e.attrib['href']) + filename = normpath(os.path.join(os.path.dirname(opf[0]), filename)) + if filename in files: + image_data.append(filename) + if image_data: + image_data.sort(key=lambda name: z.getinfo(name).file_size) + return use(image_data[-1]) + for e in list(manifest): + if 'html' in e.attrib['media-type']: + filename = unquote(e.attrib['href']) + filename = normpath(os.path.join(os.path.dirname(opf[0]), filename)) + html = z.read(filename).decode('utf-8', 'ignore') + img = re.compile('= img.size[1]: + width = size + height = int(size / aspect) + else: + width = int(size / aspect) + height = size + img = img.resize((width, height), resize_method) + img.save(output, quality=72) + return jpg + + +class FulltextPageMixin(FulltextMixin): + _ES_INDEX = "document-page-index" + + def extract_fulltext(self): + if self.document.file: + if self.document.extension == 'pdf': + return extract_text(self.document.file.path, self.page) + elif self.extension == 'epub': + # FIXME: is there a nice way to split that into pages + return epub.extract_text(self.file.path) + elif self.extension == 'txt': + data = '' + if os.path.exists(self.file.path): + with open(self.file.path) as fd: + data = fd.read() + return data + elif self.extension in IMAGE_EXTENSIONS: + return ocr_image(self.document.file.path) + elif self.extension == 'html': + # FIXME: is there a nice way to split that into pages + return self.data.get('text', '') + return '' diff --git a/pandora/document/managers.py b/pandora/document/managers/__init__.py similarity index 97% rename from pandora/document/managers.py rename to pandora/document/managers/__init__.py index 4210cd622..edcc4789e 100644 --- a/pandora/document/managers.py +++ b/pandora/document/managers/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from datetime import datetime import unicodedata from django.db.models import Q, Manager @@ -14,6 +15,7 @@ from documentcollection.models import Collection from item import utils from user.models import Group +from .pages import PageManager keymap = { 'item': 'items__public_id', @@ -61,7 +63,7 @@ def parseCondition(condition, user, item=None, owner=None): def buildCondition(k, op, v, user, exclude=False, owner=None): import entity.models - from . import models + from .. import models # fixme: frontend should never call with list if k == 'list': @@ -297,5 +299,8 @@ class DocumentManager(Manager): q |= Q(groups__in=user.groups.all()) rendered_q |= Q(groups__in=user.groups.all()) qs = qs.filter(q) + max_level = len(settings.CONFIG['documentRightsLevels']) + qs = qs.filter(rightslevel__lte=max_level) return qs + diff --git a/pandora/document/managers/pages.py b/pandora/document/managers/pages.py new file mode 100644 index 000000000..2fbfa4fe6 --- /dev/null +++ b/pandora/document/managers/pages.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +import unicodedata + +from six import string_types +from django.db.models import Q, Manager +from django.conf import settings + +import ox +from oxdjango.query import QuerySet + +import entity.managers +from oxdjango.managers import get_operator + +from documentcollection.models import Collection +from item import utils +from user.models import Group + + +keymap = { + 'item': 'items__public_id', +} +default_key = 'title' + +def get_key_type(k): + key_type = (utils.get_by_id(settings.CONFIG['documentKeys'], k) or {'type': 'string'}).get('type') + if isinstance(key_type, list): + key_type = key_type[0] + key_type = { + 'title': 'string', + 'person': 'string', + 'text': 'string', + 'year': 'string', + 'length': 'string', + 'layer': 'string', + 'list': 'list', + }.get(key_type, key_type) + return key_type + + +def parseCondition(condition, user, item=None, owner=None): + ''' + ''' + k = condition.get('key', default_key) + k = keymap.get(k, k) + if not k: + k = default_key + if item and k == 'description': + item_conditions = condition.copy() + item_conditions['key'] = 'items__itemproperties__description' + return parseCondition(condition, user) | parseCondition(item_conditions, user) + + v = condition['value'] + op = condition.get('operator') + if not op: + op = '=' + + if op.startswith('!'): + return buildCondition(k, op[1:], v, user, True, owner=owner) + else: + return buildCondition(k, op, v, user, owner=owner) + +def buildCondition(k, op, v, user, exclude=False, owner=None): + import entity.models + from .. import models + + # fixme: frontend should never call with list + if k == 'list': + print('fixme: frontend should never call with list', k, op, v) + k = 'collection' + + key_type = get_key_type(k) + + key_config = (utils.get_by_id(settings.CONFIG['documentKeys'], k) or {'type': 'string'}) + + facet_keys = models.Document.facet_keys + if k == 'document': + k = 'document__id' + if op == '&' and isinstance(v, list): + v = [ox.fromAZ(id_) for id_ in v] + k += get_operator(op) + else: + v = ox.fromAZ(v) + q = Q(**{k: v}) + if exclude: + q = ~Q(document__id__in=models.Document.objects.filter(q)) + return q + elif k == 'rightslevel': + q = Q(document__rightslevel=v) + if exclude: + q = ~Q(document__rightslevel=v) + return q + elif k == 'groups': + if op == '==' and v == '$my': + if not owner: + owner = user + groups = owner.groups.all() + else: + key = 'name' + get_operator(op) + groups = Group.objects.filter(**{key: v}) + if not groups.count(): + return Q(id=0) + q = Q(document__groups__in=groups) + if exclude: + q = ~q + return q + elif k in ('oshash', 'items__public_id'): + q = Q(**{k: v}) + if exclude: + q = ~Q(id__in=models.Document.objects.filter(q)) + return q + elif isinstance(v, bool): + key = k + elif k == 'entity': + entity_key, entity_v = entity.managers.namePredicate(op, v) + key = 'id__in' + v = entity.models.DocumentProperties.objects.filter(**{ + 'entity__' + entity_key: entity_v + }).values_list('document_id', flat=True) + elif k == 'collection': + q = Q(id=0) + l = v.split(":", 1) + if len(l) >= 2: + lqs = list(Collection.objects.filter(name=l[1], user__username=l[0])) + if len(lqs) == 1 and lqs[0].accessible(user): + l = lqs[0] + if l.query.get('static', False) is False: + data = l.query + q = parseConditions(data.get('conditions', []), + data.get('operator', '&'), + user, owner=l.user) + else: + q = Q(id__in=l.documents.all()) + else: + q = Q(id=0) + return q + elif key_config.get('fulltext'): + qs = models.Page.find_fulltext_ids(v) + q = Q(id__in=qs) + if exclude: + q = ~Q(id__in=qs) + return q + elif key_type == 'boolean': + q = Q(**{'find__key': k, 'find__value': v}) + if exclude: + q = ~Q(id__in=models.Document.objects.filter(q)) + return q + elif key_type == "string": + in_find = True + if in_find: + value_key = 'find__value' + else: + value_key = k + if isinstance(v, string_types): + v = unicodedata.normalize('NFKD', v).lower() + if k in facet_keys: + in_find = False + facet_value = 'facets__value' + get_operator(op, 'istr') + v = models.Document.objects.filter(**{'facets__key': k, facet_value: v}) + value_key = 'id__in' + else: + value_key = value_key + get_operator(op) + k = str(k) + value_key = str(value_key) + if k == '*': + q = Q(**{'find__value' + get_operator(op): v}) | \ + Q(**{'facets__value' + get_operator(op, 'istr'): v}) + elif in_find: + q = Q(**{'find__key': k, value_key: v}) + else: + q = Q(**{value_key: v}) + if exclude: + q = ~Q(id__in=models.Document.objects.filter(q)) + return q + elif key_type == 'date': + def parse_date(d): + while len(d) < 3: + d.append(1) + return datetime(*[int(i) for i in d]) + + #using sort here since find only contains strings + v = parse_date(v.split('-')) + vk = 'sort__%s%s' % (k, get_operator(op, 'int')) + vk = str(vk) + q = Q(**{vk: v}) + if exclude: + q = ~q + return q + else: # integer, float, list, time + #use sort table here + if key_type == 'time': + v = int(utils.parse_time(v)) + + vk = 'sort__%s%s' % (k, get_operator(op, 'int')) + vk = str(vk) + q = Q(**{vk: v}) + if exclude: + q = ~q + return q + key = str(key) + q = Q(**{key: v}) + if exclude: + q = ~q + return q + + +def parseConditions(conditions, operator, user, item=None, owner=None): + ''' + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + ''' + conn = [] + for condition in conditions: + if 'conditions' in condition: + q = parseConditions(condition['conditions'], + condition.get('operator', '&'), user, item, owner=owner) + if q: + conn.append(q) + pass + else: + conn.append(parseCondition(condition, user, item, owner=owner)) + if conn: + q = conn[0] + for c in conn[1:]: + if operator == '|': + q = q | c + else: + q = q & c + return q + return None + + +class PageManager(Manager): + + def get_query_set(self): + return QuerySet(self.model) + + def find(self, data, user, item=None): + ''' + query: { + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + } + ''' + + #join query with operator + qs = self.get_query_set() + query = data.get('query', {}) + conditions = parseConditions(query.get('conditions', []), + query.get('operator', '&'), + user, item) + if conditions: + qs = qs.filter(conditions) + qs = qs.distinct() + + #anonymous can only see public items + if not user or user.is_anonymous: + level = 'guest' + allowed_level = settings.CONFIG['capabilities']['canSeeDocument'][level] + qs = qs.filter(document__rightslevel__lte=allowed_level) + rendered_q = Q(rendered=True) + #users can see public items, there own items and items of there groups + else: + level = user.profile.get_level() + allowed_level = settings.CONFIG['capabilities']['canSeeDocument'][level] + q = Q(document__rightslevel__lte=allowed_level) | Q(document__user=user) + rendered_q = Q(rendered=True) | Q(document__user=user) + if user.groups.count(): + q |= Q(document__groups__in=user.groups.all()) + rendered_q |= Q(document__groups__in=user.groups.all()) + qs = qs.filter(q) + + return qs + diff --git a/pandora/document/migrations/0012_auto_20200513_0001.py b/pandora/document/migrations/0012_auto_20200513_0001.py new file mode 100644 index 000000000..2bf7b0abe --- /dev/null +++ b/pandora/document/migrations/0012_auto_20200513_0001.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2020-05-13 00:01 +from __future__ import unicode_literals + +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion +import document.fulltext +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('document', '0011_jsonfield'), + ] + + operations = [ + migrations.CreateModel( + name='Page', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('page', models.IntegerField(default=1)), + ('data', oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder)), + ], + bases=(models.Model, document.fulltext.FulltextPageMixin), + ), + migrations.AddField( + model_name='page', + name='document', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pages_set', to='document.Document'), + ), + ] diff --git a/pandora/document/migrations/0013_id_bigint.py b/pandora/document/migrations/0013_id_bigint.py new file mode 100644 index 000000000..09dd44ed4 --- /dev/null +++ b/pandora/document/migrations/0013_id_bigint.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:24 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('document', '0012_auto_20200513_0001'), + ] + + operations = [ + migrations.AlterField( + model_name='access', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='document', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='document', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='facet', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='find', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='itemproperties', + name='description', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='itemproperties', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='page', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/document/models.py b/pandora/document/models.py index e25012440..ef664df64 100644 --- a/pandora/document/models.py +++ b/pandora/document/models.py @@ -6,11 +6,12 @@ import os import re import unicodedata +from django.conf import settings +from django.contrib.auth import get_user_model from django.db import models, transaction from django.db.models import Q, Sum, Max -from django.contrib.auth import get_user_model from django.db.models.signals import pre_delete -from django.conf import settings +from django.utils import datetime_safe from oxdjango.fields import JSONField from PIL import Image @@ -21,15 +22,18 @@ from oxdjango.sortmodel import get_sort_field from person.models import get_name_sort from item.models import Item from annotation.models import Annotation -from archive.extract import resize_image +from archive.extract import resize_image, open_image_rgb from archive.chunk import save_chunk from user.models import Group from user.utils import update_groups +from . import cbr +from . import epub from . import managers -from . import utils from . import tasks -from .fulltext import FulltextMixin +from . import txt +from . import utils +from .fulltext import FulltextMixin, FulltextPageMixin User = get_user_model() @@ -79,7 +83,7 @@ class Document(models.Model, FulltextMixin): current_values = [] for k in settings.CONFIG['documentKeys']: if k.get('sortType') == 'person': - current_values += self.get(k['id'], []) + current_values += self.get_value(k['id'], []) if not isinstance(current_values, list): if not current_values: current_values = [] @@ -173,13 +177,21 @@ class Document(models.Model, FulltextMixin): if self.extension == 'pdf': prefix = 2 value = self.pages + elif self.extension == 'epub': + prefix = 3 + value = self.pages + elif self.extension == 'txt': + prefix = 4 + value = self.pages + elif self.extension in ('cbr', 'cbz'): + prefix = 5 + value = self.pages + elif self.extension == 'html': + prefix = 1 + value = self.dimensions else: - if self.extension == 'html': - prefix = 1 - value = self.dimensions - else: - prefix = 0 - value = self.width * self.height + prefix = 0 + value = self.width * self.height if value < 0: value = 0 s.dimensions = ox.sort_string('%d' % prefix) + ox.sort_string('%d' % value) @@ -327,6 +339,9 @@ class Document(models.Model, FulltextMixin): def editable(self, user, item=None): if not user or user.is_anonymous: return False + max_level = len(settings.CONFIG['rightsLevels']) + if self.rightslevel > max_level: + return False if self.user == user or \ self.groups.filter(id__in=user.groups.all()).count() > 0 or \ user.is_staff or \ @@ -346,6 +361,8 @@ class Document(models.Model, FulltextMixin): groups = data.pop('groups') update_groups(self, groups) for key in data: + if key == "id": + continue k = list(filter(lambda i: i['id'] == key, settings.CONFIG['documentKeys'])) ktype = k and k[0].get('type') or '' if key == 'text' and self.extension == 'html': @@ -384,7 +401,13 @@ class Document(models.Model, FulltextMixin): @property def dimensions(self): - if self.extension == 'pdf': + if self.extension in ( + 'cbr', + 'cbz', + 'epub', + 'pdf', + 'txt', + ): return self.pages elif self.extension == 'html': return len(self.data.get('text', '').split(' ')) @@ -515,14 +538,17 @@ class Document(models.Model, FulltextMixin): return save_chunk(self, self.file, chunk, offset, name, done_cb) return False, 0 - def thumbnail(self, size=None, page=None): + def thumbnail(self, size=None, page=None, accept=None): if not self.file: return os.path.join(settings.STATIC_ROOT, 'png/document.png') src = self.file.path folder = os.path.dirname(src) if size: size = int(size) - path = os.path.join(folder, '%d.jpg' % size) + ext = 'jpg' + if accept and 'image/avif' in accept and size > 512: + ext = 'avif' + path = os.path.join(folder, '%d.%s' % (size, ext)) else: path = src if self.extension == 'pdf': @@ -546,35 +572,59 @@ class Document(models.Model, FulltextMixin): if len(crop) == 4: path = os.path.join(folder, '%dp%d,%s.jpg' % (1024, page, ','.join(map(str, crop)))) if not os.path.exists(path): - img = Image.open(src).crop(crop) + img = open_image_rgb(src).crop(crop) img.save(path) else: - img = Image.open(path) + img = open_image_rgb(path) src = path if size < max(img.size): path = os.path.join(folder, '%dp%d,%s.jpg' % (size, page, ','.join(map(str, crop)))) if not os.path.exists(path): resize_image(src, path, size=size) - elif self.extension in ('jpg', 'png', 'gif'): + elif self.extension in ('cbr', 'cbz'): + path = os.path.join(folder, '1024.jpg') + if os.path.exists(src) and not os.path.exists(path): + data = cbr.cover(src) + if data: + with open(path, "wb") as fd: + fd.write(data) + else: + return os.path.join(settings.STATIC_ROOT, 'png/document.png') + elif self.extension == 'epub': + path = os.path.join(folder, '1024.jpg') + if os.path.exists(src) and not os.path.exists(path): + data = epub.cover(src) + if data: + with open(path, "wb") as fd: + fd.write(data) + else: + return os.path.join(settings.STATIC_ROOT, 'png/document.png') + elif self.extension == 'txt': + path = os.path.join(folder, '1024.jpg') + if os.path.exists(src) and not os.path.exists(path): + txt.render(src, path) + if not os.path.exists(path): + return os.path.join(settings.STATIC_ROOT, 'png/document.png') + elif self.extension in ('jpg', 'png', 'gif', 'webp', 'heic', 'heif', 'cr2'): if os.path.exists(src): if size and page: crop = list(map(int, page.split(','))) if len(crop) == 4: path = os.path.join(folder, '%s.jpg' % ','.join(map(str, crop))) if not os.path.exists(path): - img = Image.open(src).crop(crop) + img = open_image_rgb(src).convert('RGB').crop(crop) img.save(path) else: - img = Image.open(path) + img = open_image_rgb(path) src = path if size < max(img.size): - path = os.path.join(folder, '%sp%s.jpg' % (size, ','.join(map(str, crop)))) + path = os.path.join(folder, '%sp%s.%s' % (size, ','.join(map(str, crop)), ext)) if not os.path.exists(path): resize_image(src, path, size=size) if os.path.exists(src) and not os.path.exists(path): image_size = max(self.width, self.height) if image_size == -1: - image_size = max(*Image.open(src).size) + image_size = max(*open_image_rgb(src).size) if size > image_size: path = src else: @@ -586,6 +636,11 @@ class Document(models.Model, FulltextMixin): image = os.path.join(os.path.dirname(pdf), '1024p%d.jpg' % page) utils.extract_pdfpage(pdf, image, page) + def create_pages(self): + for page in range(self.pages): + page += 1 + p, c = Page.objects.get_or_create(document=self, page=page) + def get_info(self): if self.extension == 'pdf': self.thumbnail(1024) @@ -593,12 +648,28 @@ class Document(models.Model, FulltextMixin): self.width = -1 self.height = -1 self.pages = utils.pdfpages(self.file.path) + elif self.extension in ('cbr', 'cbz'): + from . import cbr + thumb = self.thumbnail(1024) + if thumb: + self.width, self.height = open_image_rgb(thumb).size + self.pages = cbr.get_pages(self.file.path) + elif self.extension == 'epub': + thumb = self.thumbnail(1024) + if thumb: + self.width, self.height = open_image_rgb(thumb).size + self.pages = 1 + elif self.extension == 'txt': + thumb = self.thumbnail(1024) + if thumb: + self.width, self.height = open_image_rgb(thumb).size + self.pages = 1 elif self.width == -1: self.pages = -1 - self.width, self.height = Image.open(self.file.path).size + self.width, self.height = open_image_rgb(self.file.path).size def get_ratio(self): - if self.extension == 'pdf': + if self.extension in ('pdf', 'epub', 'txt'): image = self.thumbnail(1024) try: size = Image.open(image).size @@ -702,6 +773,41 @@ class ItemProperties(models.Model): super(ItemProperties, self).save(*args, **kwargs) +class Page(models.Model, FulltextPageMixin): + + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + document = models.ForeignKey(Document, related_name='pages_set', on_delete=models.CASCADE) + page = models.IntegerField(default=1) + data = JSONField(default=dict, editable=False) + + objects = managers.PageManager() + + def __str__(self): + return u"%s:%s" % (self.document, self.page) + + def json(self, keys=None, user=None): + data = {} + data['document'] = ox.toAZ(self.document.id) + data['page'] = self.page + data['id'] = '{document}/{page}'.format(**data) + document_keys = [] + if keys: + for key in list(data): + if key not in keys: + del data[key] + for key in keys: + if 'fulltext' in key: + data['fulltext'] = self.extract_fulltext() + elif key in ('document', 'page', 'id'): + pass + else: + document_keys.append(key) + if document_keys: + data.update(self.document.json(document_keys, user)) + return data + class Access(models.Model): class Meta: unique_together = ("document", "user") diff --git a/pandora/document/page_views.py b/pandora/document/page_views.py new file mode 100644 index 000000000..d0ae7c0bc --- /dev/null +++ b/pandora/document/page_views.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +import os +import re +from glob import glob +import unicodedata + +import ox +from ox.utils import json +from oxdjango.api import actions +from oxdjango.decorators import login_required_json +from oxdjango.http import HttpFileResponse +from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response, HttpErrorJson +from django import forms +from django.db.models import Count, Sum +from django.conf import settings + +from item import utils +from item.models import Item +from itemlist.models import List +from entity.models import Entity +from archive.chunk import process_chunk +from changelog.models import add_changelog + +from . import models +from . import tasks + +def parse_query(data, user): + query = {} + query['range'] = [0, 100] + query['sort'] = [{'key': 'page', 'operator': '+'}, {'key': 'title', 'operator': '+'}] + for key in ('keys', 'group', 'file', 'range', 'position', 'positions', 'sort'): + if key in data: + query[key] = data[key] + query['qs'] = models.Page.objects.find(data, user) + return query + +def _order_query(qs, sort): + prefix = 'document__sort__' + order_by = [] + for e in sort: + operator = e['operator'] + if operator != '-': + operator = '' + key = { + 'index': 'document__items__itemproperties__index', + 'position': 'id', + 'name': 'title', + }.get(e['key'], e['key']) + if key == 'resolution': + order_by.append('%swidth' % operator) + order_by.append('%sheight' % operator) + else: + if '__' not in key and key not in ('created', 'modified', 'page'): + key = "%s%s" % (prefix, key) + order = '%s%s' % (operator, key) + order_by.append(order) + if order_by: + qs = qs.order_by(*order_by, nulls_last=True) + qs = qs.distinct() + return qs + +def _order_by_group(query): + prefix = 'document__sort__' + if 'sort' in query: + op = '-' if query['sort'][0]['operator'] == '-' else '' + if len(query['sort']) == 1 and query['sort'][0]['key'] == 'items': + order_by = op + prefix + 'items' + if query['group'] == "year": + secondary = op + prefix + 'sortvalue' + order_by = (order_by, secondary) + elif query['group'] != "keyword": + order_by = (order_by, prefix + 'sortvalue') + else: + order_by = (order_by, 'value') + else: + order_by = op + prefix + 'sortvalue' + order_by = (order_by, prefix + 'items') + else: + order_by = ('-' + prefix + 'sortvalue', prefix + 'items') + return order_by + +def findPages(request, data): + ''' + Finds documents pages for a given query + takes { + query: object, // query object, see `find` + sort: [object], // list of sort objects, see `find` + range: [int, int], // range of results, per current sort order + keys: [string] // list of keys to return + } + returns { + items: [{ // list of pages + id: string + page: int + }] + } + ''' + query = parse_query(data, request.user) + #order + qs = _order_query(query['qs'], query['sort']) + + response = json_response() + if 'group' in query: + response['data']['items'] = [] + items = 'items' + document_qs = query['qs'] + order_by = _order_by_group(query) + qs = models.Facet.objects.filter(key=query['group']).filter(document__id__in=document_qs) + qs = qs.values('value').annotate(items=Count('id')).order_by(*order_by) + + if 'positions' in query: + response['data']['positions'] = {} + ids = [j['value'] for j in qs] + response['data']['positions'] = utils.get_positions(ids, query['positions']) + elif 'range' in data: + qs = qs[query['range'][0]:query['range'][1]] + response['data']['items'] = [{'name': i['value'], 'items': i[items]} for i in qs] + else: + response['data']['items'] = qs.count() + elif 'keys' in data: + qs = qs[query['range'][0]:query['range'][1]] + + response['data']['items'] = [l.json(data['keys'], request.user) for l in qs] + elif 'position' in data: + #FIXME: actually implement position requests + response['data']['position'] = 0 + elif 'positions' in data: + ids = list(qs.values_list('id', flat=True)) + response['data']['positions'] = utils.get_positions(ids, query['positions'], decode_id=True) + else: + response['data']['items'] = qs.count() + return render_to_json_response(response) +actions.register(findPages) + diff --git a/pandora/document/tasks.py b/pandora/document/tasks.py index 7bede1a90..fcfb5576d 100644 --- a/pandora/document/tasks.py +++ b/pandora/document/tasks.py @@ -1,21 +1,26 @@ import ox -from celery.task import task +from app.celery import app -@task(queue="encoding") +@app.task(queue="encoding") def extract_fulltext(id): from . import models d = models.Document.objects.get(id=id) d.update_fulltext() + d.create_pages() + for page in d.pages_set.all(): + page.update_fulltext() -@task(queue='default') +@app.task(queue='default') def bulk_edit(data, username): from django.db import transaction from . import models from item.models import Item user = models.User.objects.get(username=username) item = 'item' in data and Item.objects.get(public_id=data['item']) or None - documents = models.Document.objects.filter(pk__in=map(ox.fromAZ, data['id'])) + ids = data['id'] + del data['id'] + documents = models.Document.objects.filter(pk__in=map(ox.fromAZ, ids)) for document in documents: if document.editable(user, item): with transaction.atomic(): diff --git a/pandora/document/txt.py b/pandora/document/txt.py new file mode 100755 index 000000000..6189b9ac4 --- /dev/null +++ b/pandora/document/txt.py @@ -0,0 +1,71 @@ +import os + +from PIL import Image +from argparse import ArgumentParser +from ox.image import drawText, wrapText + +from django.conf import settings + + +def decode_line(line): + try: + line = line.decode('utf-8') + except: + try: + line = line.decode('latin-1') + except: + line = line.decode('utf-8', errors='replace') + return line + +def render(infile, outfile): + + with open(infile, 'rb') as f: + + image_size = (768, 1024) + margin = 64 + offset = margin + font_file = settings.TXT_TTF + font_size = 24 + line_height = 32 + max_lines = (image_size[1] - 2 * margin) / line_height + + image = Image.new('L', image_size, (255)) + + for line in f: + line = decode_line(line) + + for line_ in line.strip().split('\r'): + + lines = wrapText( + line_, + image_size[0] - 2 * margin, + # we don't want the last line that ends with an ellipsis + max_lines + 1, + font_file, + font_size + ) + + for line__ in lines: + drawText( + image, + (margin, offset), + line__, + font_file, + font_size, + (0) + ) + offset += line_height + max_lines -= 1 + + if max_lines == 0: + break + + if max_lines == 0: + break + + if max_lines == 0: + break + + image.save(outfile, quality=50) + + diff --git a/pandora/document/views.py b/pandora/document/views.py index 5fc474661..843518edd 100644 --- a/pandora/document/views.py +++ b/pandora/document/views.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- +from glob import glob +import mimetypes import os import re -from glob import glob import unicodedata +import zipfile import ox from ox.utils import json @@ -12,8 +14,10 @@ from oxdjango.decorators import login_required_json from oxdjango.http import HttpFileResponse from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response, HttpErrorJson from django import forms -from django.db.models import Count, Sum from django.conf import settings +from django.db.models import Count, Sum +from django.http import HttpResponse, Http404 +from django.shortcuts import render from item import utils from item.models import Item @@ -24,6 +28,7 @@ from changelog.models import add_changelog from . import models from . import tasks +from . import page_views def get_document_or_404_json(request, id): response = {'status': {'code': 404, @@ -375,12 +380,25 @@ actions.register(sortDocuments, cache=False) def file(request, id, name=None): document = get_document_or_404_json(request, id) + accept = request.headers.get("Accept") + mime_type = mimetypes.guess_type(document.file.path)[0] + mime_type = 'image/%s' % document.extension + if accept and 'image/' in accept and document.extension in ( + 'webp', 'heif', 'heic', 'avif', 'tiff', + ) and mime_type not in accept: + image_size = max(document.width, document.height) + return HttpFileResponse(document.thumbnail(image_size, accept=accept)) return HttpFileResponse(document.file.path) def thumbnail(request, id, size=256, page=None): size = int(size) document = get_document_or_404_json(request, id) - return HttpFileResponse(document.thumbnail(size, page=page)) + accept = request.headers.get("Accept") + if "q" in request.GET and page: + img = document.highlight_page(page, request.GET["q"], size) + return HttpResponse(img, content_type="image/jpeg") + return HttpFileResponse(document.thumbnail(size, page=page, accept=accept)) + @login_required_json def upload(request): @@ -506,3 +524,58 @@ def autocompleteDocuments(request, data): response['data']['items'] = [i['value'] for i in qs] return render_to_json_response(response) actions.register(autocompleteDocuments) + + +def document(request, fragment): + context = {} + parts = fragment.split('/') + # FIXME: parse collection urls and return the right metadata for those + id = parts[0] + page = None + crop = None + if len(parts) == 2: + rect = parts[1].split(',') + if len(rect) == 1: + page = rect[0] + else: + crop = rect + try: + document = models.Document.objects.filter(id=ox.fromAZ(id)).first() + except: + document = None + if document and document.access(request.user): + context['title'] = document.data['title'] + if document.data.get('description'): + context['description'] = document.data['description'] + link = request.build_absolute_uri(document.get_absolute_url()) + public_id = ox.toAZ(document.id) + preview = '/documents/%s/512p.jpg' % public_id + if page: + preview = '/documents/%s/512p%s.jpg' % (public_id, page) + if crop: + preview = '/documents/%s/512p%s.jpg' % (public_id, ','.join(crop)) + context['preview'] = request.build_absolute_uri(preview) + context['url'] = request.build_absolute_uri('/documents/' + fragment) + context['settings'] = settings + return render(request, "document.html", context) + +def epub(request, id, filename): + document = get_document_or_404_json(request, id) + if not document.access(request.user): + raise Http404 + if document.extension != 'epub': + raise Http404 + z = zipfile.ZipFile(document.file.path) + if filename == '': + context = {} + context["epub"] = document + return render(request, "epub.html", context) + elif filename not in [f.filename for f in z.filelist]: + raise Http404 + else: + content_type = { + 'xpgt': 'application/vnd.adobe-page-template+xml' + }.get(filename.split('.')[0], mimetypes.guess_type(filename)[0]) or 'text/plain' + content = z.read(filename) + response = HttpResponse(content, content_type=content_type) + return response diff --git a/pandora/documentcollection/apps.py b/pandora/documentcollection/apps.py new file mode 100644 index 000000000..10e2984fa --- /dev/null +++ b/pandora/documentcollection/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class DocumentcollectionConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'documentcollection' + diff --git a/pandora/documentcollection/migrations/0005_alter_collection_description_alter_collection_id_and_more.py b/pandora/documentcollection/migrations/0005_alter_collection_description_alter_collection_id_and_more.py new file mode 100644 index 000000000..72155308b --- /dev/null +++ b/pandora/documentcollection/migrations/0005_alter_collection_description_alter_collection_id_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import documentcollection.models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('documentcollection', '0004_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='collection', + name='description', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='collection', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='collection', + name='poster_frames', + field=oxdjango.fields.JSONField(default=list, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='collection', + name='query', + field=oxdjango.fields.JSONField(default=documentcollection.models.default_query, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='collection', + name='sort', + field=oxdjango.fields.JSONField(default=documentcollection.models.get_collectionsort, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='collection', + name='status', + field=models.CharField(default='private', max_length=20), + ), + migrations.AlterField( + model_name='collection', + name='type', + field=models.CharField(default='static', max_length=255), + ), + migrations.AlterField( + model_name='collectiondocument', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='position', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/documentcollection/models.py b/pandora/documentcollection/models.py index 8504ae475..7c471ce2e 100644 --- a/pandora/documentcollection/models.py +++ b/pandora/documentcollection/models.py @@ -34,6 +34,9 @@ def get_collectionview(): def get_collectionsort(): return tuple(settings.CONFIG['user']['ui']['collectionSort']) +def default_query(): + return {"static": True} + class Collection(models.Model): class Meta: @@ -46,7 +49,7 @@ class Collection(models.Model): name = models.CharField(max_length=255) status = models.CharField(max_length=20, default='private') _status = ['private', 'public', 'featured'] - query = JSONField(default=lambda: {"static": True}, editable=False) + query = JSONField(default=default_query, editable=False) type = models.CharField(max_length=255, default='static') description = models.TextField(default='') @@ -185,7 +188,7 @@ class Collection(models.Model): self.status = value elif key == 'name': - data['name'] = re.sub(' \[\d+\]$', '', data['name']).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data['name']).strip() if not data['name']: data['name'] = "Untitled" name = data['name'] diff --git a/pandora/documentcollection/views.py b/pandora/documentcollection/views.py index 66fa746db..a678bd171 100644 --- a/pandora/documentcollection/views.py +++ b/pandora/documentcollection/views.py @@ -86,6 +86,11 @@ def findCollections(request, data): for x in data.get('query', {}).get('conditions', []) ) + is_personal = request.user.is_authenticated and any( + (x['key'] == 'user' and x['value'] == request.user.username and x['operator'] == '==') + for x in data.get('query', {}).get('conditions', []) + ) + if is_section_request: qs = query['qs'] if not is_featured and not request.user.is_anonymous: @@ -94,6 +99,9 @@ def findCollections(request, data): else: qs = _order_query(query['qs'], query['sort']) + if is_personal and request.user.profile.ui.get('hidden', {}).get('collections'): + qs = qs.exclude(name__in=request.user.profile.ui['hidden']['collections']) + response = json_response() if 'keys' in data: qs = qs[query['range'][0]:query['range'][1]] @@ -238,7 +246,7 @@ def addCollection(request, data): 'type' and 'view'. see: editCollection, findCollections, getCollection, removeCollection, sortCollections ''' - data['name'] = re.sub(' \[\d+\]$', '', data.get('name', 'Untitled')).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data.get('name', 'Untitled')).strip() name = data['name'] if not name: name = "Untitled" diff --git a/pandora/edit/apps.py b/pandora/edit/apps.py new file mode 100644 index 000000000..56f70942e --- /dev/null +++ b/pandora/edit/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class EditConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'edit' + diff --git a/pandora/edit/migrations/0006_id_bigint_jsonfield.py b/pandora/edit/migrations/0006_id_bigint_jsonfield.py new file mode 100644 index 000000000..a2e58e5c4 --- /dev/null +++ b/pandora/edit/migrations/0006_id_bigint_jsonfield.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import edit.models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('edit', '0005_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='clip', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='edit', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='edit', + name='poster_frames', + field=oxdjango.fields.JSONField(default=list, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='edit', + name='query', + field=oxdjango.fields.JSONField(default=edit.models.default_query, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='position', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/edit/models.py b/pandora/edit/models.py index d71525a06..1ab58541b 100644 --- a/pandora/edit/models.py +++ b/pandora/edit/models.py @@ -13,6 +13,7 @@ from django.conf import settings from django.db import models, transaction from django.db.models import Max from django.contrib.auth import get_user_model +from django.core.cache import cache from oxdjango.fields import JSONField @@ -24,6 +25,7 @@ import clip.models from archive import extract from user.utils import update_groups from user.models import Group +from clip.utils import add_cuts from . import managers @@ -33,6 +35,9 @@ User = get_user_model() def get_path(f, x): return f.path(x) def get_icon_path(f, x): return get_path(f, 'icon.jpg') +def default_query(): + return {"static": True} + class Edit(models.Model): class Meta: @@ -51,7 +56,7 @@ class Edit(models.Model): description = models.TextField(default='') rightslevel = models.IntegerField(db_index=True, default=0) - query = JSONField(default=lambda: {"static": True}, editable=False) + query = JSONField(default=default_query, editable=False) type = models.CharField(max_length=255, default='static') icon = models.ImageField(default=None, blank=True, null=True, upload_to=get_icon_path) @@ -93,6 +98,8 @@ class Edit(models.Model): # dont add clip if in/out are invalid if not c.annotation: duration = c.item.sort.duration + if c.start is None or c.end is None: + return False if c.start > c.end \ or round(c.start, 3) >= round(duration, 3) \ or round(c.end, 3) > round(duration, 3): @@ -189,7 +196,7 @@ class Edit(models.Model): self.status = value elif key == 'name': - data['name'] = re.sub(' \[\d+\]$', '', data['name']).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data['name']).strip() if not data['name']: data['name'] = "Untitled" name = data['name'] @@ -488,6 +495,9 @@ class Clip(models.Model): 'id': self.get_id(), 'index': self.index, 'volume': self.volume, + 'hue': self.hue, + 'saturation': self.saturation, + 'lightness': self.lightness, } if self.annotation: data['annotation'] = self.annotation.public_id @@ -507,7 +517,7 @@ class Clip(models.Model): if value: data[key] = value data['duration'] = data['out'] - data['in'] - data['cuts'] = tuple([c for c in self.item.get('cuts', []) if c > self.start and c < self.end]) + add_cuts(data, self.item, self.start, self.end) data['layers'] = self.get_layers(user) data['streams'] = [s.file.oshash for s in self.item.streams()] return data diff --git a/pandora/edit/views.py b/pandora/edit/views.py index 092613109..c096b464f 100644 --- a/pandora/edit/views.py +++ b/pandora/edit/views.py @@ -3,14 +3,16 @@ import os import re -import ox +from oxdjango.api import actions from oxdjango.decorators import login_required_json +from oxdjango.http import HttpFileResponse from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response +import ox + +from django.conf import settings from django.db import transaction from django.db.models import Max -from oxdjango.http import HttpFileResponse -from oxdjango.api import actions -from django.conf import settings +from django.db.models import Sum from item import utils from changelog.models import add_changelog @@ -190,7 +192,7 @@ def _order_clips(edit, sort): 'in': 'start', 'out': 'end', 'text': 'sortvalue', - 'volume': 'sortvolume', + 'volume': 'volume' if edit.type == 'smart' else 'sortvolume', 'item__sort__item': 'item__sort__public_id', }.get(key, key) order = '%s%s' % (operator, key) @@ -260,7 +262,7 @@ def addEdit(request, data): } see: editEdit, findEdit, getEdit, removeEdit, sortEdits ''' - data['name'] = re.sub(' \[\d+\]$', '', data.get('name', 'Untitled')).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data.get('name', 'Untitled')).strip() name = data['name'] if not name: name = "Untitled" @@ -412,6 +414,11 @@ def findEdits(request, data): is_featured = any(filter(is_featured_condition, data.get('query', {}).get('conditions', []))) + is_personal = request.user.is_authenticated and any( + (x['key'] == 'user' and x['value'] == request.user.username and x['operator'] == '==') + for x in data.get('query', {}).get('conditions', []) + ) + if is_section_request: qs = query['qs'] if not is_featured and not request.user.is_anonymous: @@ -420,6 +427,9 @@ def findEdits(request, data): else: qs = _order_query(query['qs'], query['sort']) + if is_personal and request.user.profile.ui.get('hidden', {}).get('edits'): + qs = qs.exclude(name__in=request.user.profile.ui['hidden']['edits']) + response = json_response() if 'keys' in data: qs = qs[query['range'][0]:query['range'][1]] diff --git a/pandora/encoding.conf.in b/pandora/encoding.conf.in new file mode 100644 index 000000000..af15426c5 --- /dev/null +++ b/pandora/encoding.conf.in @@ -0,0 +1,3 @@ +LOGLEVEL=info +MAX_TASKS_PER_CHILD=500 +CONCURRENCY=1 diff --git a/pandora/entity/apps.py b/pandora/entity/apps.py new file mode 100644 index 000000000..621c71353 --- /dev/null +++ b/pandora/entity/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EntityConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'entity' diff --git a/pandora/entity/migrations/0007_alter_documentproperties_data_and_more.py b/pandora/entity/migrations/0007_alter_documentproperties_data_and_more.py new file mode 100644 index 000000000..83d9c0bda --- /dev/null +++ b/pandora/entity/migrations/0007_alter_documentproperties_data_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('entity', '0006_auto_20180918_0903'), + ] + + operations = [ + migrations.AlterField( + model_name='documentproperties', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='documentproperties', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='entity', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='entity', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='entity', + name='name_find', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='find', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='link', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/entity/models.py b/pandora/entity/models.py index e97a69634..d43444de7 100644 --- a/pandora/entity/models.py +++ b/pandora/entity/models.py @@ -131,7 +131,7 @@ class Entity(models.Model): config_keys = {k['id']: k for k in entity['keys']} for key, value in data.items(): if key == 'name': - data['name'] = re.sub(' \[\d+\]$', '', data['name']).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data['name']).strip() if not data['name']: data['name'] = "Unnamed" name = data['name'] @@ -147,7 +147,7 @@ class Entity(models.Model): names = [] for v in data[key]: name = ox.decode_html(v) - name = re.sub(' \[\d+\]$', '', name).strip() + name = re.sub(r' \[\d+\]$', '', name).strip() name_ = name n = 1 while name in used_names or \ diff --git a/pandora/event/apps.py b/pandora/event/apps.py new file mode 100644 index 000000000..ff9d6ab73 --- /dev/null +++ b/pandora/event/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EventConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'event' diff --git a/pandora/event/migrations/0004_alter_event_duration_alter_event_end_alter_event_id_and_more.py b/pandora/event/migrations/0004_alter_event_duration_alter_event_end_alter_event_id_and_more.py new file mode 100644 index 000000000..7d422113c --- /dev/null +++ b/pandora/event/migrations/0004_alter_event_duration_alter_event_end_alter_event_id_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0003_auto_20160304_1644'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='duration', + field=models.CharField(default='', max_length=255), + ), + migrations.AlterField( + model_name='event', + name='end', + field=models.CharField(default='', max_length=255), + ), + migrations.AlterField( + model_name='event', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='event', + name='name_find', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='event', + name='start', + field=models.CharField(default='', max_length=255), + ), + migrations.AlterField( + model_name='event', + name='type', + field=models.CharField(default='', max_length=255), + ), + ] diff --git a/pandora/event/tasks.py b/pandora/event/tasks.py index 234dd5a76..46acdda29 100644 --- a/pandora/event/tasks.py +++ b/pandora/event/tasks.py @@ -1,20 +1,26 @@ # -*- coding: utf-8 -*- -from celery.task import task +from app.celery import app from .models import Event ''' -@periodic_task(run_every=crontab(hour=7, minute=30), queue='encoding') +from celery.schedules import crontab + +@app.task(ignore_results=True, queue='encoding') def update_all_matches(**kwargs): ids = [e['id'] for e in Event.objects.all().values('id')] for i in ids: e = Event.objects.get(pk=i) e.update_matches() + +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(crontab(hour=7, minute=30), update_all_matches.s()) ''' -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_matches(eventId): event = Event.objects.get(pk=eventId) event.update_matches() diff --git a/pandora/home/apps.py b/pandora/home/apps.py index 90dc7137f..8bb98271d 100644 --- a/pandora/home/apps.py +++ b/pandora/home/apps.py @@ -2,4 +2,5 @@ from django.apps import AppConfig class HomeConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" name = 'home' diff --git a/pandora/home/migrations/0003_alter_item_data_alter_item_id_alter_item_index.py b/pandora/home/migrations/0003_alter_item_data_alter_item_id_alter_item_index.py new file mode 100644 index 000000000..84328309f --- /dev/null +++ b/pandora/home/migrations/0003_alter_item_data_alter_item_id_alter_item_index.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0002_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='item', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='item', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='item', + name='index', + field=models.IntegerField(default=-1), + ), + ] diff --git a/pandora/item/apps.py b/pandora/item/apps.py new file mode 100644 index 000000000..5c6b394d2 --- /dev/null +++ b/pandora/item/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ItemConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'item' diff --git a/pandora/item/managers.py b/pandora/item/managers.py index 654f1dfe3..f7398a0e7 100644 --- a/pandora/item/managers.py +++ b/pandora/item/managers.py @@ -33,7 +33,7 @@ def parseCondition(condition, user, owner=None): k = {'id': 'public_id'}.get(k, k) if not k: k = '*' - v = condition['value'] + v = condition.get('value', '') op = condition.get('operator') if not op: op = '=' @@ -62,6 +62,9 @@ def parseCondition(condition, user, owner=None): if k == 'list': key_type = '' + if k in ('width', 'height'): + key_type = 'integer' + if k == 'groups': if op == '==' and v == '$my': if not owner: @@ -86,8 +89,11 @@ def parseCondition(condition, user, owner=None): elif k == 'rendered': return Q(rendered=v) elif k == 'resolution': - q = parseCondition({'key': 'width', 'value': v[0], 'operator': op}, user) \ - & parseCondition({'key': 'height', 'value': v[1], 'operator': op}, user) + if isinstance(v, list) and len(v) == 2: + q = parseCondition({'key': 'width', 'value': v[0], 'operator': op}, user) \ + & parseCondition({'key': 'height', 'value': v[1], 'operator': op}, user) + else: + q = Q(id=0) if exclude: q = ~q return q @@ -318,6 +324,8 @@ class ItemManager(Manager): q |= Q(groups__in=user.groups.all()) rendered_q |= Q(groups__in=user.groups.all()) qs = qs.filter(q) + max_level = len(settings.CONFIG['rightsLevels']) + qs = qs.filter(level__lte=max_level) if settings.CONFIG.get('itemRequiresVideo') and level != 'admin': qs = qs.filter(rendered_q) return qs diff --git a/pandora/item/migrations/0001_initial.py b/pandora/item/migrations/0001_initial.py index ef095583c..a92c0e663 100644 --- a/pandora/item/migrations/0001_initial.py +++ b/pandora/item/migrations/0001_initial.py @@ -71,7 +71,7 @@ class Migration(migrations.Migration): ('poster_width', models.IntegerField(default=0)), ('poster_frame', models.FloatField(default=-1)), ('icon', models.ImageField(blank=True, default=None, upload_to=item.models.get_icon_path)), - ('torrent', models.FileField(blank=True, default=None, max_length=1000, upload_to=item.models.get_torrent_path)), + ('torrent', models.FileField(blank=True, default=None, max_length=1000)), ('stream_info', oxdjango.fields.DictField(default={}, editable=False)), ('stream_aspect', models.FloatField(default=1.3333333333333333)), ], diff --git a/pandora/item/migrations/0005_auto_20230710_0852.py b/pandora/item/migrations/0005_auto_20230710_0852.py new file mode 100644 index 000000000..156ef5266 --- /dev/null +++ b/pandora/item/migrations/0005_auto_20230710_0852.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.10 on 2023-07-10 08:52 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('item', '0004_json_cache'), + ] + + operations = [ + migrations.RemoveField( + model_name='item', + name='torrent', + ), + ] diff --git a/pandora/item/migrations/0006_id_bigint_jsonfied_and_more.py b/pandora/item/migrations/0006_id_bigint_jsonfied_and_more.py new file mode 100644 index 000000000..dd85858e3 --- /dev/null +++ b/pandora/item/migrations/0006_id_bigint_jsonfied_and_more.py @@ -0,0 +1,65 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('item', '0005_auto_20230710_0852'), + ] + + operations = [ + migrations.AlterField( + model_name='access', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='annotationsequence', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='description', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='facet', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='item', + name='cache', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='item', + name='data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='item', + name='external_data', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='item', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='item', + name='stream_info', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='itemfind', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/item/models.py b/pandora/item/models.py index 5172990c3..138b4fcd0 100644 --- a/pandora/item/models.py +++ b/pandora/item/models.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import json +import logging import os import re import shutil @@ -42,6 +43,7 @@ from user.utils import update_groups from user.models import Group import archive.models +logger = logging.getLogger('pandora.' + __name__) User = get_user_model() @@ -155,9 +157,6 @@ def get_icon_path(f, x): def get_poster_path(f, x): return get_path(f, 'poster.jpg') -def get_torrent_path(f, x): - return get_path(f, 'torrent.torrent') - class Item(models.Model): created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) @@ -183,7 +182,6 @@ class Item(models.Model): icon = models.ImageField(default=None, blank=True, upload_to=get_icon_path) - torrent = models.FileField(default=None, blank=True, max_length=1000, upload_to=get_torrent_path) stream_info = JSONField(default=dict, editable=False) # stream related fields @@ -231,6 +229,9 @@ class Item(models.Model): def editable(self, user): if user.is_anonymous: return False + max_level = len(settings.CONFIG['rightsLevels']) + if self.level > max_level: + return False if user.profile.capability('canEditMetadata') or \ user.is_staff or \ self.user == user or \ @@ -238,7 +239,7 @@ class Item(models.Model): return True return False - def edit(self, data): + def edit(self, data, is_task=False): data = data.copy() # FIXME: how to map the keys to the right place to write them to? if 'id' in data: @@ -255,11 +256,12 @@ class Item(models.Model): description = data.pop(key) if isinstance(description, dict): for value in description: + value = ox.sanitize_html(value) d, created = Description.objects.get_or_create(key=k, value=value) d.description = ox.sanitize_html(description[value]) d.save() else: - value = data.get(k, self.get(k, '')) + value = ox.sanitize_html(data.get(k, self.get(k, ''))) if not description: description = '' d, created = Description.objects.get_or_create(key=k, value=value) @@ -294,7 +296,10 @@ class Item(models.Model): self.data[key] = ox.escape_html(data[key]) p = self.save() if not settings.USE_IMDB and list(filter(lambda k: k in self.poster_keys, data)): - p = tasks.update_poster.delay(self.public_id) + if is_task: + tasks.update_poster(self.public_id) + else: + p = tasks.update_poster.delay(self.public_id) return p def update_external(self): @@ -473,7 +478,8 @@ class Item(models.Model): for a in self.annotations.all().order_by('id'): a.item = other - a.set_public_id() + with transaction.atomic(): + a.set_public_id() Annotation.objects.filter(id=a.id).update(item=other, public_id=a.public_id) try: other_sort = other.sort @@ -517,6 +523,7 @@ class Item(models.Model): cmd, stdout=open('/dev/null', 'w'), stderr=open('/dev/null', 'w'), close_fds=True) p.wait() os.unlink(tmp_output_txt) + os.close(fd) return True else: return None @@ -634,11 +641,11 @@ class Item(models.Model): if self.poster_height: i['posterRatio'] = self.poster_width / self.poster_height - if keys and 'source' in keys: - i['source'] = self.streams().exclude(file__data='').exists() + if keys and 'hasSource' in keys: + i['hasSource'] = self.streams().exclude(file__data='').exists() streams = self.streams() - i['durations'] = [s.duration for s in streams] + i['durations'] = [s[0] for s in streams.values_list('duration')] i['duration'] = sum(i['durations']) i['audioTracks'] = self.audio_tracks() if not i['audioTracks']: @@ -694,10 +701,12 @@ class Item(models.Model): else: values = self.get(key) if values: + values = [ox.sanitize_html(value) for value in values] for d in Description.objects.filter(key=key, value__in=values): i['%sdescription' % key][d.value] = d.description else: - qs = Description.objects.filter(key=key, value=self.get(key, '')) + value = ox.sanitize_html(self.get(key, '')) + qs = Description.objects.filter(key=key, value=value) i['%sdescription' % key] = '' if qs.count() == 0 else qs[0].description if keys: info = {} @@ -855,7 +864,7 @@ class Item(models.Model): values = list(set(values)) else: values = self.get(key, '') - if isinstance(values, list): + if values and isinstance(values, list) and isinstance(values[0], str): save(key, '\n'.join(values)) else: save(key, values) @@ -1017,10 +1026,14 @@ class Item(models.Model): set_value(s, name, value) elif sort_type == 'person': value = sortNames(self.get(source, [])) + if value is None: + value = '' value = utils.sort_string(value)[:955] set_value(s, name, value) elif sort_type == 'string': value = self.get(source, '') + if value is None: + value = '' if isinstance(value, list): value = ','.join([str(v) for v in value]) value = utils.sort_string(value)[:955] @@ -1099,7 +1112,11 @@ class Item(models.Model): _current_values.append(value[0]) current_values = _current_values - current_values = list(set(current_values)) + try: + current_values = list(set(current_values)) + except: + logger.error('invalid facet data for %s: %s', key, current_values) + current_values = [] current_values = [ox.decode_html(ox.strip_tags(v)) for v in current_values] current_values = [unicodedata.normalize('NFKD', v) for v in current_values] self.update_facet_values(key, current_values) @@ -1192,7 +1209,7 @@ class Item(models.Model): if not r: return False path = video.name - duration = sum(item.cache['durations']) + duration = sum(self.item.cache['durations']) else: path = stream.media.path duration = stream.info['duration'] @@ -1288,90 +1305,6 @@ class Item(models.Model): self.files.filter(selected=True).update(selected=False) self.save() - def get_torrent(self, request): - if self.torrent: - self.torrent.seek(0) - data = ox.torrent.bdecode(self.torrent.read()) - url = request.build_absolute_uri("%s/torrent/" % self.get_absolute_url()) - if url.startswith('https://'): - url = 'http' + url[5:] - data['url-list'] = ['%s%s' % (url, u.split('torrent/')[1]) for u in data['url-list']] - return ox.torrent.bencode(data) - - def make_torrent(self): - if not settings.CONFIG['video'].get('torrent'): - return - streams = self.streams() - if streams.count() == 0: - return - base = self.path('torrent') - base = os.path.abspath(os.path.join(settings.MEDIA_ROOT, base)) - if not isinstance(base, bytes): - base = base.encode('utf-8') - if os.path.exists(base): - shutil.rmtree(base) - ox.makedirs(base) - - filename = utils.safe_filename(ox.decode_html(self.get('title'))) - base = self.path('torrent/%s' % filename) - base = os.path.abspath(os.path.join(settings.MEDIA_ROOT, base)) - size = 0 - duration = 0.0 - if streams.count() == 1: - v = streams[0] - media_path = v.media.path - extension = media_path.split('.')[-1] - url = "%s/torrent/%s.%s" % (self.get_absolute_url(), - quote(filename.encode('utf-8')), - extension) - video = "%s.%s" % (base, extension) - if not isinstance(media_path, bytes): - media_path = media_path.encode('utf-8') - if not isinstance(video, bytes): - video = video.encode('utf-8') - media_path = os.path.relpath(media_path, os.path.dirname(video)) - os.symlink(media_path, video) - size = v.media.size - duration = v.duration - else: - url = "%s/torrent/" % self.get_absolute_url() - part = 1 - ox.makedirs(base) - for v in streams: - media_path = v.media.path - extension = media_path.split('.')[-1] - video = "%s/%s.Part %d.%s" % (base, filename, part, extension) - part += 1 - if not isinstance(media_path, bytes): - media_path = media_path.encode('utf-8') - if not isinstance(video, bytes): - video = video.encode('utf-8') - media_path = os.path.relpath(media_path, os.path.dirname(video)) - os.symlink(media_path, video) - size += v.media.size - duration += v.duration - video = base - - torrent = '%s.torrent' % base - url = "http://%s%s" % (settings.CONFIG['site']['url'], url) - meta = { - 'filesystem_encoding': 'utf-8', - 'target': torrent, - 'url-list': url, - } - if duration: - meta['playtime'] = ox.format_duration(duration*1000)[:-4] - - # slightly bigger torrent file but better for streaming - piece_size_pow2 = 15 # 1 mbps -> 32KB pieces - if size / duration >= 1000000: - piece_size_pow2 = 16 # 2 mbps -> 64KB pieces - meta['piece_size_pow2'] = piece_size_pow2 - - ox.torrent.create_torrent(video, settings.TRACKER_URL, meta) - self.torrent.name = torrent[len(settings.MEDIA_ROOT)+1:] - self.save() - def audio_tracks(self): tracks = [f['language'] for f in self.files.filter(selected=True).filter(Q(is_video=True) | Q(is_audio=True)).values('language') @@ -1379,11 +1312,10 @@ class Item(models.Model): return sorted(set(tracks)) def streams(self, track=None): + files = self.files.filter(selected=True).filter(Q(is_audio=True) | Q(is_video=True)) qs = archive.models.Stream.objects.filter( - source=None, available=True, file__item=self, file__selected=True - ).filter( - Q(file__is_audio=True) | Q(file__is_video=True) - ) + file__in=files, source=None, available=True + ).select_related() if not track: tracks = self.audio_tracks() if len(tracks) > 1: @@ -1422,7 +1354,6 @@ class Item(models.Model): self.select_frame() self.make_poster() self.make_icon() - self.make_torrent() self.rendered = streams.count() > 0 self.save() if self.rendered: @@ -1444,7 +1375,7 @@ class Item(models.Model): self.poster_height = self.poster.height self.poster_width = self.poster.width self.clear_poster_cache(self.poster.path) - if self.cache.get('posterRatio') != self.poster_width / self.poster_height: + if self.poster_width and self.cache.get('posterRatio') != self.poster_width / self.poster_height: self.update_cache(poster_width=self.poster_width, poster_height=self.poster_height) @@ -1608,8 +1539,15 @@ class Item(models.Model): cmd += ['-l', timeline] if frame: cmd += ['-f', frame] - p = subprocess.Popen(cmd, close_fds=True) - p.wait() + if settings.ITEM_ICON_DATA: + cmd += '-d', '-' + data = self.json() + data = utils.normalize_dict('NFC', data) + p = subprocess.Popen(cmd, stdin=subprocess.PIPE, close_fds=True) + p.communicate(json.dumps(data, default=to_json).encode('utf-8')) + else: + p = subprocess.Popen(cmd, close_fds=True) + p.wait() # remove cached versions icon = os.path.abspath(os.path.join(settings.MEDIA_ROOT, icon)) for f in glob(icon.replace('.jpg', '*.jpg')): @@ -1621,11 +1559,13 @@ class Item(models.Model): return icon def add_empty_clips(self): + if not settings.EMPTY_CLIPS: + return subtitles = utils.get_by_key(settings.CONFIG['layers'], 'isSubtitles', True) if not subtitles: return # otherwise add empty 5 seconds annotation every minute - duration = sum([s.duration for s in self.streams()]) + duration = sum([s[0] for s in self.streams().values_list('duration')]) layer = subtitles['id'] # FIXME: allow annotations from no user instead? user = User.objects.all().order_by('id')[0] @@ -1874,6 +1814,8 @@ class Description(models.Model): value = models.CharField(max_length=1000, db_index=True) description = models.TextField() + def __str__(self): + return "%s=%s" % (self.key, self.value) class AnnotationSequence(models.Model): item = models.OneToOneField('Item', related_name='_annotation_sequence', on_delete=models.CASCADE) @@ -1889,13 +1831,12 @@ class AnnotationSequence(models.Model): @classmethod def nextid(cls, item): - with transaction.atomic(): - s, created = cls.objects.get_or_create(item=item) - if created: - nextid = s.value - else: - cursor = connection.cursor() - sql = "UPDATE %s SET value = value + 1 WHERE item_id = %s RETURNING value" % (cls._meta.db_table, item.id) - cursor.execute(sql) - nextid = cursor.fetchone()[0] + s, created = cls.objects.get_or_create(item=item) + if created: + nextid = s.value + else: + cursor = connection.cursor() + sql = "UPDATE %s SET value = value + 1 WHERE item_id = %s RETURNING value" % (cls._meta.db_table, item.id) + cursor.execute(sql) + nextid = cursor.fetchone()[0] return "%s/%s" % (item.public_id, ox.toAZ(nextid)) diff --git a/pandora/item/site.py b/pandora/item/site.py index cb186eab3..a67871db8 100644 --- a/pandora/item/site.py +++ b/pandora/item/site.py @@ -24,10 +24,6 @@ urls = [ re_path(r'^(?P[A-Z0-9].*)/(?P\d+)p(?P\d*)\.(?Pwebm|ogv|mp4)$', views.video), re_path(r'^(?P[A-Z0-9].*)/(?P\d+)p(?P\d*)\.(?P.+)\.(?Pwebm|ogv|mp4)$', views.video), - #torrent - re_path(r'^(?P[A-Z0-9].*)/torrent$', views.torrent), - re_path(r'^(?P[A-Z0-9].*)/torrent/(?P.*?)$', views.torrent), - #export re_path(r'^(?P[A-Z0-9].*)/json$', views.item_json), re_path(r'^(?P[A-Z0-9].*)/xml$', views.item_xml), diff --git a/pandora/item/tasks.py b/pandora/item/tasks.py index a7c0c68a3..92b3b08fd 100644 --- a/pandora/item/tasks.py +++ b/pandora/item/tasks.py @@ -2,31 +2,35 @@ from datetime import timedelta, datetime from urllib.parse import quote +import xml.etree.ElementTree as ET import gzip import os import random import logging -from celery.task import task, periodic_task +from app.celery import app +from celery.schedules import crontab from django.conf import settings from django.db import connection, transaction from django.db.models import Q -from ox.utils import ET from app.utils import limit_rate from taskqueue.models import Task -logger = logging.getLogger(__name__) +logger = logging.getLogger('pandora.' + __name__) - -@periodic_task(run_every=timedelta(days=1), queue='encoding') +@app.task(queue='encoding') def cronjob(**kwargs): if limit_rate('item.tasks.cronjob', 8 * 60 * 60): update_random_sort() update_random_clip_sort() clear_cache.delay() +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(timedelta(days=1), cronjob.s()) + def update_random_sort(): from . import models if list(filter(lambda f: f['id'] == 'random', settings.CONFIG['itemKeys'])): @@ -54,7 +58,7 @@ def update_random_clip_sort(): cursor.execute(row) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_clips(public_id): from . import models try: @@ -63,7 +67,7 @@ def update_clips(public_id): return item.clips.all().update(user=item.user.id) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_poster(public_id): from . import models try: @@ -81,7 +85,7 @@ def update_poster(public_id): icon=item.icon.name ) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_file_paths(public_id): from . import models try: @@ -90,7 +94,7 @@ def update_file_paths(public_id): return item.update_file_paths() -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_external(public_id): from . import models try: @@ -99,7 +103,7 @@ def update_external(public_id): return item.update_external() -@task(queue="encoding") +@app.task(queue="encoding") def update_timeline(public_id): from . import models try: @@ -109,7 +113,7 @@ def update_timeline(public_id): item.update_timeline(async_=False) Task.finish(item) -@task(queue="encoding") +@app.task(queue="encoding") def rebuild_timeline(public_id): from . import models i = models.Item.objects.get(public_id=public_id) @@ -117,7 +121,7 @@ def rebuild_timeline(public_id): s.make_timeline() i.update_timeline(async_=False) -@task(queue="encoding") +@app.task(queue="encoding") def load_subtitles(public_id): from . import models try: @@ -130,7 +134,7 @@ def load_subtitles(public_id): item.update_facets() -@task(queue="encoding") +@app.task(queue="encoding") def extract_clip(public_id, in_, out, resolution, format, track=None): from . import models try: @@ -142,7 +146,7 @@ def extract_clip(public_id, in_, out, resolution, format, track=None): return False -@task(queue="encoding") +@app.task(queue="encoding") def clear_cache(days=60): import subprocess path = os.path.join(settings.MEDIA_ROOT, 'media') @@ -156,7 +160,7 @@ def clear_cache(days=60): subprocess.check_output(cmd) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_sitemap(base_url): from . import models sitemap = os.path.abspath(os.path.join(settings.MEDIA_ROOT, 'sitemap.xml.gz')) @@ -356,7 +360,7 @@ def update_sitemap(base_url): f.write(data) -@task(queue='default') +@app.task(queue='default') def bulk_edit(data, username): from django.db import transaction from . import models @@ -367,5 +371,5 @@ def bulk_edit(data, username): if item.editable(user): with transaction.atomic(): item.refresh_from_db() - response = edit_item(user, item, data) + response = edit_item(user, item, data, is_task=True) return {} diff --git a/pandora/item/timelines.py b/pandora/item/timelines.py index f6ff9f72e..a39ceee6b 100644 --- a/pandora/item/timelines.py +++ b/pandora/item/timelines.py @@ -27,7 +27,7 @@ def join_tiles(source_paths, durations, target_path): def get_file_info(file_name): for mode in modes: - if re.match('^timeline' + mode + '64p\d+\.jpg', file_name): + if re.match(r'^timeline' + mode + r'64p\d+\.jpg', file_name): return { 'file': file_name, 'mode': mode, @@ -71,7 +71,7 @@ def join_tiles(source_paths, durations, target_path): if not w or large_tile_i < large_tile_n - 1: w = 60 data['target_images']['large'] = data['target_images']['large'].resize( - (w, small_tile_h), Image.ANTIALIAS + (w, small_tile_h), Image.LANCZOS ) if data['target_images']['small']: data['target_images']['small'].paste( @@ -90,7 +90,7 @@ def join_tiles(source_paths, durations, target_path): if data['full_tile_widths'][0]: resized = data['target_images']['large'].resize(( data['full_tile_widths'][0], large_tile_h - ), Image.ANTIALIAS) + ), Image.LANCZOS) data['target_images']['full'].paste(resized, (data['full_tile_offset'], 0)) data['full_tile_offset'] += data['full_tile_widths'][0] data['full_tile_widths'] = data['full_tile_widths'][1:] @@ -196,7 +196,7 @@ def join_tiles(source_paths, durations, target_path): #print(image_file) image_file = '%stimeline%s%dp.jpg' % (target_path, full_tile_mode, small_tile_h) data['target_images']['full'].resize( - (full_tile_w, small_tile_h), Image.ANTIALIAS + (full_tile_w, small_tile_h), Image.LANCZOS ).save(image_file) #print(image_file) @@ -227,8 +227,8 @@ def split_tiles(path, paths, durations): file_names = list(filter(is_timeline_file, os.listdir(path))) tiles = {} for file_name in file_names: - mode = re.split('\d+', file_name[8:])[0] - split = re.split('[a-z]+', file_name[8 + len(mode):-4]) + mode = re.split(r'\d+', file_name[8:])[0] + split = re.split(r'[a-z]+', file_name[8 + len(mode):-4]) height, index = map(lambda x: int(x) if len(x) else -1, split) if mode not in tiles: tiles[mode] = {} diff --git a/pandora/item/utils.py b/pandora/item/utils.py index fce7d7247..70a88086e 100644 --- a/pandora/item/utils.py +++ b/pandora/item/utils.py @@ -61,7 +61,7 @@ def sort_title(title): title = sort_string(title) #title - title = re.sub('[\'!¿¡,\.;\-"\:\*\[\]]', '', title) + title = re.sub(r'[\'!¿¡,\.;\-"\:\*\[\]]', '', title) return title.strip() def get_positions(ids, pos, decode_id=False): @@ -75,6 +75,7 @@ def get_positions(ids, pos, decode_id=False): if decode_id: positions[i] = ids.index(ox.fromAZ(i)) else: + i = unicodedata.normalize('NFKD', i) positions[i] = ids.index(i) except: pass diff --git a/pandora/item/views.py b/pandora/item/views.py index 62c6b21ae..4c5943e9a 100644 --- a/pandora/item/views.py +++ b/pandora/item/views.py @@ -16,12 +16,14 @@ from wsgiref.util import FileWrapper from django.conf import settings from ox.utils import json, ET - -from oxdjango.decorators import login_required_json -from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response -from oxdjango.http import HttpFileResponse import ox +from oxdjango.api import actions +from oxdjango.decorators import login_required_json +from oxdjango.http import HttpFileResponse +from oxdjango.shortcuts import render_to_json_response, get_object_or_404_json, json_response +import oxdjango + from . import models from . import utils from . import tasks @@ -32,7 +34,6 @@ from clip.models import Clip from user.models import has_capability from changelog.models import add_changelog -from oxdjango.api import actions def _order_query(qs, sort, prefix='sort__'): @@ -308,7 +309,7 @@ def find(request, data): responsive UI: First leave out `keys` to get totals as fast as possible, then pass `positions` to get the positions of previously selected items, finally make the query with the `keys` you need and an appropriate `range`. - For more examples, see https://wiki.0x2620.org/wiki/pandora/QuerySyntax. + For more examples, see https://code.0x2620.org/0x2620/pandora/wiki/QuerySyntax. see: add, edit, get, lookup, remove, upload ''' if settings.JSON_DEBUG: @@ -533,7 +534,7 @@ def get(request, data): return render_to_json_response(response) actions.register(get) -def edit_item(user, item, data): +def edit_item(user, item, data, is_task=False): data = data.copy() update_clips = False response = json_response(status=200, text='ok') @@ -558,7 +559,7 @@ def edit_item(user, item, data): user_groups = set([g.name for g in user.groups.all()]) other_groups = list(groups - user_groups) data['groups'] = [g for g in data['groups'] if g in user_groups] + other_groups - r = item.edit(data) + r = item.edit(data, is_task=is_task) if r: r.wait() if update_clips: @@ -595,7 +596,7 @@ def add(request, data): if p: p.wait() else: - i.make_poster() + item.make_poster() del data['title'] if data: response = edit_item(request.user, item, data) @@ -948,9 +949,11 @@ def timeline(request, id, size, position=-1, format='jpg', mode=None): if not item.access(request.user): return HttpResponseForbidden() + modes = [t['id'] for t in settings.CONFIG['timelines']] if not mode: mode = 'antialias' - modes = [t['id'] for t in settings.CONFIG['timelines']] + if mode not in modes: + mode = modes[0] if mode not in modes: raise Http404 modes.pop(modes.index(mode)) @@ -977,6 +980,8 @@ def download_source(request, id, part=None): item = get_object_or_404(models.Item, public_id=id) if not item.access(request.user): return HttpResponseForbidden() + if not has_capability(request.user, 'canDownloadSource'): + return HttpResponseForbidden() if part: part = int(part) - 1 else: @@ -1001,7 +1006,9 @@ def download_source(request, id, part=None): response['Content-Disposition'] = "attachment; filename*=UTF-8''%s" % quote(filename.encode('utf-8')) return response -def download(request, id, resolution=None, format='webm', part=None): +def download(request, id, resolution=None, format=None, part=None): + if format is None: + format = settings.CONFIG['video']['formats'][0] item = get_object_or_404(models.Item, public_id=id) if not resolution or int(resolution) not in settings.CONFIG['video']['resolutions']: resolution = max(settings.CONFIG['video']['resolutions']) @@ -1044,27 +1051,6 @@ def download(request, id, resolution=None, format='webm', part=None): response['Content-Disposition'] = "attachment; filename*=UTF-8''%s" % quote(filename.encode('utf-8')) return response -def torrent(request, id, filename=None): - item = get_object_or_404(models.Item, public_id=id) - if not item.access(request.user): - return HttpResponseForbidden() - if not item.torrent: - raise Http404 - if not filename or filename.endswith('.torrent'): - response = HttpResponse(item.get_torrent(request), - content_type='application/x-bittorrent') - filename = utils.safe_filename("%s.torrent" % item.get('title')) - response['Content-Disposition'] = "attachment; filename*=UTF-8''%s" % quote(filename.encode('utf-8')) - return response - while filename.startswith('/'): - filename = filename[1:] - filename = filename.replace('/../', '/') - filename = item.path('torrent/%s' % filename) - filename = os.path.abspath(os.path.join(settings.MEDIA_ROOT, filename)) - response = HttpFileResponse(filename) - response['Content-Disposition'] = "attachment; filename*=UTF-8''%s" % \ - quote(os.path.basename(filename.encode('utf-8'))) - return response def video(request, id, resolution, format, index=None, track=None): resolution = int(resolution) @@ -1286,12 +1272,6 @@ def atom_xml(request): el.text = "1:1" if has_capability(request.user, 'canDownloadVideo'): - if item.torrent: - el = ET.SubElement(entry, "link") - el.attrib['rel'] = 'enclosure' - el.attrib['type'] = 'application/x-bittorrent' - el.attrib['href'] = '%s/torrent/' % page_link - el.attrib['length'] = '%s' % ox.get_torrent_size(item.torrent.path) # FIXME: loop over streams # for s in item.streams().filter(resolution=max(settings.CONFIG['video']['resolutions'])): for s in item.streams().filter(source=None): @@ -1314,12 +1294,15 @@ def atom_xml(request): 'application/atom+xml' ) + def oembed(request): format = request.GET.get('format', 'json') maxwidth = int(request.GET.get('maxwidth', 640)) maxheight = int(request.GET.get('maxheight', 480)) - url = request.GET['url'] + url = request.GET.get('url') + if not url: + raise Http404 parts = urlparse(url).path.split('/') if len(parts) < 2: raise Http404 diff --git a/pandora/itemlist/apps.py b/pandora/itemlist/apps.py new file mode 100644 index 000000000..541419f0d --- /dev/null +++ b/pandora/itemlist/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class ItemListConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'itemlist' + diff --git a/pandora/itemlist/migrations/0004_alter_list_description_alter_list_id_and_more.py b/pandora/itemlist/migrations/0004_alter_list_description_alter_list_id_and_more.py new file mode 100644 index 000000000..f4f04ebf8 --- /dev/null +++ b/pandora/itemlist/migrations/0004_alter_list_description_alter_list_id_and_more.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import itemlist.models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('itemlist', '0003_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='list', + name='description', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='list', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='list', + name='poster_frames', + field=oxdjango.fields.JSONField(default=list, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='list', + name='query', + field=oxdjango.fields.JSONField(default=itemlist.models.default_query, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='list', + name='sort', + field=oxdjango.fields.JSONField(default=itemlist.models.get_listsort, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='list', + name='status', + field=models.CharField(default='private', max_length=20), + ), + migrations.AlterField( + model_name='list', + name='type', + field=models.CharField(default='static', max_length=255), + ), + migrations.AlterField( + model_name='listitem', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='position', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/itemlist/models.py b/pandora/itemlist/models.py index ad2cfb141..2c7a49017 100644 --- a/pandora/itemlist/models.py +++ b/pandora/itemlist/models.py @@ -26,6 +26,9 @@ def get_icon_path(f, x): return get_path(f, 'icon.jpg') def get_listview(): return settings.CONFIG['user']['ui']['listView'] def get_listsort(): return tuple(settings.CONFIG['user']['ui']['listSort']) +def default_query(): + return {"static": True} + class List(models.Model): class Meta: @@ -38,7 +41,7 @@ class List(models.Model): name = models.CharField(max_length=255) status = models.CharField(max_length=20, default='private') _status = ['private', 'public', 'featured'] - query = JSONField(default=lambda: {"static": True}, editable=False) + query = JSONField(default=default_query, editable=False) type = models.CharField(max_length=255, default='static') description = models.TextField(default='') @@ -178,7 +181,7 @@ class List(models.Model): self.status = value elif key == 'name': - data['name'] = re.sub(' \[\d+\]$', '', data['name']).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data['name']).strip() if not data['name']: data['name'] = "Untitled" name = data['name'] diff --git a/pandora/itemlist/views.py b/pandora/itemlist/views.py index 04edcc9d5..a55f5f044 100644 --- a/pandora/itemlist/views.py +++ b/pandora/itemlist/views.py @@ -84,6 +84,11 @@ def findLists(request, data): for x in data.get('query', {}).get('conditions', []) ) + is_personal = request.user.is_authenticated and any( + (x['key'] == 'user' and x['value'] == request.user.username and x['operator'] == '==') + for x in data.get('query', {}).get('conditions', []) + ) + if is_section_request: qs = query['qs'] if not is_featured and not request.user.is_anonymous: @@ -92,6 +97,9 @@ def findLists(request, data): else: qs = _order_query(query['qs'], query['sort']) + if is_personal and request.user.profile.ui.get('hidden', {}).get('lists'): + qs = qs.exclude(name__in=request.user.profile.ui['hidden']['lists']) + response = json_response() if 'keys' in data: qs = qs[query['range'][0]:query['range'][1]] @@ -234,7 +242,7 @@ def addList(request, data): 'type' and 'view'. see: editList, findLists, getList, removeList, sortLists ''' - data['name'] = re.sub(' \[\d+\]$', '', data.get('name', 'Untitled')).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data.get('name', 'Untitled')).strip() name = data['name'] if not name: name = "Untitled" @@ -412,7 +420,10 @@ def sortLists(request, data): models.Position.objects.filter(section=section, list=l).exclude(id=pos.id).delete() else: for i in ids: - l = get_list_or_404_json(i) + try: + l = get_list_or_404_json(i) + except: + continue pos, created = models.Position.objects.get_or_create(list=l, user=request.user, section=section) if pos.position != position: diff --git a/pandora/log/apps.py b/pandora/log/apps.py new file mode 100644 index 000000000..b7cd1d6a3 --- /dev/null +++ b/pandora/log/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class LogConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'log' + diff --git a/pandora/log/migrations/0002_alter_log_id_alter_log_url.py b/pandora/log/migrations/0002_alter_log_id_alter_log_url.py new file mode 100644 index 000000000..4fe906450 --- /dev/null +++ b/pandora/log/migrations/0002_alter_log_id_alter_log_url.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('log', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='log', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='log', + name='url', + field=models.CharField(default='', max_length=1000), + ), + ] diff --git a/pandora/log/tasks.py b/pandora/log/tasks.py index 768fd7310..bdd9cd1f1 100644 --- a/pandora/log/tasks.py +++ b/pandora/log/tasks.py @@ -2,10 +2,15 @@ from datetime import timedelta, datetime -from celery.task import periodic_task +from app.celery import app +from celery.schedules import crontab from . import models -@periodic_task(run_every=timedelta(days=1), queue='encoding') +@app.task(queue='encoding') def cronjob(**kwargs): models.Log.objects.filter(modified__lt=datetime.now()-timedelta(days=30)).delete() + +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(timedelta(days=1), cronjob.s()) diff --git a/pandora/log/views.py b/pandora/log/views.py index d8dfa4f42..c0fa76756 100644 --- a/pandora/log/views.py +++ b/pandora/log/views.py @@ -65,7 +65,7 @@ actions.register(removeErrorLogs, cache=False) def parse_query(data, user): query = {} query['range'] = [0, 100] - query['sort'] = [{'key':'name', 'operator':'+'}] + query['sort'] = [{'key': 'modified', 'operator': '-'}] for key in ('keys', 'group', 'list', 'range', 'sort', 'query'): if key in data: query[key] = data[key] diff --git a/pandora/manage.py b/pandora/manage.py index 4800a3661..56cc6a7b2 100755 --- a/pandora/manage.py +++ b/pandora/manage.py @@ -10,7 +10,8 @@ def activate_venv(base): bin_path = os.path.join(base, 'bin') if bin_path not in old_os_path: os.environ['PATH'] = os.path.join(base, 'bin') + os.pathsep + old_os_path - site_packages = os.path.join(base, 'lib', 'python%s' % sys.version[:3], 'site-packages') + version = '%s.%s' % (sys.version_info.major, sys.version_info.minor) + site_packages = os.path.join(base, 'lib', 'python%s' % version, 'site-packages') prev_sys_path = list(sys.path) import site site.addsitedir(site_packages) diff --git a/pandora/mobile/__init__.py b/pandora/mobile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pandora/mobile/admin.py b/pandora/mobile/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/pandora/mobile/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/pandora/mobile/apps.py b/pandora/mobile/apps.py new file mode 100644 index 000000000..708babdbe --- /dev/null +++ b/pandora/mobile/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MobileConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'mobile' diff --git a/pandora/mobile/migrations/__init__.py b/pandora/mobile/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pandora/mobile/models.py b/pandora/mobile/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/pandora/mobile/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/pandora/mobile/tests.py b/pandora/mobile/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/pandora/mobile/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/pandora/mobile/views.py b/pandora/mobile/views.py new file mode 100644 index 000000000..57a190538 --- /dev/null +++ b/pandora/mobile/views.py @@ -0,0 +1,120 @@ +from django.conf import settings +from django.shortcuts import render + +import ox + + +def index(request, fragment): + from item.models import Item, Annotation + from edit.models import Edit + from document.models import Document + from edit.views import _order_clips + context = {} + parts = fragment.split('/') + if parts[0] in ('document', 'documents'): + type = 'document' + id = parts[1] + page = None + crop = None + if len(parts) == 3: + rect = parts[2].split(',') + if len(rect) == 1: + page = rect[0] + else: + crop = rect + document = Document.objects.filter(id=ox.fromAZ(id)).first() + if document and document.access(request.user): + context['title'] = document.data['title'] + if document.data.get('description'): + context['description'] = document.data['description'] + link = request.build_absolute_uri(document.get_absolute_url()) + # FIXME: get preview image or fragment parse from url + public_id = ox.toAZ(document.id) + if document.extension != 'html': + preview = '/documents/%s/512p.jpg' % public_id + if page: + preview = '/documents/%s/512p%s.jpg' % (public_id, page) + if crop: + preview = '/documents/%s/512p%s.jpg' % (public_id, ','.join(crop)) + context['preview'] = request.build_absolute_uri(preview) + + elif parts[0] == 'edits': + type = 'edit' + id = parts[1] + id = id.split(':') + username = id[0] + name = ":".join(id[1:]) + name = name.replace('_', ' ') + edit = Edit.objects.filter(user__username=username, name=name).first() + if edit and edit.accessible(request.user): + link = request.build_absolute_uri('/m' + edit.get_absolute_url()) + context['title'] = name + context['description'] = edit.description.split('\n\n')[0] + resolution = max(settings.CONFIG['video']['resolutions']) + if len(parts) > 3: + sort = parts[3] + if sort[0] in ('+', '-'): + sort = [{ + 'operator': sort[0], + 'key': sort[1:], + }] + else: + sort = [{ + 'operator': '+', + 'key': sort, + }] + clips = _order_clips(edit, sort) + else: + clips = edit.get_clips(request.user) + + preview = None + + if len(parts) >= 3 and ':' in parts[-1]: + ts = ox.parse_timecode(parts[-1]) + position = 0 + for clip in clips: + c = clip.json() + if ts > position and ts < position + c['duration']: + start = ts - position + c.get('in', 0) + preview = '/%s/%sp%0.03f.jpg' % (clip.item.public_id, resolution, float(start)) + break + position += c['duration'] + if not preview: + clip = clips.first() + if clip: + start = clip.json()['in'] + preview = '/%s/%sp%0.03f.jpg' % (clip.item.public_id, resolution, float(start)) + if preview: + context['preview'] = request.build_absolute_uri(preview) + else: + type = 'item' + id = parts[0] + item = Item.objects.filter(public_id=id).first() + if item and item.access(request.user): + link = request.build_absolute_uri(item.get_absolute_url()) + if len(parts) > 1 and parts[1] in ('editor', 'player'): + parts = [parts[0]] + parts[2:] + if len(parts) > 1: + aid = '%s/%s' % (id, parts[1]) + annotation = Annotation.objects.filter(public_id=aid).first() + if annotation: + inout = [annotation.start, annotation.end] + else: + inout = parts[1] + if '-' in inout: + inout = inout.split('-') + else: + inout = inout.split(',') + print(inout) + if len(inout) == 3: + inout.pop(1) + if len(inout) == 2: + inout = [ox.parse_timecode(p) for p in inout] + context['preview'] = link + '/480p%s.jpg' % inout[0] + else: + context['preview'] = link + '/480p.jpg' + context['title'] = item.get('title') + if context: + context['url'] = request.build_absolute_uri('/m/' + fragment) + context['settings'] = settings + return render(request, "mobile/index.html", context) diff --git a/pandora/news/apps.py b/pandora/news/apps.py new file mode 100644 index 000000000..8ebaec283 --- /dev/null +++ b/pandora/news/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class NewsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'news' diff --git a/pandora/news/managers.py b/pandora/news/managers.py index bb1f242d1..213d5ee18 100644 --- a/pandora/news/managers.py +++ b/pandora/news/managers.py @@ -11,7 +11,7 @@ keymap = { default_key = 'name' def parseCondition(condition, user): - k = condition.get('key', defauly_key) + k = condition.get('key', default_key) k = keymap.get(k, k) if not k: k = default_key diff --git a/pandora/news/migrations/0002_alter_news_id.py b/pandora/news/migrations/0002_alter_news_id.py new file mode 100644 index 000000000..06cfafd2c --- /dev/null +++ b/pandora/news/migrations/0002_alter_news_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='news', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/oxdjango/api/views.py b/pandora/oxdjango/api/views.py index 3a4a2e899..ce7df8924 100644 --- a/pandora/oxdjango/api/views.py +++ b/pandora/oxdjango/api/views.py @@ -34,8 +34,15 @@ def api(request): return response if request.META.get('CONTENT_TYPE') == 'application/json': r = json.loads(request.body.decode('utf-8')) - action = r['action'] - data = r.get('data', {}) + if 'action' not in r: + logger.error("invalid api request: %s", r) + response = render_to_json_response(json_response(status=400, + text='Invalid request')) + response['Access-Control-Allow-Origin'] = '*' + return response + else: + action = r['action'] + data = r.get('data', {}) else: action = request.POST['action'] data = json.loads(request.POST.get('data', '{}')) diff --git a/pandora/oxdjango/fields.py b/pandora/oxdjango/fields.py index daebd27c4..75ddd3d8a 100644 --- a/pandora/oxdjango/fields.py +++ b/pandora/oxdjango/fields.py @@ -5,12 +5,12 @@ import copy from django.db import models from django.utils import datetime_safe -import django.contrib.postgres.fields from django.core.serializers.json import DjangoJSONEncoder from ox.utils import json -class JSONField(django.contrib.postgres.fields.JSONField): + +class JSONField(models.JSONField): def __init__(self, *args, **kwargs): if 'encoder' not in kwargs: @@ -104,7 +104,7 @@ class TupleField(DictField): try: from south.modelsinspector import add_introspection_rules - add_introspection_rules([], ["^oxdjango\.fields\.DictField"]) - add_introspection_rules([], ["^oxdjango\.fields\.TupleField"]) + add_introspection_rules([], [r"^oxdjango\.fields\.DictField"]) + add_introspection_rules([], [r"^oxdjango\.fields\.TupleField"]) except: pass diff --git a/pandora/oxdjango/query.py b/pandora/oxdjango/query.py index 465007170..474f894ae 100644 --- a/pandora/oxdjango/query.py +++ b/pandora/oxdjango/query.py @@ -37,12 +37,12 @@ class NullsLastQuery(Query): obj.nulls_last = self.nulls_last return obj - def get_compiler(self, using=None, connection=None): + def get_compiler(self, using=None, connection=None, elide_empty=True): if using is None and connection is None: raise ValueError("Need either using or connection") if using: connection = connections[using] - return NullLastSQLCompiler(self, connection, using) + return NullLastSQLCompiler(self, connection, using, elide_empty) class QuerySet(django.db.models.query.QuerySet): diff --git a/pandora/person/apps.py b/pandora/person/apps.py new file mode 100644 index 000000000..100989a3b --- /dev/null +++ b/pandora/person/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class PersonConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'person' + diff --git a/pandora/person/migrations/0003_alter_person_id.py b/pandora/person/migrations/0003_alter_person_id.py new file mode 100644 index 000000000..36e5f691d --- /dev/null +++ b/pandora/person/migrations/0003_alter_person_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0002_auto_20190723_1446'), + ] + + operations = [ + migrations.AlterField( + model_name='person', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/person/tasks.py b/pandora/person/tasks.py index a94ed2737..4dcb7bad8 100644 --- a/pandora/person/tasks.py +++ b/pandora/person/tasks.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -from celery.task import task from . import models +from app.celery import app -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_itemsort(id): try: p = models.Person.objects.get(pk=id) @@ -13,7 +13,7 @@ def update_itemsort(id): except models.Person.DoesNotExist: pass -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_file_paths(id): from item.models import Item, ItemFind p = models.Person.objects.get(pk=id) diff --git a/pandora/place/apps.py b/pandora/place/apps.py new file mode 100644 index 000000000..6dcfb91a3 --- /dev/null +++ b/pandora/place/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class PlaceConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'place' + diff --git a/pandora/place/migrations/0003_alter_place_countrycode_alter_place_id_and_more.py b/pandora/place/migrations/0003_alter_place_countrycode_alter_place_id_and_more.py new file mode 100644 index 000000000..66a7c3550 --- /dev/null +++ b/pandora/place/migrations/0003_alter_place_countrycode_alter_place_id_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('place', '0002_auto_20160304_1644'), + ] + + operations = [ + migrations.AlterField( + model_name='place', + name='countryCode', + field=models.CharField(db_index=True, default='', max_length=16), + ), + migrations.AlterField( + model_name='place', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='place', + name='name_find', + field=models.TextField(default='', editable=False), + ), + migrations.AlterField( + model_name='place', + name='type', + field=models.CharField(db_index=True, default='', max_length=1000), + ), + ] diff --git a/pandora/place/tasks.py b/pandora/place/tasks.py index 3feb88dd1..96926528f 100644 --- a/pandora/place/tasks.py +++ b/pandora/place/tasks.py @@ -1,20 +1,26 @@ # -*- coding: utf-8 -*- -from celery.task import task +from app.celery import app from . import models ''' -@periodic_task(run_every=crontab(hour=6, minute=30), queue='encoding') +from celery.schedules import crontab + +@app.task(queue='encoding') def update_all_matches(**kwargs): ids = [p['id'] for p in models.Place.objects.all().values('id')] for i in ids: p = models.Place.objects.get(pk=i) p.update_matches() + +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(crontab(hour=6, minute=30), update_all_matches.s()) ''' -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_matches(id): place = models.Place.objects.get(pk=id) place.update_matches() diff --git a/pandora/sequence/migrations/0002_alter_sequence_mode.py b/pandora/sequence/migrations/0002_alter_sequence_mode.py new file mode 100644 index 000000000..06ce99db0 --- /dev/null +++ b/pandora/sequence/migrations/0002_alter_sequence_mode.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sequence', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='sequence', + name='mode', + field=models.IntegerField(choices=[(0, 'shape'), (1, 'color')], default=0), + ), + ] diff --git a/pandora/sequence/migrations/0003_alter_sequence_id.py b/pandora/sequence/migrations/0003_alter_sequence_id.py new file mode 100644 index 000000000..711e7d416 --- /dev/null +++ b/pandora/sequence/migrations/0003_alter_sequence_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-29 11:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sequence', '0002_alter_sequence_mode'), + ] + + operations = [ + migrations.AlterField( + model_name='sequence', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/sequence/tasks.py b/pandora/sequence/tasks.py index b90c0d5a4..0747bf8c8 100644 --- a/pandora/sequence/tasks.py +++ b/pandora/sequence/tasks.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- from django.db import connection, transaction -from celery.task import task +from app.celery import app import item.models from . import extract -@task(ignore_results=True, queue='encoding') +@app.task(ignore_results=True, queue='encoding') def get_sequences(public_id): from . import models i = item.models.Item.objects.get(public_id=public_id) diff --git a/pandora/settings.py b/pandora/settings.py index b25960150..7c9a890ab 100644 --- a/pandora/settings.py +++ b/pandora/settings.py @@ -67,7 +67,8 @@ STATICFILES_DIRS = ( STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - #'django.contrib.staticfiles.finders.DefaultStorageFinder', + "sass_processor.finders.CssFinder", + "compressor.finders.CompressorFinder", ) GEOIP_PATH = normpath(join(PROJECT_ROOT, '..', 'data', 'geo')) @@ -110,6 +111,7 @@ ROOT_URLCONF = 'urls' INSTALLED_APPS = ( 'django.contrib.auth', + 'mozilla_django_oidc', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', @@ -123,6 +125,10 @@ INSTALLED_APPS = ( 'django_extensions', 'django_celery_results', + 'django_celery_beat', + 'compressor', + 'sass_processor', + 'app', 'log', 'annotation', @@ -149,9 +155,31 @@ INSTALLED_APPS = ( 'websocket', 'taskqueue', 'home', + 'mobile', ) AUTH_USER_MODEL = 'system.User' +AUTH_PROFILE_MODULE = 'user.UserProfile' +AUTH_CHECK_USERNAME = True + +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', +) + +# OpenID Connect login support +LOGIN_REDIRECT_URL = "/grid" +LOGOUT_REDIRECT_URL = "/grid" +OIDC_USERNAME_ALGO = "app.oidc.generate_username" +OIDC_RP_CLIENT_ID = None + +# define those in local_settings to enable OCID based login +#OIDC_RP_CLIENT_ID = "" +#OIDC_RP_CLIENT_SECRET = "" +#OIDC_RP_SIGN_ALGO = "RS256" +#OIDC_OP_JWKS_ENDPOINT = "" +#OIDC_OP_AUTHORIZATION_ENDPOINT = "" +#OIDC_OP_TOKEN_ENDPOINT = "" +#OIDC_OP_USER_ENDPOINT = "" # Log errors into db LOGGING = { @@ -161,13 +189,18 @@ LOGGING = { 'errors': { 'level': 'ERROR', 'class': 'log.utils.ErrorHandler' - } + }, }, 'loggers': { - 'django.request': { + 'django': { 'handlers': ['errors'], 'level': 'ERROR', - 'propagate': True, + 'propagate': False, + }, + 'pandora': { + 'handlers': ['errors'], + 'level': 'ERROR', + 'propagate': False, }, } } @@ -179,10 +212,12 @@ CACHES = { } } -AUTH_PROFILE_MODULE = 'user.UserProfile' -AUTH_CHECK_USERNAME = True +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + + FFMPEG = 'ffmpeg' FFPROBE = 'ffprobe' +USE_VP9 = True FFMPEG_SUPPORTS_VP9 = True FFMPEG_DEBUG = False @@ -204,6 +239,8 @@ CELERY_RESULT_BACKEND = 'django-db' CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TIMEZONE = 'UTC' +CELERY_ENABLE_UTC = True CELERY_BROKER_URL = 'amqp://pandora:box@localhost:5672//pandora' @@ -221,9 +258,6 @@ XACCELREDIRECT = False SITE_CONFIG = join(PROJECT_ROOT, 'config.jsonc') DEFAULT_CONFIG = join(PROJECT_ROOT, 'config.pandora.jsonc') -#used if CONFIG['canDownloadVideo'] is set -TRACKER_URL = "udp://tracker.openbittorrent.com:80" - DATA_SERVICE = '' POSTER_PRECEDENCE = () POSTER_ONLY_PORTRAIT = () @@ -264,6 +298,7 @@ SCRIPT_ROOT = normpath(join(PROJECT_ROOT, '..', 'scripts')) #change script to customize ITEM_POSTER = join(SCRIPT_ROOT, 'poster.py') ITEM_ICON = join(SCRIPT_ROOT, 'item_icon.py') +ITEM_ICON_DATA = False LIST_ICON = join(SCRIPT_ROOT, 'list_icon.py') COLLECTION_ICON = join(SCRIPT_ROOT, 'list_icon.py') @@ -274,9 +309,17 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') DATA_UPLOAD_MAX_MEMORY_SIZE = 32 * 1024 * 1024 +EMPTY_CLIPS = True + +YT_DLP_EXTRA = [] + +TXT_TTF = "/usr/share/fonts/truetype/msttcorefonts/Georgia.ttf" +TXT_TTF = "/usr/share/fonts/truetype/noto/NotoSansMono-Regular.ttf" + #you can ignore things below this line #========================================================================= LOCAL_APPS = [] +LOCAL_URLPATTERNS = [] #load installation specific settings from local_settings.py try: from local_settings import * @@ -303,4 +346,7 @@ except NameError: INSTALLED_APPS = tuple(list(INSTALLED_APPS) + LOCAL_APPS) - +if OIDC_RP_CLIENT_ID: + AUTHENTICATION_BACKENDS = list(AUTHENTICATION_BACKENDS) + [ + 'app.oidc.OIDCAuthenticationBackend' + ] diff --git a/pandora/system/apps.py b/pandora/system/apps.py index 5dc4d64bc..b3f493ea5 100644 --- a/pandora/system/apps.py +++ b/pandora/system/apps.py @@ -2,4 +2,5 @@ from django.apps import AppConfig class SystemConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" name = 'system' diff --git a/pandora/system/migrations/0004_alter_user_first_name_alter_user_id.py b/pandora/system/migrations/0004_alter_user_first_name_alter_user_id.py new file mode 100644 index 000000000..06f57e296 --- /dev/null +++ b/pandora/system/migrations/0004_alter_user_first_name_alter_user_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('system', '0003_field_length'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='first_name', + field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + migrations.AlterField( + model_name='user', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/system/models.py b/pandora/system/models.py index 2f59135cd..cbb4d971e 100644 --- a/pandora/system/models.py +++ b/pandora/system/models.py @@ -1,6 +1,6 @@ from django.db import models from django.contrib.auth.models import AbstractUser -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class User(AbstractUser): diff --git a/pandora/taskqueue/apps.py b/pandora/taskqueue/apps.py new file mode 100644 index 000000000..b699dccd6 --- /dev/null +++ b/pandora/taskqueue/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class TaskQueueConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'taskqueue' + diff --git a/pandora/taskqueue/migrations/0002_alter_task_id_alter_task_status.py b/pandora/taskqueue/migrations/0002_alter_task_id_alter_task_status.py new file mode 100644 index 000000000..636274624 --- /dev/null +++ b/pandora/taskqueue/migrations/0002_alter_task_id_alter_task_status.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('taskqueue', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='task', + name='status', + field=models.CharField(default='unknown', max_length=32), + ), + ] diff --git a/pandora/taskqueue/models.py b/pandora/taskqueue/models.py index 214f9a417..0d03f920c 100644 --- a/pandora/taskqueue/models.py +++ b/pandora/taskqueue/models.py @@ -8,10 +8,11 @@ from django.contrib.auth import get_user_model from django.conf import settings from django.db import models from django.db.models import Q -import celery.task.control -import kombu.five import ox +from app.celery import app + + User = get_user_model() def get_tasks(username): @@ -87,6 +88,9 @@ class Task(models.Model): except Item.DoesNotExist: return False + if self.status == 'transcribing': + return False + if self.item.files.filter(wanted=True, available=False).count(): status = 'pending' elif self.item.files.filter(uploading=True).count(): @@ -111,7 +115,7 @@ class Task(models.Model): return False def get_job(self): - c = celery.task.control.inspect() + c = app.control.inspect() active = c.active(safe=True) if active: for queue in active: @@ -166,7 +170,7 @@ class Task(models.Model): job = self.get_job() if job: print(job) - r = celery.task.control.revoke(job['id']) + r = app.control.revoke(job['id']) print(r) for f in self.item.files.filter(encoding=True): f.delete() diff --git a/pandora/tasks.conf.in b/pandora/tasks.conf.in new file mode 100644 index 000000000..245a3562f --- /dev/null +++ b/pandora/tasks.conf.in @@ -0,0 +1,3 @@ +LOGLEVEL=info +MAX_TASKS_PER_CHILD=1000 +CONCURRENCY=2 diff --git a/pandora/templates/document.html b/pandora/templates/document.html new file mode 100644 index 000000000..34139cc4c --- /dev/null +++ b/pandora/templates/document.html @@ -0,0 +1,38 @@ + + + + + {{head_title}} + + {% include "baseheader.html" %} + + {%if description %} {%endif%} + + + + + + + {%if description %}{%endif%} + + + + + + diff --git a/pandora/templates/epub.html b/pandora/templates/epub.html new file mode 100644 index 000000000..b619315e2 --- /dev/null +++ b/pandora/templates/epub.html @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ Menu +
+
+ +   –   + +
+
+ Bookmark +
+
+ +
+ +
+ + +
+
+ +
+ + + diff --git a/pandora/templates/item.html b/pandora/templates/item.html index 6b6e22fd2..20a84eb79 100644 --- a/pandora/templates/item.html +++ b/pandora/templates/item.html @@ -22,7 +22,7 @@
-
+
{{title}}
{{description_html|safe}}
{% for c in clips %} diff --git a/pandora/templates/mobile/index.html b/pandora/templates/mobile/index.html new file mode 100644 index 000000000..e274b941c --- /dev/null +++ b/pandora/templates/mobile/index.html @@ -0,0 +1,45 @@ +{% load static sass_tags compress %} + + + + + {% if title %} + {{title}} + + + {% endif %} + {%if description %} + + + {%endif%} + {% if preview %} + + + + {% endif %} + {% if url %} + + {% endif %} + + {% compress css file m %} + + + {% endcompress %} + + + +
+ {% compress js file m %} + + + + + + + + + + + {% endcompress %} + + diff --git a/pandora/text/apps.py b/pandora/text/apps.py new file mode 100644 index 000000000..7f181df12 --- /dev/null +++ b/pandora/text/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class TextConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'text' + diff --git a/pandora/text/migrations/0002_alter_position_id_alter_text_description_and_more.py b/pandora/text/migrations/0002_alter_position_id_alter_text_description_and_more.py new file mode 100644 index 000000000..d77e3220e --- /dev/null +++ b/pandora/text/migrations/0002_alter_position_id_alter_text_description_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('text', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='position', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='text', + name='description', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='text', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='text', + name='status', + field=models.CharField(default='private', max_length=20), + ), + migrations.AlterField( + model_name='text', + name='text', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='text', + name='type', + field=models.CharField(default='html', max_length=255), + ), + ] diff --git a/pandora/text/models.py b/pandora/text/models.py index 2973ba6d5..0489db31e 100644 --- a/pandora/text/models.py +++ b/pandora/text/models.py @@ -131,7 +131,7 @@ class Text(models.Model): self.status = value elif key == 'name': - data['name'] = re.sub(' \[\d+\]$', '', data['name']).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data['name']).strip() if not data['name']: data['name'] = "Untitled" name = data['name'] @@ -215,7 +215,7 @@ class Text(models.Model): response['embeds'] = self.embeds response['names'] = [] else: - response['names'] = re.compile('<[^<>]*?data-name="(.+?)"').findall(self.text) + response['names'] = re.compile(r'<[^<>]*?data-name="(.+?)"').findall(self.text) for key in list(response): if key not in keys + default_keys: diff --git a/pandora/text/views.py b/pandora/text/views.py index 33338675f..552db47f4 100644 --- a/pandora/text/views.py +++ b/pandora/text/views.py @@ -39,7 +39,7 @@ def addText(request, data): } see: editText, findTexts, getText, removeText, sortTexts ''' - data['name'] = re.sub(' \[\d+\]$', '', data.get('name', 'Untitled')).strip() + data['name'] = re.sub(r' \[\d+\]$', '', data.get('name', 'Untitled')).strip() name = data['name'] if not name: name = "Untitled" diff --git a/pandora/title/apps.py b/pandora/title/apps.py new file mode 100644 index 000000000..d9e33f020 --- /dev/null +++ b/pandora/title/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class TitleConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'title' + diff --git a/pandora/title/migrations/0003_alter_title_id.py b/pandora/title/migrations/0003_alter_title_id.py new file mode 100644 index 000000000..dec185420 --- /dev/null +++ b/pandora/title/migrations/0003_alter_title_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('title', '0002_auto_20190723_1446'), + ] + + operations = [ + migrations.AlterField( + model_name='title', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/translation/apps.py b/pandora/translation/apps.py index bb46e697a..0e3f39b0e 100644 --- a/pandora/translation/apps.py +++ b/pandora/translation/apps.py @@ -2,4 +2,5 @@ from django.apps import AppConfig class TranslationConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" name = 'translation' diff --git a/pandora/translation/migrations/0003_alter_translation_id.py b/pandora/translation/migrations/0003_alter_translation_id.py new file mode 100644 index 000000000..4daa151c4 --- /dev/null +++ b/pandora/translation/migrations/0003_alter_translation_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('translation', '0002_translation_type'), + ] + + operations = [ + migrations.AlterField( + model_name='translation', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/translation/tasks.py b/pandora/translation/tasks.py index 8b0be30b2..9e9143d7a 100644 --- a/pandora/translation/tasks.py +++ b/pandora/translation/tasks.py @@ -2,17 +2,22 @@ from datetime import timedelta, datetime -from celery.task import task, periodic_task from django.conf import settings from app.utils import limit_rate +from app.celery import app +from celery.schedules import crontab -@periodic_task(run_every=timedelta(days=1), queue='encoding') +@app.task(queue='encoding') def cronjob(**kwargs): if limit_rate('translations.tasks.cronjob', 8 * 60 * 60): load_translations() -@task(ignore_results=True, queue='encoding') +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(timedelta(days=1), cronjob.s()) + +@app.task(ignore_results=True, queue='encoding') def load_translations(): from .models import load_itemkey_translations, load_translations load_translations() diff --git a/pandora/tv/apps.py b/pandora/tv/apps.py new file mode 100644 index 000000000..66d6f7832 --- /dev/null +++ b/pandora/tv/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class TvConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'tv' + diff --git a/pandora/tv/migrations/0003_id_bigint.py b/pandora/tv/migrations/0003_id_bigint.py new file mode 100644 index 000000000..08bc4cd1a --- /dev/null +++ b/pandora/tv/migrations/0003_id_bigint.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tv', '0002_auto_20160219_1734'), + ] + + operations = [ + migrations.AlterField( + model_name='channel', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='program', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/tv/models.py b/pandora/tv/models.py index e9778d70b..600c06d06 100644 --- a/pandora/tv/models.py +++ b/pandora/tv/models.py @@ -31,23 +31,28 @@ class Channel(models.Model): items = Item.objects.filter(rendered=True, level__lte=cansee, sort__duration__gt=0) if items.count() == 0: return None - program = self.program.order_by('-start') changed = False - while program.count() < 1 or program[0].end < now: - not_played = items.exclude(program__in=self.program.filter(run=self.run)) - not_played_count = not_played.count() - if not_played_count == 0: - self.run += 1 + play_now = program.filter(start__lte=now, end__gt=now).first() + while not play_now or not play_now.next(): + played = self.program.filter(run=self.run) + if played.exists(): + not_played = items.exclude(program__in=self.program.filter(run=self.run)) + not_played_count = not_played.count() + if not_played_count == 0: + self.run += 1 + changed = True + else: changed = True + if changed: not_played = items not_played_count = not_played.count() - if not_played_count > 1: + if not_played_count > 1 and program.exists(): not_played = not_played.exclude(id=program[0].id) not_played_count = not_played.count() item = not_played[randint(0, not_played_count-1)] - if program.count() > 0: - start = program.aggregate(Max('end'))['end__max'] + if program.exists(): + start = program.order_by('-end')[0].end else: start = now p = Program() @@ -58,9 +63,10 @@ class Channel(models.Model): p.channel = self p.save() program = self.program.order_by('-start') + play_now = program.filter(start__lte=now, end__gt=now).first() if changed: self.save() - return program[0] + return play_now def json(self, user): now = datetime.now() @@ -82,6 +88,9 @@ class Program(models.Model): def __str__(self): return "%s %s" % (self.item, self.start) + def next(self): + return self.channel.program.filter(start__gte=self.end).order_by('start').first() + def json(self, user, current=False): item_json = self.item.json() r = { @@ -92,8 +101,6 @@ class Program(models.Model): r['layers'] = self.item.get_layers(user) r['streams'] = [s.file.oshash for s in self.item.streams()] if current: - #requires python2.7 - #r['position'] = (current - self.start).total_seconds() - td = current - self.start - r['position'] = td.seconds + td.days * 24 * 3600 + float(td.microseconds)/10**6 + r['position'] = (current - self.start).total_seconds() + r['next'] = self.next().json(user) return r diff --git a/pandora/tv/tasks.py b/pandora/tv/tasks.py index 20a9df943..35dcf4163 100644 --- a/pandora/tv/tasks.py +++ b/pandora/tv/tasks.py @@ -2,17 +2,22 @@ from datetime import datetime, timedelta -from celery.task import periodic_task +from app.celery import app +from celery.schedules import crontab from app.utils import limit_rate from . import models -@periodic_task(run_every=timedelta(days=1), queue='encoding') +@app.task(queue='encoding') def update_program(**kwargs): if limit_rate('tv.tasks.update_program', 8 * 60 * 60): for c in models.Channel.objects.all(): c.update_program() old = datetime.now() - timedelta(days=180) models.Program.objects.filter(created__lt=old).delete() + +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(timedelta(days=1), update_program.s()) diff --git a/pandora/urlalias/apps.py b/pandora/urlalias/apps.py new file mode 100644 index 000000000..4a518b48f --- /dev/null +++ b/pandora/urlalias/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class UrlaliasConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'urlalias' + diff --git a/pandora/urlalias/migrations/0002_id_bigint.py b/pandora/urlalias/migrations/0002_id_bigint.py new file mode 100644 index 000000000..0b979d0dd --- /dev/null +++ b/pandora/urlalias/migrations/0002_id_bigint.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('urlalias', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='alias', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='idalias', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='layeralias', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='listalias', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/pandora/urlalias/views.py b/pandora/urlalias/views.py index 9ed1a36cc..541f61d8d 100644 --- a/pandora/urlalias/views.py +++ b/pandora/urlalias/views.py @@ -64,7 +64,7 @@ def padma_video(request, url): except: return app.views.index(request) if view: - timecodes = re.compile('(\d{2}:\d{2}:\d{2}\.\d{3})-(\d{2}:\d{2}:\d{2}\.\d{3})').findall(view) + timecodes = re.compile(r'(\d{2}:\d{2}:\d{2}\.\d{3})-(\d{2}:\d{2}:\d{2}\.\d{3})').findall(view) if timecodes: view = ','.join(timecodes[0]) if view: diff --git a/pandora/urls.py b/pandora/urls.py index cd241d664..c07886468 100644 --- a/pandora/urls.py +++ b/pandora/urls.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- import os +import importlib -from django.urls import path, re_path +from django.urls import path, re_path, include from oxdjango.http import HttpFileResponse from django.conf import settings @@ -25,15 +26,22 @@ import edit.views import itemlist.views import item.views import item.site +import mobile.views import translation.views import urlalias.views def serve_static_file(path, location, content_type): return HttpFileResponse(location, content_type=content_type) + urlpatterns = [ #path('admin/', admin.site.urls), - +] +if settings.OIDC_RP_CLIENT_ID: + urlpatterns += [ + path('oidc/', include('mozilla_django_oidc.urls')), + ] +urlpatterns += [ re_path(r'^api/locale.(?P.*).json$', translation.views.locale_json), re_path(r'^api/upload/text/?$', text.views.upload), re_path(r'^api/upload/document/?$', document.views.upload), @@ -45,7 +53,9 @@ urlpatterns = [ re_path(r'^resetUI$', user.views.reset_ui), re_path(r'^collection/(?P.*?)/icon(?P\d*).jpg$', documentcollection.views.icon), re_path(r'^documents/(?P[A-Z0-9]+)/(?P\d*)p(?P[\d,]*).jpg$', document.views.thumbnail), - re_path(r'^documents/(?P[A-Z0-9]+)/(?P.*?\.[^\d]{3})$', document.views.file), + re_path(r'^documents/(?P[A-Z0-9]+)/epub/(?P.*?)$', document.views.epub), + re_path(r'^documents/(?P[A-Z0-9]+)/(?P.*?\.[^\d]{3,4})$', document.views.file), + re_path(r'^documents/(?P.*?)$', document.views.document), re_path(r'^edit/(?P.*?)/icon(?P\d*).jpg$', edit.views.icon), re_path(r'^list/(?P.*?)/icon(?P\d*).jpg$', itemlist.views.icon), re_path(r'^text/(?P.*?)/icon(?P\d*).jpg$', text.views.icon), @@ -64,6 +74,7 @@ urlpatterns = [ re_path(r'^robots.txt$', app.views.robots_txt), re_path(r'^sitemap.xml$', item.views.sitemap_xml), re_path(r'^sitemap(?P\d+).xml$', item.views.sitemap_part_xml), + re_path(r'm/(?P.*?)$', mobile.views.index), path(r'', item.site.urls), ] #sould this not be enabled by default? nginx should handle those @@ -88,3 +99,17 @@ urlpatterns += [ path(r'', app.views.index), ] +if settings.LOCAL_URLPATTERNS: + patterns = [] + for pattern, fn in settings.LOCAL_URLPATTERNS: + if isinstance(fn, str): + m, f = fn.rsplit('.', 1) + try: + m = importlib.import_module(m) + except ImportError: + logger.error('failed to import urllib module: %s', fn, exc_info=True) + continue + fn = getattr(m, f) + patterns.append(re_path(pattern, fn)) + urlpatterns = patterns + urlpatterns + diff --git a/pandora/user/apps.py b/pandora/user/apps.py new file mode 100644 index 000000000..b21c7aba4 --- /dev/null +++ b/pandora/user/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'user' + diff --git a/pandora/user/migrations/0005_id_bigint_jsonfield.py b/pandora/user/migrations/0005_id_bigint_jsonfield.py new file mode 100644 index 000000000..b164e3875 --- /dev/null +++ b/pandora/user/migrations/0005_id_bigint_jsonfield.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.3 on 2023-07-27 21:28 + +import django.core.serializers.json +from django.db import migrations, models +import oxdjango.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0004_jsonfield'), + ] + + operations = [ + migrations.AlterField( + model_name='sessiondata', + name='info', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='userprofile', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='userprofile', + name='notes', + field=models.TextField(default=''), + ), + migrations.AlterField( + model_name='userprofile', + name='preferences', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AlterField( + model_name='userprofile', + name='ui', + field=oxdjango.fields.JSONField(default=dict, editable=False, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + ] diff --git a/pandora/user/models.py b/pandora/user/models.py index fba065617..78c68483d 100644 --- a/pandora/user/models.py +++ b/pandora/user/models.py @@ -79,7 +79,9 @@ class SessionData(models.Model): self.level = self.user.profile.level self.firstseen = self.user.date_joined if self.user.groups.exists(): - self.groupssort = ''.join([g.name for g in self.user.groups.all()]) + self.groupsort = ''.join( + ox.sorted_strings(self.user.groups.all().values_list('name', flat=True)) + ).lower() else: self.groupssort = None self.numberoflists = self.user.lists.count() @@ -100,6 +102,8 @@ class SessionData(models.Model): data, created = cls.objects.get_or_create(session_key=session_key) if request.user.is_authenticated: data.user = request.user + else: + data.user = None data.ip = get_ip(request) data.useragent = request.META.get('HTTP_USER_AGENT', '')[:4096] info = json.loads(request.POST.get('data', '{}')) @@ -434,8 +438,7 @@ def has_capability(user, capability): level = 'guest' else: level = user.profile.get_level() - return level in settings.CONFIG['capabilities'][capability] \ - and settings.CONFIG['capabilities'][capability][level] + return settings.CONFIG['capabilities'].get(capability, {}).get(level) def merge_users(old, new): old.annotations.all().update(user=new) diff --git a/pandora/user/tasks.py b/pandora/user/tasks.py index c1da304c7..ad3869c86 100644 --- a/pandora/user/tasks.py +++ b/pandora/user/tasks.py @@ -4,17 +4,22 @@ from datetime import timedelta from itertools import zip_longest import json -from celery.task import task, periodic_task +from celery.schedules import crontab +from app.celery import app from app.utils import limit_rate from app.models import Settings from .statistics import Statistics -@periodic_task(run_every=timedelta(hours=1), queue='encoding') +@app.task(queue='encoding') def cronjob(**kwargs): if limit_rate('user.tasks.cronjob', 30 * 60): update_statistics() +@app.on_after_finalize.connect +def setup_periodic_tasks(sender, **kwargs): + sender.add_periodic_task(timedelta(hours=1), cronjob.s()) + def update_statistics(): from . import models @@ -31,7 +36,7 @@ def update_statistics(): stats.add(u.json()) Settings.set('statistics', stats) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def parse_data(key): from . import models try: @@ -41,7 +46,7 @@ def parse_data(key): session_data.parse_data() session_data.save() -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_numberoflists(username): from . import models user = models.User.objects.get(username=username) @@ -51,7 +56,7 @@ def update_numberoflists(username): numberoflists=user.lists.count() ) -@task(ignore_results=True, queue='default') +@app.task(ignore_results=True, queue='default') def update_numberofcollections(username): from . import models user = models.User.objects.get(username=username) diff --git a/pandora/user/urls.py b/pandora/user/urls.py deleted file mode 100644 index 429e1da90..000000000 --- a/pandora/user/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.conf.urls.defaults import * - - -urlpatterns = patterns("user.views", - (r'^preferences', 'preferences'), - (r'^login', 'login'), - (r'^logout', 'logout'), - (r'^register', 'register'), -) - diff --git a/pandora/user/utils.py b/pandora/user/utils.py index 5328a4646..9abecaaa3 100644 --- a/pandora/user/utils.py +++ b/pandora/user/utils.py @@ -3,6 +3,38 @@ from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception import ox +def prepare_user(user): + from django.contrib.auth import get_user_model + from django.conf import settings + from itemlist.models import List, Position + from django.db.models import Max + + User = get_user_model() + + first_user_qs = User.objects.all() + if user.id: + first_user_qs = first_user_qs.exclude(id=user.id) + if first_user_qs.count() == 0: + user.is_superuser = True + user.is_staff = True + user.save() + + for l in settings.CONFIG['personalLists']: + list = List(name=l['title'], user=user) + for key in ('query', 'public', 'featured'): + if key in l: + setattr(list, key, l[key]) + if key == 'query': + for c in list.query['conditions']: + if c['key'] == 'user': + c['value'] = c['value'].format(username=user.username) + list.save() + pos = Position(list=list, section='personal', user=user) + qs = Position.objects.filter(user=user, section='personal') + pos.position = (qs.aggregate(Max('position'))['position__max'] or 0) + 1 + pos.save() + + def get_ip(request): if 'HTTP_X_FORWARDED_FOR' in request.META: ip = request.META['HTTP_X_FORWARDED_FOR'].split(',')[0] @@ -24,7 +56,7 @@ def get_location(ip): location = g.city(ip) except: try: - location = g.country(s.ip) + location = g.country(ip) except: location = None if location: diff --git a/pandora/user/views.py b/pandora/user/views.py index aa452cecb..86e315377 100644 --- a/pandora/user/views.py +++ b/pandora/user/views.py @@ -28,7 +28,7 @@ from user.models import Group from . import models from .decorators import capability_required_json -from .utils import rename_user +from .utils import rename_user, prepare_user User = get_user_model() @@ -177,28 +177,10 @@ def signup(request, data): } }) else: - first_user = User.objects.count() == 0 user = User(username=data['username'], email=data['email']) user.set_password(data['password']) - #make first user admin - user.is_superuser = first_user - user.is_staff = first_user user.save() - #create default user lists: - for l in settings.CONFIG['personalLists']: - list = List(name=l['title'], user=user) - for key in ('query', 'public', 'featured'): - if key in l: - setattr(list, key, l[key]) - if key == 'query': - for c in list.query['conditions']: - if c['key'] == 'user': - c['value'] = c['value'].format(username=user.username) - list.save() - pos = Position(list=list, section='personal', user=user) - qs = Position.objects.filter(user=user, section='personal') - pos.position = (qs.aggregate(Max('position'))['position__max'] or 0) + 1 - pos.save() + prepare_user(user) if request.session.session_key: models.SessionData.objects.filter(session_key=request.session.session_key).update(user=user) ui = json.loads(request.session.get('ui', 'null')) @@ -914,7 +896,7 @@ def addGroup(request, data): created = False n = 1 name = data['name'] - _name = re.sub(' \[\d+\]$', '', name).strip() + _name = re.sub(r' \[\d+\]$', '', name).strip() while not created: g, created = Group.objects.get_or_create(name=name) n += 1 @@ -946,7 +928,7 @@ def editGroup(request, data): name = data['name'] n = 1 name = data['name'] - _name = re.sub(' \[\d+\]$', '', name).strip() + _name = re.sub(r' \[\d+\]$', '', name).strip() while Group.objects.filter(name=name).count(): n += 1 name = '%s [%d]' % (_name, n) diff --git a/pandora/websocket/__init__.py b/pandora/websocket/__init__.py index e012f8683..ef74deedb 100644 --- a/pandora/websocket/__init__.py +++ b/pandora/websocket/__init__.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -from celery.execute import send_task from django.conf import settings +from app.celery import app key = 'websocket' def trigger_event(event, data): if settings.WEBSOCKET: - send_task('trigger_event', [event, data], exchange=key, routing_key=key) + app.send_task('trigger_event', [event, data], exchange=key, routing_key=key) diff --git a/pandora/websocket/apps.py b/pandora/websocket/apps.py new file mode 100644 index 000000000..63053af1c --- /dev/null +++ b/pandora/websocket/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class WebsocketConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = 'websocket' + diff --git a/requirements.txt b/requirements.txt index e2f2c2eb7..12abf9581 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,25 @@ -Django==3.0.10 -simplejson +Django==4.2.26 chardet -celery<5.0,>4.3 -django-celery-results -django-extensions==2.2.9 +celery==5.3.5 +django-celery-results==2.5.1 +django-celery-beat==2.5.0 +django-extensions==3.2.3 +libsass +django-compressor +django-sass-processor gunicorn==20.0.4 html5lib requests<3.0.0,>=2.24.0 urllib3<2.0.0,>=1.25.2 -tornado<5 -geoip2==4.1.0 -youtube-dl>=2020.5.8 +tornado==6.3.3 +geoip2==4.7.0 +yt-dlp>=2024.10.22 python-memcached -elasticsearch +elasticsearch<8 future +pytz +pypdfium2 +Pillow>=10 +pillow-heif +pillow-avif-plugin +mozilla-django-oidc==4.0.1 diff --git a/scripts/item_icon.pandora.py b/scripts/item_icon.pandora.py index 4f4b1e354..c069bbbb2 100755 --- a/scripts/item_icon.pandora.py +++ b/scripts/item_icon.pandora.py @@ -20,11 +20,11 @@ def render_icon(frame, timeline, icon): frame_image = Image.open(frame) if frame else Image.new('RGB', (1024, 768), (0, 0, 0)) frame_image_ratio = frame_image.size[0] / frame_image.size[1] if frame_ratio < frame_image_ratio: - frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.ANTIALIAS) + frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.LANCZOS) left = int((frame_image.size[0] - frame_width) / 2) frame_image = frame_image.crop((left, 0, left + frame_width, frame_height)) else: - frame_image = frame_image.resize((frame_width, int(frame_width / frame_image_ratio)), Image.ANTIALIAS) + frame_image = frame_image.resize((frame_width, int(frame_width / frame_image_ratio)), Image.LANCZOS) top = int((frame_image.size[1] - frame_height) / 2) frame_image = frame_image.crop((0, top, frame_width, top + frame_height)) icon_image.paste(frame_image, (0, 0)) @@ -36,7 +36,7 @@ def render_icon(frame, timeline, icon): mask_image = Image.open(os.path.join(static_root, 'iconTimelineOuterMask.png')) icon_image.paste(timeline_image, (timeline_left - 4, timeline_top - 4), mask=mask_image) timeline_image = Image.open(timeline) if timeline else Image.new('RGB', (timeline_width, timeline_height), (0, 0, 0)) - timeline_image = timeline_image.resize((timeline_width, timeline_height), Image.ANTIALIAS) + timeline_image = timeline_image.resize((timeline_width, timeline_height), Image.LANCZOS) mask_image = Image.open(os.path.join(static_root, 'iconTimelineInnerMask.png')) icon_image.paste(timeline_image, (timeline_left, timeline_top), mask=mask_image) # we're using jpegs with border-radius diff --git a/scripts/list_icon.pandora.py b/scripts/list_icon.pandora.py index 8c930b7db..3d0fbbf1c 100755 --- a/scripts/list_icon.pandora.py +++ b/scripts/list_icon.pandora.py @@ -24,11 +24,11 @@ def render_list_icon(frames, icon): frame_image_ratio = frame_image.size[0] / frame_image.size[1] frame_width_ = frame_width + (1 if i % 2 == 1 else 0) if frame_ratio < frame_image_ratio: - frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.ANTIALIAS) + frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.LANCZOS) left = int((frame_image.size[0] - frame_width_) / 2) frame_image = frame_image.crop((left, 0, left + frame_width_, frame_height)) else: - frame_image = frame_image.resize((frame_width_, int(frame_width_ / frame_image_ratio)), Image.ANTIALIAS) + frame_image = frame_image.resize((frame_width_, int(frame_width_ / frame_image_ratio)), Image.LANCZOS) top = int((frame_image.size[1] - frame_height) / 2) frame_image = frame_image.crop((0, top, frame_width_, top + frame_height)) icon_image.paste(frame_image, (i % 2 * frame_width + (1 if i % 2 == 2 else 0), diff --git a/scripts/poster.0xdb.py b/scripts/poster.0xdb.py index 9b9f24701..7666702a4 100755 --- a/scripts/poster.0xdb.py +++ b/scripts/poster.0xdb.py @@ -64,11 +64,11 @@ def render_poster(data, poster): frame_image = Image.open(frame) frame_image_ratio = frame_image.size[0] / frame_image.size[1] if frame_ratio < frame_image_ratio: - frame_image = frame_image.resize((int(frame_size[1] * frame_image_ratio), frame_size[1]), Image.ANTIALIAS) + frame_image = frame_image.resize((int(frame_size[1] * frame_image_ratio), frame_size[1]), Image.LANCZOS) left = int((frame_image.size[0] - frame_size[0]) / 2) frame_image = frame_image.crop((left, 0, left + frame_size[0], frame_size[1])) else: - frame_image = frame_image.resize((frame_size[0], int(frame_size[0] / frame_image_ratio)), Image.ANTIALIAS) + frame_image = frame_image.resize((frame_size[0], int(frame_size[0] / frame_image_ratio)), Image.LANCZOS) top = int((frame_image.size[1] - frame_size[1]) / 2) frame_image = frame_image.crop((0, top, frame_size[0], top + frame_size[1])) poster_image.paste(frame_image, (0, 0)) @@ -77,7 +77,7 @@ def render_poster(data, poster): # logo logo_image = Image.open(os.path.join(static_root, 'logo.0xdb.png')) - logo_image = logo_image.resize(logo_size, Image.ANTIALIAS) + logo_image = logo_image.resize(logo_size, Image.LANCZOS) for y in range(logo_size[1]): for x in range(logo_size[0]): poster_color = poster_image.getpixel((margin + x, margin + y)) @@ -95,11 +95,11 @@ def render_poster(data, poster): if small_frame_image: small_frame_image_ratio = small_frame_image.size[0] / small_frame_image.size[1] if small_frame_ratio < small_frame_image_ratio: - small_frame_image = small_frame_image.resize((int(small_frame_size[1] * small_frame_image_ratio), small_frame_size[1]), Image.ANTIALIAS) + small_frame_image = small_frame_image.resize((int(small_frame_size[1] * small_frame_image_ratio), small_frame_size[1]), Image.LANCZOS) left = int((small_frame_image.size[0] - small_frame_size[0]) / 2) small_frame_image = small_frame_image.crop((left, 0, left + small_frame_size[0], small_frame_size[1])) else: - small_frame_image = small_frame_image.resize((small_frame_size[0], int(small_frame_size[0] / small_frame_image_ratio)), Image.ANTIALIAS) + small_frame_image = small_frame_image.resize((small_frame_size[0], int(small_frame_size[0] / small_frame_image_ratio)), Image.LANCZOS) top = int((small_frame_image.size[1] - small_frame_size[1]) / 2) small_frame_image = small_frame_image.crop((0, top, small_frame_size[0], top + small_frame_size[1])) poster_image.paste(small_frame_image, (i * small_frame_size[0], frame_size[1])) @@ -189,7 +189,7 @@ def render_poster(data, poster): # timeline if timeline: timeline_image = Image.open(timeline) - timeline_image = timeline_image.resize(timeline_size, Image.ANTIALIAS) + timeline_image = timeline_image.resize(timeline_size, Image.LANCZOS) poster_image.paste(timeline_image, (0, poster_size[1] - timeline_size[1])) else: draw.rectangle(((0, poster_size[1] - timeline_size[1]), poster_size), fill=image_color) diff --git a/scripts/poster.indiancinema.py b/scripts/poster.indiancinema.py index bc37fca5e..9146af319 100755 --- a/scripts/poster.indiancinema.py +++ b/scripts/poster.indiancinema.py @@ -160,7 +160,7 @@ def render_poster(data, poster): if frame_ratio < frame_image_ratio: frame_image = frame_image.resize( (int(frame_height * frame_image_ratio), frame_height), - Image.ANTIALIAS + Image.LANCZOS ) left = int((frame_image.size[0] - poster_width) / 2) frame_image = frame_image.crop( @@ -169,7 +169,7 @@ def render_poster(data, poster): else: frame_image = frame_image.resize( (poster_width, int(poster_width / frame_image_ratio)), - Image.ANTIALIAS + Image.LANCZOS ) top = int((frame_image.size[1] - frame_height) / 2) frame_image = frame_image.crop( @@ -187,7 +187,7 @@ def render_poster(data, poster): timeline_image = Image.open(timeline) timeline_image = timeline_image.resize( (poster_width, timeline_height), - Image.ANTIALIAS + Image.LANCZOS ) poster_image.paste(timeline_image, (0, poster_height - timeline_height)) else: diff --git a/scripts/poster.padma.py b/scripts/poster.padma.py index 015aaa9b8..bf949801f 100755 --- a/scripts/poster.padma.py +++ b/scripts/poster.padma.py @@ -33,7 +33,7 @@ def render_poster(data, poster): timeline_lines = 16 if timeline: timeline_image = Image.open(timeline) - timeline_image = timeline_image.resize((10240, timeline_height), Image.ANTIALIAS) + timeline_image = timeline_image.resize((10240, timeline_height), Image.LANCZOS) for i in range(timeline_lines): line_image = timeline_image.crop((i * poster_width, 0, (i + 1) * poster_width, 64)) poster_image.paste(line_image, (0, i * timeline_height)) diff --git a/scripts/poster.pandora.py b/scripts/poster.pandora.py index 5f1965623..eef8cdace 100755 --- a/scripts/poster.pandora.py +++ b/scripts/poster.pandora.py @@ -62,11 +62,11 @@ def render_poster(data, poster): frame_image = Image.open(frame) frame_image_ratio = frame_image.size[0] / frame_image.size[1] if frame_ratio < frame_image_ratio: - frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.ANTIALIAS) + frame_image = frame_image.resize((int(frame_height * frame_image_ratio), frame_height), Image.LANCZOS) left = int((frame_image.size[0] - frame_width) / 2) frame_image = frame_image.crop((left, 0, left + frame_width, frame_height)) else: - frame_image = frame_image.resize((frame_width, int(frame_width / frame_image_ratio)), Image.ANTIALIAS) + frame_image = frame_image.resize((frame_width, int(frame_width / frame_image_ratio)), Image.LANCZOS) top = int((frame_image.size[1] - frame_height) / 2) frame_image = frame_image.crop((0, top, frame_width, top + frame_height)) poster_image.paste(frame_image, (0, 0)) @@ -76,7 +76,7 @@ def render_poster(data, poster): timeline_height = 64 if timeline: timeline_image = Image.open(timeline) - timeline_image = timeline_image.resize((timeline_width, timeline_height), Image.ANTIALIAS) + timeline_image = timeline_image.resize((timeline_width, timeline_height), Image.LANCZOS) poster_image.paste(timeline_image, (0, frame_height)) # text @@ -115,7 +115,7 @@ def render_poster(data, poster): logo_height = 32 logo_image = Image.open(os.path.join(static_root, '..', '..', 'static', 'png', 'logo.png')) logo_width = int(round(logo_height * logo_image.size[0] / logo_image.size[1])) - logo_image = logo_image.resize((logo_width, logo_height), Image.ANTIALIAS) + logo_image = logo_image.resize((logo_width, logo_height), Image.LANCZOS) logo_left = text_width - text_margin - logo_width logo_top = text_bottom - text_margin - logo_height for y in range(logo_height): diff --git a/static/cbr.js/bitjs/archive.js b/static/cbr.js/bitjs/archive.js new file mode 100644 index 000000000..1596b74b6 --- /dev/null +++ b/static/cbr.js/bitjs/archive.js @@ -0,0 +1,353 @@ +/** + * archive.js + * + * Provides base functionality for unarchiving. + * + * Licensed under the MIT License + * + * Copyright(c) 2011 Google Inc. + */ + +var bitjs = bitjs || {}; +bitjs.archive = bitjs.archive || {}; + +(function() { + +// =========================================================================== +// Stolen from Closure because it's the best way to do Java-like inheritance. +bitjs.base = function(me, opt_methodName, var_args) { + var caller = arguments.callee.caller; + if (caller.superClass_) { + // This is a constructor. Call the superclass constructor. + return caller.superClass_.constructor.apply( + me, Array.prototype.slice.call(arguments, 1)); + } + + var args = Array.prototype.slice.call(arguments, 2); + var foundCaller = false; + for (var ctor = me.constructor; + ctor; ctor = ctor.superClass_ && ctor.superClass_.constructor) { + if (ctor.prototype[opt_methodName] === caller) { + foundCaller = true; + } else if (foundCaller) { + return ctor.prototype[opt_methodName].apply(me, args); + } + } + + // If we did not find the caller in the prototype chain, + // then one of two things happened: + // 1) The caller is an instance method. + // 2) This method was not called by the right caller. + if (me[opt_methodName] === caller) { + return me.constructor.prototype[opt_methodName].apply(me, args); + } else { + throw Error( + 'goog.base called from a method of one name ' + + 'to a method of a different name'); + } +}; +bitjs.inherits = function(childCtor, parentCtor) { + /** @constructor */ + function tempCtor() {}; + tempCtor.prototype = parentCtor.prototype; + childCtor.superClass_ = parentCtor.prototype; + childCtor.prototype = new tempCtor(); + childCtor.prototype.constructor = childCtor; +}; +// =========================================================================== + +/** + * An unarchive event. + * + * @param {string} type The event type. + * @constructor + */ +bitjs.archive.UnarchiveEvent = function(type) { + /** + * The event type. + * + * @type {string} + */ + this.type = type; +}; + +/** + * The UnarchiveEvent types. + */ +bitjs.archive.UnarchiveEvent.Type = { + START: 'start', + PROGRESS: 'progress', + EXTRACT: 'extract', + FINISH: 'finish', + INFO: 'info', + ERROR: 'error' +}; + +/** + * Useful for passing info up to the client (for debugging). + * + * @param {string} msg The info message. + */ +bitjs.archive.UnarchiveInfoEvent = function(msg) { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.INFO); + + /** + * The information message. + * + * @type {string} + */ + this.msg = msg; +}; +bitjs.inherits(bitjs.archive.UnarchiveInfoEvent, bitjs.archive.UnarchiveEvent); + +/** + * An unrecoverable error has occured. + * + * @param {string} msg The error message. + */ +bitjs.archive.UnarchiveErrorEvent = function(msg) { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.ERROR); + + /** + * The information message. + * + * @type {string} + */ + this.msg = msg; +}; +bitjs.inherits(bitjs.archive.UnarchiveErrorEvent, bitjs.archive.UnarchiveEvent); + +/** + * Start event. + * + * @param {string} msg The info message. + */ +bitjs.archive.UnarchiveStartEvent = function() { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.START); +}; +bitjs.inherits(bitjs.archive.UnarchiveStartEvent, bitjs.archive.UnarchiveEvent); + +/** + * Finish event. + * + * @param {string} msg The info message. + */ +bitjs.archive.UnarchiveFinishEvent = function() { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.FINISH); +}; +bitjs.inherits(bitjs.archive.UnarchiveFinishEvent, bitjs.archive.UnarchiveEvent); + +/** + * Progress event. + */ +bitjs.archive.UnarchiveProgressEvent = function( + currentFilename, + currentFileNumber, + currentBytesUnarchivedInFile, + currentBytesUnarchived, + totalUncompressedBytesInArchive, + totalFilesInArchive) { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.PROGRESS); + + this.currentFilename = currentFilename; + this.currentFileNumber = currentFileNumber; + this.currentBytesUnarchivedInFile = currentBytesUnarchivedInFile; + this.totalFilesInArchive = totalFilesInArchive; + this.currentBytesUnarchived = currentBytesUnarchived; + this.totalUncompressedBytesInArchive = totalUncompressedBytesInArchive; +}; +bitjs.inherits(bitjs.archive.UnarchiveProgressEvent, bitjs.archive.UnarchiveEvent); + +/** + * All extracted files returned by an Unarchiver will implement + * the following interface: + * + * interface UnarchivedFile { + * string filename + * TypedArray fileData + * } + * + */ + +/** + * Extract event. + */ +bitjs.archive.UnarchiveExtractEvent = function(unarchivedFile) { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.EXTRACT); + + /** + * @type {UnarchivedFile} + */ + this.unarchivedFile = unarchivedFile; +}; +bitjs.inherits(bitjs.archive.UnarchiveExtractEvent, bitjs.archive.UnarchiveEvent); + + +/** + * Base class for all Unarchivers. + * + * @param {ArrayBuffer} arrayBuffer The Array Buffer. + * @param {string} opt_pathToBitJS Optional string for where the BitJS files are located. + * @constructor + */ +bitjs.archive.Unarchiver = function(arrayBuffer, opt_pathToBitJS) { + /** + * The ArrayBuffer object. + * @type {ArrayBuffer} + * @protected + */ + this.ab = arrayBuffer; + + /** + * The path to the BitJS files. + * @type {string} + * @private + */ + this.pathToBitJS_ = opt_pathToBitJS || ''; + + /** + * A map from event type to an array of listeners. + * @type {Map.} + */ + this.listeners_ = {}; + for (var type in bitjs.archive.UnarchiveEvent.Type) { + this.listeners_[bitjs.archive.UnarchiveEvent.Type[type]] = []; + } +}; + +/** + * Private web worker initialized during start(). + * @type {Worker} + * @private + */ +bitjs.archive.Unarchiver.prototype.worker_ = null; + +/** + * This method must be overridden by the subclass to return the script filename. + * @return {string} The script filename. + * @protected. + */ +bitjs.archive.Unarchiver.prototype.getScriptFileName = function() { + throw 'Subclasses of AbstractUnarchiver must overload getScriptFileName()'; +}; + +/** + * Adds an event listener for UnarchiveEvents. + * + * @param {string} Event type. + * @param {function} An event handler function. + */ +bitjs.archive.Unarchiver.prototype.addEventListener = function(type, listener) { + if (type in this.listeners_) { + if (this.listeners_[type].indexOf(listener) == -1) { + this.listeners_[type].push(listener); + } + } +}; + +/** + * Removes an event listener. + * + * @param {string} Event type. + * @param {EventListener|function} An event listener or handler function. + */ +bitjs.archive.Unarchiver.prototype.removeEventListener = function(type, listener) { + if (type in this.listeners_) { + var index = this.listeners_[type].indexOf(listener); + if (index != -1) { + this.listeners_[type].splice(index, 1); + } + } +}; + +/** + * Receive an event and pass it to the listener functions. + * + * @param {bitjs.archive.UnarchiveEvent} e + * @private + */ +bitjs.archive.Unarchiver.prototype.handleWorkerEvent_ = function(e) { + if ((e instanceof bitjs.archive.UnarchiveEvent || e.type) && + this.listeners_[e.type] instanceof Array) { + this.listeners_[e.type].forEach(function (listener) { listener(e) }); + if (e.type == bitjs.archive.UnarchiveEvent.Type.FINISH) { + this.worker_.terminate(); + } + } else { + console.log(e); + } +}; + +/** + * Starts the unarchive in a separate Web Worker thread and returns immediately. + */ + bitjs.archive.Unarchiver.prototype.start = function() { + var me = this; + var scriptFileName = this.pathToBitJS_ + this.getScriptFileName(); + if (scriptFileName) { + this.worker_ = new Worker(scriptFileName); + + this.worker_.onerror = function(e) { + console.log('Worker error: message = ' + e.message); + throw e; + }; + + this.worker_.onmessage = function(e) { + if (typeof e.data == 'string') { + // Just log any strings the workers pump our way. + console.log(e.data); + } else { + // Assume that it is an UnarchiveEvent. Some browsers preserve the 'type' + // so that instanceof UnarchiveEvent returns true, but others do not. + me.handleWorkerEvent_(e.data); + } + }; + + this.worker_.postMessage({file: this.ab}); + } +}; + +/** + * Terminates the Web Worker for this Unarchiver and returns immediately. + */ +bitjs.archive.Unarchiver.prototype.stop = function() { + if (this.worker_) { + this.worker_.terminate(); + } +}; + + +/** + * Unzipper + * @extends {bitjs.archive.Unarchiver} + * @constructor + */ +bitjs.archive.Unzipper = function(arrayBuffer, opt_pathToBitJS) { + bitjs.base(this, arrayBuffer, opt_pathToBitJS); +}; +bitjs.inherits(bitjs.archive.Unzipper, bitjs.archive.Unarchiver); +bitjs.archive.Unzipper.prototype.getScriptFileName = function() { return 'unzip.js' }; + +/** + * Unrarrer + * @extends {bitjs.archive.Unarchiver} + * @constructor + */ +bitjs.archive.Unrarrer = function(arrayBuffer, opt_pathToBitJS) { + bitjs.base(this, arrayBuffer, opt_pathToBitJS); +}; +bitjs.inherits(bitjs.archive.Unrarrer, bitjs.archive.Unarchiver); +bitjs.archive.Unrarrer.prototype.getScriptFileName = function() { return 'unrar.js' }; + +/** + * Untarrer + * @extends {bitjs.archive.Unarchiver} + * @constructor + */ +bitjs.archive.Untarrer = function(arrayBuffer, opt_pathToBitJS) { + bitjs.base(this, arrayBuffer, opt_pathToBitJS); +}; +bitjs.inherits(bitjs.archive.Untarrer, bitjs.archive.Unarchiver); +bitjs.archive.Untarrer.prototype.getScriptFileName = function() { return 'untar.js' }; + +})(); \ No newline at end of file diff --git a/static/cbr.js/bitjs/io.js b/static/cbr.js/bitjs/io.js new file mode 100644 index 000000000..6eabfe9a8 --- /dev/null +++ b/static/cbr.js/bitjs/io.js @@ -0,0 +1,483 @@ +/* + * io.js + * + * Provides readers for bit/byte streams (reading) and a byte buffer (writing). + * + * Licensed under the MIT License + * + * Copyright(c) 2011 Google Inc. + * Copyright(c) 2011 antimatter15 + */ + +var bitjs = bitjs || {}; +bitjs.io = bitjs.io || {}; + +(function() { + +// mask for getting the Nth bit (zero-based) +bitjs.BIT = [ 0x01, 0x02, 0x04, 0x08, + 0x10, 0x20, 0x40, 0x80, + 0x100, 0x200, 0x400, 0x800, + 0x1000, 0x2000, 0x4000, 0x8000]; + +// mask for getting N number of bits (0-8) +var BITMASK = [0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF ]; + + +/** + * This bit stream peeks and consumes bits out of a binary stream. + * + * @param {ArrayBuffer} ab An ArrayBuffer object or a Uint8Array. + * @param {boolean} rtl Whether the stream reads bits from the byte starting + * from bit 7 to 0 (true) or bit 0 to 7 (false). + * @param {Number} opt_offset The offset into the ArrayBuffer + * @param {Number} opt_length The length of this BitStream + */ +bitjs.io.BitStream = function(ab, rtl, opt_offset, opt_length) { + if (!ab || !ab.toString || ab.toString() !== "[object ArrayBuffer]") { + throw "Error! BitArray constructed with an invalid ArrayBuffer object"; + } + + var offset = opt_offset || 0; + var length = opt_length || ab.byteLength; + this.bytes = new Uint8Array(ab, offset, length); + this.bytePtr = 0; // tracks which byte we are on + this.bitPtr = 0; // tracks which bit we are on (can have values 0 through 7) + this.peekBits = rtl ? this.peekBits_rtl : this.peekBits_ltr; +}; + + +/** + * byte0 byte1 byte2 byte3 + * 7......0 | 7......0 | 7......0 | 7......0 + * + * The bit pointer starts at bit0 of byte0 and moves left until it reaches + * bit7 of byte0, then jumps to bit0 of byte1, etc. + * @param {number} n The number of bits to peek. + * @param {boolean=} movePointers Whether to move the pointer, defaults false. + * @return {number} The peeked bits, as an unsigned number. + */ +bitjs.io.BitStream.prototype.peekBits_ltr = function(n, movePointers) { + if (n <= 0 || typeof n != typeof 1) { + return 0; + } + + var movePointers = movePointers || false, + bytePtr = this.bytePtr, + bitPtr = this.bitPtr, + result = 0, + bitsIn = 0, + bytes = this.bytes; + + // keep going until we have no more bits left to peek at + // TODO: Consider putting all bits from bytes we will need into a variable and then + // shifting/masking it to just extract the bits we want. + // This could be considerably faster when reading more than 3 or 4 bits at a time. + while (n > 0) { + if (bytePtr >= bytes.length) { + throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" + + bytes.length + ", bitPtr=" + bitPtr; + return -1; + } + + var numBitsLeftInThisByte = (8 - bitPtr); + if (n >= numBitsLeftInThisByte) { + var mask = (BITMASK[numBitsLeftInThisByte] << bitPtr); + result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); + + bytePtr++; + bitPtr = 0; + bitsIn += numBitsLeftInThisByte; + n -= numBitsLeftInThisByte; + } + else { + var mask = (BITMASK[n] << bitPtr); + result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); + + bitPtr += n; + bitsIn += n; + n = 0; + } + } + + if (movePointers) { + this.bitPtr = bitPtr; + this.bytePtr = bytePtr; + } + + return result; +}; + + +/** + * byte0 byte1 byte2 byte3 + * 7......0 | 7......0 | 7......0 | 7......0 + * + * The bit pointer starts at bit7 of byte0 and moves right until it reaches + * bit0 of byte0, then goes to bit7 of byte1, etc. + * @param {number} n The number of bits to peek. + * @param {boolean=} movePointers Whether to move the pointer, defaults false. + * @return {number} The peeked bits, as an unsigned number. + */ +bitjs.io.BitStream.prototype.peekBits_rtl = function(n, movePointers) { + if (n <= 0 || typeof n != typeof 1) { + return 0; + } + + var movePointers = movePointers || false, + bytePtr = this.bytePtr, + bitPtr = this.bitPtr, + result = 0, + bytes = this.bytes; + + // keep going until we have no more bits left to peek at + // TODO: Consider putting all bits from bytes we will need into a variable and then + // shifting/masking it to just extract the bits we want. + // This could be considerably faster when reading more than 3 or 4 bits at a time. + while (n > 0) { + + if (bytePtr >= bytes.length) { + throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" + + bytes.length + ", bitPtr=" + bitPtr; + return -1; + } + + var numBitsLeftInThisByte = (8 - bitPtr); + if (n >= numBitsLeftInThisByte) { + result <<= numBitsLeftInThisByte; + result |= (BITMASK[numBitsLeftInThisByte] & bytes[bytePtr]); + bytePtr++; + bitPtr = 0; + n -= numBitsLeftInThisByte; + } + else { + result <<= n; + result |= ((bytes[bytePtr] & (BITMASK[n] << (8 - n - bitPtr))) >> (8 - n - bitPtr)); + + bitPtr += n; + n = 0; + } + } + + if (movePointers) { + this.bitPtr = bitPtr; + this.bytePtr = bytePtr; + } + + return result; +}; + + +/** + * Some voodoo magic. + */ +bitjs.io.BitStream.prototype.getBits = function() { + return (((((this.bytes[this.bytePtr] & 0xff) << 16) + + ((this.bytes[this.bytePtr+1] & 0xff) << 8) + + ((this.bytes[this.bytePtr+2] & 0xff))) >>> (8-this.bitPtr)) & 0xffff); +}; + + +/** + * Reads n bits out of the stream, consuming them (moving the bit pointer). + * @param {number} n The number of bits to read. + * @return {number} The read bits, as an unsigned number. + */ +bitjs.io.BitStream.prototype.readBits = function(n) { + return this.peekBits(n, true); +}; + + +/** + * This returns n bytes as a sub-array, advancing the pointer if movePointers + * is true. Only use this for uncompressed blocks as this throws away remaining + * bits in the current byte. + * @param {number} n The number of bytes to peek. + * @param {boolean=} movePointers Whether to move the pointer, defaults false. + * @return {Uint8Array} The subarray. + */ +bitjs.io.BitStream.prototype.peekBytes = function(n, movePointers) { + if (n <= 0 || typeof n != typeof 1) { + return 0; + } + + // from http://tools.ietf.org/html/rfc1951#page-11 + // "Any bits of input up to the next byte boundary are ignored." + while (this.bitPtr != 0) { + this.readBits(1); + } + + var movePointers = movePointers || false; + var bytePtr = this.bytePtr, + bitPtr = this.bitPtr; + + var result = this.bytes.subarray(bytePtr, bytePtr + n); + + if (movePointers) { + this.bytePtr += n; + } + + return result; +}; + + +/** + * @param {number} n The number of bytes to read. + * @return {Uint8Array} The subarray. + */ +bitjs.io.BitStream.prototype.readBytes = function(n) { + return this.peekBytes(n, true); +}; + + +/** + * This object allows you to peek and consume bytes as numbers and strings + * out of an ArrayBuffer. In this buffer, everything must be byte-aligned. + * + * @param {ArrayBuffer} ab The ArrayBuffer object. + * @param {number=} opt_offset The offset into the ArrayBuffer + * @param {number=} opt_length The length of this BitStream + * @constructor + */ +bitjs.io.ByteStream = function(ab, opt_offset, opt_length) { + var offset = opt_offset || 0; + var length = opt_length || ab.byteLength; + this.bytes = new Uint8Array(ab, offset, length); + this.ptr = 0; +}; + + +/** + * Peeks at the next n bytes as an unsigned number but does not advance the + * pointer + * TODO: This apparently cannot read more than 4 bytes as a number? + * @param {number} n The number of bytes to peek at. + * @return {number} The n bytes interpreted as an unsigned number. + */ +bitjs.io.ByteStream.prototype.peekNumber = function(n) { + // TODO: return error if n would go past the end of the stream? + if (n <= 0 || typeof n != typeof 1) + return -1; + + var result = 0; + // read from last byte to first byte and roll them in + var curByte = this.ptr + n - 1; + while (curByte >= this.ptr) { + result <<= 8; + result |= this.bytes[curByte]; + --curByte; + } + return result; +}; + + +/** + * Returns the next n bytes as an unsigned number (or -1 on error) + * and advances the stream pointer n bytes. + * @param {number} n The number of bytes to read. + * @return {number} The n bytes interpreted as an unsigned number. + */ +bitjs.io.ByteStream.prototype.readNumber = function(n) { + var num = this.peekNumber( n ); + this.ptr += n; + return num; +}; + + +/** + * Returns the next n bytes as a signed number but does not advance the + * pointer. + * @param {number} n The number of bytes to read. + * @return {number} The bytes interpreted as a signed number. + */ +bitjs.io.ByteStream.prototype.peekSignedNumber = function(n) { + var num = this.peekNumber(n); + var HALF = Math.pow(2, (n * 8) - 1); + var FULL = HALF * 2; + + if (num >= HALF) num -= FULL; + + return num; +}; + + +/** + * Returns the next n bytes as a signed number and advances the stream pointer. + * @param {number} n The number of bytes to read. + * @return {number} The bytes interpreted as a signed number. + */ +bitjs.io.ByteStream.prototype.readSignedNumber = function(n) { + var num = this.peekSignedNumber(n); + this.ptr += n; + return num; +}; + + +/** + * This returns n bytes as a sub-array, advancing the pointer if movePointers + * is true. + * @param {number} n The number of bytes to read. + * @param {boolean} movePointers Whether to move the pointers. + * @return {Uint8Array} The subarray. + */ +bitjs.io.ByteStream.prototype.peekBytes = function(n, movePointers) { + if (n <= 0 || typeof n != typeof 1) { + return null; + } + + var result = this.bytes.subarray(this.ptr, this.ptr + n); + + if (movePointers) { + this.ptr += n; + } + + return result; +}; + + +/** + * Reads the next n bytes as a sub-array. + * @param {number} n The number of bytes to read. + * @return {Uint8Array} The subarray. + */ +bitjs.io.ByteStream.prototype.readBytes = function(n) { + return this.peekBytes(n, true); +}; + + +/** + * Peeks at the next n bytes as a string but does not advance the pointer. + * @param {number} n The number of bytes to peek at. + * @return {string} The next n bytes as a string. + */ +bitjs.io.ByteStream.prototype.peekString = function(n) { + if (n <= 0 || typeof n != typeof 1) { + return ""; + } + + var result = ""; + for (var p = this.ptr, end = this.ptr + n; p < end; ++p) { + result += String.fromCharCode(this.bytes[p]); + } + return result; +}; + + +/** + * Returns the next n bytes as an ASCII string and advances the stream pointer + * n bytes. + * @param {number} n The number of bytes to read. + * @return {string} The next n bytes as a string. + */ +bitjs.io.ByteStream.prototype.readString = function(n) { + var strToReturn = this.peekString(n); + this.ptr += n; + return strToReturn; +}; + + +/** + * A write-only Byte buffer which uses a Uint8 Typed Array as a backing store. + * @param {number} numBytes The number of bytes to allocate. + * @constructor + */ +bitjs.io.ByteBuffer = function(numBytes) { + if (typeof numBytes != typeof 1 || numBytes <= 0) { + throw "Error! ByteBuffer initialized with '" + numBytes + "'"; + } + this.data = new Uint8Array(numBytes); + this.ptr = 0; +}; + + +/** + * @param {number} b The byte to insert. + */ +bitjs.io.ByteBuffer.prototype.insertByte = function(b) { + // TODO: throw if byte is invalid? + this.data[this.ptr++] = b; +}; + + +/** + * @param {Array.|Uint8Array|Int8Array} bytes The bytes to insert. + */ +bitjs.io.ByteBuffer.prototype.insertBytes = function(bytes) { + // TODO: throw if bytes is invalid? + this.data.set(bytes, this.ptr); + this.ptr += bytes.length; +}; + + +/** + * Writes an unsigned number into the next n bytes. If the number is too large + * to fit into n bytes or is negative, an error is thrown. + * @param {number} num The unsigned number to write. + * @param {number} numBytes The number of bytes to write the number into. + */ +bitjs.io.ByteBuffer.prototype.writeNumber = function(num, numBytes) { + if (numBytes < 1) { + throw 'Trying to write into too few bytes: ' + numBytes; + } + if (num < 0) { + throw 'Trying to write a negative number (' + num + + ') as an unsigned number to an ArrayBuffer'; + } + if (num > (Math.pow(2, numBytes * 8) - 1)) { + throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; + } + + // Roll 8-bits at a time into an array of bytes. + var bytes = []; + while (numBytes-- > 0) { + var eightBits = num & 255; + bytes.push(eightBits); + num >>= 8; + } + + this.insertBytes(bytes); +}; + + +/** + * Writes a signed number into the next n bytes. If the number is too large + * to fit into n bytes, an error is thrown. + * @param {number} num The signed number to write. + * @param {number} numBytes The number of bytes to write the number into. + */ +bitjs.io.ByteBuffer.prototype.writeSignedNumber = function(num, numBytes) { + if (numBytes < 1) { + throw 'Trying to write into too few bytes: ' + numBytes; + } + + var HALF = Math.pow(2, (numBytes * 8) - 1); + if (num >= HALF || num < -HALF) { + throw 'Trying to write ' + num + ' into only ' + numBytes + ' bytes'; + } + + // Roll 8-bits at a time into an array of bytes. + var bytes = []; + while (numBytes-- > 0) { + var eightBits = num & 255; + bytes.push(eightBits); + num >>= 8; + } + + this.insertBytes(bytes); +}; + + +/** + * @param {string} str The ASCII string to write. + */ +bitjs.io.ByteBuffer.prototype.writeASCIIString = function(str) { + for (var i = 0; i < str.length; ++i) { + var curByte = str.charCodeAt(i); + if (curByte < 0 || curByte > 255) { + throw 'Trying to write a non-ASCII string!'; + } + this.insertByte(curByte); + } +}; + +})(); diff --git a/static/cbr.js/bitjs/unrar.js b/static/cbr.js/bitjs/unrar.js new file mode 100644 index 000000000..15273cd87 --- /dev/null +++ b/static/cbr.js/bitjs/unrar.js @@ -0,0 +1,913 @@ +/** + * unrar.js + * + * Copyright(c) 2011 Google Inc. + * Copyright(c) 2011 antimatter15 + * + * Reference Documentation: + * + * http://kthoom.googlecode.com/hg/docs/unrar.html + */ + +// This file expects to be invoked as a Worker (see onmessage below). +importScripts('io.js'); +importScripts('archive.js'); + +// Progress variables. +var currentFilename = ""; +var currentFileNumber = 0; +var currentBytesUnarchivedInFile = 0; +var currentBytesUnarchived = 0; +var totalUncompressedBytesInArchive = 0; +var totalFilesInArchive = 0; + +// Helper functions. +var info = function(str) { + postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); +}; +var err = function(str) { + postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); +}; +var postProgress = function() { + postMessage(new bitjs.archive.UnarchiveProgressEvent( + currentFilename, + currentFileNumber, + currentBytesUnarchivedInFile, + currentBytesUnarchived, + totalUncompressedBytesInArchive, + totalFilesInArchive)); +}; + +// shows a byte value as its hex representation +var nibble = "0123456789ABCDEF"; +var byteValueToHexString = function(num) { + return nibble[num>>4] + nibble[num&0xF]; +}; +var twoByteValueToHexString = function(num) { + return nibble[(num>>12)&0xF] + nibble[(num>>8)&0xF] + nibble[(num>>4)&0xF] + nibble[num&0xF]; +}; + + +// Volume Types +var MARK_HEAD = 0x72, + MAIN_HEAD = 0x73, + FILE_HEAD = 0x74, + COMM_HEAD = 0x75, + AV_HEAD = 0x76, + SUB_HEAD = 0x77, + PROTECT_HEAD = 0x78, + SIGN_HEAD = 0x79, + NEWSUB_HEAD = 0x7a, + ENDARC_HEAD = 0x7b; + +// bstream is a bit stream +var RarVolumeHeader = function(bstream) { + + var headPos = bstream.bytePtr; + // byte 1,2 + info("Rar Volume Header @"+bstream.bytePtr); + + this.crc = bstream.readBits(16); + info(" crc=" + this.crc); + + // byte 3 + this.headType = bstream.readBits(8); + info(" headType=" + this.headType); + + // Get flags + // bytes 4,5 + this.flags = {}; + this.flags.value = bstream.peekBits(16); + + info(" flags=" + twoByteValueToHexString(this.flags.value)); + switch (this.headType) { + case MAIN_HEAD: + this.flags.MHD_VOLUME = !!bstream.readBits(1); + this.flags.MHD_COMMENT = !!bstream.readBits(1); + this.flags.MHD_LOCK = !!bstream.readBits(1); + this.flags.MHD_SOLID = !!bstream.readBits(1); + this.flags.MHD_PACK_COMMENT = !!bstream.readBits(1); + this.flags.MHD_NEWNUMBERING = this.flags.MHD_PACK_COMMENT; + this.flags.MHD_AV = !!bstream.readBits(1); + this.flags.MHD_PROTECT = !!bstream.readBits(1); + this.flags.MHD_PASSWORD = !!bstream.readBits(1); + this.flags.MHD_FIRSTVOLUME = !!bstream.readBits(1); + this.flags.MHD_ENCRYPTVER = !!bstream.readBits(1); + bstream.readBits(6); // unused + break; + case FILE_HEAD: + this.flags.LHD_SPLIT_BEFORE = !!bstream.readBits(1); // 0x0001 + this.flags.LHD_SPLIT_AFTER = !!bstream.readBits(1); // 0x0002 + this.flags.LHD_PASSWORD = !!bstream.readBits(1); // 0x0004 + this.flags.LHD_COMMENT = !!bstream.readBits(1); // 0x0008 + this.flags.LHD_SOLID = !!bstream.readBits(1); // 0x0010 + bstream.readBits(3); // unused + this.flags.LHD_LARGE = !!bstream.readBits(1); // 0x0100 + this.flags.LHD_UNICODE = !!bstream.readBits(1); // 0x0200 + this.flags.LHD_SALT = !!bstream.readBits(1); // 0x0400 + this.flags.LHD_VERSION = !!bstream.readBits(1); // 0x0800 + this.flags.LHD_EXTTIME = !!bstream.readBits(1); // 0x1000 + this.flags.LHD_EXTFLAGS = !!bstream.readBits(1); // 0x2000 + bstream.readBits(2); // unused + info(" LHD_SPLIT_BEFORE = " + this.flags.LHD_SPLIT_BEFORE); + break; + default: + bstream.readBits(16); + } + + // byte 6,7 + this.headSize = bstream.readBits(16); + info(" headSize=" + this.headSize); + switch (this.headType) { + case MAIN_HEAD: + this.highPosAv = bstream.readBits(16); + this.posAv = bstream.readBits(32); + if (this.flags.MHD_ENCRYPTVER) { + this.encryptVer = bstream.readBits(8); + } + info("Found MAIN_HEAD with highPosAv=" + this.highPosAv + ", posAv=" + this.posAv); + break; + case FILE_HEAD: + this.packSize = bstream.readBits(32); + this.unpackedSize = bstream.readBits(32); + this.hostOS = bstream.readBits(8); + this.fileCRC = bstream.readBits(32); + this.fileTime = bstream.readBits(32); + this.unpVer = bstream.readBits(8); + this.method = bstream.readBits(8); + this.nameSize = bstream.readBits(16); + this.fileAttr = bstream.readBits(32); + + if (this.flags.LHD_LARGE) { + info("Warning: Reading in LHD_LARGE 64-bit size values"); + this.HighPackSize = bstream.readBits(32); + this.HighUnpSize = bstream.readBits(32); + } else { + this.HighPackSize = 0; + this.HighUnpSize = 0; + if (this.unpackedSize == 0xffffffff) { + this.HighUnpSize = 0x7fffffff + this.unpackedSize = 0xffffffff; + } + } + this.fullPackSize = 0; + this.fullUnpackSize = 0; + this.fullPackSize |= this.HighPackSize; + this.fullPackSize <<= 32; + this.fullPackSize |= this.packSize; + + // read in filename + + this.filename = bstream.readBytes(this.nameSize); + for (var _i = 0, _s = ''; _i < this.filename.length; _i++) { + _s += String.fromCharCode(this.filename[_i]); + } + + this.filename = _s; + + if (this.flags.LHD_SALT) { + info("Warning: Reading in 64-bit salt value"); + this.salt = bstream.readBits(64); // 8 bytes + } + + if (this.flags.LHD_EXTTIME) { + // 16-bit flags + var extTimeFlags = bstream.readBits(16); + + // this is adapted straight out of arcread.cpp, Archive::ReadHeader() + for (var I = 0; I < 4; ++I) { + var rmode = extTimeFlags >> ((3-I)*4); + if ((rmode & 8)==0) + continue; + if (I!=0) + bstream.readBits(16); + var count = (rmode&3); + for (var J = 0; J < count; ++J) + bstream.readBits(8); + } + } + + if (this.flags.LHD_COMMENT) { + info("Found a LHD_COMMENT"); + } + + + while(headPos + this.headSize > bstream.bytePtr) bstream.readBits(1); + + info("Found FILE_HEAD with packSize=" + this.packSize + ", unpackedSize= " + this.unpackedSize + ", hostOS=" + this.hostOS + ", unpVer=" + this.unpVer + ", method=" + this.method + ", filename=" + this.filename); + + break; + default: + info("Found a header of type 0x" + byteValueToHexString(this.headType)); + // skip the rest of the header bytes (for now) + bstream.readBytes( this.headSize - 7 ); + break; + } +}; + +var BLOCK_LZ = 0, + BLOCK_PPM = 1; + +var rLDecode = [0,1,2,3,4,5,6,7,8,10,12,14,16,20,24,28,32,40,48,56,64,80,96,112,128,160,192,224], + rLBits = [0,0,0,0,0,0,0,0,1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5], + rDBitLengthCounts = [4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,14,0,12], + rSDDecode = [0,4,8,16,32,64,128,192], + rSDBits = [2,2,3, 4, 5, 6, 6, 6]; + +var rDDecode = [0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, + 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, + 4096, 6144, 8192, 12288, 16384, 24576, 32768, 49152, 65536, 98304, + 131072, 196608, 262144, 327680, 393216, 458752, 524288, 589824, + 655360, 720896, 786432, 851968, 917504, 983040]; + +var rDBits = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, + 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, + 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16]; + +var rLOW_DIST_REP_COUNT = 16; + +var rNC = 299, + rDC = 60, + rLDC = 17, + rRC = 28, + rBC = 20, + rHUFF_TABLE_SIZE = (rNC+rDC+rRC+rLDC); + +var UnpBlockType = BLOCK_LZ; +var UnpOldTable = new Array(rHUFF_TABLE_SIZE); + +var BD = { //bitdecode + DecodeLen: new Array(16), + DecodePos: new Array(16), + DecodeNum: new Array(rBC) +}; +var LD = { //litdecode + DecodeLen: new Array(16), + DecodePos: new Array(16), + DecodeNum: new Array(rNC) +}; +var DD = { //distdecode + DecodeLen: new Array(16), + DecodePos: new Array(16), + DecodeNum: new Array(rDC) +}; +var LDD = { //low dist decode + DecodeLen: new Array(16), + DecodePos: new Array(16), + DecodeNum: new Array(rLDC) +}; +var RD = { //rep decode + DecodeLen: new Array(16), + DecodePos: new Array(16), + DecodeNum: new Array(rRC) +}; + +var rBuffer; + +// read in Huffman tables for RAR +function RarReadTables(bstream) { + var BitLength = new Array(rBC), + Table = new Array(rHUFF_TABLE_SIZE); + + // before we start anything we need to get byte-aligned + bstream.readBits( (8 - bstream.bitPtr) & 0x7 ); + + if (bstream.readBits(1)) { + info("Error! PPM not implemented yet"); + return; + } + + if (!bstream.readBits(1)) { //discard old table + for (var i = UnpOldTable.length; i--;) UnpOldTable[i] = 0; + } + + // read in bit lengths + for (var I = 0; I < rBC; ++I) { + + var Length = bstream.readBits(4); + if (Length == 15) { + var ZeroCount = bstream.readBits(4); + if (ZeroCount == 0) { + BitLength[I] = 15; + } + else { + ZeroCount += 2; + while (ZeroCount-- > 0 && I < rBC) + BitLength[I++] = 0; + --I; + } + } + else { + BitLength[I] = Length; + } + } + + // now all 20 bit lengths are obtained, we construct the Huffman Table: + + RarMakeDecodeTables(BitLength, 0, BD, rBC); + + var TableSize = rHUFF_TABLE_SIZE; + //console.log(DecodeLen, DecodePos, DecodeNum); + for (var i = 0; i < TableSize;) { + var num = RarDecodeNumber(bstream, BD); + if (num < 16) { + Table[i] = (num + UnpOldTable[i]) & 0xf; + i++; + } else if(num < 18) { + var N = (num == 16) ? (bstream.readBits(3) + 3) : (bstream.readBits(7) + 11); + + while (N-- > 0 && i < TableSize) { + Table[i] = Table[i - 1]; + i++; + } + } else { + var N = (num == 18) ? (bstream.readBits(3) + 3) : (bstream.readBits(7) + 11); + + while (N-- > 0 && i < TableSize) { + Table[i++] = 0; + } + } + } + + RarMakeDecodeTables(Table, 0, LD, rNC); + RarMakeDecodeTables(Table, rNC, DD, rDC); + RarMakeDecodeTables(Table, rNC + rDC, LDD, rLDC); + RarMakeDecodeTables(Table, rNC + rDC + rLDC, RD, rRC); + + for (var i = UnpOldTable.length; i--;) { + UnpOldTable[i] = Table[i]; + } + return true; +} + + +function RarDecodeNumber(bstream, dec) { + var DecodeLen = dec.DecodeLen, DecodePos = dec.DecodePos, DecodeNum = dec.DecodeNum; + var bitField = bstream.getBits() & 0xfffe; + //some sort of rolled out binary search + var bits = ((bitField < DecodeLen[8])? + ((bitField < DecodeLen[4])? + ((bitField < DecodeLen[2])? + ((bitField < DecodeLen[1])?1:2) + :((bitField < DecodeLen[3])?3:4)) + :(bitField < DecodeLen[6])? + ((bitField < DecodeLen[5])?5:6) + :((bitField < DecodeLen[7])?7:8)) + :((bitField < DecodeLen[12])? + ((bitField < DecodeLen[10])? + ((bitField < DecodeLen[9])?9:10) + :((bitField < DecodeLen[11])?11:12)) + :(bitField < DecodeLen[14])? + ((bitField < DecodeLen[13])?13:14) + :15)); + bstream.readBits(bits); + var N = DecodePos[bits] + ((bitField - DecodeLen[bits -1]) >>> (16 - bits)); + + return DecodeNum[N]; +} + + + +function RarMakeDecodeTables(BitLength, offset, dec, size) { + var DecodeLen = dec.DecodeLen, DecodePos = dec.DecodePos, DecodeNum = dec.DecodeNum; + var LenCount = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], + TmpPos = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], + N = 0, M = 0; + for (var i = DecodeNum.length; i--;) DecodeNum[i] = 0; + for (var i = 0; i < size; i++) { + LenCount[BitLength[i + offset] & 0xF]++; + } + LenCount[0] = 0; + TmpPos[0] = 0; + DecodePos[0] = 0; + DecodeLen[0] = 0; + + for (var I = 1; I < 16; ++I) { + N = 2 * (N+LenCount[I]); + M = (N << (15-I)); + if (M > 0xFFFF) + M = 0xFFFF; + DecodeLen[I] = M; + DecodePos[I] = DecodePos[I-1] + LenCount[I-1]; + TmpPos[I] = DecodePos[I]; + } + for (I = 0; I < size; ++I) + if (BitLength[I + offset] != 0) + DecodeNum[ TmpPos[ BitLength[offset + I] & 0xF ]++] = I; + +} + +// TODO: implement +function Unpack15(bstream, Solid) { + info("ERROR! RAR 1.5 compression not supported"); +} + +function Unpack20(bstream, Solid) { + var destUnpSize = rBuffer.data.length; + var oldDistPtr = 0; + + RarReadTables20(bstream); + while (destUnpSize > rBuffer.ptr) { + var num = RarDecodeNumber(bstream, LD); + if (num < 256) { + rBuffer.insertByte(num); + continue; + } + if (num > 269) { + var Length = rLDecode[num -= 270] + 3; + if ((Bits = rLBits[num]) > 0) { + Length += bstream.readBits(Bits); + } + var DistNumber = RarDecodeNumber(bstream, DD); + var Distance = rDDecode[DistNumber] + 1; + if ((Bits = rDBits[DistNumber]) > 0) { + Distance += bstream.readBits(Bits); + } + if (Distance >= 0x2000) { + Length++; + if(Distance >= 0x40000) Length++; + } + lastLength = Length; + lastDist = rOldDist[oldDistPtr++ & 3] = Distance; + RarCopyString(Length, Distance); + continue; + } + if (num == 269) { + RarReadTables20(bstream); + + RarUpdateProgress() + + continue; + } + if (num == 256) { + lastDist = rOldDist[oldDistPtr++ & 3] = lastDist; + RarCopyString(lastLength, lastDist); + continue; + } + if (num < 261) { + var Distance = rOldDist[(oldDistPtr - (num - 256)) & 3]; + var LengthNumber = RarDecodeNumber(bstream, RD); + var Length = rLDecode[LengthNumber] +2; + if ((Bits = rLBits[LengthNumber]) > 0) { + Length += bstream.readBits(Bits); + } + if (Distance >= 0x101) { + Length++; + if (Distance >= 0x2000) { + Length++ + if (Distance >= 0x40000) Length++; + } + } + lastLength = Length; + lastDist = rOldDist[oldDistPtr++ & 3] = Distance; + RarCopyString(Length, Distance); + continue; + } + if (num < 270) { + var Distance = rSDDecode[num -= 261] + 1; + if ((Bits = rSDBits[num]) > 0) { + Distance += bstream.readBits(Bits); + } + lastLength = 2; + lastDist = rOldDist[oldDistPtr++ & 3] = Distance; + RarCopyString(2, Distance); + continue; + } + + } + RarUpdateProgress() +} + +function RarUpdateProgress() { + var change = rBuffer.ptr - currentBytesUnarchivedInFile; + currentBytesUnarchivedInFile = rBuffer.ptr; + currentBytesUnarchived += change; + postProgress(); +} + + +var rNC20 = 298, + rDC20 = 48, + rRC20 = 28, + rBC20 = 19, + rMC20 = 257; + +var UnpOldTable20 = new Array(rMC20 * 4); + +function RarReadTables20(bstream) { + var BitLength = new Array(rBC20); + var Table = new Array(rMC20 * 4); + var TableSize, N, I; + var AudioBlock = bstream.readBits(1); + if (!bstream.readBits(1)) + for (var i = UnpOldTable20.length; i--;) UnpOldTable20[i] = 0; + TableSize = rNC20 + rDC20 + rRC20; + for (var I = 0; I < rBC20; I++) + BitLength[I] = bstream.readBits(4); + RarMakeDecodeTables(BitLength, 0, BD, rBC20); + I = 0; + while (I < TableSize) { + var num = RarDecodeNumber(bstream, BD); + if (num < 16) { + Table[I] = num + UnpOldTable20[I] & 0xf; + I++; + } else if(num == 16) { + N = bstream.readBits(2) + 3; + while (N-- > 0 && I < TableSize) { + Table[I] = Table[I - 1]; + I++; + } + } else { + if (num == 17) { + N = bstream.readBits(3) + 3; + } else { + N = bstream.readBits(7) + 11; + } + while (N-- > 0 && I < TableSize) { + Table[I++] = 0; + } + } + } + RarMakeDecodeTables(Table, 0, LD, rNC20); + RarMakeDecodeTables(Table, rNC20, DD, rDC20); + RarMakeDecodeTables(Table, rNC20 + rDC20, RD, rRC20); + for (var i = UnpOldTable20.length; i--;) UnpOldTable20[i] = Table[i]; +} + +var lowDistRepCount = 0, prevLowDist = 0; + +var rOldDist = [0,0,0,0]; +var lastDist; +var lastLength; + + +function Unpack29(bstream, Solid) { + // lazy initialize rDDecode and rDBits + + var DDecode = new Array(rDC); + var DBits = new Array(rDC); + + var Dist=0,BitLength=0,Slot=0; + + for (var I = 0; I < rDBitLengthCounts.length; I++,BitLength++) { + for (var J = 0; J < rDBitLengthCounts[I]; J++,Slot++,Dist+=(1<= 271) { + var Length = rLDecode[num -= 271] + 3; + if ((Bits = rLBits[num]) > 0) { + Length += bstream.readBits(Bits); + } + var DistNumber = RarDecodeNumber(bstream, DD); + var Distance = DDecode[DistNumber]+1; + if ((Bits = DBits[DistNumber]) > 0) { + if (DistNumber > 9) { + if (Bits > 4) { + Distance += ((bstream.getBits() >>> (20 - Bits)) << 4); + bstream.readBits(Bits - 4); + //todo: check this + } + if (lowDistRepCount > 0) { + lowDistRepCount--; + Distance += prevLowDist; + } else { + var LowDist = RarDecodeNumber(bstream, LDD); + if (LowDist == 16) { + lowDistRepCount = rLOW_DIST_REP_COUNT - 1; + Distance += prevLowDist; + } else { + Distance += LowDist; + prevLowDist = LowDist; + } + } + } else { + Distance += bstream.readBits(Bits); + } + } + if (Distance >= 0x2000) { + Length++; + if (Distance >= 0x40000) { + Length++; + } + } + RarInsertOldDist(Distance); + RarInsertLastMatch(Length, Distance); + RarCopyString(Length, Distance); + continue; + } + if (num == 256) { + if (!RarReadEndOfBlock(bstream)) break; + + continue; + } + if (num == 257) { + //console.log("READVMCODE"); + if (!RarReadVMCode(bstream)) break; + continue; + } + if (num == 258) { + if (lastLength != 0) { + RarCopyString(lastLength, lastDist); + } + continue; + } + if (num < 263) { + var DistNum = num - 259; + var Distance = rOldDist[DistNum]; + + for (var I = DistNum; I > 0; I--) { + rOldDist[I] = rOldDist[I-1]; + } + rOldDist[0] = Distance; + + var LengthNumber = RarDecodeNumber(bstream, RD); + var Length = rLDecode[LengthNumber] + 2; + if ((Bits = rLBits[LengthNumber]) > 0) { + Length += bstream.readBits(Bits); + } + RarInsertLastMatch(Length, Distance); + RarCopyString(Length, Distance); + continue; + } + if (num < 272) { + var Distance = rSDDecode[num -= 263] + 1; + if ((Bits = rSDBits[num]) > 0) { + Distance += bstream.readBits(Bits); + } + RarInsertOldDist(Distance); + RarInsertLastMatch(2, Distance); + RarCopyString(2, Distance); + continue; + } + + } + RarUpdateProgress() +} + +function RarReadEndOfBlock(bstream) { + + RarUpdateProgress() + + + var NewTable = false, NewFile = false; + if (bstream.readBits(1)) { + NewTable = true; + } else { + NewFile = true; + NewTable = !!bstream.readBits(1); + } + //tablesRead = !NewTable; + return !(NewFile || NewTable && !RarReadTables(bstream)); +} + + +function RarReadVMCode(bstream) { + var FirstByte = bstream.readBits(8); + var Length = (FirstByte & 7) + 1; + if (Length == 7) { + Length = bstream.readBits(8) + 7; + } else if(Length == 8) { + Length = bstream.readBits(16); + } + var vmCode = []; + for(var I = 0; I < Length; I++) { + //do something here with cheking readbuf + vmCode.push(bstream.readBits(8)); + } + return RarAddVMCode(FirstByte, vmCode, Length); +} + +function RarAddVMCode(firstByte, vmCode, length) { + //console.log(vmCode); + if (vmCode.length > 0) { + info("Error! RarVM not supported yet!"); + } + return true; +} + +function RarInsertLastMatch(length, distance) { + lastDist = distance; + lastLength = length; +} + +function RarInsertOldDist(distance) { + rOldDist.splice(3,1); + rOldDist.splice(0,0,distance); +} + +//this is the real function, the other one is for debugging +function RarCopyString(length, distance) { + var destPtr = rBuffer.ptr - distance; + if(destPtr < 0){ + var l = rOldBuffers.length; + while(destPtr < 0){ + destPtr = rOldBuffers[--l].data.length + destPtr; + } + //TODO: lets hope that it never needs to read beyond file boundaries + while(length--) rBuffer.insertByte(rOldBuffers[l].data[destPtr++]); + + } + if (length > distance) { + while(length--) rBuffer.insertByte(rBuffer.data[destPtr++]); + } else { + rBuffer.insertBytes(rBuffer.data.subarray(destPtr, destPtr + length)); + } + +} + +var rOldBuffers = [] +// v must be a valid RarVolume +function unpack(v) { + + // TODO: implement what happens when unpVer is < 15 + var Ver = v.header.unpVer <= 15 ? 15 : v.header.unpVer, + Solid = v.header.LHD_SOLID, + bstream = new bitjs.io.BitStream(v.fileData.buffer, true /* rtl */, v.fileData.byteOffset, v.fileData.byteLength ); + + rBuffer = new bitjs.io.ByteBuffer(v.header.unpackedSize); + + info("Unpacking "+v.filename+" RAR v"+Ver); + + switch(Ver) { + case 15: // rar 1.5 compression + Unpack15(bstream, Solid); + break; + case 20: // rar 2.x compression + case 26: // files larger than 2GB + Unpack20(bstream, Solid); + break; + case 29: // rar 3.x compression + case 36: // alternative hash + Unpack29(bstream, Solid); + break; + } // switch(method) + + rOldBuffers.push(rBuffer); + //TODO: clear these old buffers when there's over 4MB of history + return rBuffer.data; +} + +// bstream is a bit stream +var RarLocalFile = function(bstream) { + + this.header = new RarVolumeHeader(bstream); + this.filename = this.header.filename; + + if (this.header.headType != FILE_HEAD && this.header.headType != ENDARC_HEAD) { + this.isValid = false; + info("Error! RAR Volume did not include a FILE_HEAD header "); + } + else { + // read in the compressed data + this.fileData = null; + if (this.header.packSize > 0) { + this.fileData = bstream.readBytes(this.header.packSize); + this.isValid = true; + } + } +}; + +RarLocalFile.prototype.unrar = function() { + + if (!this.header.flags.LHD_SPLIT_BEFORE) { + // unstore file + if (this.header.method == 0x30) { + info("Unstore "+this.filename); + this.isValid = true; + + currentBytesUnarchivedInFile += this.fileData.length; + currentBytesUnarchived += this.fileData.length; + + // Create a new buffer and copy it over. + var len = this.header.packSize; + var newBuffer = new bitjs.io.ByteBuffer(len); + newBuffer.insertBytes(this.fileData); + this.fileData = newBuffer.data; + } else { + this.isValid = true; + this.fileData = unpack(this); + } + } +} + +var unrar = function(arrayBuffer) { + currentFilename = ""; + currentFileNumber = 0; + currentBytesUnarchivedInFile = 0; + currentBytesUnarchived = 0; + totalUncompressedBytesInArchive = 0; + totalFilesInArchive = 0; + + postMessage(new bitjs.archive.UnarchiveStartEvent()); + var bstream = new bitjs.io.BitStream(arrayBuffer, false /* rtl */); + + var header = new RarVolumeHeader(bstream); + if (header.crc == 0x6152 && + header.headType == 0x72 && + header.flags.value == 0x1A21 && + header.headSize == 7) { + info("Found RAR signature"); + + var mhead = new RarVolumeHeader(bstream); + if (mhead.headType != MAIN_HEAD) { + info("Error! RAR did not include a MAIN_HEAD header"); + } + else { + var localFiles = [], + localFile = null; + do { + try { + localFile = new RarLocalFile(bstream); + info("RAR localFile isValid=" + localFile.isValid + ", volume packSize=" + localFile.header.packSize); + if (localFile && localFile.isValid && localFile.header.packSize > 0) { + totalUncompressedBytesInArchive += localFile.header.unpackedSize; + localFiles.push(localFile); + } else if (localFile.header.packSize == 0 && localFile.header.unpackedSize == 0) { + localFile.isValid = true; + } + } catch(err) { + break; + } + //info("bstream" + bstream.bytePtr+"/"+bstream.bytes.length); + } while( localFile.isValid ); + totalFilesInArchive = localFiles.length; + + // now we have all information but things are unpacked + // TODO: unpack + localFiles = localFiles.sort(function(a,b) { + var aname = a.filename; + var bname = b.filename; + return aname > bname ? 1 : -1; + + // extract the number at the end of both filenames + /* + var aindex = aname.length, bindex = bname.length; + + // Find the last number character from the back of the filename. + while (aname[aindex-1] < '0' || aname[aindex-1] > '9') --aindex; + while (bname[bindex-1] < '0' || bname[bindex-1] > '9') --bindex; + + // Find the first number character from the back of the filename + while (aname[aindex-1] >= '0' && aname[aindex-1] <= '9') --aindex; + while (bname[bindex-1] >= '0' && bname[bindex-1] <= '9') --bindex; + + // parse them into numbers and return comparison + var anum = parseInt(aname.substr(aindex), 10), + bnum = parseInt(bname.substr(bindex), 10); + return bnum - anum;*/ + }); + + info(localFiles.map(function(a){return a.filename}).join(', ')); + for (var i = 0; i < localFiles.length; ++i) { + var localfile = localFiles[i]; + + // update progress + currentFilename = localfile.header.filename; + currentBytesUnarchivedInFile = 0; + + // actually do the unzipping + localfile.unrar(); + + if (localfile.isValid) { + postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); + postProgress(); + } + } + + postProgress(); + } + } + else { + err("Invalid RAR file"); + } + postMessage(new bitjs.archive.UnarchiveFinishEvent()); +}; + +// event.data.file has the ArrayBuffer. +onmessage = function(event) { + var ab = event.data.file; + unrar(ab, true); +}; diff --git a/static/cbr.js/bitjs/untar.js b/static/cbr.js/bitjs/untar.js new file mode 100644 index 000000000..4eafbb323 --- /dev/null +++ b/static/cbr.js/bitjs/untar.js @@ -0,0 +1,188 @@ +/** + * untar.js + * + * Copyright(c) 2011 Google Inc. + * + * Reference Documentation: + * + * TAR format: http://www.gnu.org/software/automake/manual/tar/Standard.html + */ + +// This file expects to be invoked as a Worker (see onmessage below). +importScripts('io.js'); +importScripts('archive.js'); + +// Progress variables. +var currentFilename = ""; +var currentFileNumber = 0; +var currentBytesUnarchivedInFile = 0; +var currentBytesUnarchived = 0; +var totalUncompressedBytesInArchive = 0; +var totalFilesInArchive = 0; + +// Helper functions. +var info = function(str) { + postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); +}; +var err = function(str) { + postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); +}; +var postProgress = function() { + postMessage(new bitjs.archive.UnarchiveProgressEvent( + currentFilename, + currentFileNumber, + currentBytesUnarchivedInFile, + currentBytesUnarchived, + totalUncompressedBytesInArchive, + totalFilesInArchive)); +}; + +// Removes all characters from the first zero-byte in the string onwards. +var readCleanString = function(bstr, numBytes) { + var str = bstr.readString(numBytes); + var zIndex = str.indexOf(String.fromCharCode(0)); + return zIndex != -1 ? str.substr(0, zIndex) : str; +}; + +// takes a ByteStream and parses out the local file information +var TarLocalFile = function(bstream) { + this.isValid = false; + + // Read in the header block + this.name = readCleanString(bstream, 100); + this.mode = readCleanString(bstream, 8); + this.uid = readCleanString(bstream, 8); + this.gid = readCleanString(bstream, 8); + this.size = parseInt(readCleanString(bstream, 12), 8); + this.mtime = readCleanString(bstream, 12); + this.chksum = readCleanString(bstream, 8); + this.typeflag = readCleanString(bstream, 1); + this.linkname = readCleanString(bstream, 100); + this.maybeMagic = readCleanString(bstream, 6); + + if (this.maybeMagic == "ustar") { + this.version = readCleanString(bstream, 2); + this.uname = readCleanString(bstream, 32); + this.gname = readCleanString(bstream, 32); + this.devmajor = readCleanString(bstream, 8); + this.devminor = readCleanString(bstream, 8); + this.prefix = readCleanString(bstream, 155); + + if (this.prefix.length) { + this.name = this.prefix + this.name; + } + bstream.readBytes(12); // 512 - 500 + } else { + bstream.readBytes(255); // 512 - 257 + } + + // Done header, now rest of blocks are the file contents. + this.filename = this.name; + this.fileData = null; + + info("Untarring file '" + this.filename + "'"); + info(" size = " + this.size); + info(" typeflag = " + this.typeflag); + + // A regular file. + if (this.typeflag == 0) { + info(" This is a regular file."); + var sizeInBytes = parseInt(this.size); + this.fileData = new Uint8Array(bstream.bytes.buffer, bstream.ptr, this.size); + if (this.name.length > 0 && this.size > 0 && this.fileData && this.fileData.buffer) { + this.isValid = true; + } + + bstream.readBytes(this.size); + + // Round up to 512-byte blocks. + var remaining = 512 - this.size % 512; + if (remaining > 0 && remaining < 512) { + bstream.readBytes(remaining); + } + } else if (this.typeflag == 5) { + info(" This is a directory.") + } +}; + +// Takes an ArrayBuffer of a tar file in +// returns null on error +// returns an array of DecompressedFile objects on success +var untar = function(arrayBuffer) { + currentFilename = ""; + currentFileNumber = 0; + currentBytesUnarchivedInFile = 0; + currentBytesUnarchived = 0; + totalUncompressedBytesInArchive = 0; + totalFilesInArchive = 0; + + postMessage(new bitjs.archive.UnarchiveStartEvent()); + var bstream = new bitjs.io.ByteStream(arrayBuffer); + var localFiles = []; + + // While we don't encounter an empty block, keep making TarLocalFiles. + while (bstream.peekNumber(4) != 0) { + var oneLocalFile = new TarLocalFile(bstream); + if (oneLocalFile && oneLocalFile.isValid) { + localFiles.push(oneLocalFile); + totalUncompressedBytesInArchive += oneLocalFile.size; + } + } + totalFilesInArchive = localFiles.length; + + // got all local files, now sort them + localFiles.sort(function(a,b) { + var aname = a.filename; + var bname = b.filename; + return aname > bname ? 1 : -1; + + // extract the number at the end of both filenames + /* + var aname = a.filename; + var bname = b.filename; + var aindex = aname.length, bindex = bname.length; + + // Find the last number character from the back of the filename. + while (aname[aindex-1] < '0' || aname[aindex-1] > '9') --aindex; + while (bname[bindex-1] < '0' || bname[bindex-1] > '9') --bindex; + + // Find the first number character from the back of the filename + while (aname[aindex-1] >= '0' && aname[aindex-1] <= '9') --aindex; + while (bname[bindex-1] >= '0' && bname[bindex-1] <= '9') --bindex; + + // parse them into numbers and return comparison + var anum = parseInt(aname.substr(aindex), 10), + bnum = parseInt(bname.substr(bindex), 10); + return anum - bnum; + */ + }); + + // report # files and total length + if (localFiles.length > 0) { + postProgress(); + } + + // now do the shipping of each file + for (var i = 0; i < localFiles.length; ++i) { + var localfile = localFiles[i]; + info("Sending file '" + localfile.filename + "' up"); + + // update progress + currentFilename = localfile.filename; + currentFileNumber = i; + currentBytesUnarchivedInFile = localfile.size; + currentBytesUnarchived += localfile.size; + postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); + postProgress(); + } + + postProgress(); + + postMessage(new bitjs.archive.UnarchiveFinishEvent()); +}; + +// event.data.file has the ArrayBuffer. +onmessage = function(event) { + var ab = event.data.file; + untar(ab); +}; diff --git a/static/cbr.js/bitjs/unzip.js b/static/cbr.js/bitjs/unzip.js new file mode 100644 index 000000000..1a6562121 --- /dev/null +++ b/static/cbr.js/bitjs/unzip.js @@ -0,0 +1,637 @@ +/** + * unzip.js + * + * Copyright(c) 2011 Google Inc. + * Copyright(c) 2011 antimatter15 + * + * Reference Documentation: + * + * ZIP format: http://www.pkware.com/documents/casestudies/APPNOTE.TXT + * DEFLATE format: http://tools.ietf.org/html/rfc1951 + */ + +// This file expects to be invoked as a Worker (see onmessage below). +importScripts('io.js'); +importScripts('archive.js'); + +// Progress variables. +var currentFilename = ""; +var currentFileNumber = 0; +var currentBytesUnarchivedInFile = 0; +var currentBytesUnarchived = 0; +var totalUncompressedBytesInArchive = 0; +var totalFilesInArchive = 0; + +// Helper functions. +var info = function(str) { + postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); +}; +var err = function(str) { + postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); +}; +var postProgress = function() { + postMessage(new bitjs.archive.UnarchiveProgressEvent( + currentFilename, + currentFileNumber, + currentBytesUnarchivedInFile, + currentBytesUnarchived, + totalUncompressedBytesInArchive, + totalFilesInArchive)); +}; + +var zLocalFileHeaderSignature = 0x04034b50; +var zArchiveExtraDataSignature = 0x08064b50; +var zCentralFileHeaderSignature = 0x02014b50; +var zDigitalSignatureSignature = 0x05054b50; +var zEndOfCentralDirSignature = 0x06064b50; +var zEndOfCentralDirLocatorSignature = 0x07064b50; + +// takes a ByteStream and parses out the local file information +var ZipLocalFile = function(bstream) { + if (typeof bstream != typeof {} || !bstream.readNumber || typeof bstream.readNumber != typeof function(){}) { + return null; + } + + bstream.readNumber(4); // swallow signature + this.version = bstream.readNumber(2); + this.generalPurpose = bstream.readNumber(2); + this.compressionMethod = bstream.readNumber(2); + this.lastModFileTime = bstream.readNumber(2); + this.lastModFileDate = bstream.readNumber(2); + this.crc32 = bstream.readNumber(4); + this.compressedSize = bstream.readNumber(4); + this.uncompressedSize = bstream.readNumber(4); + this.fileNameLength = bstream.readNumber(2); + this.extraFieldLength = bstream.readNumber(2); + + this.filename = null; + if (this.fileNameLength > 0) { + this.filename = bstream.readString(this.fileNameLength); + } + + info("Zip Local File Header:"); + info(" version=" + this.version); + info(" general purpose=" + this.generalPurpose); + info(" compression method=" + this.compressionMethod); + info(" last mod file time=" + this.lastModFileTime); + info(" last mod file date=" + this.lastModFileDate); + info(" crc32=" + this.crc32); + info(" compressed size=" + this.compressedSize); + info(" uncompressed size=" + this.uncompressedSize); + info(" file name length=" + this.fileNameLength); + info(" extra field length=" + this.extraFieldLength); + info(" filename = '" + this.filename + "'"); + + this.extraField = null; + if (this.extraFieldLength > 0) { + this.extraField = bstream.readString(this.extraFieldLength); + info(" extra field=" + this.extraField); + } + + // read in the compressed data + this.fileData = null; + if (this.compressedSize > 0) { + this.fileData = new Uint8Array(bstream.bytes.buffer, bstream.ptr, this.compressedSize); + bstream.ptr += this.compressedSize; + } + + // TODO: deal with data descriptor if present (we currently assume no data descriptor!) + // "This descriptor exists only if bit 3 of the general purpose bit flag is set" + // But how do you figure out how big the file data is if you don't know the compressedSize + // from the header?!? + if ((this.generalPurpose & bitjs.BIT[3]) != 0) { + this.crc32 = bstream.readNumber(4); + this.compressedSize = bstream.readNumber(4); + this.uncompressedSize = bstream.readNumber(4); + } +}; + +// determine what kind of compressed data we have and decompress +ZipLocalFile.prototype.unzip = function() { + + // Zip Version 1.0, no compression (store only) + if (this.compressionMethod == 0 ) { + info("ZIP v"+this.version+", store only: " + this.filename + " (" + this.compressedSize + " bytes)"); + currentBytesUnarchivedInFile = this.compressedSize; + currentBytesUnarchived += this.compressedSize; + } + // version == 20, compression method == 8 (DEFLATE) + else if (this.compressionMethod == 8) { + info("ZIP v2.0, DEFLATE: " + this.filename + " (" + this.compressedSize + " bytes)"); + this.fileData = inflate(this.fileData, this.uncompressedSize); + } + else { + err("UNSUPPORTED VERSION/FORMAT: ZIP v" + this.version + ", compression method=" + this.compressionMethod + ": " + this.filename + " (" + this.compressedSize + " bytes)"); + this.fileData = null; + } +}; + + +// Takes an ArrayBuffer of a zip file in +// returns null on error +// returns an array of DecompressedFile objects on success +var unzip = function(arrayBuffer) { + postMessage(new bitjs.archive.UnarchiveStartEvent()); + + currentFilename = ""; + currentFileNumber = 0; + currentBytesUnarchivedInFile = 0; + currentBytesUnarchived = 0; + totalUncompressedBytesInArchive = 0; + totalFilesInArchive = 0; + currentBytesUnarchived = 0; + + var bstream = new bitjs.io.ByteStream(arrayBuffer); + // detect local file header signature or return null + if (bstream.peekNumber(4) == zLocalFileHeaderSignature) { + var localFiles = []; + // loop until we don't see any more local files + while (bstream.peekNumber(4) == zLocalFileHeaderSignature) { + var oneLocalFile = new ZipLocalFile(bstream); + // this should strip out directories/folders + if (oneLocalFile && oneLocalFile.uncompressedSize > 0 && oneLocalFile.fileData) { + localFiles.push(oneLocalFile); + totalUncompressedBytesInArchive += oneLocalFile.uncompressedSize; + } + } + totalFilesInArchive = localFiles.length; + + // got all local files, now sort them + localFiles.sort(function(a,b) { + var aname = a.filename; + var bname = b.filename; + return aname > bname ? 1 : -1; + + // extract the number at the end of both filenames + /* + var aname = a.filename; + var bname = b.filename; + var aindex = aname.length, bindex = bname.length; + + // Find the last number character from the back of the filename. + while (aname[aindex-1] < '0' || aname[aindex-1] > '9') --aindex; + while (bname[bindex-1] < '0' || bname[bindex-1] > '9') --bindex; + + // Find the first number character from the back of the filename + while (aname[aindex-1] >= '0' && aname[aindex-1] <= '9') --aindex; + while (bname[bindex-1] >= '0' && bname[bindex-1] <= '9') --bindex; + + // parse them into numbers and return comparison + var anum = parseInt(aname.substr(aindex), 10), + bnum = parseInt(bname.substr(bindex), 10); + return anum - bnum; + */ + }); + + // archive extra data record + if (bstream.peekNumber(4) == zArchiveExtraDataSignature) { + info(" Found an Archive Extra Data Signature"); + + // skipping this record for now + bstream.readNumber(4); + var archiveExtraFieldLength = bstream.readNumber(4); + bstream.readString(archiveExtraFieldLength); + } + + // central directory structure + // TODO: handle the rest of the structures (Zip64 stuff) + if (bstream.peekNumber(4) == zCentralFileHeaderSignature) { + info(" Found a Central File Header"); + + // read all file headers + while (bstream.peekNumber(4) == zCentralFileHeaderSignature) { + bstream.readNumber(4); // signature + bstream.readNumber(2); // version made by + bstream.readNumber(2); // version needed to extract + bstream.readNumber(2); // general purpose bit flag + bstream.readNumber(2); // compression method + bstream.readNumber(2); // last mod file time + bstream.readNumber(2); // last mod file date + bstream.readNumber(4); // crc32 + bstream.readNumber(4); // compressed size + bstream.readNumber(4); // uncompressed size + var fileNameLength = bstream.readNumber(2); // file name length + var extraFieldLength = bstream.readNumber(2); // extra field length + var fileCommentLength = bstream.readNumber(2); // file comment length + bstream.readNumber(2); // disk number start + bstream.readNumber(2); // internal file attributes + bstream.readNumber(4); // external file attributes + bstream.readNumber(4); // relative offset of local header + + bstream.readString(fileNameLength); // file name + bstream.readString(extraFieldLength); // extra field + bstream.readString(fileCommentLength); // file comment + } + } + + // digital signature + if (bstream.peekNumber(4) == zDigitalSignatureSignature) { + info(" Found a Digital Signature"); + + bstream.readNumber(4); + var sizeOfSignature = bstream.readNumber(2); + bstream.readString(sizeOfSignature); // digital signature data + } + + // report # files and total length + if (localFiles.length > 0) { + postProgress(); + } + + // now do the unzipping of each file + for (var i = 0; i < localFiles.length; ++i) { + var localfile = localFiles[i]; + + // update progress + currentFilename = localfile.filename; + currentFileNumber = i; + currentBytesUnarchivedInFile = 0; + + // actually do the unzipping + localfile.unzip(); + + if (localfile.fileData != null) { + postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); + postProgress(); + } + } + postProgress(); + postMessage(new bitjs.archive.UnarchiveFinishEvent()); + } +} + +// returns a table of Huffman codes +// each entry's index is its code and its value is a JavaScript object +// containing {length: 6, symbol: X} +function getHuffmanCodes(bitLengths) { + // ensure bitLengths is an array containing at least one element + if (typeof bitLengths != typeof [] || bitLengths.length < 1) { + err("Error! getHuffmanCodes() called with an invalid array"); + return null; + } + + // Reference: http://tools.ietf.org/html/rfc1951#page-8 + var numLengths = bitLengths.length, + bl_count = [], + MAX_BITS = 1; + + // Step 1: count up how many codes of each length we have + for (var i = 0; i < numLengths; ++i) { + var length = bitLengths[i]; + // test to ensure each bit length is a positive, non-zero number + if (typeof length != typeof 1 || length < 0) { + err("bitLengths contained an invalid number in getHuffmanCodes(): " + length + " of type " + (typeof length)); + return null; + } + // increment the appropriate bitlength count + if (bl_count[length] == undefined) bl_count[length] = 0; + // a length of zero means this symbol is not participating in the huffman coding + if (length > 0) bl_count[length]++; + + if (length > MAX_BITS) MAX_BITS = length; + } + + // Step 2: Find the numerical value of the smallest code for each code length + var next_code = [], + code = 0; + for (var bits = 1; bits <= MAX_BITS; ++bits) { + var length = bits-1; + // ensure undefined lengths are zero + if (bl_count[length] == undefined) bl_count[length] = 0; + code = (code + bl_count[bits-1]) << 1; + next_code[bits] = code; + } + + // Step 3: Assign numerical values to all codes + var table = {}, tableLength = 0; + for (var n = 0; n < numLengths; ++n) { + var len = bitLengths[n]; + if (len != 0) { + table[next_code[len]] = { length: len, symbol: n }; //, bitstring: binaryValueToString(next_code[len],len) }; + tableLength++; + next_code[len]++; + } + } + table.maxLength = tableLength; + + return table; +} + +/* + The Huffman codes for the two alphabets are fixed, and are not + represented explicitly in the data. The Huffman code lengths + for the literal/length alphabet are: + + Lit Value Bits Codes + --------- ---- ----- + 0 - 143 8 00110000 through + 10111111 + 144 - 255 9 110010000 through + 111111111 + 256 - 279 7 0000000 through + 0010111 + 280 - 287 8 11000000 through + 11000111 +*/ +// fixed Huffman codes go from 7-9 bits, so we need an array whose index can hold up to 9 bits +var fixedHCtoLiteral = null; +var fixedHCtoDistance = null; +function getFixedLiteralTable() { + // create once + if (!fixedHCtoLiteral) { + var bitlengths = new Array(288); + for (var i = 0; i <= 143; ++i) bitlengths[i] = 8; + for (i = 144; i <= 255; ++i) bitlengths[i] = 9; + for (i = 256; i <= 279; ++i) bitlengths[i] = 7; + for (i = 280; i <= 287; ++i) bitlengths[i] = 8; + + // get huffman code table + fixedHCtoLiteral = getHuffmanCodes(bitlengths); + } + return fixedHCtoLiteral; +} +function getFixedDistanceTable() { + // create once + if (!fixedHCtoDistance) { + var bitlengths = new Array(32); + for (var i = 0; i < 32; ++i) { bitlengths[i] = 5; } + + // get huffman code table + fixedHCtoDistance = getHuffmanCodes(bitlengths); + } + return fixedHCtoDistance; +} + +// extract one bit at a time until we find a matching Huffman Code +// then return that symbol +function decodeSymbol(bstream, hcTable) { + var code = 0, len = 0; + var match = false; + + // loop until we match + for (;;) { + // read in next bit + var bit = bstream.readBits(1); + code = (code<<1) | bit; + ++len; + + // check against Huffman Code table and break if found + if (hcTable.hasOwnProperty(code) && hcTable[code].length == len) { + + break; + } + if (len > hcTable.maxLength) { + err("Bit stream out of sync, didn't find a Huffman Code, length was " + len + + " and table only max code length of " + hcTable.maxLength); + break; + } + } + return hcTable[code].symbol; +} + + +var CodeLengthCodeOrder = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; + /* + Extra Extra Extra + Code Bits Length(s) Code Bits Lengths Code Bits Length(s) + ---- ---- ------ ---- ---- ------- ---- ---- ------- + 257 0 3 267 1 15,16 277 4 67-82 + 258 0 4 268 1 17,18 278 4 83-98 + 259 0 5 269 2 19-22 279 4 99-114 + 260 0 6 270 2 23-26 280 4 115-130 + 261 0 7 271 2 27-30 281 5 131-162 + 262 0 8 272 2 31-34 282 5 163-194 + 263 0 9 273 3 35-42 283 5 195-226 + 264 0 10 274 3 43-50 284 5 227-257 + 265 1 11,12 275 3 51-58 285 0 258 + 266 1 13,14 276 3 59-66 + + */ +var LengthLookupTable = [ + [0,3], [0,4], [0,5], [0,6], + [0,7], [0,8], [0,9], [0,10], + [1,11], [1,13], [1,15], [1,17], + [2,19], [2,23], [2,27], [2,31], + [3,35], [3,43], [3,51], [3,59], + [4,67], [4,83], [4,99], [4,115], + [5,131], [5,163], [5,195], [5,227], + [0,258] +]; + /* + Extra Extra Extra + Code Bits Dist Code Bits Dist Code Bits Distance + ---- ---- ---- ---- ---- ------ ---- ---- -------- + 0 0 1 10 4 33-48 20 9 1025-1536 + 1 0 2 11 4 49-64 21 9 1537-2048 + 2 0 3 12 5 65-96 22 10 2049-3072 + 3 0 4 13 5 97-128 23 10 3073-4096 + 4 1 5,6 14 6 129-192 24 11 4097-6144 + 5 1 7,8 15 6 193-256 25 11 6145-8192 + 6 2 9-12 16 7 257-384 26 12 8193-12288 + 7 2 13-16 17 7 385-512 27 12 12289-16384 + 8 3 17-24 18 8 513-768 28 13 16385-24576 + 9 3 25-32 19 8 769-1024 29 13 24577-32768 + */ +var DistLookupTable = [ + [0,1], [0,2], [0,3], [0,4], + [1,5], [1,7], + [2,9], [2,13], + [3,17], [3,25], + [4,33], [4,49], + [5,65], [5,97], + [6,129], [6,193], + [7,257], [7,385], + [8,513], [8,769], + [9,1025], [9,1537], + [10,2049], [10,3073], + [11,4097], [11,6145], + [12,8193], [12,12289], + [13,16385], [13,24577] +]; + +function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) { + /* + loop (until end of block code recognized) + decode literal/length value from input stream + if value < 256 + copy value (literal byte) to output stream + otherwise + if value = end of block (256) + break from loop + otherwise (value = 257..285) + decode distance from input stream + + move backwards distance bytes in the output + stream, and copy length bytes from this + position to the output stream. + */ + var numSymbols = 0, blockSize = 0; + for (;;) { + var symbol = decodeSymbol(bstream, hcLiteralTable); + ++numSymbols; + if (symbol < 256) { + // copy literal byte to output + buffer.insertByte(symbol); + blockSize++; + } + else { + // end of block reached + if (symbol == 256) { + break; + } + else { + var lengthLookup = LengthLookupTable[symbol-257], + length = lengthLookup[1] + bstream.readBits(lengthLookup[0]), + distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)], + distance = distLookup[1] + bstream.readBits(distLookup[0]); + + // now apply length and distance appropriately and copy to output + + // TODO: check that backward distance < data.length? + + // http://tools.ietf.org/html/rfc1951#page-11 + // "Note also that the referenced string may overlap the current + // position; for example, if the last 2 bytes decoded have values + // X and Y, a string reference with + // adds X,Y,X,Y,X to the output stream." + // + // loop for each character + var ch = buffer.ptr - distance; + blockSize += length; + if(length > distance) { + var data = buffer.data; + while (length--) { + buffer.insertByte(data[ch++]); + } + } else { + buffer.insertBytes(buffer.data.subarray(ch, ch + length)) + } + + } // length-distance pair + } // length-distance pair or end-of-block + } // loop until we reach end of block + return blockSize; +} + +// {Uint8Array} compressedData A Uint8Array of the compressed file data. +// compression method 8 +// deflate: http://tools.ietf.org/html/rfc1951 +function inflate(compressedData, numDecompressedBytes) { + // Bit stream representing the compressed data. + var bstream = new bitjs.io.BitStream(compressedData.buffer, + false /* rtl */, + compressedData.byteOffset, + compressedData.byteLength); + var buffer = new bitjs.io.ByteBuffer(numDecompressedBytes); + var numBlocks = 0, blockSize = 0; + + // block format: http://tools.ietf.org/html/rfc1951#page-9 + do { + var bFinal = bstream.readBits(1), + bType = bstream.readBits(2); + blockSize = 0; + ++numBlocks; + // no compression + if (bType == 0) { + // skip remaining bits in this byte + while (bstream.bitPtr != 0) bstream.readBits(1); + var len = bstream.readBits(16), + nlen = bstream.readBits(16); + // TODO: check if nlen is the ones-complement of len? + + if(len > 0) buffer.insertBytes(bstream.readBytes(len)); + blockSize = len; + } + // fixed Huffman codes + else if(bType == 1) { + blockSize = inflateBlockData(bstream, getFixedLiteralTable(), getFixedDistanceTable(), buffer); + } + // dynamic Huffman codes + else if(bType == 2) { + var numLiteralLengthCodes = bstream.readBits(5) + 257; + var numDistanceCodes = bstream.readBits(5) + 1, + numCodeLengthCodes = bstream.readBits(4) + 4; + + // populate the array of code length codes (first de-compaction) + var codeLengthsCodeLengths = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + for (var i = 0; i < numCodeLengthCodes; ++i) { + codeLengthsCodeLengths[ CodeLengthCodeOrder[i] ] = bstream.readBits(3); + } + + // get the Huffman Codes for the code lengths + var codeLengthsCodes = getHuffmanCodes(codeLengthsCodeLengths); + + // now follow this mapping + /* + 0 - 15: Represent code lengths of 0 - 15 + 16: Copy the previous code length 3 - 6 times. + The next 2 bits indicate repeat length + (0 = 3, ... , 3 = 6) + Example: Codes 8, 16 (+2 bits 11), + 16 (+2 bits 10) will expand to + 12 code lengths of 8 (1 + 6 + 5) + 17: Repeat a code length of 0 for 3 - 10 times. + (3 bits of length) + 18: Repeat a code length of 0 for 11 - 138 times + (7 bits of length) + */ + // to generate the true code lengths of the Huffman Codes for the literal + // and distance tables together + var literalCodeLengths = []; + var prevCodeLength = 0; + while (literalCodeLengths.length < numLiteralLengthCodes + numDistanceCodes) { + var symbol = decodeSymbol(bstream, codeLengthsCodes); + if (symbol <= 15) { + literalCodeLengths.push(symbol); + prevCodeLength = symbol; + } + else if (symbol == 16) { + var repeat = bstream.readBits(2) + 3; + while (repeat--) { + literalCodeLengths.push(prevCodeLength); + } + } + else if (symbol == 17) { + var repeat = bstream.readBits(3) + 3; + while (repeat--) { + literalCodeLengths.push(0); + } + } + else if (symbol == 18) { + var repeat = bstream.readBits(7) + 11; + while (repeat--) { + literalCodeLengths.push(0); + } + } + } + + // now split the distance code lengths out of the literal code array + var distanceCodeLengths = literalCodeLengths.splice(numLiteralLengthCodes, numDistanceCodes); + + // now generate the true Huffman Code tables using these code lengths + var hcLiteralTable = getHuffmanCodes(literalCodeLengths), + hcDistanceTable = getHuffmanCodes(distanceCodeLengths); + blockSize = inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer); + } + // error + else { + err("Error! Encountered deflate block of type 3"); + return null; + } + + // update progress + currentBytesUnarchivedInFile += blockSize; + currentBytesUnarchived += blockSize; + postProgress(); + + } while (bFinal != 1); + // we are done reading blocks if the bFinal bit was set for this block + + // return the buffer data bytes + return buffer.data; +} + +// event.data.file has the ArrayBuffer. +onmessage = function(event) { + unzip(event.data.file, true); +}; diff --git a/static/cbr.js/cbr.js b/static/cbr.js/cbr.js new file mode 100644 index 000000000..c675fbcec --- /dev/null +++ b/static/cbr.js/cbr.js @@ -0,0 +1,462 @@ +this.cbrjs = {}; + +cbrjs.open = function(url) { + cbrjs.url = url + cbrjs.loadSettings() + Ox.load('UI', function() { + var $body = Ox.$('body') + .css({ + backgroundColor: 'rgb(255, 255, 255)', + overflowX: 'hidden' + }); + window.app = cbrjs.CBRViewer(Ox.extend({ + url: url, + }, cbrjs.settings) + ).bindEvent({ + page: cbrjs.updateSettings, + fitMode: cbrjs.updateSettings, + }).appendTo($body); + Ox.$window.on({ + resize: app.resize + }); + + }); + +}; + +cbrjs.loadSettings = function() { + var settings = {}; + try { + settings = JSON.parse(localStorage['cbrjs.' + cbrjs.url]); + } catch(e) { + settings.page = 1; + settings.fitMode = 'B'; + } + cbrjs.settings = settings; +} + +cbrjs.updateSettings = function(data) { + Ox.forEach(data, function(value, key) { + cbrjs.settings[key] = value; + }); + localStorage['cbrjs.' + cbrjs.url] = JSON.stringify(cbrjs.settings); +} + +cbrjs.CBRViewer = function(options, self) { + self = self || {}; + var that = Ox.Element({}, self) + .defaults({ + url: '', + page: 1 + }) + .options(options || {}) + .update({ + page: setPage, + fitMode: function() { + resize() + that.triggerEvent('fitMode', {fitMode: self.options.fitMode}); + }, + url: loadBook + }), + moveTimeout, + canvas; + + self.pages = []; + self.rotateTimes = 0; + self.hflip = false; + self.vflip = false; + + self.mimeTypes = { + 'png': 'image/png', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'gif': 'image/gif', + }; + + self.$frame = Ox.Element() + .on({ + mousemove: showMenu, + mousedown: function() { + self.$frame.gainFocus(); + }, + }) + .bindEvent({ + key_down: function() { + self.$frame.scrollTop(self.$frame.scrollTop() + self.$frame.height()) + }, + key_left: function() { + that.options({ + page: Math.max(self.options.page - 1, 1) + }) + }, + key_pagedown: function() { + that.options({ + page: self.pages.length + }) + }, + key_pageup: function() { + that.options({ + page: 1 + }) + }, + key_right: function() { + that.options({ + page: Math.min(self.options.page + 1, self.pages.length) + }) + }, + key_up: function() { + self.$frame.scrollTop(self.$frame.scrollTop() - self.$frame.height()) + }, + key_n: function() { + that.options({ + fitMode: 'N' + }) + }, + key_h: function() { + that.options({ + fitMode: 'H' + }) + }, + key_b: function() { + that.options({ + fitMode: 'B' + }) + }, + key_w: function() { + that.options({ + fitMode: 'W' + }) + }, + key_l: function() { + self.rotateTimes--; + if (self.rotateTimes < 0) { + self.rotateTimes = 3; + } + setPage(); + }, + key_f: function() { + if (!self.hflip && !self.vflip) { + self.hflip = true; + } else if(self.hflip == true) { + self.vflip = true; + self.hflip = false; + } else if(self.vflip == true) { + self.vflip = false; + } + setPage(); + }, + key_space: function() { + var old = self.$frame.scrollTop() + self.$frame.scrollTop(self.$frame.scrollTop() + self.$frame.height()) + if (self.$frame.scrollTop() == old) { + that.options({ + page: Math.min(self.options.page + 1, self.pages.length) + }) + } + }, + singleclick: function(data) { + if (data.clientX < (that.width() / 2)) { + that.options({ + page: Math.max(self.options.page - 1, 1) + }); + } else { + that.options({ + page: Math.min(self.options.page + 1, self.pages.length) + }); + } + } + }) + .css({ + margin: 0, + overflow: 'auto', + padding: 0, + textAlign: 'center' + }); + self.$canvas = Ox.Element('') + .appendTo(self.$frame); + canvas = self.$canvas[0]; + self.$scrollbar = Ox.Range({ + arrows: true, + max: self.options.page, + min: 1, + orientation: 'horizontal', + step: 1, + value: self.options.page, + thumbValue: true, + thumbSize: 64 + }).bindEvent({ + change: function(data) { + Ox.print('change', data); + that.options({ + page: data.value + }) + } + }); + + self.$panel = Ox.SplitPanel({}) + .appendTo(that); + that.setElement( + self.$mainPanel = Ox.SplitPanel({ + elements: [ + { + element: self.$frame, + }, + { + element: self.$scrollbar, + size: 16 + } + ], + orientation: 'vertical' + }) + ); + + self.$loading = Ox.LoadingScreen({ + size: 16, + }) + .css({ + margin: 'auto', + position: 'absolute' + }) + .start() + .appendTo(that); + + self.$menu = Ox.Element().css({ + position: 'fixed', + right: (Ox.UI.SCROLLBAR_SIZE + 4) + 'px', + top: '4px', + opacity: 0 + }) + .addClass('menu') + .appendTo(that); + + self.$zoom = Ox.Button({ + style: 'symbol', + title: 'fill', + tooltip: Ox._('Zoom to fill'), + type: 'image' + }).css({ + width: '16px', + height: '16px', + }).on({ + mousedown: function(event) { + if (self.$zoom.options('title') == 'fill') { + self.$zoom.options({ + title: 'fit', + tooltip: Ox._('Zoom to fit') + }) + that.options({ + fitMode: 'W' + }) + } else { + self.$zoom.options({ + title: 'fill', + tooltip: Ox._('Zoom to fill') + }) + that.options({ + fitMode: 'B' + }) + } + event.stopPropagation() + event.preventDefault(); + } + }) + .appendTo(self.$menu); + + loadBook(); + + function createURLFromArray(array, mimeType) { + var blob = new Blob([array], {type: mimeType}); + blob = blob.slice(array.byteOffset, array.byteOffset + array.byteLength, mimeType); + return URL.createObjectURL(blob); + } + + function isImage(filename) { + var extension = filename.split('.').pop().toLowerCase(); + return !Ox.isUndefined(self.mimeTypes[extension]); + } + + function loadBook() { + var xhr = new XMLHttpRequest(); + xhr.open('GET', self.options.url, true); + xhr.responseType = "arraybuffer"; + xhr.onload = function() { + loadFromArrayBuffer(this.response, function(pages, progress, total) { + self.pages = pages; + self.$scrollbar.options({ + max: Math.max(pages.length, self.options.page), + size: that.width() + }) + if (pages.length == self.options.page) { + setPage(); + if (self.$loading) { + self.$loading.remove(); + delete self.$loading; + } + } + }); + }; + xhr.send(null); + } + + function loadFromArrayBuffer(buffer, progress) { + var extract, + start = (new Date).getTime(), + h = new Uint8Array(buffer, 0, 10), + pathToBitJS = $('script').map(function(i, script) { + return script.src; + }).filter(function(i, url) { + return url.indexOf('bitjs') > -1; + })[0].replace('archive.js', ''); + + var pages = [], filenames = [], total; + + if (h[0] == 0x52 && h[1] == 0x61 && h[2] == 0x72 && h[3] == 0x21) { // RAR + extract = new bitjs.archive.Unrarrer(buffer, pathToBitJS); + } else if (h[0] == 80 && h[1] == 75) { // ZIP + extract = new bitjs.archive.Unzipper(buffer, pathToBitJS); + } else { // try tar otherwise + extract = new bitjs.archive.Untarrer(buffer, pathToBitJS); + } + if (extract) { + extract.addEventListener( + bitjs.archive.UnarchiveEvent.Type.PROGRESS, + function(e) { + var percentage = e.currentBytesUnarchived / e.totalUncompressedBytesInArchive; + total = e.totalFilesInArchive + progress([], percentage, total); + } + ); + extract.addEventListener( + bitjs.archive.UnarchiveEvent.Type.INFO, + function(e) { + Ox.Log('', e.msg); + } + ); + extract.addEventListener( + bitjs.archive.UnarchiveEvent.Type.EXTRACT, + function(e) { + // convert DecompressedFile into a bunch of ImageFiles + if (e.unarchivedFile) { + var f = e.unarchivedFile; + // add any new pages based on the filename + if (!Ox.contains(filenames, f.filename) && isImage(f.filename)) { + filenames.push(f.filename); + pages.push(f); + } else { + total--; + } + progress(pages, pages.length/total, total); + } + } + ); + extract.addEventListener( + bitjs.archive.UnarchiveEvent.Type.FINISH, + function(e) { + total = pages.length; + progress(pages, 1, pages.length); + var diff = ((new Date).getTime() - start)/1000; + Ox.Log('', 'Unarchiving done in ' + diff + 's'); + } + ); + extract.start(); + } else { + Ox.Log('', 'bitjs.archive failed to open file'); + } + } + + function resize(clear) { + canvas.style.width = ''; + canvas.style.height = ''; + canvas.style.maxWidth = ''; + canvas.style.maxHeight = ''; + var maxheight = self.$frame.height() - 4; + if (clear || self.options.fitMode == 'N') { + + } else if (self.options.fitMode == 'B') { + canvas.style.maxWidth = '100%'; + canvas.style.maxHeight = maxheight + 'px'; + } else if (self.options.fitMode == 'H') { + canvas.style.height = maxheight + 'px'; + } else if (self.options.fitMode == 'W') { + canvas.style.width = '100%'; + } + self.$scrollbar.options({ + size: that.width() + }); + } + + function setImage(url) { + var ctx = canvas.getContext('2d'), + img = new Image(); + img.onerror = function(e) { + canvas.width = innerWidth - 100; + canvas.height = 300; + resize(true); + ctx.fillStyle = 'orange'; + ctx.font = '50px sans-serif'; + ctx.strokeStyle = 'black'; + ctx.fillText('Page #' + (currentImage+1) + ' (' + + imageFiles[currentImage].filename + ')', 100, 100) + ctx.fillStyle = 'red'; + ctx.fillText('Is corrupt or not an image', 100, 200); + }; + img.onload = function() { + var h = img.height, + w = img.width, + sw = w, + sh = h; + self.rotateTimes = (4 + self.rotateTimes) % 4; + ctx.save(); + if (self.rotateTimes % 2 == 1) { sh = w; sw = h;} + canvas.height = sh; + canvas.width = sw; + ctx.translate(sw/2, sh/2); + ctx.rotate(Math.PI/2 * self.rotateTimes); + ctx.translate(-w/2, -h/2); + if (self.vflip) { + ctx.scale(1, -1) + ctx.translate(0, -h); + } + if (self.hflip) { + ctx.scale(-1, 1) + ctx.translate(-w, 0); + } + canvas.style.display = 'none'; + scrollTo(0,0); + ctx.drawImage(img, 0, 0); + + resize(); + + canvas.style.display = ''; + document.body.style.overflowY = ''; + ctx.restore(); + }; + img.src = url; + } + + function setPage() { + var file = self.pages[self.options.page - 1], + filename = file.filename, + extension = file.filename.split('.').pop().toLowerCase(), + mimeType = self.mimeTypes[extension]; + setImage(createURLFromArray(file.fileData, mimeType)); + self.$scrollbar.options({value: self.options.page}); + that.triggerEvent('page', {page: self.options.page}); + } + function showMenu() { + if (moveTimeout) { + clearTimeout(moveTimeout); + } else { + self.$menu.animate({opacity: 1}, 250) + } + moveTimeout = setTimeout(function() { + self.$menu.animate({opacity: 0}, 250) + moveTimeout = null + }, 5000); + } + + that.resize = function() { + resize(); + }; + return that; +}; diff --git a/static/cbr.js/index.html b/static/cbr.js/index.html new file mode 100644 index 000000000..a071f617e --- /dev/null +++ b/static/cbr.js/index.html @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/static/css/pandora.css b/static/css/pandora.css new file mode 100644 index 000000000..5a99c177c --- /dev/null +++ b/static/css/pandora.css @@ -0,0 +1,6 @@ +.InlineImages img { + float: left; + max-width: 256px; + max-height: 256px; + margin: 0 16px 16px 0; +} diff --git a/static/epub.js/css/annotations.css b/static/epub.js/css/annotations.css new file mode 100644 index 000000000..7a77e668d --- /dev/null +++ b/static/epub.js/css/annotations.css @@ -0,0 +1,3 @@ +.annotator-adder { + width: 80px; +} diff --git a/static/epub.js/css/main.css b/static/epub.js/css/main.css new file mode 100755 index 000000000..27e77a326 --- /dev/null +++ b/static/epub.js/css/main.css @@ -0,0 +1,817 @@ +@font-face { + font-family: 'fontello'; + src: url('../font/fontello.eot?60518104'); + src: url('../font/fontello.eot?60518104#iefix') format('embedded-opentype'), + url('../font/fontello.woff?60518104') format('woff'), + url('../font/fontello.ttf?60518104') format('truetype'), + url('../font/fontello.svg?60518104#fontello') format('svg'); + font-weight: normal; + font-style: normal; +} + +body { + background: #4e4e4e; + overflow: hidden; +} + +#main { + /* height: 500px; */ + position: absolute; + width: 100%; + height: 100%; + right: 0; + /* left: 40px; */ +/* -webkit-transform: translate(40px, 0); + -moz-transform: translate(40px, 0); */ + + /* border-radius: 5px 0px 0px 5px; */ + border-radius: 5px; + background: #fff; + overflow: hidden; + -webkit-transition: -webkit-transform .4s, width .2s; + -moz-transition: -webkit-transform .4s, width .2s; + -ms-transition: -webkit-transform .4s, width .2s; + + -moz-box-shadow: inset 0 0 50px rgba(0,0,0,.1); + -webkit-box-shadow: inset 0 0 50px rgba(0,0,0,.1); + -ms-box-shadow: inset 0 0 50px rgba(0,0,0,.1); + box-shadow: inset 0 0 50px rgba(0,0,0,.1); +} + + +#titlebar { + height: 8%; + min-height: 20px; + padding: 10px; + /* margin: 0 50px 0 50px; */ + position: relative; + color: #4f4f4f; + font-weight: 100; + font-family: Georgia, "Times New Roman", Times, serif; + opacity: .5; + text-align: center; + -webkit-transition: opacity .5s; + -moz-transition: opacity .5s; + -ms-transition: opacity .5s; + z-index: 10; +} + +#titlebar:hover { + opacity: 1; +} + +#titlebar a { + width: 18px; + height: 19px; + line-height: 20px; + overflow: hidden; + display: inline-block; + opacity: .5; + padding: 4px; + border-radius: 4px; +} + +#titlebar a::before { + visibility: visible; +} + +#titlebar a:hover { + opacity: .8; + border: 1px rgba(0,0,0,.2) solid; + padding: 3px; +} + +#titlebar a:active { + opacity: 1; + color: rgba(0,0,0,.6); + /* margin: 1px -1px -1px 1px; */ + -moz-box-shadow: inset 0 0 6px rgba(155,155,155,.8); + -webkit-box-shadow: inset 0 0 6px rgba(155,155,155,.8); + -ms-box-shadow: inset 0 0 6px rgba(155,155,155,.8); + box-shadow: inset 0 0 6px rgba(155,155,155,.8); +} + +#book-title { + font-weight: 600; +} + +#title-seperator { + display: none; +} + +#viewer { + width: 80%; + height: 80%; + /* margin-left: 10%; */ + margin: 0 auto; + max-width: 1250px; + z-index: 2; + position: relative; + overflow: hidden; +} + +#viewer iframe { + border: none; +} + +#prev { + left: 40px; +} + +#next { + right: 40px; +} + +.arrow { + position: absolute; + top: 50%; + margin-top: -32px; + font-size: 64px; + color: #E2E2E2; + font-family: arial, sans-serif; + font-weight: bold; + cursor: pointer; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.arrow:hover { + color: #777; +} + +.arrow:active, +.arrow.active { + color: #000; +} + +#sidebar { + background: #6b6b6b; + position: absolute; + /* left: -260px; */ + /* -webkit-transform: translate(-260px, 0); + -moz-transform: translate(-260px, 0); */ + top: 0; + min-width: 300px; + width: 25%; + height: 100%; + -webkit-transition: -webkit-transform .5s; + -moz-transition: -moz-transform .5s; + -ms-transition: -moz-transform .5s; + + overflow: hidden; +} + +#sidebar.open { + /* left: 0; */ + /* -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); */ +} + +#main.closed { + /* left: 300px; */ + -webkit-transform: translate(300px, 0); + -moz-transform: translate(300px, 0); + -ms-transform: translate(300px, 0); +} + +#main.single { + width: 75%; +} + +#main.single #viewer { + /* width: 60%; + margin-left: 20%; */ +} + +#panels { + background: #4e4e4e; + position: absolute; + left: 0; + top: 0; + width: 100%; + padding: 13px 0; + height: 14px; + -moz-box-shadow: 0px 1px 3px rgba(0,0,0,.6); + -webkit-box-shadow: 0px 1px 3px rgba(0,0,0,.6); + -ms-box-shadow: 0px 1px 3px rgba(0,0,0,.6); + box-shadow: 0px 1px 3px rgba(0,0,0,.6); +} + +#opener { + /* padding: 10px 10px; */ + float: left; +} + +/* #opener #slider { + width: 25px; +} */ + +#metainfo { + display: inline-block; + text-align: center; + max-width: 80%; +} + +#title-controls { + float: right; +} + +#panels a { + visibility: hidden; + width: 18px; + height: 20px; + overflow: hidden; + display: inline-block; + color: #ccc; + margin-left: 6px; +} + +#panels a::before { + visibility: visible; +} + +#panels a:hover { + color: #AAA; +} + +#panels a:active { + color: #AAA; + margin: 1px 0 -1px 6px; +} + +#panels a.active, +#panels a.active:hover { + color: #AAA; +} + +#searchBox { + width: 165px; + float: left; + margin-left: 10px; + margin-top: -1px; + /* + border-radius: 5px; + background: #9b9b9b; + float: left; + margin-left: 5px; + margin-top: -5px; + padding: 3px 10px; + color: #000; + border: none; + outline: none; */ + +} + +input::-webkit-input-placeholder { + color: #454545; +} +input:-moz-placeholder { + color: #454545; +} +input:-ms-placeholder { + color: #454545; +} + +#divider { + position: absolute; + width: 1px; + border-right: 1px #000 solid; + height: 80%; + z-index: 1; + left: 50%; + margin-left: -1px; + top: 10%; + opacity: .15; + box-shadow: -2px 0 15px rgba(0, 0, 0, 1); + display: none; +} + +#divider.show { + display: block; +} + +#loader { + position: absolute; + z-index: 10; + left: 50%; + top: 50%; + margin: -33px 0 0 -33px; +} + +#tocView, +#bookmarksView { + overflow-x: hidden; + overflow-y: hidden; + min-width: 300px; + width: 25%; + height: 100%; + visibility: hidden; + -webkit-transition: visibility 0 ease .5s; + -moz-transition: visibility 0 ease .5s; + -ms-transition: visibility 0 ease .5s; +} + + + +#sidebar.open #tocView, +#sidebar.open #bookmarksView { + overflow-y: auto; + visibility: visible; + -webkit-transition: visibility 0 ease 0; + -moz-transition: visibility 0 ease 0; + -ms-transition: visibility 0 ease 0; +} + +#sidebar.open #tocView { + display: block; +} + +#tocView > ul, +#bookmarksView > ul { + margin-top: 15px; + margin-bottom: 50px; + padding-left: 20px; + display: block; +} + +#tocView li, +#bookmarksView li { + margin-bottom:10px; + width: 225px; + font-family: Georgia, "Times New Roman", Times, serif; + list-style: none; + text-transform: capitalize; +} + +#tocView li:active, +#tocView li.currentChapter +{ + list-style: none; +} + +.list_item a { + color: #AAA; + text-decoration: none; +} + +.list_item a.chapter { + font-size: 1em; +} + +.list_item a.section { + font-size: .8em; +} + +.list_item.currentChapter > a, +.list_item a:hover { + color: #f1f1f1 +} + +/* #tocView li.openChapter > a, */ +.list_item a:hover { + color: #E2E2E2; +} + +.list_item ul { + padding-left:10px; + margin-top: 8px; + display: none; +} + +.list_item.currentChapter > ul, +.list_item.openChapter > ul { + display: block; +} + +#tocView.hidden { + display: none; +} + +.toc_toggle { + display: inline-block; + width: 14px; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.toc_toggle:before { + content: '▸'; + color: #fff; + margin-right: -4px; +} + +.currentChapter > .toc_toggle:before, +.openChapter > .toc_toggle:before { + content: '▾'; +} + +.view { + width: 300px; + height: 100%; + display: none; + padding-top: 50px; + overflow-y: auto; +} + +#searchResults { + margin-bottom: 50px; + padding-left: 20px; + display: block; +} + +#searchResults li { + margin-bottom:10px; + width: 225px; + font-family: Georgia, "Times New Roman", Times, serif; + list-style: none; +} + +#searchResults a { + color: #AAA; + text-decoration: none; +} + +#searchResults p { + text-decoration: none; + font-size: 12px; + line-height: 16px; +} + +#searchResults p .match { + background: #ccc; + color: #000; +} + +#searchResults li > p { + color: #AAA; +} + +#searchResults li a:hover { + color: #E2E2E2; +} + +#searchView.shown { + display: block; + overflow-y: scroll; +} + +#notes { + padding: 0 0 0 34px; +} + +#notes li { + color: #eee; + font-size: 12px; + width: 240px; + border-top: 1px #fff solid; + padding-top: 6px; + margin-bottom: 6px; +} + +#notes li a { + color: #fff; + display: inline-block; + margin-left: 6px; +} + +#notes li a:hover { + text-decoration: underline; +} + +#notes li img { + max-width: 240px; +} + +#note-text { + display: block; + width: 260px; + height: 80px; + margin: 0 auto; + padding: 5px; + border-radius: 5px; +} + +#note-text[disabled], #note-text[disabled="disabled"]{ + opacity: .5; +} + +#note-anchor { + margin-left: 218px; + margin-top: 5px; +} + +#settingsPanel { + display:none; +} + +#settingsPanel h3 { + color:#f1f1f1; + font-family:Georgia, "Times New Roman", Times, serif; + margin-bottom:10px; +} + +#settingsPanel ul { + margin-top:60px; + list-style-type:none; +} + +#settingsPanel li { + font-size:1em; + color:#f1f1f1; +} + +#settingsPanel .xsmall { font-size:x-small; } +#settingsPanel .small { font-size:small; } +#settingsPanel .medium { font-size:medium; } +#settingsPanel .large { font-size:large; } +#settingsPanel .xlarge { font-size:x-large; } + +.highlight { background-color: yellow } + +.modal { + position: fixed; + top: 50%; + left: 50%; + width: 50%; + width: 630px; + + height: auto; + z-index: 2000; + visibility: hidden; + margin-left: -320px; + margin-top: -160px; + +} + +.overlay { + position: fixed; + width: 100%; + height: 100%; + visibility: hidden; + top: 0; + left: 0; + z-index: 1000; + opacity: 0; + background: rgba(255,255,255,0.8); + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; +} + +.md-show { + visibility: visible; +} + +.md-show ~ .overlay { + opacity: 1; + visibility: visible; +} + +/* Content styles */ +.md-content { + color: #fff; + background: #6b6b6b; + position: relative; + border-radius: 3px; + margin: 0 auto; + height: 320px; +} + +.md-content h3 { + margin: 0; + padding: 6px; + text-align: center; + font-size: 22px; + font-weight: 300; + opacity: 0.8; + background: rgba(0,0,0,0.1); + border-radius: 3px 3px 0 0; +} + +.md-content > div { + padding: 15px 40px 30px; + margin: 0; + font-weight: 300; + font-size: 14px; +} + +.md-content > div p { + margin: 0; + padding: 10px 0; +} + +.md-content > div ul { + margin: 0; + padding: 0 0 30px 20px; +} + +.md-content > div ul li { + padding: 5px 0; +} + +.md-content button { + display: block; + margin: 0 auto; + font-size: 0.8em; +} + +/* Effect 1: Fade in and scale up */ +.md-effect-1 .md-content { + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + opacity: 0; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; +} + +.md-show.md-effect-1 .md-content { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + opacity: 1; +} + +.md-content > .closer { + font-size: 18px; + position: absolute; + right: 0; + top: 0; + font-size: 24px; + padding: 4px; +} + +@media only screen and (max-width: 1040px) { + #viewer{ + width: 50%; + margin-left: 25%; + } + + #divider, + #divider.show { + display: none; + } +} + +@media only screen and (max-width: 900px) { + #viewer{ + width: 60%; + margin-left: 20%; + } + + #prev { + left: 20px; + } + + #next { + right: 20px; + } +} + +@media only screen and (max-width: 550px) { + #viewer{ + width: 80%; + margin-left: 10%; + } + + #prev { + left: 0; + } + + #next { + right: 0; + } + + .arrow { + height: 100%; + top: 45px; + width: 10%; + text-indent: -10000px; + } + + #main { + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -webkit-transition: -webkit-transform .3s; + -moz-transition: -moz-transform .3s; + -ms-transition: -moz-transform .3s; + } + + #main.closed { + -webkit-transform: translate(260px, 0); + -moz-transform: translate(260px, 0); + -ms-transform: translate(260px, 0); + } + + #titlebar { + /* font-size: 16px; */ + /* margin: 0 50px 0 50px; */ + } + + #metainfo { + font-size: 10px; + } + + #tocView { + width: 260px; + } + + #tocView li { + font-size: 12px; + } + + #tocView > ul{ + padding-left: 10px; + } +} + + +/* For iPad portrait layouts only */ +@media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation: portrait) { + #viewer iframe { + width: 460px; + height: 740px; + } +} + /*For iPad landscape layouts only */ +@media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation: landscape) { + #viewer iframe { + width: 460px; + height: 415px; + } +} +/* For iPhone portrait layouts only */ +@media only screen and (max-device-width: 480px) and (orientation: portrait) { + #viewer { + width: 256px; + height: 432px; + } + #viewer iframe { + width: 256px; + height: 432px; + } +} +/* For iPhone landscape layouts only */ +@media only screen and (max-device-width: 480px) and (orientation: landscape) { + #viewer iframe { + width: 256px; + height: 124px; + } +} + +[class^="icon-"]:before, [class*=" icon-"]:before { + font-family: "fontello"; + font-style: normal; + font-weight: normal; + speak: none; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* you can be more comfortable with increased icons size */ + font-size: 112%; +} + + +.icon-search:before { content: '\e807'; } /* '' */ +.icon-resize-full-1:before { content: '\e804'; } /* '' */ +.icon-cancel-circled2:before { content: '\e80f'; } /* '' */ +.icon-link:before { content: '\e80d'; } /* '' */ +.icon-bookmark:before { content: '\e805'; } /* '' */ +.icon-bookmark-empty:before { content: '\e806'; } /* '' */ +.icon-download-cloud:before { content: '\e811'; } /* '' */ +.icon-edit:before { content: '\e814'; } /* '' */ +.icon-menu:before { content: '\e802'; } /* '' */ +.icon-cog:before { content: '\e813'; } /* '' */ +.icon-resize-full:before { content: '\e812'; } /* '' */ +.icon-cancel-circled:before { content: '\e80e'; } /* '' */ +.icon-up-dir:before { content: '\e80c'; } /* '' */ +.icon-right-dir:before { content: '\e80b'; } /* '' */ +.icon-angle-right:before { content: '\e809'; } /* '' */ +.icon-angle-down:before { content: '\e80a'; } /* '' */ +.icon-right:before { content: '\e815'; } /* '' */ +.icon-list-1:before { content: '\e803'; } /* '' */ +.icon-list-numbered:before { content: '\e801'; } /* '' */ +.icon-columns:before { content: '\e810'; } /* '' */ +.icon-list:before { content: '\e800'; } /* '' */ +.icon-resize-small:before { content: '\e808'; } /* '' */ diff --git a/static/epub.js/css/normalize.css b/static/epub.js/css/normalize.css new file mode 100755 index 000000000..c3e014d95 --- /dev/null +++ b/static/epub.js/css/normalize.css @@ -0,0 +1,505 @@ +/*! normalize.css v1.0.1 | MIT License | git.io/normalize */ + +/* ========================================================================== + HTML5 display definitions + ========================================================================== */ + +/* + * Corrects `block` display not defined in IE 6/7/8/9 and Firefox 3. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary { + display: block; +} + +/* + * Corrects `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. + */ + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +/* + * Prevents modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/* + * Addresses styling for `hidden` attribute not present in IE 7/8/9, Firefox 3, + * and Safari 4. + * Known issue: no IE 6 support. + */ + +[hidden] { + display: none; +} + +/* ========================================================================== + Base + ========================================================================== */ + +/* + * 1. Corrects text resizing oddly in IE 6/7 when body `font-size` is set using + * `em` units. + * 2. Prevents iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-size: 100%; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -ms-text-size-adjust: 100%; /* 2 */ +} + +/* + * Addresses `font-family` inconsistency between `textarea` and other form + * elements. + */ + +html, +button, +input, +select, +textarea { + font-family: sans-serif; +} + +/* + * Addresses margins handled incorrectly in IE 6/7. + */ + +body { + margin: 0; +} + +/* ========================================================================== + Links + ========================================================================== */ + +/* + * Addresses `outline` inconsistency between Chrome and other browsers. + */ + +a:focus { + outline: thin dotted; +} + +/* + * Improves readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* ========================================================================== + Typography + ========================================================================== */ + +/* + * Addresses font sizes and margins set differently in IE 6/7. + * Addresses font sizes within `section` and `article` in Firefox 4+, Safari 5, + * and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.75em; + margin: 2.33em 0; +} + +/* + * Addresses styling not present in IE 7/8/9, Safari 5, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/* + * Addresses style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +/* + * Addresses styling not present in Safari 5 and Chrome. + */ + +dfn { + font-style: italic; +} + +/* + * Addresses styling not present in IE 6/7/8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/* + * Addresses margins set differently in IE 6/7. + */ + +p, +pre { + margin: 1em 0; +} + +/* + * Corrects font family set oddly in IE 6, Safari 4/5, and Chrome. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, serif; + _font-family: 'courier new', monospace; + font-size: 1em; +} + +/* + * Improves readability of pre-formatted text in all browsers. + */ + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* + * Addresses CSS quotes not supported in IE 6/7. + */ + +q { + quotes: none; +} + +/* + * Addresses `quotes` property not supported in Safari 4. + */ + +q:before, +q:after { + content: ''; + content: none; +} + +/* + * Addresses inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/* + * Prevents `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* ========================================================================== + Lists + ========================================================================== */ + +/* + * Addresses margins set differently in IE 6/7. + */ + +dl, +menu, +ol, +ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +/* + * Addresses paddings set differently in IE 6/7. + */ + +menu, +ol, +ul { + padding: 0 0 0 40px; +} + +/* + * Corrects list images handled incorrectly in IE 7. + */ + +nav ul, +nav ol { + list-style: none; + list-style-image: none; +} + +/* ========================================================================== + Embedded content + ========================================================================== */ + +/* + * 1. Removes border when inside `a` element in IE 6/7/8/9 and Firefox 3. + * 2. Improves image quality when scaled in IE 7. + */ + +img { + border: 0; /* 1 */ + -ms-interpolation-mode: bicubic; /* 2 */ +} + +/* + * Corrects overflow displayed oddly in IE 9. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* ========================================================================== + Figures + ========================================================================== */ + +/* + * Addresses margin not present in IE 6/7/8/9, Safari 5, and Opera 11. + */ + +figure { + margin: 0; +} + +/* ========================================================================== + Forms + ========================================================================== */ + +/* + * Corrects margin displayed oddly in IE 6/7. + */ + +form { + margin: 0; +} + +/* + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/* + * 1. Corrects color not being inherited in IE 6/7/8/9. + * 2. Corrects text not wrapping in Firefox 3. + * 3. Corrects alignment displayed oddly in IE 6/7. + */ + +legend { + border: 0; /* 1 */ + padding: 0; + white-space: normal; /* 2 */ + *margin-left: -7px; /* 3 */ +} + +/* + * 1. Corrects font size not being inherited in all browsers. + * 2. Addresses margins set differently in IE 6/7, Firefox 3+, Safari 5, + * and Chrome. + * 3. Improves appearance and consistency in all browsers. + */ + +button, +input, +select, +textarea { + font-size: 100%; /* 1 */ + margin: 0; /* 2 */ + vertical-align: baseline; /* 3 */ + *vertical-align: middle; /* 3 */ +} + +/* + * Addresses Firefox 3+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +button, +input { + line-height: normal; +} + +/* + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Corrects inability to style clickable `input` types in iOS. + * 3. Improves usability and consistency of cursor style between image-type + * `input` and others. + * 4. Removes inner spacing in IE 7 without affecting normal text inputs. + * Known issue: inner spacing remains in IE 6. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ + *overflow: visible; /* 4 */ +} + +/* + * Re-set default cursor for disabled elements. + */ + +button[disabled], +input[disabled] { + cursor: default; +} + +/* + * 1. Addresses box sizing set to content-box in IE 8/9. + * 2. Removes excess padding in IE 8/9. + * 3. Removes excess padding in IE 7. + * Known issue: excess padding remains in IE 6. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + *height: 13px; /* 3 */ + *width: 13px; /* 3 */ +} + +/* + * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. + * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome + * (include `-moz` to future-proof). + */ +/* +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; +} +*/ + +/* + * Removes inner padding and search cancel button in Safari 5 and Chrome + * on OS X. + */ + +/* input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} */ + +/* + * Removes inner padding and border in Firefox 3+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/* + * 1. Removes default vertical scrollbar in IE 6/7/8/9. + * 2. Improves readability and alignment in all browsers. + */ + +textarea { + overflow: auto; /* 1 */ + vertical-align: top; /* 2 */ +} + +/* ========================================================================== + Tables + ========================================================================== */ + +/* + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/static/epub.js/css/popup.css b/static/epub.js/css/popup.css new file mode 100644 index 000000000..c41aac716 --- /dev/null +++ b/static/epub.js/css/popup.css @@ -0,0 +1,96 @@ +/* http://davidwalsh.name/css-tooltips */ +/* base CSS element */ +.popup { + background: #eee; + border: 1px solid #ccc; + padding: 10px; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + position: fixed; + max-width: 300px; + font-size: 12px; + + display: none; + margin-left: 2px; + + margin-top: 30px; +} + +.popup.above { + margin-top: -10px; +} + +.popup.left { + margin-left: -20px; +} + +.popup.right { + margin-left: 40px; +} + +.pop_content { + max-height: 225px; + overflow-y: auto; +} + +.pop_content > p { + margin-top: 0; +} + +/* below */ +.popup:before { + position: absolute; + display: inline-block; + border-bottom: 10px solid #eee; + border-right: 10px solid transparent; + border-left: 10px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + left: 50%; + top: -10px; + margin-left: -6px; + content: ''; +} + +.popup:after { + position: absolute; + display: inline-block; + border-bottom: 9px solid #eee; + border-right: 9px solid transparent; + border-left: 9px solid transparent; + left: 50%; + top: -9px; + margin-left: -5px; + content: ''; +} + +/* above */ +.popup.above:before { + border-bottom: none; + border-top: 10px solid #eee; + border-top-color: rgba(0, 0, 0, 0.2); + top: 100%; +} + +.popup.above:after { + border-bottom: none; + border-top: 9px solid #eee; + top: 100%; +} + +.popup.left:before, +.popup.left:after +{ + left: 20px; +} + +.popup.right:before, +.popup.right:after +{ + left: auto; + right: 20px; +} + + +.popup.show, .popup.on { + display: block; +} \ No newline at end of file diff --git a/static/epub.js/font/fontello.eot b/static/epub.js/font/fontello.eot new file mode 100644 index 000000000..f63ffa043 Binary files /dev/null and b/static/epub.js/font/fontello.eot differ diff --git a/static/epub.js/font/fontello.svg b/static/epub.js/font/fontello.svg new file mode 100644 index 000000000..2db13984a --- /dev/null +++ b/static/epub.js/font/fontello.svg @@ -0,0 +1,33 @@ + + + +Copyright (C) 2013 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/epub.js/font/fontello.ttf b/static/epub.js/font/fontello.ttf new file mode 100644 index 000000000..95715f866 Binary files /dev/null and b/static/epub.js/font/fontello.ttf differ diff --git a/static/epub.js/font/fontello.woff b/static/epub.js/font/fontello.woff new file mode 100644 index 000000000..084f0c55c Binary files /dev/null and b/static/epub.js/font/fontello.woff differ diff --git a/static/epub.js/img/.gitignore b/static/epub.js/img/.gitignore new file mode 100755 index 000000000..e69de29bb diff --git a/static/epub.js/img/annotator-glyph-sprite.png b/static/epub.js/img/annotator-glyph-sprite.png new file mode 100644 index 000000000..5bb11cd84 Binary files /dev/null and b/static/epub.js/img/annotator-glyph-sprite.png differ diff --git a/static/epub.js/img/annotator-icon-sprite.png b/static/epub.js/img/annotator-icon-sprite.png new file mode 100644 index 000000000..3058651c5 Binary files /dev/null and b/static/epub.js/img/annotator-icon-sprite.png differ diff --git a/static/epub.js/img/apple-touch-icon.png b/static/epub.js/img/apple-touch-icon.png new file mode 100755 index 000000000..efc5c5a61 Binary files /dev/null and b/static/epub.js/img/apple-touch-icon.png differ diff --git a/static/epub.js/img/cancelfullscreen.png b/static/epub.js/img/cancelfullscreen.png new file mode 100644 index 000000000..bcf409e78 Binary files /dev/null and b/static/epub.js/img/cancelfullscreen.png differ diff --git a/static/epub.js/img/close.png b/static/epub.js/img/close.png new file mode 100644 index 000000000..46189e543 Binary files /dev/null and b/static/epub.js/img/close.png differ diff --git a/static/epub.js/img/fullscreen.png b/static/epub.js/img/fullscreen.png new file mode 100644 index 000000000..2f8d48ded Binary files /dev/null and b/static/epub.js/img/fullscreen.png differ diff --git a/static/epub.js/img/loader.gif b/static/epub.js/img/loader.gif new file mode 100644 index 000000000..68005bcbe Binary files /dev/null and b/static/epub.js/img/loader.gif differ diff --git a/static/epub.js/img/menu-icon.png b/static/epub.js/img/menu-icon.png new file mode 100644 index 000000000..5f40e0e9c Binary files /dev/null and b/static/epub.js/img/menu-icon.png differ diff --git a/static/epub.js/img/save.png b/static/epub.js/img/save.png new file mode 100644 index 000000000..5a5bc0e09 Binary files /dev/null and b/static/epub.js/img/save.png differ diff --git a/static/epub.js/img/saved.png b/static/epub.js/img/saved.png new file mode 100644 index 000000000..0f1981bdd Binary files /dev/null and b/static/epub.js/img/saved.png differ diff --git a/static/epub.js/img/settings-s.png b/static/epub.js/img/settings-s.png new file mode 100644 index 000000000..fc21e7873 Binary files /dev/null and b/static/epub.js/img/settings-s.png differ diff --git a/static/epub.js/img/settings.png b/static/epub.js/img/settings.png new file mode 100644 index 000000000..be20cba08 Binary files /dev/null and b/static/epub.js/img/settings.png differ diff --git a/static/epub.js/img/star.png b/static/epub.js/img/star.png new file mode 100644 index 000000000..451089359 Binary files /dev/null and b/static/epub.js/img/star.png differ diff --git a/static/epub.js/index.html b/static/epub.js/index.html new file mode 100755 index 000000000..8b1378917 --- /dev/null +++ b/static/epub.js/index.html @@ -0,0 +1 @@ + diff --git a/static/epub.js/js/epub.js b/static/epub.js/js/epub.js new file mode 100644 index 000000000..646e33953 --- /dev/null +++ b/static/epub.js/js/epub.js @@ -0,0 +1,23139 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("xmldom"), (function webpackLoadOptionalExternalModule() { try { return require("jszip"); } catch(e) {} }())); + else if(typeof define === 'function' && define.amd) + define(["xmldom", "jszip"], factory); + else if(typeof exports === 'object') + exports["ePub"] = factory(require("xmldom"), (function webpackLoadOptionalExternalModule() { try { return require("jszip"); } catch(e) {} }())); + else + root["ePub"] = factory(root["xmldom"], root["jszip"]); +})(typeof self !== 'undefined' ? self : this, function(__WEBPACK_EXTERNAL_MODULE_42__, __WEBPACK_EXTERNAL_MODULE_72__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "/dist/"; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 25); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +exports.uuid = uuid; +exports.documentHeight = documentHeight; +exports.isElement = isElement; +exports.isNumber = isNumber; +exports.isFloat = isFloat; +exports.prefixed = prefixed; +exports.defaults = defaults; +exports.extend = extend; +exports.insert = insert; +exports.locationOf = locationOf; +exports.indexOfSorted = indexOfSorted; +exports.bounds = bounds; +exports.borders = borders; +exports.nodeBounds = nodeBounds; +exports.windowBounds = windowBounds; +exports.indexOfNode = indexOfNode; +exports.indexOfTextNode = indexOfTextNode; +exports.indexOfElementNode = indexOfElementNode; +exports.isXml = isXml; +exports.createBlob = createBlob; +exports.createBlobUrl = createBlobUrl; +exports.revokeBlobUrl = revokeBlobUrl; +exports.createBase64Url = createBase64Url; +exports.type = type; +exports.parse = parse; +exports.qs = qs; +exports.qsa = qsa; +exports.qsp = qsp; +exports.sprint = sprint; +exports.treeWalker = treeWalker; +exports.walk = walk; +exports.blob2base64 = blob2base64; +exports.defer = defer; +exports.querySelectorByType = querySelectorByType; +exports.findChildren = findChildren; +exports.parents = parents; +exports.filterChildren = filterChildren; +exports.getParentByTagName = getParentByTagName; + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * Core Utilities and Helpers + * @module Core +*/ + +/** + * Vendor prefixed requestAnimationFrame + * @returns {function} requestAnimationFrame + * @memberof Core + */ +var requestAnimationFrame = exports.requestAnimationFrame = typeof window != "undefined" ? window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame : false; +var ELEMENT_NODE = 1; +var TEXT_NODE = 3; +var COMMENT_NODE = 8; +var DOCUMENT_NODE = 9; +var _URL = typeof URL != "undefined" ? URL : typeof window != "undefined" ? window.URL || window.webkitURL || window.mozURL : undefined; + +/** + * Generates a UUID + * based on: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript + * @returns {string} uuid + * @memberof Core + */ +function uuid() { + var d = new Date().getTime(); + var uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c == "x" ? r : r & 0x7 | 0x8).toString(16); + }); + return uuid; +} + +/** + * Gets the height of a document + * @returns {number} height + * @memberof Core + */ +function documentHeight() { + return Math.max(document.documentElement.clientHeight, document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight); +} + +/** + * Checks if a node is an element + * @param {object} obj + * @returns {boolean} + * @memberof Core + */ +function isElement(obj) { + return !!(obj && obj.nodeType == 1); +} + +/** + * @param {any} n + * @returns {boolean} + * @memberof Core + */ +function isNumber(n) { + return !isNaN(parseFloat(n)) && isFinite(n); +} + +/** + * @param {any} n + * @returns {boolean} + * @memberof Core + */ +function isFloat(n) { + var f = parseFloat(n); + + if (isNumber(n) === false) { + return false; + } + + if (typeof n === "string" && n.indexOf(".") > -1) { + return true; + } + + return Math.floor(f) !== f; +} + +/** + * Get a prefixed css property + * @param {string} unprefixed + * @returns {string} + * @memberof Core + */ +function prefixed(unprefixed) { + var vendors = ["Webkit", "webkit", "Moz", "O", "ms"]; + var prefixes = ["-webkit-", "-webkit-", "-moz-", "-o-", "-ms-"]; + var upper = unprefixed[0].toUpperCase() + unprefixed.slice(1); + var length = vendors.length; + + if (typeof document === "undefined" || typeof document.body.style[unprefixed] != "undefined") { + return unprefixed; + } + + for (var i = 0; i < length; i++) { + if (typeof document.body.style[vendors[i] + upper] != "undefined") { + return prefixes[i] + unprefixed; + } + } + + return unprefixed; +} + +/** + * Apply defaults to an object + * @param {object} obj + * @returns {object} + * @memberof Core + */ +function defaults(obj) { + for (var i = 1, length = arguments.length; i < length; i++) { + var source = arguments[i]; + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } + } + return obj; +} + +/** + * Extend properties of an object + * @param {object} target + * @returns {object} + * @memberof Core + */ +function extend(target) { + var sources = [].slice.call(arguments, 1); + sources.forEach(function (source) { + if (!source) return; + Object.getOwnPropertyNames(source).forEach(function (propName) { + Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName)); + }); + }); + return target; +} + +/** + * Fast quicksort insert for sorted array -- based on: + * http://stackoverflow.com/questions/1344500/efficient-way-to-insert-a-number-into-a-sorted-array-of-numbers + * @param {any} item + * @param {array} array + * @param {function} [compareFunction] + * @returns {number} location (in array) + * @memberof Core + */ +function insert(item, array, compareFunction) { + var location = locationOf(item, array, compareFunction); + array.splice(location, 0, item); + + return location; +} + +/** + * Finds where something would fit into a sorted array + * @param {any} item + * @param {array} array + * @param {function} [compareFunction] + * @param {function} [_start] + * @param {function} [_end] + * @returns {number} location (in array) + * @memberof Core + */ +function locationOf(item, array, compareFunction, _start, _end) { + var start = _start || 0; + var end = _end || array.length; + var pivot = parseInt(start + (end - start) / 2); + var compared; + if (!compareFunction) { + compareFunction = function compareFunction(a, b) { + if (a > b) return 1; + if (a < b) return -1; + if (a == b) return 0; + }; + } + if (end - start <= 0) { + return pivot; + } + + compared = compareFunction(array[pivot], item); + if (end - start === 1) { + return compared >= 0 ? pivot : pivot + 1; + } + if (compared === 0) { + return pivot; + } + if (compared === -1) { + return locationOf(item, array, compareFunction, pivot, end); + } else { + return locationOf(item, array, compareFunction, start, pivot); + } +} + +/** + * Finds index of something in a sorted array + * Returns -1 if not found + * @param {any} item + * @param {array} array + * @param {function} [compareFunction] + * @param {function} [_start] + * @param {function} [_end] + * @returns {number} index (in array) or -1 + * @memberof Core + */ +function indexOfSorted(item, array, compareFunction, _start, _end) { + var start = _start || 0; + var end = _end || array.length; + var pivot = parseInt(start + (end - start) / 2); + var compared; + if (!compareFunction) { + compareFunction = function compareFunction(a, b) { + if (a > b) return 1; + if (a < b) return -1; + if (a == b) return 0; + }; + } + if (end - start <= 0) { + return -1; // Not found + } + + compared = compareFunction(array[pivot], item); + if (end - start === 1) { + return compared === 0 ? pivot : -1; + } + if (compared === 0) { + return pivot; // Found + } + if (compared === -1) { + return indexOfSorted(item, array, compareFunction, pivot, end); + } else { + return indexOfSorted(item, array, compareFunction, start, pivot); + } +} +/** + * Find the bounds of an element + * taking padding and margin into account + * @param {element} el + * @returns {{ width: Number, height: Number}} + * @memberof Core + */ +function bounds(el) { + + var style = window.getComputedStyle(el); + var widthProps = ["width", "paddingRight", "paddingLeft", "marginRight", "marginLeft", "borderRightWidth", "borderLeftWidth"]; + var heightProps = ["height", "paddingTop", "paddingBottom", "marginTop", "marginBottom", "borderTopWidth", "borderBottomWidth"]; + + var width = 0; + var height = 0; + + widthProps.forEach(function (prop) { + width += parseFloat(style[prop]) || 0; + }); + + heightProps.forEach(function (prop) { + height += parseFloat(style[prop]) || 0; + }); + + return { + height: height, + width: width + }; +} + +/** + * Find the bounds of an element + * taking padding, margin and borders into account + * @param {element} el + * @returns {{ width: Number, height: Number}} + * @memberof Core + */ +function borders(el) { + + var style = window.getComputedStyle(el); + var widthProps = ["paddingRight", "paddingLeft", "marginRight", "marginLeft", "borderRightWidth", "borderLeftWidth"]; + var heightProps = ["paddingTop", "paddingBottom", "marginTop", "marginBottom", "borderTopWidth", "borderBottomWidth"]; + + var width = 0; + var height = 0; + + widthProps.forEach(function (prop) { + width += parseFloat(style[prop]) || 0; + }); + + heightProps.forEach(function (prop) { + height += parseFloat(style[prop]) || 0; + }); + + return { + height: height, + width: width + }; +} + +/** + * Find the bounds of any node + * allows for getting bounds of text nodes by wrapping them in a range + * @param {node} node + * @returns {BoundingClientRect} + * @memberof Core + */ +function nodeBounds(node) { + var elPos = void 0; + var doc = node.ownerDocument; + if (node.nodeType == Node.TEXT_NODE) { + var elRange = doc.createRange(); + elRange.selectNodeContents(node); + elPos = elRange.getBoundingClientRect(); + } else { + elPos = node.getBoundingClientRect(); + } + return elPos; +} + +/** + * Find the equivelent of getBoundingClientRect of a browser window + * @returns {{ width: Number, height: Number, top: Number, left: Number, right: Number, bottom: Number }} + * @memberof Core + */ +function windowBounds() { + + var width = window.innerWidth; + var height = window.innerHeight; + + return { + top: 0, + left: 0, + right: width, + bottom: height, + width: width, + height: height + }; +} + +/** + * Gets the index of a node in its parent + * @param {Node} node + * @param {string} typeId + * @return {number} index + * @memberof Core + */ +function indexOfNode(node, typeId) { + var parent = node.parentNode; + var children = parent.childNodes; + var sib; + var index = -1; + for (var i = 0; i < children.length; i++) { + sib = children[i]; + if (sib.nodeType === typeId) { + index++; + } + if (sib == node) break; + } + + return index; +} + +/** + * Gets the index of a text node in its parent + * @param {node} textNode + * @returns {number} index + * @memberof Core + */ +function indexOfTextNode(textNode) { + return indexOfNode(textNode, TEXT_NODE); +} + +/** + * Gets the index of an element node in its parent + * @param {element} elementNode + * @returns {number} index + * @memberof Core + */ +function indexOfElementNode(elementNode) { + return indexOfNode(elementNode, ELEMENT_NODE); +} + +/** + * Check if extension is xml + * @param {string} ext + * @returns {boolean} + * @memberof Core + */ +function isXml(ext) { + return ["xml", "opf", "ncx"].indexOf(ext) > -1; +} + +/** + * Create a new blob + * @param {any} content + * @param {string} mime + * @returns {Blob} + * @memberof Core + */ +function createBlob(content, mime) { + return new Blob([content], { type: mime }); +} + +/** + * Create a new blob url + * @param {any} content + * @param {string} mime + * @returns {string} url + * @memberof Core + */ +function createBlobUrl(content, mime) { + var tempUrl; + var blob = createBlob(content, mime); + + tempUrl = _URL.createObjectURL(blob); + + return tempUrl; +} + +/** + * Remove a blob url + * @param {string} url + * @memberof Core + */ +function revokeBlobUrl(url) { + return _URL.revokeObjectURL(url); +} + +/** + * Create a new base64 encoded url + * @param {any} content + * @param {string} mime + * @returns {string} url + * @memberof Core + */ +function createBase64Url(content, mime) { + var data; + var datauri; + + if (typeof content !== "string") { + // Only handles strings + return; + } + + data = btoa(encodeURIComponent(content)); + + datauri = "data:" + mime + ";base64," + data; + + return datauri; +} + +/** + * Get type of an object + * @param {object} obj + * @returns {string} type + * @memberof Core + */ +function type(obj) { + return Object.prototype.toString.call(obj).slice(8, -1); +} + +/** + * Parse xml (or html) markup + * @param {string} markup + * @param {string} mime + * @param {boolean} forceXMLDom force using xmlDom to parse instead of native parser + * @returns {document} document + * @memberof Core + */ +function parse(markup, mime, forceXMLDom) { + var doc; + var Parser; + + if (typeof DOMParser === "undefined" || forceXMLDom) { + Parser = __webpack_require__(42).DOMParser; + } else { + Parser = DOMParser; + } + + // Remove byte order mark before parsing + // https://www.w3.org/International/questions/qa-byte-order-mark + if (markup.charCodeAt(0) === 0xFEFF) { + markup = markup.slice(1); + } + + doc = new Parser().parseFromString(markup, mime); + + return doc; +} + +/** + * querySelector polyfill + * @param {element} el + * @param {string} sel selector string + * @returns {element} element + * @memberof Core + */ +function qs(el, sel) { + var elements; + if (!el) { + throw new Error("No Element Provided"); + } + + if (typeof el.querySelector != "undefined") { + return el.querySelector(sel); + } else { + elements = el.getElementsByTagName(sel); + if (elements.length) { + return elements[0]; + } + } +} + +/** + * querySelectorAll polyfill + * @param {element} el + * @param {string} sel selector string + * @returns {element[]} elements + * @memberof Core + */ +function qsa(el, sel) { + + if (typeof el.querySelector != "undefined") { + return el.querySelectorAll(sel); + } else { + return el.getElementsByTagName(sel); + } +} + +/** + * querySelector by property + * @param {element} el + * @param {string} sel selector string + * @param {object[]} props + * @returns {element[]} elements + * @memberof Core + */ +function qsp(el, sel, props) { + var q, filtered; + if (typeof el.querySelector != "undefined") { + sel += "["; + for (var prop in props) { + sel += prop + "~='" + props[prop] + "'"; + } + sel += "]"; + return el.querySelector(sel); + } else { + q = el.getElementsByTagName(sel); + filtered = Array.prototype.slice.call(q, 0).filter(function (el) { + for (var prop in props) { + if (el.getAttribute(prop) === props[prop]) { + return true; + } + } + return false; + }); + + if (filtered) { + return filtered[0]; + } + } +} + +/** + * Sprint through all text nodes in a document + * @memberof Core + * @param {element} root element to start with + * @param {function} func function to run on each element + */ +function sprint(root, func) { + var doc = root.ownerDocument || root; + if (typeof doc.createTreeWalker !== "undefined") { + treeWalker(root, func, NodeFilter.SHOW_TEXT); + } else { + walk(root, function (node) { + if (node && node.nodeType === 3) { + // Node.TEXT_NODE + func(node); + } + }, true); + } +} + +/** + * Create a treeWalker + * @memberof Core + * @param {element} root element to start with + * @param {function} func function to run on each element + * @param {function | object} filter funtion or object to filter with + */ +function treeWalker(root, func, filter) { + var treeWalker = document.createTreeWalker(root, filter, null, false); + var node = void 0; + while (node = treeWalker.nextNode()) { + func(node); + } +} + +/** + * @memberof Core + * @param {node} node + * @param {callback} return false for continue,true for break inside callback + */ +function walk(node, callback) { + if (callback(node)) { + return true; + } + node = node.firstChild; + if (node) { + do { + var walked = walk(node, callback); + if (walked) { + return true; + } + node = node.nextSibling; + } while (node); + } +} + +/** + * Convert a blob to a base64 encoded string + * @param {Blog} blob + * @returns {string} + * @memberof Core + */ +function blob2base64(blob) { + return new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = function () { + resolve(reader.result); + }; + }); +} + +/** + * Creates a new pending promise and provides methods to resolve or reject it. + * From: https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred#backwards_forwards_compatible + * @memberof Core + */ +function defer() { + var _this = this; + + /* A method to resolve the associated Promise with the value passed. + * If the promise is already settled it does nothing. + * + * @param {anything} value : This value is used to resolve the promise + * If the value is a Promise then the associated promise assumes the state + * of Promise passed as value. + */ + this.resolve = null; + + /* A method to reject the assocaited Promise with the value passed. + * If the promise is already settled it does nothing. + * + * @param {anything} reason: The reason for the rejection of the Promise. + * Generally its an Error object. If however a Promise is passed, then the Promise + * itself will be the reason for rejection no matter the state of the Promise. + */ + this.reject = null; + + this.id = uuid(); + + /* A newly created Pomise object. + * Initially in pending state. + */ + this.promise = new Promise(function (resolve, reject) { + _this.resolve = resolve; + _this.reject = reject; + }); + Object.freeze(this); +} + +/** + * querySelector with filter by epub type + * @param {element} html + * @param {string} element element type to find + * @param {string} type epub type to find + * @returns {element[]} elements + * @memberof Core + */ +function querySelectorByType(html, element, type) { + var query; + if (typeof html.querySelector != "undefined") { + query = html.querySelector(element + "[*|type=\"" + type + "\"]"); + } + // Handle IE not supporting namespaced epub:type in querySelector + if (!query || query.length === 0) { + query = qsa(html, element); + for (var i = 0; i < query.length; i++) { + if (query[i].getAttributeNS("http://www.idpf.org/2007/ops", "type") === type || query[i].getAttribute("epub:type") === type) { + return query[i]; + } + } + } else { + return query; + } +} + +/** + * Find direct decendents of an element + * @param {element} el + * @returns {element[]} children + * @memberof Core + */ +function findChildren(el) { + var result = []; + var childNodes = el.childNodes; + for (var i = 0; i < childNodes.length; i++) { + var node = childNodes[i]; + if (node.nodeType === 1) { + result.push(node); + } + } + return result; +} + +/** + * Find all parents (ancestors) of an element + * @param {element} node + * @returns {element[]} parents + * @memberof Core + */ +function parents(node) { + var nodes = [node]; + for (; node; node = node.parentNode) { + nodes.unshift(node); + } + return nodes; +} + +/** + * Find all direct decendents of a specific type + * @param {element} el + * @param {string} nodeName + * @param {boolean} [single] + * @returns {element[]} children + * @memberof Core + */ +function filterChildren(el, nodeName, single) { + var result = []; + var childNodes = el.childNodes; + for (var i = 0; i < childNodes.length; i++) { + var node = childNodes[i]; + if (node.nodeType === 1 && node.nodeName.toLowerCase() === nodeName) { + if (single) { + return node; + } else { + result.push(node); + } + } + } + if (!single) { + return result; + } +} + +/** + * Filter all parents (ancestors) with tag name + * @param {element} node + * @param {string} tagname + * @returns {element[]} parents + * @memberof Core + */ +function getParentByTagName(node, tagname) { + var parent = void 0; + if (node === null || tagname === '') return; + parent = node.parentNode; + while (parent.nodeType === 1) { + if (parent.tagName.toLowerCase() === tagname) { + return parent; + } + parent = parent.parentNode; + } +} + +/** + * Lightweight Polyfill for DOM Range + * @class + * @memberof Core + */ + +var RangeObject = exports.RangeObject = function () { + function RangeObject() { + _classCallCheck(this, RangeObject); + + this.collapsed = false; + this.commonAncestorContainer = undefined; + this.endContainer = undefined; + this.endOffset = undefined; + this.startContainer = undefined; + this.startOffset = undefined; + } + + _createClass(RangeObject, [{ + key: "setStart", + value: function setStart(startNode, startOffset) { + this.startContainer = startNode; + this.startOffset = startOffset; + + if (!this.endContainer) { + this.collapse(true); + } else { + this.commonAncestorContainer = this._commonAncestorContainer(); + } + + this._checkCollapsed(); + } + }, { + key: "setEnd", + value: function setEnd(endNode, endOffset) { + this.endContainer = endNode; + this.endOffset = endOffset; + + if (!this.startContainer) { + this.collapse(false); + } else { + this.collapsed = false; + this.commonAncestorContainer = this._commonAncestorContainer(); + } + + this._checkCollapsed(); + } + }, { + key: "collapse", + value: function collapse(toStart) { + this.collapsed = true; + if (toStart) { + this.endContainer = this.startContainer; + this.endOffset = this.startOffset; + this.commonAncestorContainer = this.startContainer.parentNode; + } else { + this.startContainer = this.endContainer; + this.startOffset = this.endOffset; + this.commonAncestorContainer = this.endOffset.parentNode; + } + } + }, { + key: "selectNode", + value: function selectNode(referenceNode) { + var parent = referenceNode.parentNode; + var index = Array.prototype.indexOf.call(parent.childNodes, referenceNode); + this.setStart(parent, index); + this.setEnd(parent, index + 1); + } + }, { + key: "selectNodeContents", + value: function selectNodeContents(referenceNode) { + var end = referenceNode.childNodes[referenceNode.childNodes - 1]; + var endIndex = referenceNode.nodeType === 3 ? referenceNode.textContent.length : parent.childNodes.length; + this.setStart(referenceNode, 0); + this.setEnd(referenceNode, endIndex); + } + }, { + key: "_commonAncestorContainer", + value: function _commonAncestorContainer(startContainer, endContainer) { + var startParents = parents(startContainer || this.startContainer); + var endParents = parents(endContainer || this.endContainer); + + if (startParents[0] != endParents[0]) return undefined; + + for (var i = 0; i < startParents.length; i++) { + if (startParents[i] != endParents[i]) { + return startParents[i - 1]; + } + } + } + }, { + key: "_checkCollapsed", + value: function _checkCollapsed() { + if (this.startContainer === this.endContainer && this.startOffset === this.endOffset) { + this.collapsed = true; + } else { + this.collapsed = false; + } + } + }, { + key: "toString", + value: function toString() { + // TODO: implement walking between start and end to find text + } + }]); + + return RangeObject; +}(); + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _core = __webpack_require__(0); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var ELEMENT_NODE = 1; +var TEXT_NODE = 3; +var COMMENT_NODE = 8; +var DOCUMENT_NODE = 9; + +/** + * Parsing and creation of EpubCFIs: http://www.idpf.org/epub/linking/cfi/epub-cfi.html + + * Implements: + * - Character Offset: epubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3) + * - Simple Ranges : epubcfi(/6/4[chap01ref]!/4[body01]/10[para05],/2/1:1,/3:4) + + * Does Not Implement: + * - Temporal Offset (~) + * - Spatial Offset (@) + * - Temporal-Spatial Offset (~ + @) + * - Text Location Assertion ([) + * @class + @param {string | Range | Node } [cfiFrom] + @param {string | object} [base] + @param {string} [ignoreClass] class to ignore when parsing DOM +*/ + +var EpubCFI = function () { + function EpubCFI(cfiFrom, base, ignoreClass) { + _classCallCheck(this, EpubCFI); + + var type; + + this.str = ""; + + this.base = {}; + this.spinePos = 0; // For compatibility + + this.range = false; // true || false; + + this.path = {}; + this.start = null; + this.end = null; + + // Allow instantiation without the "new" keyword + if (!(this instanceof EpubCFI)) { + return new EpubCFI(cfiFrom, base, ignoreClass); + } + + if (typeof base === "string") { + this.base = this.parseComponent(base); + } else if ((typeof base === "undefined" ? "undefined" : _typeof(base)) === "object" && base.steps) { + this.base = base; + } + + type = this.checkType(cfiFrom); + + if (type === "string") { + this.str = cfiFrom; + return (0, _core.extend)(this, this.parse(cfiFrom)); + } else if (type === "range") { + return (0, _core.extend)(this, this.fromRange(cfiFrom, this.base, ignoreClass)); + } else if (type === "node") { + return (0, _core.extend)(this, this.fromNode(cfiFrom, this.base, ignoreClass)); + } else if (type === "EpubCFI" && cfiFrom.path) { + return cfiFrom; + } else if (!cfiFrom) { + return this; + } else { + throw new TypeError("not a valid argument for EpubCFI"); + } + } + + /** + * Check the type of constructor input + * @private + */ + + + _createClass(EpubCFI, [{ + key: "checkType", + value: function checkType(cfi) { + + if (this.isCfiString(cfi)) { + return "string"; + // Is a range object + } else if (cfi && (typeof cfi === "undefined" ? "undefined" : _typeof(cfi)) === "object" && ((0, _core.type)(cfi) === "Range" || typeof cfi.startContainer != "undefined")) { + return "range"; + } else if (cfi && (typeof cfi === "undefined" ? "undefined" : _typeof(cfi)) === "object" && typeof cfi.nodeType != "undefined") { + // || typeof cfi === "function" + return "node"; + } else if (cfi && (typeof cfi === "undefined" ? "undefined" : _typeof(cfi)) === "object" && cfi instanceof EpubCFI) { + return "EpubCFI"; + } else { + return false; + } + } + + /** + * Parse a cfi string to a CFI object representation + * @param {string} cfiStr + * @returns {object} cfi + */ + + }, { + key: "parse", + value: function parse(cfiStr) { + var cfi = { + spinePos: -1, + range: false, + base: {}, + path: {}, + start: null, + end: null + }; + var baseComponent, pathComponent, range; + + if (typeof cfiStr !== "string") { + return { spinePos: -1 }; + } + + if (cfiStr.indexOf("epubcfi(") === 0 && cfiStr[cfiStr.length - 1] === ")") { + // Remove intial epubcfi( and ending ) + cfiStr = cfiStr.slice(8, cfiStr.length - 1); + } + + baseComponent = this.getChapterComponent(cfiStr); + + // Make sure this is a valid cfi or return + if (!baseComponent) { + return { spinePos: -1 }; + } + + cfi.base = this.parseComponent(baseComponent); + + pathComponent = this.getPathComponent(cfiStr); + cfi.path = this.parseComponent(pathComponent); + + range = this.getRange(cfiStr); + + if (range) { + cfi.range = true; + cfi.start = this.parseComponent(range[0]); + cfi.end = this.parseComponent(range[1]); + } + + // Get spine node position + // cfi.spineSegment = cfi.base.steps[1]; + + // Chapter segment is always the second step + cfi.spinePos = cfi.base.steps[1].index; + + return cfi; + } + }, { + key: "parseComponent", + value: function parseComponent(componentStr) { + var component = { + steps: [], + terminal: { + offset: null, + assertion: null + } + }; + var parts = componentStr.split(":"); + var steps = parts[0].split("/"); + var terminal; + + if (parts.length > 1) { + terminal = parts[1]; + component.terminal = this.parseTerminal(terminal); + } + + if (steps[0] === "") { + steps.shift(); // Ignore the first slash + } + + component.steps = steps.map(function (step) { + return this.parseStep(step); + }.bind(this)); + + return component; + } + }, { + key: "parseStep", + value: function parseStep(stepStr) { + var type, num, index, has_brackets, id; + + has_brackets = stepStr.match(/\[(.*)\]/); + if (has_brackets && has_brackets[1]) { + id = has_brackets[1]; + } + + //-- Check if step is a text node or element + num = parseInt(stepStr); + + if (isNaN(num)) { + return; + } + + if (num % 2 === 0) { + // Even = is an element + type = "element"; + index = num / 2 - 1; + } else { + type = "text"; + index = (num - 1) / 2; + } + + return { + "type": type, + "index": index, + "id": id || null + }; + } + }, { + key: "parseTerminal", + value: function parseTerminal(termialStr) { + var characterOffset, textLocationAssertion; + var assertion = termialStr.match(/\[(.*)\]/); + + if (assertion && assertion[1]) { + characterOffset = parseInt(termialStr.split("[")[0]); + textLocationAssertion = assertion[1]; + } else { + characterOffset = parseInt(termialStr); + } + + if (!(0, _core.isNumber)(characterOffset)) { + characterOffset = null; + } + + return { + "offset": characterOffset, + "assertion": textLocationAssertion + }; + } + }, { + key: "getChapterComponent", + value: function getChapterComponent(cfiStr) { + + var indirection = cfiStr.split("!"); + + return indirection[0]; + } + }, { + key: "getPathComponent", + value: function getPathComponent(cfiStr) { + + var indirection = cfiStr.split("!"); + + if (indirection[1]) { + var ranges = indirection[1].split(","); + return ranges[0]; + } + } + }, { + key: "getRange", + value: function getRange(cfiStr) { + + var ranges = cfiStr.split(","); + + if (ranges.length === 3) { + return [ranges[1], ranges[2]]; + } + + return false; + } + }, { + key: "getCharecterOffsetComponent", + value: function getCharecterOffsetComponent(cfiStr) { + var splitStr = cfiStr.split(":"); + return splitStr[1] || ""; + } + }, { + key: "joinSteps", + value: function joinSteps(steps) { + if (!steps) { + return ""; + } + + return steps.map(function (part) { + var segment = ""; + + if (part.type === "element") { + segment += (part.index + 1) * 2; + } + + if (part.type === "text") { + segment += 1 + 2 * part.index; // TODO: double check that this is odd + } + + if (part.id) { + segment += "[" + part.id + "]"; + } + + return segment; + }).join("/"); + } + }, { + key: "segmentString", + value: function segmentString(segment) { + var segmentString = "/"; + + segmentString += this.joinSteps(segment.steps); + + if (segment.terminal && segment.terminal.offset != null) { + segmentString += ":" + segment.terminal.offset; + } + + if (segment.terminal && segment.terminal.assertion != null) { + segmentString += "[" + segment.terminal.assertion + "]"; + } + + return segmentString; + } + + /** + * Convert CFI to a epubcfi(...) string + * @returns {string} epubcfi + */ + + }, { + key: "toString", + value: function toString() { + var cfiString = "epubcfi("; + + cfiString += this.segmentString(this.base); + + cfiString += "!"; + cfiString += this.segmentString(this.path); + + // Add Range, if present + if (this.range && this.start) { + cfiString += ","; + cfiString += this.segmentString(this.start); + } + + if (this.range && this.end) { + cfiString += ","; + cfiString += this.segmentString(this.end); + } + + cfiString += ")"; + + return cfiString; + } + + /** + * Compare which of two CFIs is earlier in the text + * @returns {number} First is earlier = -1, Second is earlier = 1, They are equal = 0 + */ + + }, { + key: "compare", + value: function compare(cfiOne, cfiTwo) { + var stepsA, stepsB; + var terminalA, terminalB; + + var rangeAStartSteps, rangeAEndSteps; + var rangeBEndSteps, rangeBEndSteps; + var rangeAStartTerminal, rangeAEndTerminal; + var rangeBStartTerminal, rangeBEndTerminal; + + if (typeof cfiOne === "string") { + cfiOne = new EpubCFI(cfiOne); + } + if (typeof cfiTwo === "string") { + cfiTwo = new EpubCFI(cfiTwo); + } + // Compare Spine Positions + if (cfiOne.spinePos > cfiTwo.spinePos) { + return 1; + } + if (cfiOne.spinePos < cfiTwo.spinePos) { + return -1; + } + + if (cfiOne.range) { + stepsA = cfiOne.path.steps.concat(cfiOne.start.steps); + terminalA = cfiOne.start.terminal; + } else { + stepsA = cfiOne.path.steps; + terminalA = cfiOne.path.terminal; + } + + if (cfiTwo.range) { + stepsB = cfiTwo.path.steps.concat(cfiTwo.start.steps); + terminalB = cfiTwo.start.terminal; + } else { + stepsB = cfiTwo.path.steps; + terminalB = cfiTwo.path.terminal; + } + + // Compare Each Step in the First item + for (var i = 0; i < stepsA.length; i++) { + if (!stepsA[i]) { + return -1; + } + if (!stepsB[i]) { + return 1; + } + if (stepsA[i].index > stepsB[i].index) { + return 1; + } + if (stepsA[i].index < stepsB[i].index) { + return -1; + } + // Otherwise continue checking + } + + // All steps in First equal to Second and First is Less Specific + if (stepsA.length < stepsB.length) { + return 1; + } + + // Compare the charecter offset of the text node + if (terminalA.offset > terminalB.offset) { + return 1; + } + if (terminalA.offset < terminalB.offset) { + return -1; + } + + // CFI's are equal + return 0; + } + }, { + key: "step", + value: function step(node) { + var nodeType = node.nodeType === TEXT_NODE ? "text" : "element"; + + return { + "id": node.id, + "tagName": node.tagName, + "type": nodeType, + "index": this.position(node) + }; + } + }, { + key: "filteredStep", + value: function filteredStep(node, ignoreClass) { + var filteredNode = this.filter(node, ignoreClass); + var nodeType; + + // Node filtered, so ignore + if (!filteredNode) { + return; + } + + // Otherwise add the filter node in + nodeType = filteredNode.nodeType === TEXT_NODE ? "text" : "element"; + + return { + "id": filteredNode.id, + "tagName": filteredNode.tagName, + "type": nodeType, + "index": this.filteredPosition(filteredNode, ignoreClass) + }; + } + }, { + key: "pathTo", + value: function pathTo(node, offset, ignoreClass) { + var segment = { + steps: [], + terminal: { + offset: null, + assertion: null + } + }; + var currentNode = node; + var step; + + while (currentNode && currentNode.parentNode && currentNode.parentNode.nodeType != DOCUMENT_NODE) { + + if (ignoreClass) { + step = this.filteredStep(currentNode, ignoreClass); + } else { + step = this.step(currentNode); + } + + if (step) { + segment.steps.unshift(step); + } + + currentNode = currentNode.parentNode; + } + + if (offset != null && offset >= 0) { + + segment.terminal.offset = offset; + + // Make sure we are getting to a textNode if there is an offset + if (segment.steps[segment.steps.length - 1].type != "text") { + segment.steps.push({ + "type": "text", + "index": 0 + }); + } + } + + return segment; + } + }, { + key: "equalStep", + value: function equalStep(stepA, stepB) { + if (!stepA || !stepB) { + return false; + } + + if (stepA.index === stepB.index && stepA.id === stepB.id && stepA.type === stepB.type) { + return true; + } + + return false; + } + + /** + * Create a CFI object from a Range + * @param {Range} range + * @param {string | object} base + * @param {string} [ignoreClass] + * @returns {object} cfi + */ + + }, { + key: "fromRange", + value: function fromRange(range, base, ignoreClass) { + var cfi = { + range: false, + base: {}, + path: {}, + start: null, + end: null + }; + + var start = range.startContainer; + var end = range.endContainer; + + var startOffset = range.startOffset; + var endOffset = range.endOffset; + + var needsIgnoring = false; + + if (ignoreClass) { + // Tell pathTo if / what to ignore + needsIgnoring = start.ownerDocument.querySelector("." + ignoreClass) != null; + } + + if (typeof base === "string") { + cfi.base = this.parseComponent(base); + cfi.spinePos = cfi.base.steps[1].index; + } else if ((typeof base === "undefined" ? "undefined" : _typeof(base)) === "object") { + cfi.base = base; + } + + if (range.collapsed) { + if (needsIgnoring) { + startOffset = this.patchOffset(start, startOffset, ignoreClass); + } + cfi.path = this.pathTo(start, startOffset, ignoreClass); + } else { + cfi.range = true; + + if (needsIgnoring) { + startOffset = this.patchOffset(start, startOffset, ignoreClass); + } + + cfi.start = this.pathTo(start, startOffset, ignoreClass); + if (needsIgnoring) { + endOffset = this.patchOffset(end, endOffset, ignoreClass); + } + + cfi.end = this.pathTo(end, endOffset, ignoreClass); + + // Create a new empty path + cfi.path = { + steps: [], + terminal: null + }; + + // Push steps that are shared between start and end to the common path + var len = cfi.start.steps.length; + var i; + + for (i = 0; i < len; i++) { + if (this.equalStep(cfi.start.steps[i], cfi.end.steps[i])) { + if (i === len - 1) { + // Last step is equal, check terminals + if (cfi.start.terminal === cfi.end.terminal) { + // CFI's are equal + cfi.path.steps.push(cfi.start.steps[i]); + // Not a range + cfi.range = false; + } + } else { + cfi.path.steps.push(cfi.start.steps[i]); + } + } else { + break; + } + } + + cfi.start.steps = cfi.start.steps.slice(cfi.path.steps.length); + cfi.end.steps = cfi.end.steps.slice(cfi.path.steps.length); + + // TODO: Add Sanity check to make sure that the end if greater than the start + } + + return cfi; + } + + /** + * Create a CFI object from a Node + * @param {Node} anchor + * @param {string | object} base + * @param {string} [ignoreClass] + * @returns {object} cfi + */ + + }, { + key: "fromNode", + value: function fromNode(anchor, base, ignoreClass) { + var cfi = { + range: false, + base: {}, + path: {}, + start: null, + end: null + }; + + if (typeof base === "string") { + cfi.base = this.parseComponent(base); + cfi.spinePos = cfi.base.steps[1].index; + } else if ((typeof base === "undefined" ? "undefined" : _typeof(base)) === "object") { + cfi.base = base; + } + + cfi.path = this.pathTo(anchor, null, ignoreClass); + + return cfi; + } + }, { + key: "filter", + value: function filter(anchor, ignoreClass) { + var needsIgnoring; + var sibling; // to join with + var parent, previousSibling, nextSibling; + var isText = false; + + if (anchor.nodeType === TEXT_NODE) { + isText = true; + parent = anchor.parentNode; + needsIgnoring = anchor.parentNode.classList.contains(ignoreClass); + } else { + isText = false; + needsIgnoring = anchor.classList.contains(ignoreClass); + } + + if (needsIgnoring && isText) { + previousSibling = parent.previousSibling; + nextSibling = parent.nextSibling; + + // If the sibling is a text node, join the nodes + if (previousSibling && previousSibling.nodeType === TEXT_NODE) { + sibling = previousSibling; + } else if (nextSibling && nextSibling.nodeType === TEXT_NODE) { + sibling = nextSibling; + } + + if (sibling) { + return sibling; + } else { + // Parent will be ignored on next step + return anchor; + } + } else if (needsIgnoring && !isText) { + // Otherwise just skip the element node + return false; + } else { + // No need to filter + return anchor; + } + } + }, { + key: "patchOffset", + value: function patchOffset(anchor, offset, ignoreClass) { + if (anchor.nodeType != TEXT_NODE) { + throw new Error("Anchor must be a text node"); + } + + var curr = anchor; + var totalOffset = offset; + + // If the parent is a ignored node, get offset from it's start + if (anchor.parentNode.classList.contains(ignoreClass)) { + curr = anchor.parentNode; + } + + while (curr.previousSibling) { + if (curr.previousSibling.nodeType === ELEMENT_NODE) { + // Originally a text node, so join + if (curr.previousSibling.classList.contains(ignoreClass)) { + totalOffset += curr.previousSibling.textContent.length; + } else { + break; // Normal node, dont join + } + } else { + // If the previous sibling is a text node, join the nodes + totalOffset += curr.previousSibling.textContent.length; + } + + curr = curr.previousSibling; + } + + return totalOffset; + } + }, { + key: "normalizedMap", + value: function normalizedMap(children, nodeType, ignoreClass) { + var output = {}; + var prevIndex = -1; + var i, + len = children.length; + var currNodeType; + var prevNodeType; + + for (i = 0; i < len; i++) { + + currNodeType = children[i].nodeType; + + // Check if needs ignoring + if (currNodeType === ELEMENT_NODE && children[i].classList.contains(ignoreClass)) { + currNodeType = TEXT_NODE; + } + + if (i > 0 && currNodeType === TEXT_NODE && prevNodeType === TEXT_NODE) { + // join text nodes + output[i] = prevIndex; + } else if (nodeType === currNodeType) { + prevIndex = prevIndex + 1; + output[i] = prevIndex; + } + + prevNodeType = currNodeType; + } + + return output; + } + }, { + key: "position", + value: function position(anchor) { + var children, index; + if (anchor.nodeType === ELEMENT_NODE) { + children = anchor.parentNode.children; + if (!children) { + children = (0, _core.findChildren)(anchor.parentNode); + } + index = Array.prototype.indexOf.call(children, anchor); + } else { + children = this.textNodes(anchor.parentNode); + index = children.indexOf(anchor); + } + + return index; + } + }, { + key: "filteredPosition", + value: function filteredPosition(anchor, ignoreClass) { + var children, index, map; + + if (anchor.nodeType === ELEMENT_NODE) { + children = anchor.parentNode.children; + map = this.normalizedMap(children, ELEMENT_NODE, ignoreClass); + } else { + children = anchor.parentNode.childNodes; + // Inside an ignored node + if (anchor.parentNode.classList.contains(ignoreClass)) { + anchor = anchor.parentNode; + children = anchor.parentNode.childNodes; + } + map = this.normalizedMap(children, TEXT_NODE, ignoreClass); + } + + index = Array.prototype.indexOf.call(children, anchor); + + return map[index]; + } + }, { + key: "stepsToXpath", + value: function stepsToXpath(steps) { + var xpath = [".", "*"]; + + steps.forEach(function (step) { + var position = step.index + 1; + + if (step.id) { + xpath.push("*[position()=" + position + " and @id='" + step.id + "']"); + } else if (step.type === "text") { + xpath.push("text()[" + position + "]"); + } else { + xpath.push("*[" + position + "]"); + } + }); + + return xpath.join("/"); + } + + /* + To get the last step if needed: + // Get the terminal step + lastStep = steps[steps.length-1]; + // Get the query string + query = this.stepsToQuery(steps); + // Find the containing element + startContainerParent = doc.querySelector(query); + // Find the text node within that element + if(startContainerParent && lastStep.type == "text") { + container = startContainerParent.childNodes[lastStep.index]; + } + */ + + }, { + key: "stepsToQuerySelector", + value: function stepsToQuerySelector(steps) { + var query = ["html"]; + + steps.forEach(function (step) { + var position = step.index + 1; + + if (step.id) { + query.push("#" + step.id); + } else if (step.type === "text") { + // unsupported in querySelector + // query.push("text()[" + position + "]"); + } else { + query.push("*:nth-child(" + position + ")"); + } + }); + + return query.join(">"); + } + }, { + key: "textNodes", + value: function textNodes(container, ignoreClass) { + return Array.prototype.slice.call(container.childNodes).filter(function (node) { + if (node.nodeType === TEXT_NODE) { + return true; + } else if (ignoreClass && node.classList.contains(ignoreClass)) { + return true; + } + return false; + }); + } + }, { + key: "walkToNode", + value: function walkToNode(steps, _doc, ignoreClass) { + var doc = _doc || document; + var container = doc.documentElement; + var children; + var step; + var len = steps.length; + var i; + + for (i = 0; i < len; i++) { + step = steps[i]; + + if (step.type === "element") { + //better to get a container using id as some times step.index may not be correct + //For ex.https://github.com/futurepress/epub.js/issues/561 + if (step.id) { + container = doc.getElementById(step.id); + } else { + children = container.children || (0, _core.findChildren)(container); + container = children[step.index]; + } + } else if (step.type === "text") { + container = this.textNodes(container, ignoreClass)[step.index]; + } + if (!container) { + //Break the for loop as due to incorrect index we can get error if + //container is undefined so that other functionailties works fine + //like navigation + break; + } + } + + return container; + } + }, { + key: "findNode", + value: function findNode(steps, _doc, ignoreClass) { + var doc = _doc || document; + var container; + var xpath; + + if (!ignoreClass && typeof doc.evaluate != "undefined") { + xpath = this.stepsToXpath(steps); + container = doc.evaluate(xpath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; + } else if (ignoreClass) { + container = this.walkToNode(steps, doc, ignoreClass); + } else { + container = this.walkToNode(steps, doc); + } + + return container; + } + }, { + key: "fixMiss", + value: function fixMiss(steps, offset, _doc, ignoreClass) { + var container = this.findNode(steps.slice(0, -1), _doc, ignoreClass); + var children = container.childNodes; + var map = this.normalizedMap(children, TEXT_NODE, ignoreClass); + var child; + var len; + var lastStepIndex = steps[steps.length - 1].index; + + for (var childIndex in map) { + if (!map.hasOwnProperty(childIndex)) return; + + if (map[childIndex] === lastStepIndex) { + child = children[childIndex]; + len = child.textContent.length; + if (offset > len) { + offset = offset - len; + } else { + if (child.nodeType === ELEMENT_NODE) { + container = child.childNodes[0]; + } else { + container = child; + } + break; + } + } + } + + return { + container: container, + offset: offset + }; + } + + /** + * Creates a DOM range representing a CFI + * @param {document} _doc document referenced in the base + * @param {string} [ignoreClass] + * @return {Range} + */ + + }, { + key: "toRange", + value: function toRange(_doc, ignoreClass) { + var doc = _doc || document; + var range; + var start, end, startContainer, endContainer; + var cfi = this; + var startSteps, endSteps; + var needsIgnoring = ignoreClass ? doc.querySelector("." + ignoreClass) != null : false; + var missed; + + if (typeof doc.createRange !== "undefined") { + range = doc.createRange(); + } else { + range = new _core.RangeObject(); + } + + if (cfi.range) { + start = cfi.start; + startSteps = cfi.path.steps.concat(start.steps); + startContainer = this.findNode(startSteps, doc, needsIgnoring ? ignoreClass : null); + end = cfi.end; + endSteps = cfi.path.steps.concat(end.steps); + endContainer = this.findNode(endSteps, doc, needsIgnoring ? ignoreClass : null); + } else { + start = cfi.path; + startSteps = cfi.path.steps; + startContainer = this.findNode(cfi.path.steps, doc, needsIgnoring ? ignoreClass : null); + } + + if (startContainer) { + try { + + if (start.terminal.offset != null) { + range.setStart(startContainer, start.terminal.offset); + } else { + range.setStart(startContainer, 0); + } + } catch (e) { + missed = this.fixMiss(startSteps, start.terminal.offset, doc, needsIgnoring ? ignoreClass : null); + range.setStart(missed.container, missed.offset); + } + } else { + console.log("No startContainer found for", this.toString()); + // No start found + return null; + } + + if (endContainer) { + try { + + if (end.terminal.offset != null) { + range.setEnd(endContainer, end.terminal.offset); + } else { + range.setEnd(endContainer, 0); + } + } catch (e) { + missed = this.fixMiss(endSteps, cfi.end.terminal.offset, doc, needsIgnoring ? ignoreClass : null); + range.setEnd(missed.container, missed.offset); + } + } + + // doc.defaultView.getSelection().addRange(range); + return range; + } + + /** + * Check if a string is wrapped with "epubcfi()" + * @param {string} str + * @returns {boolean} + */ + + }, { + key: "isCfiString", + value: function isCfiString(str) { + if (typeof str === "string" && str.indexOf("epubcfi(") === 0 && str[str.length - 1] === ")") { + return true; + } + + return false; + } + }, { + key: "generateChapterComponent", + value: function generateChapterComponent(_spineNodeIndex, _pos, id) { + var pos = parseInt(_pos), + spineNodeIndex = (_spineNodeIndex + 1) * 2, + cfi = "/" + spineNodeIndex + "/"; + + cfi += (pos + 1) * 2; + + if (id) { + cfi += "[" + id + "]"; + } + + return cfi; + } + + /** + * Collapse a CFI Range to a single CFI Position + * @param {boolean} [toStart=false] + */ + + }, { + key: "collapse", + value: function collapse(toStart) { + if (!this.range) { + return; + } + + this.range = false; + + if (toStart) { + this.path.steps = this.path.steps.concat(this.start.steps); + this.path.terminal = this.start.terminal; + } else { + this.path.steps = this.path.steps.concat(this.end.steps); + this.path.terminal = this.end.terminal; + } + } + }]); + + return EpubCFI; +}(); + +exports.default = EpubCFI; +module.exports = exports["default"]; + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var EPUBJS_VERSION = exports.EPUBJS_VERSION = "0.3"; + +// Dom events to listen for +var DOM_EVENTS = exports.DOM_EVENTS = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart", "touchmove"]; + +var EVENTS = exports.EVENTS = { + BOOK: { + OPEN_FAILED: "openFailed" + }, + CONTENTS: { + EXPAND: "expand", + RESIZE: "resize", + SELECTED: "selected", + SELECTED_RANGE: "selectedRange", + LINK_CLICKED: "linkClicked" + }, + LOCATIONS: { + CHANGED: "changed" + }, + MANAGERS: { + RESIZE: "resize", + RESIZED: "resized", + ORIENTATION_CHANGE: "orientationchange", + ADDED: "added", + SCROLL: "scroll", + SCROLLED: "scrolled", + REMOVED: "removed" + }, + VIEWS: { + AXIS: "axis", + LOAD_ERROR: "loaderror", + RENDERED: "rendered", + RESIZED: "resized", + DISPLAYED: "displayed", + SHOWN: "shown", + HIDDEN: "hidden", + MARK_CLICKED: "markClicked" + }, + RENDITION: { + STARTED: "started", + ATTACHED: "attached", + DISPLAYED: "displayed", + DISPLAY_ERROR: "displayerror", + RENDERED: "rendered", + REMOVED: "removed", + RESIZED: "resized", + ORIENTATION_CHANGE: "orientationchange", + LOCATION_CHANGED: "locationChanged", + RELOCATED: "relocated", + MARK_CLICKED: "markClicked", + SELECTED: "selected", + LAYOUT: "layout" + }, + LAYOUT: { + UPDATED: "updated" + }, + ANNOTATION: { + ATTACH: "attach", + DETACH: "detach" + } +}; + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var d = __webpack_require__(27) + , callable = __webpack_require__(41) + + , apply = Function.prototype.apply, call = Function.prototype.call + , create = Object.create, defineProperty = Object.defineProperty + , defineProperties = Object.defineProperties + , hasOwnProperty = Object.prototype.hasOwnProperty + , descriptor = { configurable: true, enumerable: false, writable: true } + + , on, once, off, emit, methods, descriptors, base; + +on = function (type, listener) { + var data; + + callable(listener); + + if (!hasOwnProperty.call(this, '__ee__')) { + data = descriptor.value = create(null); + defineProperty(this, '__ee__', descriptor); + descriptor.value = null; + } else { + data = this.__ee__; + } + if (!data[type]) data[type] = listener; + else if (typeof data[type] === 'object') data[type].push(listener); + else data[type] = [data[type], listener]; + + return this; +}; + +once = function (type, listener) { + var once, self; + + callable(listener); + self = this; + on.call(this, type, once = function () { + off.call(self, type, once); + apply.call(listener, this, arguments); + }); + + once.__eeOnceListener__ = listener; + return this; +}; + +off = function (type, listener) { + var data, listeners, candidate, i; + + callable(listener); + + if (!hasOwnProperty.call(this, '__ee__')) return this; + data = this.__ee__; + if (!data[type]) return this; + listeners = data[type]; + + if (typeof listeners === 'object') { + for (i = 0; (candidate = listeners[i]); ++i) { + if ((candidate === listener) || + (candidate.__eeOnceListener__ === listener)) { + if (listeners.length === 2) data[type] = listeners[i ? 0 : 1]; + else listeners.splice(i, 1); + } + } + } else { + if ((listeners === listener) || + (listeners.__eeOnceListener__ === listener)) { + delete data[type]; + } + } + + return this; +}; + +emit = function (type) { + var i, l, listener, listeners, args; + + if (!hasOwnProperty.call(this, '__ee__')) return; + listeners = this.__ee__[type]; + if (!listeners) return; + + if (typeof listeners === 'object') { + l = arguments.length; + args = new Array(l - 1); + for (i = 1; i < l; ++i) args[i - 1] = arguments[i]; + + listeners = listeners.slice(); + for (i = 0; (listener = listeners[i]); ++i) { + apply.call(listener, this, args); + } + } else { + switch (arguments.length) { + case 1: + call.call(listeners, this); + break; + case 2: + call.call(listeners, this, arguments[1]); + break; + case 3: + call.call(listeners, this, arguments[1], arguments[2]); + break; + default: + l = arguments.length; + args = new Array(l - 1); + for (i = 1; i < l; ++i) { + args[i - 1] = arguments[i]; + } + apply.call(listeners, this, args); + } + } +}; + +methods = { + on: on, + once: once, + off: off, + emit: emit +}; + +descriptors = { + on: d(on), + once: d(once), + off: d(off), + emit: d(emit) +}; + +base = defineProperties({}, descriptors); + +module.exports = exports = function (o) { + return (o == null) ? create(base) : defineProperties(Object(o), descriptors); +}; +exports.methods = methods; + + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _pathWebpack = __webpack_require__(7); + +var _pathWebpack2 = _interopRequireDefault(_pathWebpack); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * Creates a Path object for parsing and manipulation of a path strings + * + * Uses a polyfill for Nodejs path: https://nodejs.org/api/path.html + * @param {string} pathString a url string (relative or absolute) + * @class + */ +var Path = function () { + function Path(pathString) { + _classCallCheck(this, Path); + + var protocol; + var parsed; + + protocol = pathString.indexOf("://"); + if (protocol > -1) { + pathString = new URL(pathString).pathname; + } + + parsed = this.parse(pathString); + + this.path = pathString; + + if (this.isDirectory(pathString)) { + this.directory = pathString; + } else { + this.directory = parsed.dir + "/"; + } + + this.filename = parsed.base; + this.extension = parsed.ext.slice(1); + } + + /** + * Parse the path: https://nodejs.org/api/path.html#path_path_parse_path + * @param {string} what + * @returns {object} + */ + + + _createClass(Path, [{ + key: "parse", + value: function parse(what) { + return _pathWebpack2.default.parse(what); + } + + /** + * @param {string} what + * @returns {boolean} + */ + + }, { + key: "isAbsolute", + value: function isAbsolute(what) { + return _pathWebpack2.default.isAbsolute(what || this.path); + } + + /** + * Check if path ends with a directory + * @param {string} what + * @returns {boolean} + */ + + }, { + key: "isDirectory", + value: function isDirectory(what) { + return what.charAt(what.length - 1) === "/"; + } + + /** + * Resolve a path against the directory of the Path + * + * https://nodejs.org/api/path.html#path_path_resolve_paths + * @param {string} what + * @returns {string} resolved + */ + + }, { + key: "resolve", + value: function resolve(what) { + return _pathWebpack2.default.resolve(this.directory, what); + } + + /** + * Resolve a path relative to the directory of the Path + * + * https://nodejs.org/api/path.html#path_path_relative_from_to + * @param {string} what + * @returns {string} relative + */ + + }, { + key: "relative", + value: function relative(what) { + var isAbsolute = what && what.indexOf("://") > -1; + + if (isAbsolute) { + return what; + } + + return _pathWebpack2.default.relative(this.directory, what); + } + }, { + key: "splitPath", + value: function splitPath(filename) { + return this.splitPathRe.exec(filename).slice(1); + } + + /** + * Return the path string + * @returns {string} path + */ + + }, { + key: "toString", + value: function toString() { + return this.path; + } + }]); + + return Path; +}(); + +exports.default = Path; +module.exports = exports["default"]; + +/***/ }), +/* 5 */ +/***/ (function(module, exports) { + +var g; + +// This works in non-strict mode +g = (function() { + return this; +})(); + +try { + // This works if eval is allowed (see CSP) + g = g || Function("return this")() || (1,eval)("this"); +} catch(e) { + // This works if the window reference is available + if(typeof window === "object") + g = window; +} + +// g can still be undefined, but nothing to do about it... +// We return undefined, instead of nothing here, so it's +// easier to handle this case. if(!global) { ...} + +module.exports = g; + + +/***/ }), +/* 6 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _path = __webpack_require__(4); + +var _path2 = _interopRequireDefault(_path); + +var _pathWebpack = __webpack_require__(7); + +var _pathWebpack2 = _interopRequireDefault(_pathWebpack); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * creates a Url object for parsing and manipulation of a url string + * @param {string} urlString a url string (relative or absolute) + * @param {string} [baseString] optional base for the url, + * default to window.location.href + */ +var Url = function () { + function Url(urlString, baseString) { + _classCallCheck(this, Url); + + var absolute = urlString.indexOf("://") > -1; + var pathname = urlString; + var basePath; + + this.Url = undefined; + this.href = urlString; + this.protocol = ""; + this.origin = ""; + this.hash = ""; + this.hash = ""; + this.search = ""; + this.base = baseString; + + if (!absolute && baseString !== false && typeof baseString !== "string" && window && window.location) { + this.base = window.location.href; + } + + // URL Polyfill doesn't throw an error if base is empty + if (absolute || this.base) { + try { + if (this.base) { + // Safari doesn't like an undefined base + this.Url = new URL(urlString, this.base); + } else { + this.Url = new URL(urlString); + } + this.href = this.Url.href; + + this.protocol = this.Url.protocol; + this.origin = this.Url.origin; + this.hash = this.Url.hash; + this.search = this.Url.search; + + pathname = this.Url.pathname; + } catch (e) { + // Skip URL parsing + this.Url = undefined; + // resolve the pathname from the base + if (this.base) { + basePath = new _path2.default(this.base); + pathname = basePath.resolve(pathname); + } + } + } + + this.Path = new _path2.default(pathname); + + this.directory = this.Path.directory; + this.filename = this.Path.filename; + this.extension = this.Path.extension; + } + + /** + * @returns {Path} + */ + + + _createClass(Url, [{ + key: "path", + value: function path() { + return this.Path; + } + + /** + * Resolves a relative path to a absolute url + * @param {string} what + * @returns {string} url + */ + + }, { + key: "resolve", + value: function resolve(what) { + var isAbsolute = what.indexOf("://") > -1; + var fullpath; + + if (isAbsolute) { + return what; + } + + fullpath = _pathWebpack2.default.resolve(this.directory, what); + return this.origin + fullpath; + } + + /** + * Resolve a path relative to the url + * @param {string} what + * @returns {string} path + */ + + }, { + key: "relative", + value: function relative(what) { + return _pathWebpack2.default.relative(what, this.directory); + } + + /** + * @returns {string} + */ + + }, { + key: "toString", + value: function toString() { + return this.href; + } + }]); + + return Url; +}(); + +exports.default = Url; +module.exports = exports["default"]; + +/***/ }), +/* 7 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +if (!process) { + var process = { + "cwd" : function () { return '/' } + }; +} + +function assertPath(path) { + if (typeof path !== 'string') { + throw new TypeError('Path must be a string. Received ' + path); + } +} + +// Resolves . and .. elements in a path with directory names +function normalizeStringPosix(path, allowAboveRoot) { + var res = ''; + var lastSlash = -1; + var dots = 0; + var code; + for (var i = 0; i <= path.length; ++i) { + if (i < path.length) + code = path.charCodeAt(i); + else if (code === 47/*/*/) + break; + else + code = 47/*/*/; + if (code === 47/*/*/) { + if (lastSlash === i - 1 || dots === 1) { + // NOOP + } else if (lastSlash !== i - 1 && dots === 2) { + if (res.length < 2 || + res.charCodeAt(res.length - 1) !== 46/*.*/ || + res.charCodeAt(res.length - 2) !== 46/*.*/) { + if (res.length > 2) { + var start = res.length - 1; + var j = start; + for (; j >= 0; --j) { + if (res.charCodeAt(j) === 47/*/*/) + break; + } + if (j !== start) { + if (j === -1) + res = ''; + else + res = res.slice(0, j); + lastSlash = i; + dots = 0; + continue; + } + } else if (res.length === 2 || res.length === 1) { + res = ''; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + if (res.length > 0) + res += '/..'; + else + res = '..'; + } + } else { + if (res.length > 0) + res += '/' + path.slice(lastSlash + 1, i); + else + res = path.slice(lastSlash + 1, i); + } + lastSlash = i; + dots = 0; + } else if (code === 46/*.*/ && dots !== -1) { + ++dots; + } else { + dots = -1; + } + } + return res; +} + +function _format(sep, pathObject) { + var dir = pathObject.dir || pathObject.root; + var base = pathObject.base || + ((pathObject.name || '') + (pathObject.ext || '')); + if (!dir) { + return base; + } + if (dir === pathObject.root) { + return dir + base; + } + return dir + sep + base; +} + +var posix = { + // path.resolve([from ...], to) + resolve: function resolve() { + var resolvedPath = ''; + var resolvedAbsolute = false; + var cwd; + + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path; + if (i >= 0) + path = arguments[i]; + else { + if (cwd === undefined) + cwd = process.cwd(); + path = cwd; + } + + assertPath(path); + + // Skip empty entries + if (path.length === 0) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charCodeAt(0) === 47/*/*/; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute); + + if (resolvedAbsolute) { + if (resolvedPath.length > 0) + return '/' + resolvedPath; + else + return '/'; + } else if (resolvedPath.length > 0) { + return resolvedPath; + } else { + return '.'; + } + }, + + + normalize: function normalize(path) { + assertPath(path); + + if (path.length === 0) + return '.'; + + var isAbsolute = path.charCodeAt(0) === 47/*/*/; + var trailingSeparator = path.charCodeAt(path.length - 1) === 47/*/*/; + + // Normalize the path + path = normalizeStringPosix(path, !isAbsolute); + + if (path.length === 0 && !isAbsolute) + path = '.'; + if (path.length > 0 && trailingSeparator) + path += '/'; + + if (isAbsolute) + return '/' + path; + return path; + }, + + + isAbsolute: function isAbsolute(path) { + assertPath(path); + return path.length > 0 && path.charCodeAt(0) === 47/*/*/; + }, + + + join: function join() { + if (arguments.length === 0) + return '.'; + var joined; + for (var i = 0; i < arguments.length; ++i) { + var arg = arguments[i]; + assertPath(arg); + if (arg.length > 0) { + if (joined === undefined) + joined = arg; + else + joined += '/' + arg; + } + } + if (joined === undefined) + return '.'; + return posix.normalize(joined); + }, + + + relative: function relative(from, to) { + assertPath(from); + assertPath(to); + + if (from === to) + return ''; + + from = posix.resolve(from); + to = posix.resolve(to); + + if (from === to) + return ''; + + // Trim any leading backslashes + var fromStart = 1; + for (; fromStart < from.length; ++fromStart) { + if (from.charCodeAt(fromStart) !== 47/*/*/) + break; + } + var fromEnd = from.length; + var fromLen = (fromEnd - fromStart); + + // Trim any leading backslashes + var toStart = 1; + for (; toStart < to.length; ++toStart) { + if (to.charCodeAt(toStart) !== 47/*/*/) + break; + } + var toEnd = to.length; + var toLen = (toEnd - toStart); + + // Compare paths to find the longest common path from root + var length = (fromLen < toLen ? fromLen : toLen); + var lastCommonSep = -1; + var i = 0; + for (; i <= length; ++i) { + if (i === length) { + if (toLen > length) { + if (to.charCodeAt(toStart + i) === 47/*/*/) { + // We get here if `from` is the exact base path for `to`. + // For example: from='/foo/bar'; to='/foo/bar/baz' + return to.slice(toStart + i + 1); + } else if (i === 0) { + // We get here if `from` is the root + // For example: from='/'; to='/foo' + return to.slice(toStart + i); + } + } else if (fromLen > length) { + if (from.charCodeAt(fromStart + i) === 47/*/*/) { + // We get here if `to` is the exact base path for `from`. + // For example: from='/foo/bar/baz'; to='/foo/bar' + lastCommonSep = i; + } else if (i === 0) { + // We get here if `to` is the root. + // For example: from='/foo'; to='/' + lastCommonSep = 0; + } + } + break; + } + var fromCode = from.charCodeAt(fromStart + i); + var toCode = to.charCodeAt(toStart + i); + if (fromCode !== toCode) + break; + else if (fromCode === 47/*/*/) + lastCommonSep = i; + } + + var out = ''; + // Generate the relative path based on the path difference between `to` + // and `from` + for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { + if (i === fromEnd || from.charCodeAt(i) === 47/*/*/) { + if (out.length === 0) + out += '..'; + else + out += '/..'; + } + } + + // Lastly, append the rest of the destination (`to`) path that comes after + // the common path parts + if (out.length > 0) + return out + to.slice(toStart + lastCommonSep); + else { + toStart += lastCommonSep; + if (to.charCodeAt(toStart) === 47/*/*/) + ++toStart; + return to.slice(toStart); + } + }, + + + _makeLong: function _makeLong(path) { + return path; + }, + + + dirname: function dirname(path) { + assertPath(path); + if (path.length === 0) + return '.'; + var code = path.charCodeAt(0); + var hasRoot = (code === 47/*/*/); + var end = -1; + var matchedSlash = true; + for (var i = path.length - 1; i >= 1; --i) { + code = path.charCodeAt(i); + if (code === 47/*/*/) { + if (!matchedSlash) { + end = i; + break; + } + } else { + // We saw the first non-path separator + matchedSlash = false; + } + } + + if (end === -1) + return hasRoot ? '/' : '.'; + if (hasRoot && end === 1) + return '//'; + return path.slice(0, end); + }, + + + basename: function basename(path, ext) { + if (ext !== undefined && typeof ext !== 'string') + throw new TypeError('"ext" argument must be a string'); + assertPath(path); + + var start = 0; + var end = -1; + var matchedSlash = true; + var i; + + if (ext !== undefined && ext.length > 0 && ext.length <= path.length) { + if (ext.length === path.length && ext === path) + return ''; + var extIdx = ext.length - 1; + var firstNonSlashEnd = -1; + for (i = path.length - 1; i >= 0; --i) { + var code = path.charCodeAt(i); + if (code === 47/*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd === -1) { + // We saw the first non-path separator, remember this index in case + // we need it if the extension ends up not matching + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx >= 0) { + // Try to match the explicit extension + if (code === ext.charCodeAt(extIdx)) { + if (--extIdx === -1) { + // We matched the extension, so mark this as the end of our path + // component + end = i; + } + } else { + // Extension does not match, so our result is the entire path + // component + extIdx = -1; + end = firstNonSlashEnd; + } + } + } + } + + if (start === end) + end = firstNonSlashEnd; + else if (end === -1) + end = path.length; + return path.slice(start, end); + } else { + for (i = path.length - 1; i >= 0; --i) { + if (path.charCodeAt(i) === 47/*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // path component + matchedSlash = false; + end = i + 1; + } + } + + if (end === -1) + return ''; + return path.slice(start, end); + } + }, + + + extname: function extname(path) { + assertPath(path); + var startDot = -1; + var startPart = 0; + var end = -1; + var matchedSlash = true; + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + var preDotState = 0; + for (var i = path.length - 1; i >= 0; --i) { + var code = path.charCodeAt(i); + if (code === 47/*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === 46/*.*/) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) + startDot = i; + else if (preDotState !== 1) + preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if (startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && + startDot === end - 1 && + startDot === startPart + 1)) { + return ''; + } + return path.slice(startDot, end); + }, + + + format: function format(pathObject) { + if (pathObject === null || typeof pathObject !== 'object') { + throw new TypeError( + 'Parameter "pathObject" must be an object, not ' + typeof(pathObject) + ); + } + return _format('/', pathObject); + }, + + + parse: function parse(path) { + assertPath(path); + + var ret = { root: '', dir: '', base: '', ext: '', name: '' }; + if (path.length === 0) + return ret; + var code = path.charCodeAt(0); + var isAbsolute = (code === 47/*/*/); + var start; + if (isAbsolute) { + ret.root = '/'; + start = 1; + } else { + start = 0; + } + var startDot = -1; + var startPart = 0; + var end = -1; + var matchedSlash = true; + var i = path.length - 1; + + // Track the state of characters (if any) we see before our first dot and + // after any path separator we find + var preDotState = 0; + + // Get non-dir info + for (; i >= start; --i) { + code = path.charCodeAt(i); + if (code === 47/*/*/) { + // If we reached a path separator that was not part of a set of path + // separators at the end of the string, stop now + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + // We saw the first non-path separator, mark this as the end of our + // extension + matchedSlash = false; + end = i + 1; + } + if (code === 46/*.*/) { + // If this is our first dot, mark it as the start of our extension + if (startDot === -1) + startDot = i; + else if (preDotState !== 1) + preDotState = 1; + } else if (startDot !== -1) { + // We saw a non-dot and non-path separator before our dot, so we should + // have a good chance at having a non-empty extension + preDotState = -1; + } + } + + if (startDot === -1 || + end === -1 || + // We saw a non-dot character immediately before the dot + preDotState === 0 || + // The (right-most) trimmed path component is exactly '..' + (preDotState === 1 && + startDot === end - 1 && + startDot === startPart + 1)) { + if (end !== -1) { + if (startPart === 0 && isAbsolute) + ret.base = ret.name = path.slice(1, end); + else + ret.base = ret.name = path.slice(startPart, end); + } + } else { + if (startPart === 0 && isAbsolute) { + ret.name = path.slice(1, startDot); + ret.base = path.slice(1, end); + } else { + ret.name = path.slice(startPart, startDot); + ret.base = path.slice(startPart, end); + } + ret.ext = path.slice(startDot, end); + } + + if (startPart > 0) + ret.dir = path.slice(0, startPart - 1); + else if (isAbsolute) + ret.dir = '/'; + + return ret; + }, + + + sep: '/', + delimiter: ':', + posix: null +}; + + +module.exports = posix; + + +/***/ }), +/* 8 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.replaceBase = replaceBase; +exports.replaceCanonical = replaceCanonical; +exports.replaceMeta = replaceMeta; +exports.replaceLinks = replaceLinks; +exports.substitute = substitute; + +var _core = __webpack_require__(0); + +var _url = __webpack_require__(6); + +var _url2 = _interopRequireDefault(_url); + +var _path = __webpack_require__(4); + +var _path2 = _interopRequireDefault(_path); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function replaceBase(doc, section) { + var base; + var head; + var url = section.url; + var absolute = url.indexOf("://") > -1; + + if (!doc) { + return; + } + + head = (0, _core.qs)(doc, "head"); + base = (0, _core.qs)(head, "base"); + + if (!base) { + base = doc.createElement("base"); + head.insertBefore(base, head.firstChild); + } + + // Fix for Safari crashing if the url doesn't have an origin + if (!absolute && window && window.location) { + url = window.location.origin + url; + } + + base.setAttribute("href", url); +} + +function replaceCanonical(doc, section) { + var head; + var link; + var url = section.canonical; + + if (!doc) { + return; + } + + head = (0, _core.qs)(doc, "head"); + link = (0, _core.qs)(head, "link[rel='canonical']"); + + if (link) { + link.setAttribute("href", url); + } else { + link = doc.createElement("link"); + link.setAttribute("rel", "canonical"); + link.setAttribute("href", url); + head.appendChild(link); + } +} + +function replaceMeta(doc, section) { + var head; + var meta; + var id = section.idref; + if (!doc) { + return; + } + + head = (0, _core.qs)(doc, "head"); + meta = (0, _core.qs)(head, "link[property='dc.identifier']"); + + if (meta) { + meta.setAttribute("content", id); + } else { + meta = doc.createElement("meta"); + meta.setAttribute("name", "dc.identifier"); + meta.setAttribute("content", id); + head.appendChild(meta); + } +} + +// TODO: move me to Contents +function replaceLinks(contents, fn) { + + var links = contents.querySelectorAll("a[href]"); + + if (!links.length) { + return; + } + + var base = (0, _core.qs)(contents.ownerDocument, "base"); + var location = base ? base.getAttribute("href") : undefined; + var replaceLink = function (link) { + var href = link.getAttribute("href"); + + if (href.indexOf("mailto:") === 0) { + return; + } + + var absolute = href.indexOf("://") > -1; + + if (absolute) { + + link.setAttribute("target", "_blank"); + } else { + var linkUrl; + try { + linkUrl = new _url2.default(href, location); + } catch (error) { + // NOOP + } + + link.onclick = function () { + + if (linkUrl && linkUrl.hash) { + fn(linkUrl.Path.path + linkUrl.hash); + } else if (linkUrl) { + fn(linkUrl.Path.path); + } else { + fn(href); + } + + return false; + }; + } + }.bind(this); + + for (var i = 0; i < links.length; i++) { + replaceLink(links[i]); + } +} + +function substitute(content, urls, replacements) { + urls.forEach(function (url, i) { + if (url && replacements[i]) { + content = content.replace(new RegExp(url, "g"), replacements[i]); + } + }); + return content; +} + +/***/ }), +/* 9 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _core = __webpack_require__(0); + +var _path = __webpack_require__(4); + +var _path2 = _interopRequireDefault(_path); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function request(url, type, withCredentials, headers) { + var supportsURL = typeof window != "undefined" ? window.URL : false; // TODO: fallback for url if window isn't defined + var BLOB_RESPONSE = supportsURL ? "blob" : "arraybuffer"; + + var deferred = new _core.defer(); + + var xhr = new XMLHttpRequest(); + + //-- Check from PDF.js: + // https://github.com/mozilla/pdf.js/blob/master/web/compatibility.js + var xhrPrototype = XMLHttpRequest.prototype; + + var header; + + if (!("overrideMimeType" in xhrPrototype)) { + // IE10 might have response, but not overrideMimeType + Object.defineProperty(xhrPrototype, "overrideMimeType", { + value: function xmlHttpRequestOverrideMimeType() {} + }); + } + + if (withCredentials) { + xhr.withCredentials = true; + } + + xhr.onreadystatechange = handler; + xhr.onerror = err; + + xhr.open("GET", url, true); + + for (header in headers) { + xhr.setRequestHeader(header, headers[header]); + } + + if (type == "json") { + xhr.setRequestHeader("Accept", "application/json"); + } + + // If type isn"t set, determine it from the file extension + if (!type) { + type = new _path2.default(url).extension; + } + + if (type == "blob") { + xhr.responseType = BLOB_RESPONSE; + } + + if ((0, _core.isXml)(type)) { + // xhr.responseType = "document"; + xhr.overrideMimeType("text/xml"); // for OPF parsing + } + + if (type == "xhtml") { + // xhr.responseType = "document"; + } + + if (type == "html" || type == "htm") { + // xhr.responseType = "document"; + } + + if (type == "binary") { + xhr.responseType = "arraybuffer"; + } + + xhr.send(); + + function err(e) { + deferred.reject(e); + } + + function handler() { + if (this.readyState === XMLHttpRequest.DONE) { + var responseXML = false; + + if (this.responseType === "" || this.responseType === "document") { + responseXML = this.responseXML; + } + + if (this.status === 200 || this.status === 0 || responseXML) { + //-- Firefox is reporting 0 for blob urls + var r; + + if (!this.response && !responseXML) { + deferred.reject({ + status: this.status, + message: "Empty Response", + stack: new Error().stack + }); + return deferred.promise; + } + + if (this.status === 403) { + deferred.reject({ + status: this.status, + response: this.response, + message: "Forbidden", + stack: new Error().stack + }); + return deferred.promise; + } + if (responseXML) { + r = this.responseXML; + } else if ((0, _core.isXml)(type)) { + // xhr.overrideMimeType("text/xml"); // for OPF parsing + // If this.responseXML wasn't set, try to parse using a DOMParser from text + r = (0, _core.parse)(this.response, "text/xml"); + } else if (type == "xhtml") { + r = (0, _core.parse)(this.response, "application/xhtml+xml"); + } else if (type == "html" || type == "htm") { + r = (0, _core.parse)(this.response, "text/html"); + } else if (type == "json") { + r = JSON.parse(this.response); + } else if (type == "blob") { + + if (supportsURL) { + r = this.response; + } else { + //-- Safari doesn't support responseType blob, so create a blob from arraybuffer + r = new Blob([this.response]); + } + } else { + r = this.response; + } + + deferred.resolve(r); + } else { + + deferred.reject({ + status: this.status, + message: this.response, + stack: new Error().stack + }); + } + } + } + + return deferred.promise; +} + +exports.default = request; +module.exports = exports["default"]; + +/***/ }), +/* 10 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var _undefined = __webpack_require__(34)(); // Support ES3 engines + +module.exports = function (val) { + return (val !== _undefined) && (val !== null); +}; + + +/***/ }), +/* 11 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * Hooks allow for injecting functions that must all complete in order before finishing + * They will execute in parallel but all must finish before continuing + * Functions may return a promise if they are asycn. + * @param {any} context scope of this + * @example this.content = new EPUBJS.Hook(this); + */ +var Hook = function () { + function Hook(context) { + _classCallCheck(this, Hook); + + this.context = context || this; + this.hooks = []; + } + + /** + * Adds a function to be run before a hook completes + * @example this.content.register(function(){...}); + */ + + + _createClass(Hook, [{ + key: "register", + value: function register() { + for (var i = 0; i < arguments.length; ++i) { + if (typeof arguments[i] === "function") { + this.hooks.push(arguments[i]); + } else { + // unpack array + for (var j = 0; j < arguments[i].length; ++j) { + this.hooks.push(arguments[i][j]); + } + } + } + } + + /** + * Removes a function + * @example this.content.deregister(function(){...}); + */ + + }, { + key: "deregister", + value: function deregister(func) { + var hook = void 0; + for (var i = 0; i < this.hooks.length; i++) { + hook = this.hooks[i]; + if (hook === func) { + this.hooks.splice(i, 1); + break; + } + } + } + + /** + * Triggers a hook to run all functions + * @example this.content.trigger(args).then(function(){...}); + */ + + }, { + key: "trigger", + value: function trigger() { + var args = arguments; + var context = this.context; + var promises = []; + + this.hooks.forEach(function (task) { + var executing = task.apply(context, args); + + if (executing && typeof executing["then"] === "function") { + // Task is a function that returns a promise + promises.push(executing); + } + // Otherwise Task resolves immediately, continue + }); + + return Promise.all(promises); + } + + // Adds a function to be run before a hook completes + + }, { + key: "list", + value: function list() { + return this.hooks; + } + }, { + key: "clear", + value: function clear() { + return this.hooks = []; + } + }]); + + return Hook; +}(); + +exports.default = Hook; +module.exports = exports["default"]; + +/***/ }), +/* 12 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Task = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _core = __webpack_require__(0); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * Queue for handling tasks one at a time + * @class + * @param {scope} context what this will resolve to in the tasks + */ +var Queue = function () { + function Queue(context) { + _classCallCheck(this, Queue); + + this._q = []; + this.context = context; + this.tick = _core.requestAnimationFrame; + this.running = false; + this.paused = false; + } + + /** + * Add an item to the queue + * @return {Promise} + */ + + + _createClass(Queue, [{ + key: "enqueue", + value: function enqueue() { + var deferred, promise; + var queued; + var task = [].shift.call(arguments); + var args = arguments; + + // Handle single args without context + // if(args && !Array.isArray(args)) { + // args = [args]; + // } + if (!task) { + throw new Error("No Task Provided"); + } + + if (typeof task === "function") { + + deferred = new _core.defer(); + promise = deferred.promise; + + queued = { + "task": task, + "args": args, + //"context" : context, + "deferred": deferred, + "promise": promise + }; + } else { + // Task is a promise + queued = { + "promise": task + }; + } + + this._q.push(queued); + + // Wait to start queue flush + if (this.paused == false && !this.running) { + // setTimeout(this.flush.bind(this), 0); + // this.tick.call(window, this.run.bind(this)); + this.run(); + } + + return queued.promise; + } + + /** + * Run one item + * @return {Promise} + */ + + }, { + key: "dequeue", + value: function dequeue() { + var inwait, task, result; + + if (this._q.length && !this.paused) { + inwait = this._q.shift(); + task = inwait.task; + if (task) { + // console.log(task) + + result = task.apply(this.context, inwait.args); + + if (result && typeof result["then"] === "function") { + // Task is a function that returns a promise + return result.then(function () { + inwait.deferred.resolve.apply(this.context, arguments); + }.bind(this), function () { + inwait.deferred.reject.apply(this.context, arguments); + }.bind(this)); + } else { + // Task resolves immediately + inwait.deferred.resolve.apply(this.context, result); + return inwait.promise; + } + } else if (inwait.promise) { + // Task is a promise + return inwait.promise; + } + } else { + inwait = new _core.defer(); + inwait.deferred.resolve(); + return inwait.promise; + } + } + + // Run All Immediately + + }, { + key: "dump", + value: function dump() { + while (this._q.length) { + this.dequeue(); + } + } + + /** + * Run all tasks sequentially, at convince + * @return {Promise} + */ + + }, { + key: "run", + value: function run() { + var _this = this; + + if (!this.running) { + this.running = true; + this.defered = new _core.defer(); + } + + this.tick.call(window, function () { + + if (_this._q.length) { + + _this.dequeue().then(function () { + this.run(); + }.bind(_this)); + } else { + _this.defered.resolve(); + _this.running = undefined; + } + }); + + // Unpause + if (this.paused == true) { + this.paused = false; + } + + return this.defered.promise; + } + + /** + * Flush all, as quickly as possible + * @return {Promise} + */ + + }, { + key: "flush", + value: function flush() { + + if (this.running) { + return this.running; + } + + if (this._q.length) { + this.running = this.dequeue().then(function () { + this.running = undefined; + return this.flush(); + }.bind(this)); + + return this.running; + } + } + + /** + * Clear all items in wait + */ + + }, { + key: "clear", + value: function clear() { + this._q = []; + } + + /** + * Get the number of tasks in the queue + * @return {number} tasks + */ + + }, { + key: "length", + value: function length() { + return this._q.length; + } + + /** + * Pause a running queue + */ + + }, { + key: "pause", + value: function pause() { + this.paused = true; + } + + /** + * End the queue + */ + + }, { + key: "stop", + value: function stop() { + this._q = []; + this.running = false; + this.paused = true; + } + }]); + + return Queue; +}(); + +/** + * Create a new task from a callback + * @class + * @private + * @param {function} task + * @param {array} args + * @param {scope} context + * @return {function} task + */ + + +var Task = function Task(task, args, context) { + _classCallCheck(this, Task); + + return function () { + var _this2 = this; + + var toApply = arguments || []; + + return new Promise(function (resolve, reject) { + var callback = function callback(value, err) { + if (!value && err) { + reject(err); + } else { + resolve(value); + } + }; + // Add the callback to the arguments list + toApply.push(callback); + + // Apply all arguments to the functions + task.apply(context || _this2, toApply); + }); + }; +}; + +exports.default = Queue; +exports.Task = Task; + +/***/ }), +/* 13 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +/* + From Zip.js, by Gildas Lormeau +edited down + */ + +var table = { + "application": { + "ecmascript": ["es", "ecma"], + "javascript": "js", + "ogg": "ogx", + "pdf": "pdf", + "postscript": ["ps", "ai", "eps", "epsi", "epsf", "eps2", "eps3"], + "rdf+xml": "rdf", + "smil": ["smi", "smil"], + "xhtml+xml": ["xhtml", "xht"], + "xml": ["xml", "xsl", "xsd", "opf", "ncx"], + "zip": "zip", + "x-httpd-eruby": "rhtml", + "x-latex": "latex", + "x-maker": ["frm", "maker", "frame", "fm", "fb", "book", "fbdoc"], + "x-object": "o", + "x-shockwave-flash": ["swf", "swfl"], + "x-silverlight": "scr", + "epub+zip": "epub", + "font-tdpfr": "pfr", + "inkml+xml": ["ink", "inkml"], + "json": "json", + "jsonml+json": "jsonml", + "mathml+xml": "mathml", + "metalink+xml": "metalink", + "mp4": "mp4s", + // "oebps-package+xml" : "opf", + "omdoc+xml": "omdoc", + "oxps": "oxps", + "vnd.amazon.ebook": "azw", + "widget": "wgt", + // "x-dtbncx+xml" : "ncx", + "x-dtbook+xml": "dtb", + "x-dtbresource+xml": "res", + "x-font-bdf": "bdf", + "x-font-ghostscript": "gsf", + "x-font-linux-psf": "psf", + "x-font-otf": "otf", + "x-font-pcf": "pcf", + "x-font-snf": "snf", + "x-font-ttf": ["ttf", "ttc"], + "x-font-type1": ["pfa", "pfb", "pfm", "afm"], + "x-font-woff": "woff", + "x-mobipocket-ebook": ["prc", "mobi"], + "x-mspublisher": "pub", + "x-nzb": "nzb", + "x-tgif": "obj", + "xaml+xml": "xaml", + "xml-dtd": "dtd", + "xproc+xml": "xpl", + "xslt+xml": "xslt", + "internet-property-stream": "acx", + "x-compress": "z", + "x-compressed": "tgz", + "x-gzip": "gz" + }, + "audio": { + "flac": "flac", + "midi": ["mid", "midi", "kar", "rmi"], + "mpeg": ["mpga", "mpega", "mp2", "mp3", "m4a", "mp2a", "m2a", "m3a"], + "mpegurl": "m3u", + "ogg": ["oga", "ogg", "spx"], + "x-aiff": ["aif", "aiff", "aifc"], + "x-ms-wma": "wma", + "x-wav": "wav", + "adpcm": "adp", + "mp4": "mp4a", + "webm": "weba", + "x-aac": "aac", + "x-caf": "caf", + "x-matroska": "mka", + "x-pn-realaudio-plugin": "rmp", + "xm": "xm", + "mid": ["mid", "rmi"] + }, + "image": { + "gif": "gif", + "ief": "ief", + "jpeg": ["jpeg", "jpg", "jpe"], + "pcx": "pcx", + "png": "png", + "svg+xml": ["svg", "svgz"], + "tiff": ["tiff", "tif"], + "x-icon": "ico", + "bmp": "bmp", + "webp": "webp", + "x-pict": ["pic", "pct"], + "x-tga": "tga", + "cis-cod": "cod" + }, + "text": { + "cache-manifest": ["manifest", "appcache"], + "css": "css", + "csv": "csv", + "html": ["html", "htm", "shtml", "stm"], + "mathml": "mml", + "plain": ["txt", "text", "brf", "conf", "def", "list", "log", "in", "bas"], + "richtext": "rtx", + "tab-separated-values": "tsv", + "x-bibtex": "bib" + }, + "video": { + "mpeg": ["mpeg", "mpg", "mpe", "m1v", "m2v", "mp2", "mpa", "mpv2"], + "mp4": ["mp4", "mp4v", "mpg4"], + "quicktime": ["qt", "mov"], + "ogg": "ogv", + "vnd.mpegurl": ["mxu", "m4u"], + "x-flv": "flv", + "x-la-asf": ["lsf", "lsx"], + "x-mng": "mng", + "x-ms-asf": ["asf", "asx", "asr"], + "x-ms-wm": "wm", + "x-ms-wmv": "wmv", + "x-ms-wmx": "wmx", + "x-ms-wvx": "wvx", + "x-msvideo": "avi", + "x-sgi-movie": "movie", + "x-matroska": ["mpv", "mkv", "mk3d", "mks"], + "3gpp2": "3g2", + "h261": "h261", + "h263": "h263", + "h264": "h264", + "jpeg": "jpgv", + "jpm": ["jpm", "jpgm"], + "mj2": ["mj2", "mjp2"], + "vnd.ms-playready.media.pyv": "pyv", + "vnd.uvvu.mp4": ["uvu", "uvvu"], + "vnd.vivo": "viv", + "webm": "webm", + "x-f4v": "f4v", + "x-m4v": "m4v", + "x-ms-vob": "vob", + "x-smv": "smv" + } +}; + +var mimeTypes = function () { + var type, + subtype, + val, + index, + mimeTypes = {}; + for (type in table) { + if (table.hasOwnProperty(type)) { + for (subtype in table[type]) { + if (table[type].hasOwnProperty(subtype)) { + val = table[type][subtype]; + if (typeof val == "string") { + mimeTypes[val] = type + "/" + subtype; + } else { + for (index = 0; index < val.length; index++) { + mimeTypes[val[index]] = type + "/" + subtype; + } + } + } + } + } + } + return mimeTypes; +}(); + +var defaultValue = "text/plain"; //"application/octet-stream"; + +function lookup(filename) { + return filename && mimeTypes[filename.split(".").pop().toLowerCase()] || defaultValue; +}; + +module.exports = { + 'lookup': lookup +}; + +/***/ }), +/* 14 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _eventEmitter = __webpack_require__(3); + +var _eventEmitter2 = _interopRequireDefault(_eventEmitter); + +var _core = __webpack_require__(0); + +var _epubcfi = __webpack_require__(1); + +var _epubcfi2 = _interopRequireDefault(_epubcfi); + +var _mapping = __webpack_require__(19); + +var _mapping2 = _interopRequireDefault(_mapping); + +var _replacements = __webpack_require__(8); + +var _constants = __webpack_require__(2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var hasNavigator = typeof navigator !== "undefined"; + +var isChrome = hasNavigator && /Chrome/.test(navigator.userAgent); +var isWebkit = hasNavigator && !isChrome && /AppleWebKit/.test(navigator.userAgent); + +var ELEMENT_NODE = 1; +var TEXT_NODE = 3; + +/** + * Handles DOM manipulation, queries and events for View contents + * @class + * @param {document} doc Document + * @param {element} content Parent Element (typically Body) + * @param {string} cfiBase Section component of CFIs + * @param {number} sectionIndex Index in Spine of Conntent's Section + */ + +var Contents = function () { + function Contents(doc, content, cfiBase, sectionIndex) { + _classCallCheck(this, Contents); + + // Blank Cfi for Parsing + this.epubcfi = new _epubcfi2.default(); + + this.document = doc; + this.documentElement = this.document.documentElement; + this.content = content || this.document.body; + this.window = this.document.defaultView; + + this._size = { + width: 0, + height: 0 + }; + + this.sectionIndex = sectionIndex || 0; + this.cfiBase = cfiBase || ""; + + this.epubReadingSystem("epub.js", _constants.EPUBJS_VERSION); + + this.listeners(); + } + + /** + * Get DOM events that are listened for and passed along + */ + + + _createClass(Contents, [{ + key: "width", + + + /** + * Get or Set width + * @param {number} [w] + * @returns {number} width + */ + value: function width(w) { + // var frame = this.documentElement; + var frame = this.content; + + if (w && (0, _core.isNumber)(w)) { + w = w + "px"; + } + + if (w) { + frame.style.width = w; + // this.content.style.width = w; + } + + return this.window.getComputedStyle(frame)["width"]; + } + + /** + * Get or Set height + * @param {number} [h] + * @returns {number} height + */ + + }, { + key: "height", + value: function height(h) { + // var frame = this.documentElement; + var frame = this.content; + + if (h && (0, _core.isNumber)(h)) { + h = h + "px"; + } + + if (h) { + frame.style.height = h; + // this.content.style.height = h; + } + + return this.window.getComputedStyle(frame)["height"]; + } + + /** + * Get or Set width of the contents + * @param {number} [w] + * @returns {number} width + */ + + }, { + key: "contentWidth", + value: function contentWidth(w) { + + var content = this.content || this.document.body; + + if (w && (0, _core.isNumber)(w)) { + w = w + "px"; + } + + if (w) { + content.style.width = w; + } + + return this.window.getComputedStyle(content)["width"]; + } + + /** + * Get or Set height of the contents + * @param {number} [h] + * @returns {number} height + */ + + }, { + key: "contentHeight", + value: function contentHeight(h) { + + var content = this.content || this.document.body; + + if (h && (0, _core.isNumber)(h)) { + h = h + "px"; + } + + if (h) { + content.style.height = h; + } + + return this.window.getComputedStyle(content)["height"]; + } + + /** + * Get the width of the text using Range + * @returns {number} width + */ + + }, { + key: "textWidth", + value: function textWidth() { + var rect = void 0; + var width = void 0; + var range = this.document.createRange(); + var content = this.content || this.document.body; + var border = (0, _core.borders)(content); + + // Select the contents of frame + range.selectNodeContents(content); + + // get the width of the text content + rect = range.getBoundingClientRect(); + width = rect.width; + + if (border && border.width) { + width += border.width; + } + + return Math.round(width); + } + + /** + * Get the height of the text using Range + * @returns {number} height + */ + + }, { + key: "textHeight", + value: function textHeight() { + var rect = void 0; + var height = void 0; + var range = this.document.createRange(); + var content = this.content || this.document.body; + var border = (0, _core.borders)(content); + + range.selectNodeContents(content); + + rect = range.getBoundingClientRect(); + height = rect.height; + + if (height && border.height) { + height += border.height; + } + + if (height && rect.top) { + height += rect.top; + } + + return Math.round(height); + } + + /** + * Get documentElement scrollWidth + * @returns {number} width + */ + + }, { + key: "scrollWidth", + value: function scrollWidth() { + var width = this.documentElement.scrollWidth; + + return width; + } + + /** + * Get documentElement scrollHeight + * @returns {number} height + */ + + }, { + key: "scrollHeight", + value: function scrollHeight() { + var height = this.documentElement.scrollHeight; + + return height; + } + + /** + * Set overflow css style of the contents + * @param {string} [overflow] + */ + + }, { + key: "overflow", + value: function overflow(_overflow) { + + if (_overflow) { + this.documentElement.style.overflow = _overflow; + } + + return this.window.getComputedStyle(this.documentElement)["overflow"]; + } + + /** + * Set overflowX css style of the documentElement + * @param {string} [overflow] + */ + + }, { + key: "overflowX", + value: function overflowX(overflow) { + + if (overflow) { + this.documentElement.style.overflowX = overflow; + } + + return this.window.getComputedStyle(this.documentElement)["overflowX"]; + } + + /** + * Set overflowY css style of the documentElement + * @param {string} [overflow] + */ + + }, { + key: "overflowY", + value: function overflowY(overflow) { + + if (overflow) { + this.documentElement.style.overflowY = overflow; + } + + return this.window.getComputedStyle(this.documentElement)["overflowY"]; + } + + /** + * Set Css styles on the contents element (typically Body) + * @param {string} property + * @param {string} value + * @param {boolean} [priority] set as "important" + */ + + }, { + key: "css", + value: function css(property, value, priority) { + var content = this.content || this.document.body; + + if (value) { + content.style.setProperty(property, value, priority ? "important" : ""); + } + + return this.window.getComputedStyle(content)[property]; + } + + /** + * Get or Set the viewport element + * @param {object} [options] + * @param {string} [options.width] + * @param {string} [options.height] + * @param {string} [options.scale] + * @param {string} [options.minimum] + * @param {string} [options.maximum] + * @param {string} [options.scalable] + */ + + }, { + key: "viewport", + value: function viewport(options) { + var _width, _height, _scale, _minimum, _maximum, _scalable; + // var width, height, scale, minimum, maximum, scalable; + var $viewport = this.document.querySelector("meta[name='viewport']"); + var parsed = { + "width": undefined, + "height": undefined, + "scale": undefined, + "minimum": undefined, + "maximum": undefined, + "scalable": undefined + }; + var newContent = []; + var settings = {}; + + /* + * check for the viewport size + * + */ + if ($viewport && $viewport.hasAttribute("content")) { + var content = $viewport.getAttribute("content"); + var _width2 = content.match(/width\s*=\s*([^,]*)/); + var _height2 = content.match(/height\s*=\s*([^,]*)/); + var _scale2 = content.match(/initial-scale\s*=\s*([^,]*)/); + var _minimum2 = content.match(/minimum-scale\s*=\s*([^,]*)/); + var _maximum2 = content.match(/maximum-scale\s*=\s*([^,]*)/); + var _scalable2 = content.match(/user-scalable\s*=\s*([^,]*)/); + + if (_width2 && _width2.length && typeof _width2[1] !== "undefined") { + parsed.width = _width2[1]; + } + if (_height2 && _height2.length && typeof _height2[1] !== "undefined") { + parsed.height = _height2[1]; + } + if (_scale2 && _scale2.length && typeof _scale2[1] !== "undefined") { + parsed.scale = _scale2[1]; + } + if (_minimum2 && _minimum2.length && typeof _minimum2[1] !== "undefined") { + parsed.minimum = _minimum2[1]; + } + if (_maximum2 && _maximum2.length && typeof _maximum2[1] !== "undefined") { + parsed.maximum = _maximum2[1]; + } + if (_scalable2 && _scalable2.length && typeof _scalable2[1] !== "undefined") { + parsed.scalable = _scalable2[1]; + } + } + + settings = (0, _core.defaults)(options || {}, parsed); + + if (options) { + if (settings.width) { + newContent.push("width=" + settings.width); + } + + if (settings.height) { + newContent.push("height=" + settings.height); + } + + if (settings.scale) { + newContent.push("initial-scale=" + settings.scale); + } + + if (settings.scalable === "no") { + newContent.push("minimum-scale=" + settings.scale); + newContent.push("maximum-scale=" + settings.scale); + newContent.push("user-scalable=" + settings.scalable); + } else { + + if (settings.scalable) { + newContent.push("user-scalable=" + settings.scalable); + } + + if (settings.minimum) { + newContent.push("minimum-scale=" + settings.minimum); + } + + if (settings.maximum) { + newContent.push("minimum-scale=" + settings.maximum); + } + } + + if (!$viewport) { + $viewport = this.document.createElement("meta"); + $viewport.setAttribute("name", "viewport"); + this.document.querySelector("head").appendChild($viewport); + } + + $viewport.setAttribute("content", newContent.join(", ")); + + this.window.scrollTo(0, 0); + } + + return settings; + } + + /** + * Event emitter for when the contents has expanded + * @private + */ + + }, { + key: "expand", + value: function expand() { + this.emit(_constants.EVENTS.CONTENTS.EXPAND); + } + + /** + * Add DOM listeners + * @private + */ + + }, { + key: "listeners", + value: function listeners() { + + this.imageLoadListeners(); + + this.mediaQueryListeners(); + + // this.fontLoadListeners(); + + this.addEventListeners(); + + this.addSelectionListeners(); + + // this.transitionListeners(); + + this.resizeListeners(); + + // this.resizeObservers(); + + this.linksHandler(); + } + + /** + * Remove DOM listeners + * @private + */ + + }, { + key: "removeListeners", + value: function removeListeners() { + + this.removeEventListeners(); + + this.removeSelectionListeners(); + + clearTimeout(this.expanding); + } + + /** + * Check if size of contents has changed and + * emit 'resize' event if it has. + * @private + */ + + }, { + key: "resizeCheck", + value: function resizeCheck() { + var width = this.textWidth(); + var height = this.textHeight(); + + if (width != this._size.width || height != this._size.height) { + + this._size = { + width: width, + height: height + }; + + this.onResize && this.onResize(this._size); + this.emit(_constants.EVENTS.CONTENTS.RESIZE, this._size); + } + } + + /** + * Poll for resize detection + * @private + */ + + }, { + key: "resizeListeners", + value: function resizeListeners() { + var width, height; + // Test size again + clearTimeout(this.expanding); + + requestAnimationFrame(this.resizeCheck.bind(this)); + this.expanding = setTimeout(this.resizeListeners.bind(this), 350); + } + + /** + * Use css transitions to detect resize + * @private + */ + + }, { + key: "transitionListeners", + value: function transitionListeners() { + var body = this.content; + + body.style['transitionProperty'] = "font, font-size, font-size-adjust, font-stretch, font-variation-settings, font-weight, width, height"; + body.style['transitionDuration'] = "0.001ms"; + body.style['transitionTimingFunction'] = "linear"; + body.style['transitionDelay'] = "0"; + + this._resizeCheck = this.resizeCheck.bind(this); + this.document.addEventListener('transitionend', this._resizeCheck); + } + + /** + * Listen for media query changes and emit 'expand' event + * Adapted from: https://github.com/tylergaw/media-query-events/blob/master/js/mq-events.js + * @private + */ + + }, { + key: "mediaQueryListeners", + value: function mediaQueryListeners() { + var sheets = this.document.styleSheets; + var mediaChangeHandler = function (m) { + if (m.matches && !this._expanding) { + setTimeout(this.expand.bind(this), 1); + } + }.bind(this); + + for (var i = 0; i < sheets.length; i += 1) { + var rules; + // Firefox errors if we access cssRules cross-domain + try { + rules = sheets[i].cssRules; + } catch (e) { + return; + } + if (!rules) return; // Stylesheets changed + for (var j = 0; j < rules.length; j += 1) { + //if (rules[j].constructor === CSSMediaRule) { + if (rules[j].media) { + var mql = this.window.matchMedia(rules[j].media.mediaText); + mql.addListener(mediaChangeHandler); + //mql.onchange = mediaChangeHandler; + } + } + } + } + + /** + * Use MutationObserver to listen for changes in the DOM and check for resize + * @private + */ + + }, { + key: "resizeObservers", + value: function resizeObservers() { + var _this = this; + + // create an observer instance + this.observer = new MutationObserver(function (mutations) { + _this.resizeCheck(); + }); + + // configuration of the observer: + var config = { attributes: true, childList: true, characterData: true, subtree: true }; + + // pass in the target node, as well as the observer options + this.observer.observe(this.document, config); + } + + /** + * Test if images are loaded or add listener for when they load + * @private + */ + + }, { + key: "imageLoadListeners", + value: function imageLoadListeners() { + var images = this.document.querySelectorAll("img"); + var img; + for (var i = 0; i < images.length; i++) { + img = images[i]; + + if (typeof img.naturalWidth !== "undefined" && img.naturalWidth === 0) { + img.onload = this.expand.bind(this); + } + } + } + + /** + * Listen for font load and check for resize when loaded + * @private + */ + + }, { + key: "fontLoadListeners", + value: function fontLoadListeners() { + if (!this.document || !this.document.fonts) { + return; + } + + this.document.fonts.ready.then(function () { + this.resizeCheck(); + }.bind(this)); + } + + /** + * Get the documentElement + * @returns {element} documentElement + */ + + }, { + key: "root", + value: function root() { + if (!this.document) return null; + return this.document.documentElement; + } + + /** + * Get the location offset of a EpubCFI or an #id + * @param {string | EpubCFI} target + * @param {string} [ignoreClass] for the cfi + * @returns { {left: Number, top: Number } + */ + + }, { + key: "locationOf", + value: function locationOf(target, ignoreClass) { + var position; + var targetPos = { "left": 0, "top": 0 }; + + if (!this.document) return targetPos; + + if (this.epubcfi.isCfiString(target)) { + var range = new _epubcfi2.default(target).toRange(this.document, ignoreClass); + + if (range) { + if (range.startContainer.nodeType === Node.ELEMENT_NODE) { + position = range.startContainer.getBoundingClientRect(); + targetPos.left = position.left; + targetPos.top = position.top; + } else { + // Webkit does not handle collapsed range bounds correctly + // https://bugs.webkit.org/show_bug.cgi?id=138949 + + // Construct a new non-collapsed range + if (isWebkit) { + var container = range.startContainer; + var newRange = new Range(); + try { + if (container.nodeType === ELEMENT_NODE) { + position = container.getBoundingClientRect(); + } else if (range.startOffset + 2 < container.length) { + newRange.setStart(container, range.startOffset); + newRange.setEnd(container, range.startOffset + 2); + position = newRange.getBoundingClientRect(); + } else if (range.startOffset - 2 > 0) { + newRange.setStart(container, range.startOffset - 2); + newRange.setEnd(container, range.startOffset); + position = newRange.getBoundingClientRect(); + } else { + // empty, return the parent element + position = container.parentNode.getBoundingClientRect(); + } + } catch (e) { + console.error(e, e.stack); + } + } else { + position = range.getBoundingClientRect(); + } + } + } + } else if (typeof target === "string" && target.indexOf("#") > -1) { + + var id = target.substring(target.indexOf("#") + 1); + var el = this.document.getElementById(id); + if (el) { + if (isWebkit) { + // Webkit reports incorrect bounding rects in Columns + var _newRange = new Range(); + _newRange.selectNode(el); + position = _newRange.getBoundingClientRect(); + } else { + position = el.getBoundingClientRect(); + } + } + } + + if (position) { + targetPos.left = position.left; + targetPos.top = position.top; + } + + return targetPos; + } + + /** + * Append a stylesheet link to the document head + * @param {string} src url + */ + + }, { + key: "addStylesheet", + value: function addStylesheet(src) { + return new Promise(function (resolve, reject) { + var $stylesheet; + var ready = false; + + if (!this.document) { + resolve(false); + return; + } + + // Check if link already exists + $stylesheet = this.document.querySelector("link[href='" + src + "']"); + if ($stylesheet) { + resolve(true); + return; // already present + } + + $stylesheet = this.document.createElement("link"); + $stylesheet.type = "text/css"; + $stylesheet.rel = "stylesheet"; + $stylesheet.href = src; + $stylesheet.onload = $stylesheet.onreadystatechange = function () { + if (!ready && (!this.readyState || this.readyState == "complete")) { + ready = true; + // Let apply + setTimeout(function () { + resolve(true); + }, 1); + } + }; + + this.document.head.appendChild($stylesheet); + }.bind(this)); + } + + /** + * Append stylesheet rules to a generate stylesheet + * Array: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule + * Object: https://github.com/desirable-objects/json-to-css + * @param {array | object} rules + */ + + }, { + key: "addStylesheetRules", + value: function addStylesheetRules(rules) { + var styleEl; + var styleSheet; + var key = "epubjs-inserted-css"; + + if (!this.document || !rules || rules.length === 0) return; + + // Check if link already exists + styleEl = this.document.getElementById("#" + key); + if (!styleEl) { + styleEl = this.document.createElement("style"); + styleEl.id = key; + } + + // Append style element to head + this.document.head.appendChild(styleEl); + + // Grab style sheet + styleSheet = styleEl.sheet; + + if (Object.prototype.toString.call(rules) === "[object Array]") { + for (var i = 0, rl = rules.length; i < rl; i++) { + var j = 1, + rule = rules[i], + selector = rules[i][0], + propStr = ""; + // If the second argument of a rule is an array of arrays, correct our variables. + if (Object.prototype.toString.call(rule[1][0]) === "[object Array]") { + rule = rule[1]; + j = 0; + } + + for (var pl = rule.length; j < pl; j++) { + var prop = rule[j]; + propStr += prop[0] + ":" + prop[1] + (prop[2] ? " !important" : "") + ";\n"; + } + + // Insert CSS Rule + styleSheet.insertRule(selector + "{" + propStr + "}", styleSheet.cssRules.length); + } + } else { + var selectors = Object.keys(rules); + selectors.forEach(function (selector) { + var definition = rules[selector]; + if (Array.isArray(definition)) { + definition.forEach(function (item) { + var _rules = Object.keys(item); + var result = _rules.map(function (rule) { + return rule + ":" + item[rule]; + }).join(';'); + styleSheet.insertRule(selector + "{" + result + "}", styleSheet.cssRules.length); + }); + } else { + var _rules = Object.keys(definition); + var result = _rules.map(function (rule) { + return rule + ":" + definition[rule]; + }).join(';'); + styleSheet.insertRule(selector + "{" + result + "}", styleSheet.cssRules.length); + } + }); + } + } + + /** + * Append a script tag to the document head + * @param {string} src url + * @returns {Promise} loaded + */ + + }, { + key: "addScript", + value: function addScript(src) { + + return new Promise(function (resolve, reject) { + var $script; + var ready = false; + + if (!this.document) { + resolve(false); + return; + } + + $script = this.document.createElement("script"); + $script.type = "text/javascript"; + $script.async = true; + $script.src = src; + $script.onload = $script.onreadystatechange = function () { + if (!ready && (!this.readyState || this.readyState == "complete")) { + ready = true; + setTimeout(function () { + resolve(true); + }, 1); + } + }; + + this.document.head.appendChild($script); + }.bind(this)); + } + + /** + * Add a class to the contents container + * @param {string} className + */ + + }, { + key: "addClass", + value: function addClass(className) { + var content; + + if (!this.document) return; + + content = this.content || this.document.body; + + if (content) { + content.classList.add(className); + } + } + + /** + * Remove a class from the contents container + * @param {string} removeClass + */ + + }, { + key: "removeClass", + value: function removeClass(className) { + var content; + + if (!this.document) return; + + content = this.content || this.document.body; + + if (content) { + content.classList.remove(className); + } + } + + /** + * Add DOM event listeners + * @private + */ + + }, { + key: "addEventListeners", + value: function addEventListeners() { + if (!this.document) { + return; + } + + this._triggerEvent = this.triggerEvent.bind(this); + + _constants.DOM_EVENTS.forEach(function (eventName) { + this.document.addEventListener(eventName, this._triggerEvent, { passive: true }); + }, this); + } + + /** + * Remove DOM event listeners + * @private + */ + + }, { + key: "removeEventListeners", + value: function removeEventListeners() { + if (!this.document) { + return; + } + _constants.DOM_EVENTS.forEach(function (eventName) { + this.document.removeEventListener(eventName, this._triggerEvent, { passive: true }); + }, this); + this._triggerEvent = undefined; + } + + /** + * Emit passed browser events + * @private + */ + + }, { + key: "triggerEvent", + value: function triggerEvent(e) { + this.emit(e.type, e); + } + + /** + * Add listener for text selection + * @private + */ + + }, { + key: "addSelectionListeners", + value: function addSelectionListeners() { + if (!this.document) { + return; + } + this._onSelectionChange = this.onSelectionChange.bind(this); + this.document.addEventListener("selectionchange", this._onSelectionChange, { passive: true }); + } + + /** + * Remove listener for text selection + * @private + */ + + }, { + key: "removeSelectionListeners", + value: function removeSelectionListeners() { + if (!this.document) { + return; + } + this.document.removeEventListener("selectionchange", this._onSelectionChange, { passive: true }); + this._onSelectionChange = undefined; + } + + /** + * Handle getting text on selection + * @private + */ + + }, { + key: "onSelectionChange", + value: function onSelectionChange(e) { + if (this.selectionEndTimeout) { + clearTimeout(this.selectionEndTimeout); + } + this.selectionEndTimeout = setTimeout(function () { + var selection = this.window.getSelection(); + this.triggerSelectedEvent(selection); + }.bind(this), 250); + } + + /** + * Emit event on text selection + * @private + */ + + }, { + key: "triggerSelectedEvent", + value: function triggerSelectedEvent(selection) { + var range, cfirange; + + if (selection && selection.rangeCount > 0) { + range = selection.getRangeAt(0); + if (!range.collapsed) { + // cfirange = this.section.cfiFromRange(range); + cfirange = new _epubcfi2.default(range, this.cfiBase).toString(); + this.emit(_constants.EVENTS.CONTENTS.SELECTED, cfirange); + this.emit(_constants.EVENTS.CONTENTS.SELECTED_RANGE, range); + } + } + } + + /** + * Get a Dom Range from EpubCFI + * @param {EpubCFI} _cfi + * @param {string} [ignoreClass] + * @returns {Range} range + */ + + }, { + key: "range", + value: function range(_cfi, ignoreClass) { + var cfi = new _epubcfi2.default(_cfi); + return cfi.toRange(this.document, ignoreClass); + } + + /** + * Get an EpubCFI from a Dom Range + * @param {Range} range + * @param {string} [ignoreClass] + * @returns {EpubCFI} cfi + */ + + }, { + key: "cfiFromRange", + value: function cfiFromRange(range, ignoreClass) { + return new _epubcfi2.default(range, this.cfiBase, ignoreClass).toString(); + } + + /** + * Get an EpubCFI from a Dom node + * @param {node} node + * @param {string} [ignoreClass] + * @returns {EpubCFI} cfi + */ + + }, { + key: "cfiFromNode", + value: function cfiFromNode(node, ignoreClass) { + return new _epubcfi2.default(node, this.cfiBase, ignoreClass).toString(); + } + + // TODO: find where this is used - remove? + + }, { + key: "map", + value: function map(layout) { + var map = new _mapping2.default(layout); + return map.section(); + } + + /** + * Size the contents to a given width and height + * @param {number} [width] + * @param {number} [height] + */ + + }, { + key: "size", + value: function size(width, height) { + var viewport = { scale: 1.0, scalable: "no" }; + + this.layoutStyle("scrolling"); + + if (width >= 0) { + this.width(width); + viewport.width = width; + this.css("padding", "0 " + width / 12 + "px"); + } + + if (height >= 0) { + this.height(height); + viewport.height = height; + } + + this.css("margin", "0"); + this.css("box-sizing", "border-box"); + + this.viewport(viewport); + } + + /** + * Apply columns to the contents for pagination + * @param {number} width + * @param {number} height + * @param {number} columnWidth + * @param {number} gap + */ + + }, { + key: "columns", + value: function columns(width, height, columnWidth, gap) { + var COLUMN_AXIS = (0, _core.prefixed)("column-axis"); + var COLUMN_GAP = (0, _core.prefixed)("column-gap"); + var COLUMN_WIDTH = (0, _core.prefixed)("column-width"); + var COLUMN_FILL = (0, _core.prefixed)("column-fill"); + + var writingMode = this.writingMode(); + var axis = writingMode.indexOf("vertical") === 0 ? "vertical" : "horizontal"; + + this.layoutStyle("paginated"); + + // Fix body width issues if rtl is only set on body element + if (this.content.dir === "rtl") { + this.direction("rtl"); + } + + this.width(width); + this.height(height); + + // Deal with Mobile trying to scale to viewport + this.viewport({ width: width, height: height, scale: 1.0, scalable: "no" }); + + // TODO: inline-block needs more testing + // Fixes Safari column cut offs, but causes RTL issues + // this.css("display", "inline-block"); + + this.css("overflow-y", "hidden"); + this.css("margin", "0", true); + + if (axis === "vertical") { + this.css("padding-top", gap / 2 + "px", true); + this.css("padding-bottom", gap / 2 + "px", true); + this.css("padding-left", "20px"); + this.css("padding-right", "20px"); + } else { + this.css("padding-top", "20px"); + this.css("padding-bottom", "20px"); + this.css("padding-left", gap / 2 + "px", true); + this.css("padding-right", gap / 2 + "px", true); + } + + this.css("box-sizing", "border-box"); + this.css("max-width", "inherit"); + + this.css(COLUMN_AXIS, "horizontal"); + this.css(COLUMN_FILL, "auto"); + + this.css(COLUMN_GAP, gap + "px"); + this.css(COLUMN_WIDTH, columnWidth + "px"); + } + + /** + * Scale contents from center + * @param {number} scale + * @param {number} offsetX + * @param {number} offsetY + */ + + }, { + key: "scaler", + value: function scaler(scale, offsetX, offsetY) { + var scaleStr = "scale(" + scale + ")"; + var translateStr = ""; + // this.css("position", "absolute")); + this.css("transform-origin", "top left"); + + if (offsetX >= 0 || offsetY >= 0) { + translateStr = " translate(" + (offsetX || 0) + "px, " + (offsetY || 0) + "px )"; + } + + this.css("transform", scaleStr + translateStr); + } + + /** + * Fit contents into a fixed width and height + * @param {number} width + * @param {number} height + */ + + }, { + key: "fit", + value: function fit(width, height) { + var viewport = this.viewport(); + var viewportWidth = parseInt(viewport.width); + var viewportHeight = parseInt(viewport.height); + var widthScale = width / viewportWidth; + var heightScale = height / viewportHeight; + var scale = widthScale < heightScale ? widthScale : heightScale; + + // the translate does not work as intended, elements can end up unaligned + // var offsetY = (height - (viewportHeight * scale)) / 2; + // var offsetX = 0; + // if (this.sectionIndex % 2 === 1) { + // offsetX = width - (viewportWidth * scale); + // } + + this.layoutStyle("paginated"); + + // scale needs width and height to be set + this.width(viewportWidth); + this.height(viewportHeight); + this.overflow("hidden"); + + // Scale to the correct size + this.scaler(scale, 0, 0); + // this.scaler(scale, offsetX > 0 ? offsetX : 0, offsetY); + + // background images are not scaled by transform + this.css("background-size", viewportWidth * scale + "px " + viewportHeight * scale + "px"); + + this.css("background-color", "transparent"); + } + + /** + * Set the direction of the text + * @param {string} [dir="ltr"] "rtl" | "ltr" + */ + + }, { + key: "direction", + value: function direction(dir) { + if (this.documentElement) { + this.documentElement.style["direction"] = dir; + } + } + }, { + key: "mapPage", + value: function mapPage(cfiBase, layout, start, end, dev) { + var mapping = new _mapping2.default(layout, dev); + + return mapping.page(this, cfiBase, start, end); + } + + /** + * Emit event when link in content is clicked + * @private + */ + + }, { + key: "linksHandler", + value: function linksHandler() { + var _this2 = this; + + (0, _replacements.replaceLinks)(this.content, function (href) { + _this2.emit(_constants.EVENTS.CONTENTS.LINK_CLICKED, href); + }); + } + + /** + * Set the writingMode of the text + * @param {string} [mode="horizontal-tb"] "horizontal-tb" | "vertical-rl" | "vertical-lr" + */ + + }, { + key: "writingMode", + value: function writingMode(mode) { + var WRITING_MODE = (0, _core.prefixed)("writing-mode"); + + if (mode && this.documentElement) { + this.documentElement.style[WRITING_MODE] = mode; + } + + return this.window.getComputedStyle(this.documentElement)[WRITING_MODE] || ''; + } + + /** + * Set the layoutStyle of the content + * @param {string} [style="paginated"] "scrolling" | "paginated" + * @private + */ + + }, { + key: "layoutStyle", + value: function layoutStyle(style) { + + if (style) { + this._layoutStyle = style; + navigator.epubReadingSystem.layoutStyle = this._layoutStyle; + } + + return this._layoutStyle || "paginated"; + } + + /** + * Add the epubReadingSystem object to the navigator + * @param {string} name + * @param {string} version + * @private + */ + + }, { + key: "epubReadingSystem", + value: function epubReadingSystem(name, version) { + navigator.epubReadingSystem = { + name: name, + version: version, + layoutStyle: this.layoutStyle(), + hasFeature: function hasFeature(feature) { + switch (feature) { + case "dom-manipulation": + return true; + case "layout-changes": + return true; + case "touch-events": + return true; + case "mouse-events": + return true; + case "keyboard-events": + return true; + case "spine-scripting": + return false; + default: + return false; + } + } + }; + return navigator.epubReadingSystem; + } + }, { + key: "destroy", + value: function destroy() { + // Stop observing + if (this.observer) { + this.observer.disconnect(); + } + + this.document.removeEventListener('transitionend', this._resizeCheck); + + this.removeListeners(); + } + }], [{ + key: "listenedEvents", + get: function get() { + return _constants.DOM_EVENTS; + } + }]); + + return Contents; +}(); + +(0, _eventEmitter2.default)(Contents.prototype); + +exports.default = Contents; +module.exports = exports["default"]; + +/***/ }), +/* 15 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _eventEmitter = __webpack_require__(3); + +var _eventEmitter2 = _interopRequireDefault(_eventEmitter); + +var _core = __webpack_require__(0); + +var _mapping = __webpack_require__(19); + +var _mapping2 = _interopRequireDefault(_mapping); + +var _queue = __webpack_require__(12); + +var _queue2 = _interopRequireDefault(_queue); + +var _stage = __webpack_require__(59); + +var _stage2 = _interopRequireDefault(_stage); + +var _views = __webpack_require__(69); + +var _views2 = _interopRequireDefault(_views); + +var _constants = __webpack_require__(2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var DefaultViewManager = function () { + function DefaultViewManager(options) { + _classCallCheck(this, DefaultViewManager); + + this.name = "default"; + this.optsSettings = options.settings; + this.View = options.view; + this.request = options.request; + this.renditionQueue = options.queue; + this.q = new _queue2.default(this); + + this.settings = (0, _core.extend)(this.settings || {}, { + infinite: true, + hidden: false, + width: undefined, + height: undefined, + axis: undefined, + flow: "scrolled", + ignoreClass: "", + fullsize: undefined + }); + + (0, _core.extend)(this.settings, options.settings || {}); + + this.viewSettings = { + ignoreClass: this.settings.ignoreClass, + axis: this.settings.axis, + flow: this.settings.flow, + layout: this.layout, + method: this.settings.method, // srcdoc, blobUrl, write + width: 0, + height: 0, + forceEvenPages: true + }; + + this.rendered = false; + } + + _createClass(DefaultViewManager, [{ + key: "render", + value: function render(element, size) { + var tag = element.tagName; + + if (typeof this.settings.fullsize === "undefined" && tag && (tag.toLowerCase() == "body" || tag.toLowerCase() == "html")) { + this.settings.fullsize = true; + } + + if (this.settings.fullsize) { + this.settings.overflow = "visible"; + this.overflow = this.settings.overflow; + } + + this.settings.size = size; + + // Save the stage + this.stage = new _stage2.default({ + width: size.width, + height: size.height, + overflow: this.overflow, + hidden: this.settings.hidden, + axis: this.settings.axis, + fullsize: this.settings.fullsize, + direction: this.settings.direction + }); + + this.stage.attachTo(element); + + // Get this stage container div + this.container = this.stage.getContainer(); + + // Views array methods + this.views = new _views2.default(this.container); + + // Calculate Stage Size + this._bounds = this.bounds(); + this._stageSize = this.stage.size(); + + // Set the dimensions for views + this.viewSettings.width = this._stageSize.width; + this.viewSettings.height = this._stageSize.height; + + // Function to handle a resize event. + // Will only attach if width and height are both fixed. + this.stage.onResize(this.onResized.bind(this)); + + this.stage.onOrientationChange(this.onOrientationChange.bind(this)); + + // Add Event Listeners + this.addEventListeners(); + + // Add Layout method + // this.applyLayoutMethod(); + if (this.layout) { + this.updateLayout(); + } + + this.rendered = true; + } + }, { + key: "addEventListeners", + value: function addEventListeners() { + var scroller; + + window.addEventListener("unload", function (e) { + this.destroy(); + }.bind(this)); + + if (!this.settings.fullsize) { + scroller = this.container; + } else { + scroller = window; + } + + this._onScroll = this.onScroll.bind(this); + scroller.addEventListener("scroll", this._onScroll); + } + }, { + key: "removeEventListeners", + value: function removeEventListeners() { + var scroller; + + if (!this.settings.fullsize) { + scroller = this.container; + } else { + scroller = window; + } + + scroller.removeEventListener("scroll", this._onScroll); + this._onScroll = undefined; + } + }, { + key: "destroy", + value: function destroy() { + clearTimeout(this.orientationTimeout); + clearTimeout(this.resizeTimeout); + clearTimeout(this.afterScrolled); + + this.clear(); + + this.removeEventListeners(); + + this.stage.destroy(); + + this.rendered = false; + + /* + clearTimeout(this.trimTimeout); + if(this.settings.hidden) { + this.element.removeChild(this.wrapper); + } else { + this.element.removeChild(this.container); + } + */ + } + }, { + key: "onOrientationChange", + value: function onOrientationChange(e) { + var _window = window, + orientation = _window.orientation; + + + if (this.optsSettings.resizeOnOrientationChange) { + this.resize(); + } + + // Per ampproject: + // In IOS 10.3, the measured size of an element is incorrect if the + // element size depends on window size directly and the measurement + // happens in window.resize event. Adding a timeout for correct + // measurement. See https://github.com/ampproject/amphtml/issues/8479 + clearTimeout(this.orientationTimeout); + this.orientationTimeout = setTimeout(function () { + this.orientationTimeout = undefined; + + if (this.optsSettings.resizeOnOrientationChange) { + this.resize(); + } + + this.emit(_constants.EVENTS.MANAGERS.ORIENTATION_CHANGE, orientation); + }.bind(this), 500); + } + }, { + key: "onResized", + value: function onResized(e) { + this.resize(); + } + }, { + key: "resize", + value: function resize(width, height) { + var stageSize = this.stage.size(width, height); + + // For Safari, wait for orientation to catch up + // if the window is a square + this.winBounds = (0, _core.windowBounds)(); + if (this.orientationTimeout && this.winBounds.width === this.winBounds.height) { + // reset the stage size for next resize + this._stageSize = undefined; + return; + } + + if (this._stageSize && this._stageSize.width === stageSize.width && this._stageSize.height === stageSize.height) { + // Size is the same, no need to resize + return; + } + + this._stageSize = stageSize; + + this._bounds = this.bounds(); + + // Clear current views + this.clear(); + + // Update for new views + this.viewSettings.width = this._stageSize.width; + this.viewSettings.height = this._stageSize.height; + + this.updateLayout(); + + this.emit(_constants.EVENTS.MANAGERS.RESIZED, { + width: this._stageSize.width, + height: this._stageSize.height + }); + } + }, { + key: "createView", + value: function createView(section) { + return new this.View(section, this.viewSettings); + } + }, { + key: "display", + value: function display(section, target) { + + var displaying = new _core.defer(); + var displayed = displaying.promise; + + // Check if moving to target is needed + if (target === section.href || (0, _core.isNumber)(target)) { + target = undefined; + } + + // Check to make sure the section we want isn't already shown + var visible = this.views.find(section); + + // View is already shown, just move to correct location in view + if (visible && section) { + var offset = visible.offset(); + + if (this.settings.direction === "ltr") { + this.scrollTo(offset.left, offset.top, true); + } else { + var width = visible.width(); + this.scrollTo(offset.left + width, offset.top, true); + } + + if (target) { + var _offset = visible.locationOf(target); + this.moveTo(_offset); + } + + displaying.resolve(); + return displayed; + } + + // Hide all current views + this.clear(); + + this.add(section).then(function (view) { + + // Move to correct place within the section, if needed + if (target) { + var _offset2 = view.locationOf(target); + this.moveTo(_offset2); + } + }.bind(this), function (err) { + displaying.reject(err); + }).then(function () { + var next; + if (this.layout.name === "pre-paginated" && this.layout.divisor > 1 && section.index > 0) { + // First page (cover) should stand alone for pre-paginated books + next = section.next(); + if (next) { + return this.add(next); + } + } + }.bind(this)).then(function () { + + this.views.show(); + + displaying.resolve(); + }.bind(this)); + // .then(function(){ + // return this.hooks.display.trigger(view); + // }.bind(this)) + // .then(function(){ + // this.views.show(); + // }.bind(this)); + return displayed; + } + }, { + key: "afterDisplayed", + value: function afterDisplayed(view) { + this.emit(_constants.EVENTS.MANAGERS.ADDED, view); + } + }, { + key: "afterResized", + value: function afterResized(view) { + this.emit(_constants.EVENTS.MANAGERS.RESIZE, view.section); + } + }, { + key: "moveTo", + value: function moveTo(offset) { + var distX = 0, + distY = 0; + + if (!this.isPaginated) { + distY = offset.top; + } else { + distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta; + + if (distX + this.layout.delta > this.container.scrollWidth) { + distX = this.container.scrollWidth - this.layout.delta; + } + } + this.scrollTo(distX, distY, true); + } + }, { + key: "add", + value: function add(section) { + var _this = this; + + var view = this.createView(section); + + this.views.append(view); + + // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this)); + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + view.on(_constants.EVENTS.VIEWS.AXIS, function (axis) { + _this.updateAxis(axis); + }); + + return view.display(this.request); + } + }, { + key: "append", + value: function append(section) { + var _this2 = this; + + var view = this.createView(section); + this.views.append(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + view.on(_constants.EVENTS.VIEWS.AXIS, function (axis) { + _this2.updateAxis(axis); + }); + + return view.display(this.request); + } + }, { + key: "prepend", + value: function prepend(section) { + var _this3 = this; + + var view = this.createView(section); + + view.on(_constants.EVENTS.VIEWS.RESIZED, function (bounds) { + _this3.counter(bounds); + }); + + this.views.prepend(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + view.on(_constants.EVENTS.VIEWS.AXIS, function (axis) { + _this3.updateAxis(axis); + }); + + return view.display(this.request); + } + }, { + key: "counter", + value: function counter(bounds) { + if (this.settings.axis === "vertical") { + this.scrollBy(0, bounds.heightDelta, true); + } else { + this.scrollBy(bounds.widthDelta, 0, true); + } + } + + // resizeView(view) { + // + // if(this.settings.globalLayoutProperties.layout === "pre-paginated") { + // view.lock("both", this.bounds.width, this.bounds.height); + // } else { + // view.lock("width", this.bounds.width, this.bounds.height); + // } + // + // }; + + }, { + key: "next", + value: function next() { + var next; + var left; + + var dir = this.settings.direction; + + if (!this.views.length) return; + + if (this.isPaginated && this.settings.axis === "horizontal" && (!dir || dir === "ltr")) { + + this.scrollLeft = this.container.scrollLeft; + + left = this.container.scrollLeft + this.container.offsetWidth + this.layout.delta; + + if (left <= this.container.scrollWidth) { + this.scrollBy(this.layout.delta, 0, true); + } else { + next = this.views.last().section.next(); + } + } else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") { + + this.scrollLeft = this.container.scrollLeft; + + left = this.container.scrollLeft; + + if (left > 0) { + this.scrollBy(this.layout.delta, 0, true); + } else { + next = this.views.last().section.next(); + } + } else if (this.isPaginated && this.settings.axis === "vertical") { + + this.scrollTop = this.container.scrollTop; + + var top = this.container.scrollTop + this.container.offsetHeight; + + if (top < this.container.scrollHeight) { + this.scrollBy(0, this.layout.height, true); + } else { + next = this.views.last().section.next(); + } + } else { + next = this.views.last().section.next(); + } + + if (next) { + this.clear(); + + return this.append(next).then(function () { + var right; + if (this.layout.name === "pre-paginated" && this.layout.divisor > 1) { + right = next.next(); + if (right) { + return this.append(right); + } + } + }.bind(this), function (err) { + return err; + }).then(function () { + this.views.show(); + }.bind(this)); + } + } + }, { + key: "prev", + value: function prev() { + var prev; + var left; + var dir = this.settings.direction; + + if (!this.views.length) return; + + if (this.isPaginated && this.settings.axis === "horizontal" && (!dir || dir === "ltr")) { + + this.scrollLeft = this.container.scrollLeft; + + left = this.container.scrollLeft; + + if (left > 0) { + this.scrollBy(-this.layout.delta, 0, true); + } else { + prev = this.views.first().section.prev(); + } + } else if (this.isPaginated && this.settings.axis === "horizontal" && dir === "rtl") { + + this.scrollLeft = this.container.scrollLeft; + + left = this.container.scrollLeft + this.container.offsetWidth + this.layout.delta; + + if (left <= this.container.scrollWidth) { + this.scrollBy(-this.layout.delta, 0, true); + } else { + prev = this.views.first().section.prev(); + } + } else if (this.isPaginated && this.settings.axis === "vertical") { + + this.scrollTop = this.container.scrollTop; + + var top = this.container.scrollTop; + + if (top > 0) { + this.scrollBy(0, -this.layout.height, true); + } else { + prev = this.views.first().section.prev(); + } + } else { + + prev = this.views.first().section.prev(); + } + + if (prev) { + this.clear(); + + return this.prepend(prev).then(function () { + var left; + if (this.layout.name === "pre-paginated" && this.layout.divisor > 1) { + left = prev.prev(); + if (left) { + return this.prepend(left); + } + } + }.bind(this), function (err) { + return err; + }).then(function () { + if (this.isPaginated && this.settings.axis === "horizontal") { + if (this.settings.direction === "rtl") { + this.scrollTo(0, 0, true); + } else { + this.scrollTo(this.container.scrollWidth - this.layout.delta, 0, true); + } + } + this.views.show(); + }.bind(this)); + } + } + }, { + key: "current", + value: function current() { + var visible = this.visible(); + if (visible.length) { + // Current is the last visible view + return visible[visible.length - 1]; + } + return null; + } + }, { + key: "clear", + value: function clear() { + + // this.q.clear(); + + if (this.views) { + this.views.hide(); + this.scrollTo(0, 0, true); + this.views.clear(); + } + } + }, { + key: "currentLocation", + value: function currentLocation() { + + if (this.settings.axis === "vertical") { + this.location = this.scrolledLocation(); + } else { + this.location = this.paginatedLocation(); + } + return this.location; + } + }, { + key: "scrolledLocation", + value: function scrolledLocation() { + var _this4 = this; + + var visible = this.visible(); + var container = this.container.getBoundingClientRect(); + var pageHeight = container.height < window.innerHeight ? container.height : window.innerHeight; + + var offset = 0; + var used = 0; + + if (this.settings.fullsize) { + offset = window.scrollY; + } + + var sections = visible.map(function (view) { + var _view$section = view.section, + index = _view$section.index, + href = _view$section.href; + + var position = view.position(); + var height = view.height(); + + var startPos = offset + container.top - position.top + used; + var endPos = startPos + pageHeight - used; + if (endPos > height) { + endPos = height; + used = endPos - startPos; + } + + var totalPages = _this4.layout.count(height, pageHeight).pages; + + var currPage = Math.ceil(startPos / pageHeight); + var pages = []; + var endPage = Math.ceil(endPos / pageHeight); + + pages = []; + for (var i = currPage; i <= endPage; i++) { + var pg = i + 1; + pages.push(pg); + } + + var mapping = _this4.mapping.page(view.contents, view.section.cfiBase, startPos, endPos); + + return { + index: index, + href: href, + pages: pages, + totalPages: totalPages, + mapping: mapping + }; + }); + + return sections; + } + }, { + key: "paginatedLocation", + value: function paginatedLocation() { + var _this5 = this; + + var visible = this.visible(); + var container = this.container.getBoundingClientRect(); + + var left = 0; + var used = 0; + + if (this.settings.fullsize) { + left = window.scrollX; + } + + var sections = visible.map(function (view) { + var _view$section2 = view.section, + index = _view$section2.index, + href = _view$section2.href; + + var offset = view.offset().left; + var position = view.position().left; + var width = view.width(); + + // Find mapping + var start = left + container.left - position + used; + var end = start + _this5.layout.width - used; + + var mapping = _this5.mapping.page(view.contents, view.section.cfiBase, start, end); + + // Find displayed pages + //console.log("pre", end, offset + width); + // if (end > offset + width) { + // end = offset + width; + // used = this.layout.pageWidth; + // } + // console.log("post", end); + + var totalPages = _this5.layout.count(width).pages; + var startPage = Math.floor(start / _this5.layout.pageWidth); + var pages = []; + var endPage = Math.floor(end / _this5.layout.pageWidth); + + // start page should not be negative + if (startPage < 0) { + startPage = 0; + endPage = endPage + 1; + } + + // Reverse page counts for rtl + if (_this5.settings.direction === "rtl") { + var tempStartPage = startPage; + startPage = totalPages - endPage; + endPage = totalPages - tempStartPage; + } + + for (var i = startPage + 1; i <= endPage; i++) { + var pg = i; + pages.push(pg); + } + + return { + index: index, + href: href, + pages: pages, + totalPages: totalPages, + mapping: mapping + }; + }); + + return sections; + } + }, { + key: "isVisible", + value: function isVisible(view, offsetPrev, offsetNext, _container) { + var position = view.position(); + var container = _container || this.bounds(); + + if (this.settings.axis === "horizontal" && position.right > container.left - offsetPrev && position.left < container.right + offsetNext) { + + return true; + } else if (this.settings.axis === "vertical" && position.bottom > container.top - offsetPrev && position.top < container.bottom + offsetNext) { + + return true; + } + + return false; + } + }, { + key: "visible", + value: function visible() { + var container = this.bounds(); + var views = this.views.displayed(); + var viewsLength = views.length; + var visible = []; + var isVisible; + var view; + + for (var i = 0; i < viewsLength; i++) { + view = views[i]; + isVisible = this.isVisible(view, 0, 0, container); + + if (isVisible === true) { + visible.push(view); + } + } + return visible; + } + }, { + key: "scrollBy", + value: function scrollBy(x, y, silent) { + var dir = this.settings.direction === "rtl" ? -1 : 1; + + if (silent) { + this.ignore = true; + } + + if (!this.settings.fullsize) { + if (x) this.container.scrollLeft += x * dir; + if (y) this.container.scrollTop += y; + } else { + window.scrollBy(x * dir, y * dir); + } + this.scrolled = true; + } + }, { + key: "scrollTo", + value: function scrollTo(x, y, silent) { + if (silent) { + this.ignore = true; + } + + if (!this.settings.fullsize) { + this.container.scrollLeft = x; + this.container.scrollTop = y; + } else { + window.scrollTo(x, y); + } + this.scrolled = true; + } + }, { + key: "onScroll", + value: function onScroll() { + var scrollTop = void 0; + var scrollLeft = void 0; + + if (!this.settings.fullsize) { + scrollTop = this.container.scrollTop; + scrollLeft = this.container.scrollLeft; + } else { + scrollTop = window.scrollY; + scrollLeft = window.scrollX; + } + + this.scrollTop = scrollTop; + this.scrollLeft = scrollLeft; + + if (!this.ignore) { + this.emit(_constants.EVENTS.MANAGERS.SCROLL, { + top: scrollTop, + left: scrollLeft + }); + + clearTimeout(this.afterScrolled); + this.afterScrolled = setTimeout(function () { + this.emit(_constants.EVENTS.MANAGERS.SCROLLED, { + top: this.scrollTop, + left: this.scrollLeft + }); + }.bind(this), 20); + } else { + this.ignore = false; + } + } + }, { + key: "bounds", + value: function bounds() { + var bounds; + + bounds = this.stage.bounds(); + + return bounds; + } + }, { + key: "applyLayout", + value: function applyLayout(layout) { + + this.layout = layout; + this.updateLayout(); + // this.manager.layout(this.layout.format); + } + }, { + key: "updateLayout", + value: function updateLayout() { + + if (!this.stage) { + return; + } + + this._stageSize = this.stage.size(); + + if (!this.isPaginated) { + this.layout.calculate(this._stageSize.width, this._stageSize.height); + } else { + this.layout.calculate(this._stageSize.width, this._stageSize.height, this.settings.gap); + + // Set the look ahead offset for what is visible + this.settings.offset = this.layout.delta; + + // this.stage.addStyleRules("iframe", [{"margin-right" : this.layout.gap + "px"}]); + } + + // Set the dimensions for views + this.viewSettings.width = this.layout.width; + this.viewSettings.height = this.layout.height; + + this.setLayout(this.layout); + } + }, { + key: "setLayout", + value: function setLayout(layout) { + + this.viewSettings.layout = layout; + + this.mapping = new _mapping2.default(layout.props, this.settings.direction, this.settings.axis); + + if (this.views) { + + this.views.forEach(function (view) { + if (view) { + view.setLayout(layout); + } + }); + } + } + }, { + key: "updateAxis", + value: function updateAxis(axis, forceUpdate) { + + if (!this.isPaginated) { + axis = "vertical"; + } + + if (!forceUpdate && axis === this.settings.axis) { + return; + } + + this.settings.axis = axis; + + this.stage && this.stage.axis(axis); + + this.viewSettings.axis = axis; + + if (this.mapping) { + this.mapping = new _mapping2.default(this.layout.props, this.settings.direction, this.settings.axis); + } + + if (this.layout) { + if (axis === "vertical") { + this.layout.spread("none"); + } else { + this.layout.spread(this.layout.settings.spread); + } + } + } + }, { + key: "updateFlow", + value: function updateFlow(flow) { + var defaultScrolledOverflow = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "auto"; + + var isPaginated = flow === "paginated" || flow === "auto"; + + this.isPaginated = isPaginated; + + if (flow === "scrolled-doc" || flow === "scrolled-continuous" || flow === "scrolled") { + this.updateAxis("vertical"); + } else { + this.updateAxis("horizontal"); + } + + this.viewSettings.flow = flow; + + if (!this.settings.overflow) { + this.overflow = isPaginated ? "hidden" : defaultScrolledOverflow; + } else { + this.overflow = this.settings.overflow; + } + + this.stage && this.stage.overflow(this.overflow); + + this.updateLayout(); + } + }, { + key: "getContents", + value: function getContents() { + var contents = []; + if (!this.views) { + return contents; + } + this.views.forEach(function (view) { + var viewContents = view && view.contents; + if (viewContents) { + contents.push(viewContents); + } + }); + return contents; + } + }, { + key: "direction", + value: function direction() { + var dir = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "ltr"; + + this.settings.direction = dir; + + this.stage && this.stage.direction(dir); + + this.viewSettings.direction = dir; + + this.updateLayout(); + } + }, { + key: "isRendered", + value: function isRendered() { + return this.rendered; + } + }]); + + return DefaultViewManager; +}(); + +//-- Enable binding events to Manager + + +(0, _eventEmitter2.default)(DefaultViewManager.prototype); + +exports.default = DefaultViewManager; +module.exports = exports["default"]; + +/***/ }), +/* 16 */ +/***/ (function(module, exports) { + +/** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ +function isObject(value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); +} + +module.exports = isObject; + + +/***/ }), +/* 17 */ +/***/ (function(module, exports) { + +/* + * DOM Level 2 + * Object DOMException + * @see http://www.w3.org/TR/REC-DOM-Level-1/ecma-script-language-binding.html + * @see http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/ecma-script-binding.html + */ + +function copy(src,dest){ + for(var p in src){ + dest[p] = src[p]; + } +} +/** +^\w+\.prototype\.([_\w]+)\s*=\s*((?:.*\{\s*?[\r\n][\s\S]*?^})|\S.*?(?=[;\r\n]));? +^\w+\.prototype\.([_\w]+)\s*=\s*(\S.*?(?=[;\r\n]));? + */ +function _extends(Class,Super){ + var pt = Class.prototype; + if(Object.create){ + var ppt = Object.create(Super.prototype) + pt.__proto__ = ppt; + } + if(!(pt instanceof Super)){ + function t(){}; + t.prototype = Super.prototype; + t = new t(); + copy(pt,t); + Class.prototype = pt = t; + } + if(pt.constructor != Class){ + if(typeof Class != 'function'){ + console.error("unknow Class:"+Class) + } + pt.constructor = Class + } +} +var htmlns = 'http://www.w3.org/1999/xhtml' ; +// Node Types +var NodeType = {} +var ELEMENT_NODE = NodeType.ELEMENT_NODE = 1; +var ATTRIBUTE_NODE = NodeType.ATTRIBUTE_NODE = 2; +var TEXT_NODE = NodeType.TEXT_NODE = 3; +var CDATA_SECTION_NODE = NodeType.CDATA_SECTION_NODE = 4; +var ENTITY_REFERENCE_NODE = NodeType.ENTITY_REFERENCE_NODE = 5; +var ENTITY_NODE = NodeType.ENTITY_NODE = 6; +var PROCESSING_INSTRUCTION_NODE = NodeType.PROCESSING_INSTRUCTION_NODE = 7; +var COMMENT_NODE = NodeType.COMMENT_NODE = 8; +var DOCUMENT_NODE = NodeType.DOCUMENT_NODE = 9; +var DOCUMENT_TYPE_NODE = NodeType.DOCUMENT_TYPE_NODE = 10; +var DOCUMENT_FRAGMENT_NODE = NodeType.DOCUMENT_FRAGMENT_NODE = 11; +var NOTATION_NODE = NodeType.NOTATION_NODE = 12; + +// ExceptionCode +var ExceptionCode = {} +var ExceptionMessage = {}; +var INDEX_SIZE_ERR = ExceptionCode.INDEX_SIZE_ERR = ((ExceptionMessage[1]="Index size error"),1); +var DOMSTRING_SIZE_ERR = ExceptionCode.DOMSTRING_SIZE_ERR = ((ExceptionMessage[2]="DOMString size error"),2); +var HIERARCHY_REQUEST_ERR = ExceptionCode.HIERARCHY_REQUEST_ERR = ((ExceptionMessage[3]="Hierarchy request error"),3); +var WRONG_DOCUMENT_ERR = ExceptionCode.WRONG_DOCUMENT_ERR = ((ExceptionMessage[4]="Wrong document"),4); +var INVALID_CHARACTER_ERR = ExceptionCode.INVALID_CHARACTER_ERR = ((ExceptionMessage[5]="Invalid character"),5); +var NO_DATA_ALLOWED_ERR = ExceptionCode.NO_DATA_ALLOWED_ERR = ((ExceptionMessage[6]="No data allowed"),6); +var NO_MODIFICATION_ALLOWED_ERR = ExceptionCode.NO_MODIFICATION_ALLOWED_ERR = ((ExceptionMessage[7]="No modification allowed"),7); +var NOT_FOUND_ERR = ExceptionCode.NOT_FOUND_ERR = ((ExceptionMessage[8]="Not found"),8); +var NOT_SUPPORTED_ERR = ExceptionCode.NOT_SUPPORTED_ERR = ((ExceptionMessage[9]="Not supported"),9); +var INUSE_ATTRIBUTE_ERR = ExceptionCode.INUSE_ATTRIBUTE_ERR = ((ExceptionMessage[10]="Attribute in use"),10); +//level2 +var INVALID_STATE_ERR = ExceptionCode.INVALID_STATE_ERR = ((ExceptionMessage[11]="Invalid state"),11); +var SYNTAX_ERR = ExceptionCode.SYNTAX_ERR = ((ExceptionMessage[12]="Syntax error"),12); +var INVALID_MODIFICATION_ERR = ExceptionCode.INVALID_MODIFICATION_ERR = ((ExceptionMessage[13]="Invalid modification"),13); +var NAMESPACE_ERR = ExceptionCode.NAMESPACE_ERR = ((ExceptionMessage[14]="Invalid namespace"),14); +var INVALID_ACCESS_ERR = ExceptionCode.INVALID_ACCESS_ERR = ((ExceptionMessage[15]="Invalid access"),15); + + +function DOMException(code, message) { + if(message instanceof Error){ + var error = message; + }else{ + error = this; + Error.call(this, ExceptionMessage[code]); + this.message = ExceptionMessage[code]; + if(Error.captureStackTrace) Error.captureStackTrace(this, DOMException); + } + error.code = code; + if(message) this.message = this.message + ": " + message; + return error; +}; +DOMException.prototype = Error.prototype; +copy(ExceptionCode,DOMException) +/** + * @see http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-536297177 + * The NodeList interface provides the abstraction of an ordered collection of nodes, without defining or constraining how this collection is implemented. NodeList objects in the DOM are live. + * The items in the NodeList are accessible via an integral index, starting from 0. + */ +function NodeList() { +}; +NodeList.prototype = { + /** + * The number of nodes in the list. The range of valid child node indices is 0 to length-1 inclusive. + * @standard level1 + */ + length:0, + /** + * Returns the indexth item in the collection. If index is greater than or equal to the number of nodes in the list, this returns null. + * @standard level1 + * @param index unsigned long + * Index into the collection. + * @return Node + * The node at the indexth position in the NodeList, or null if that is not a valid index. + */ + item: function(index) { + return this[index] || null; + }, + toString:function(isHTML,nodeFilter){ + for(var buf = [], i = 0;i=0){ + var lastIndex = list.length-1 + while(i0 || key == 'xmlns'){ +// return null; +// } + //console.log() + var i = this.length; + while(i--){ + var attr = this[i]; + //console.log(attr.nodeName,key) + if(attr.nodeName == key){ + return attr; + } + } + }, + setNamedItem: function(attr) { + var el = attr.ownerElement; + if(el && el!=this._ownerElement){ + throw new DOMException(INUSE_ATTRIBUTE_ERR); + } + var oldAttr = this.getNamedItem(attr.nodeName); + _addNamedNode(this._ownerElement,this,attr,oldAttr); + return oldAttr; + }, + /* returns Node */ + setNamedItemNS: function(attr) {// raises: WRONG_DOCUMENT_ERR,NO_MODIFICATION_ALLOWED_ERR,INUSE_ATTRIBUTE_ERR + var el = attr.ownerElement, oldAttr; + if(el && el!=this._ownerElement){ + throw new DOMException(INUSE_ATTRIBUTE_ERR); + } + oldAttr = this.getNamedItemNS(attr.namespaceURI,attr.localName); + _addNamedNode(this._ownerElement,this,attr,oldAttr); + return oldAttr; + }, + + /* returns Node */ + removeNamedItem: function(key) { + var attr = this.getNamedItem(key); + _removeNamedNode(this._ownerElement,this,attr); + return attr; + + + },// raises: NOT_FOUND_ERR,NO_MODIFICATION_ALLOWED_ERR + + //for level2 + removeNamedItemNS:function(namespaceURI,localName){ + var attr = this.getNamedItemNS(namespaceURI,localName); + _removeNamedNode(this._ownerElement,this,attr); + return attr; + }, + getNamedItemNS: function(namespaceURI, localName) { + var i = this.length; + while(i--){ + var node = this[i]; + if(node.localName == localName && node.namespaceURI == namespaceURI){ + return node; + } + } + return null; + } +}; +/** + * @see http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-102161490 + */ +function DOMImplementation(/* Object */ features) { + this._features = {}; + if (features) { + for (var feature in features) { + this._features = features[feature]; + } + } +}; + +DOMImplementation.prototype = { + hasFeature: function(/* string */ feature, /* string */ version) { + var versions = this._features[feature.toLowerCase()]; + if (versions && (!version || version in versions)) { + return true; + } else { + return false; + } + }, + // Introduced in DOM Level 2: + createDocument:function(namespaceURI, qualifiedName, doctype){// raises:INVALID_CHARACTER_ERR,NAMESPACE_ERR,WRONG_DOCUMENT_ERR + var doc = new Document(); + doc.implementation = this; + doc.childNodes = new NodeList(); + doc.doctype = doctype; + if(doctype){ + doc.appendChild(doctype); + } + if(qualifiedName){ + var root = doc.createElementNS(namespaceURI,qualifiedName); + doc.appendChild(root); + } + return doc; + }, + // Introduced in DOM Level 2: + createDocumentType:function(qualifiedName, publicId, systemId){// raises:INVALID_CHARACTER_ERR,NAMESPACE_ERR + var node = new DocumentType(); + node.name = qualifiedName; + node.nodeName = qualifiedName; + node.publicId = publicId; + node.systemId = systemId; + // Introduced in DOM Level 2: + //readonly attribute DOMString internalSubset; + + //TODO:.. + // readonly attribute NamedNodeMap entities; + // readonly attribute NamedNodeMap notations; + return node; + } +}; + + +/** + * @see http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 + */ + +function Node() { +}; + +Node.prototype = { + firstChild : null, + lastChild : null, + previousSibling : null, + nextSibling : null, + attributes : null, + parentNode : null, + childNodes : null, + ownerDocument : null, + nodeValue : null, + namespaceURI : null, + prefix : null, + localName : null, + // Modified in DOM Level 2: + insertBefore:function(newChild, refChild){//raises + return _insertBefore(this,newChild,refChild); + }, + replaceChild:function(newChild, oldChild){//raises + this.insertBefore(newChild,oldChild); + if(oldChild){ + this.removeChild(oldChild); + } + }, + removeChild:function(oldChild){ + return _removeChild(this,oldChild); + }, + appendChild:function(newChild){ + return this.insertBefore(newChild,null); + }, + hasChildNodes:function(){ + return this.firstChild != null; + }, + cloneNode:function(deep){ + return cloneNode(this.ownerDocument||this,this,deep); + }, + // Modified in DOM Level 2: + normalize:function(){ + var child = this.firstChild; + while(child){ + var next = child.nextSibling; + if(next && next.nodeType == TEXT_NODE && child.nodeType == TEXT_NODE){ + this.removeChild(next); + child.appendData(next.data); + }else{ + child.normalize(); + child = next; + } + } + }, + // Introduced in DOM Level 2: + isSupported:function(feature, version){ + return this.ownerDocument.implementation.hasFeature(feature,version); + }, + // Introduced in DOM Level 2: + hasAttributes:function(){ + return this.attributes.length>0; + }, + lookupPrefix:function(namespaceURI){ + var el = this; + while(el){ + var map = el._nsMap; + //console.dir(map) + if(map){ + for(var n in map){ + if(map[n] == namespaceURI){ + return n; + } + } + } + el = el.nodeType == ATTRIBUTE_NODE?el.ownerDocument : el.parentNode; + } + return null; + }, + // Introduced in DOM Level 3: + lookupNamespaceURI:function(prefix){ + var el = this; + while(el){ + var map = el._nsMap; + //console.dir(map) + if(map){ + if(prefix in map){ + return map[prefix] ; + } + } + el = el.nodeType == ATTRIBUTE_NODE?el.ownerDocument : el.parentNode; + } + return null; + }, + // Introduced in DOM Level 3: + isDefaultNamespace:function(namespaceURI){ + var prefix = this.lookupPrefix(namespaceURI); + return prefix == null; + } +}; + + +function _xmlEncoder(c){ + return c == '<' && '<' || + c == '>' && '>' || + c == '&' && '&' || + c == '"' && '"' || + '&#'+c.charCodeAt()+';' +} + + +copy(NodeType,Node); +copy(NodeType,Node.prototype); + +/** + * @param callback return true for continue,false for break + * @return boolean true: break visit; + */ +function _visitNode(node,callback){ + if(callback(node)){ + return true; + } + if(node = node.firstChild){ + do{ + if(_visitNode(node,callback)){return true} + }while(node=node.nextSibling) + } +} + + + +function Document(){ +} +function _onAddAttribute(doc,el,newAttr){ + doc && doc._inc++; + var ns = newAttr.namespaceURI ; + if(ns == 'http://www.w3.org/2000/xmlns/'){ + //update namespace + el._nsMap[newAttr.prefix?newAttr.localName:''] = newAttr.value + } +} +function _onRemoveAttribute(doc,el,newAttr,remove){ + doc && doc._inc++; + var ns = newAttr.namespaceURI ; + if(ns == 'http://www.w3.org/2000/xmlns/'){ + //update namespace + delete el._nsMap[newAttr.prefix?newAttr.localName:''] + } +} +function _onUpdateChild(doc,el,newChild){ + if(doc && doc._inc){ + doc._inc++; + //update childNodes + var cs = el.childNodes; + if(newChild){ + cs[cs.length++] = newChild; + }else{ + //console.log(1) + var child = el.firstChild; + var i = 0; + while(child){ + cs[i++] = child; + child =child.nextSibling; + } + cs.length = i; + } + } +} + +/** + * attributes; + * children; + * + * writeable properties: + * nodeValue,Attr:value,CharacterData:data + * prefix + */ +function _removeChild(parentNode,child){ + var previous = child.previousSibling; + var next = child.nextSibling; + if(previous){ + previous.nextSibling = next; + }else{ + parentNode.firstChild = next + } + if(next){ + next.previousSibling = previous; + }else{ + parentNode.lastChild = previous; + } + _onUpdateChild(parentNode.ownerDocument,parentNode); + return child; +} +/** + * preformance key(refChild == null) + */ +function _insertBefore(parentNode,newChild,nextChild){ + var cp = newChild.parentNode; + if(cp){ + cp.removeChild(newChild);//remove and update + } + if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){ + var newFirst = newChild.firstChild; + if (newFirst == null) { + return newChild; + } + var newLast = newChild.lastChild; + }else{ + newFirst = newLast = newChild; + } + var pre = nextChild ? nextChild.previousSibling : parentNode.lastChild; + + newFirst.previousSibling = pre; + newLast.nextSibling = nextChild; + + + if(pre){ + pre.nextSibling = newFirst; + }else{ + parentNode.firstChild = newFirst; + } + if(nextChild == null){ + parentNode.lastChild = newLast; + }else{ + nextChild.previousSibling = newLast; + } + do{ + newFirst.parentNode = parentNode; + }while(newFirst !== newLast && (newFirst= newFirst.nextSibling)) + _onUpdateChild(parentNode.ownerDocument||parentNode,parentNode); + //console.log(parentNode.lastChild.nextSibling == null) + if (newChild.nodeType == DOCUMENT_FRAGMENT_NODE) { + newChild.firstChild = newChild.lastChild = null; + } + return newChild; +} +function _appendSingleChild(parentNode,newChild){ + var cp = newChild.parentNode; + if(cp){ + var pre = parentNode.lastChild; + cp.removeChild(newChild);//remove and update + var pre = parentNode.lastChild; + } + var pre = parentNode.lastChild; + newChild.parentNode = parentNode; + newChild.previousSibling = pre; + newChild.nextSibling = null; + if(pre){ + pre.nextSibling = newChild; + }else{ + parentNode.firstChild = newChild; + } + parentNode.lastChild = newChild; + _onUpdateChild(parentNode.ownerDocument,parentNode,newChild); + return newChild; + //console.log("__aa",parentNode.lastChild.nextSibling == null) +} +Document.prototype = { + //implementation : null, + nodeName : '#document', + nodeType : DOCUMENT_NODE, + doctype : null, + documentElement : null, + _inc : 1, + + insertBefore : function(newChild, refChild){//raises + if(newChild.nodeType == DOCUMENT_FRAGMENT_NODE){ + var child = newChild.firstChild; + while(child){ + var next = child.nextSibling; + this.insertBefore(child,refChild); + child = next; + } + return newChild; + } + if(this.documentElement == null && newChild.nodeType == ELEMENT_NODE){ + this.documentElement = newChild; + } + + return _insertBefore(this,newChild,refChild),(newChild.ownerDocument = this),newChild; + }, + removeChild : function(oldChild){ + if(this.documentElement == oldChild){ + this.documentElement = null; + } + return _removeChild(this,oldChild); + }, + // Introduced in DOM Level 2: + importNode : function(importedNode,deep){ + return importNode(this,importedNode,deep); + }, + // Introduced in DOM Level 2: + getElementById : function(id){ + var rtv = null; + _visitNode(this.documentElement,function(node){ + if(node.nodeType == ELEMENT_NODE){ + if(node.getAttribute('id') == id){ + rtv = node; + return true; + } + } + }) + return rtv; + }, + + //document factory method: + createElement : function(tagName){ + var node = new Element(); + node.ownerDocument = this; + node.nodeName = tagName; + node.tagName = tagName; + node.childNodes = new NodeList(); + var attrs = node.attributes = new NamedNodeMap(); + attrs._ownerElement = node; + return node; + }, + createDocumentFragment : function(){ + var node = new DocumentFragment(); + node.ownerDocument = this; + node.childNodes = new NodeList(); + return node; + }, + createTextNode : function(data){ + var node = new Text(); + node.ownerDocument = this; + node.appendData(data) + return node; + }, + createComment : function(data){ + var node = new Comment(); + node.ownerDocument = this; + node.appendData(data) + return node; + }, + createCDATASection : function(data){ + var node = new CDATASection(); + node.ownerDocument = this; + node.appendData(data) + return node; + }, + createProcessingInstruction : function(target,data){ + var node = new ProcessingInstruction(); + node.ownerDocument = this; + node.tagName = node.target = target; + node.nodeValue= node.data = data; + return node; + }, + createAttribute : function(name){ + var node = new Attr(); + node.ownerDocument = this; + node.name = name; + node.nodeName = name; + node.localName = name; + node.specified = true; + return node; + }, + createEntityReference : function(name){ + var node = new EntityReference(); + node.ownerDocument = this; + node.nodeName = name; + return node; + }, + // Introduced in DOM Level 2: + createElementNS : function(namespaceURI,qualifiedName){ + var node = new Element(); + var pl = qualifiedName.split(':'); + var attrs = node.attributes = new NamedNodeMap(); + node.childNodes = new NodeList(); + node.ownerDocument = this; + node.nodeName = qualifiedName; + node.tagName = qualifiedName; + node.namespaceURI = namespaceURI; + if(pl.length == 2){ + node.prefix = pl[0]; + node.localName = pl[1]; + }else{ + //el.prefix = null; + node.localName = qualifiedName; + } + attrs._ownerElement = node; + return node; + }, + // Introduced in DOM Level 2: + createAttributeNS : function(namespaceURI,qualifiedName){ + var node = new Attr(); + var pl = qualifiedName.split(':'); + node.ownerDocument = this; + node.nodeName = qualifiedName; + node.name = qualifiedName; + node.namespaceURI = namespaceURI; + node.specified = true; + if(pl.length == 2){ + node.prefix = pl[0]; + node.localName = pl[1]; + }else{ + //el.prefix = null; + node.localName = qualifiedName; + } + return node; + } +}; +_extends(Document,Node); + + +function Element() { + this._nsMap = {}; +}; +Element.prototype = { + nodeType : ELEMENT_NODE, + hasAttribute : function(name){ + return this.getAttributeNode(name)!=null; + }, + getAttribute : function(name){ + var attr = this.getAttributeNode(name); + return attr && attr.value || ''; + }, + getAttributeNode : function(name){ + return this.attributes.getNamedItem(name); + }, + setAttribute : function(name, value){ + var attr = this.ownerDocument.createAttribute(name); + attr.value = attr.nodeValue = "" + value; + this.setAttributeNode(attr) + }, + removeAttribute : function(name){ + var attr = this.getAttributeNode(name) + attr && this.removeAttributeNode(attr); + }, + + //four real opeartion method + appendChild:function(newChild){ + if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){ + return this.insertBefore(newChild,null); + }else{ + return _appendSingleChild(this,newChild); + } + }, + setAttributeNode : function(newAttr){ + return this.attributes.setNamedItem(newAttr); + }, + setAttributeNodeNS : function(newAttr){ + return this.attributes.setNamedItemNS(newAttr); + }, + removeAttributeNode : function(oldAttr){ + //console.log(this == oldAttr.ownerElement) + return this.attributes.removeNamedItem(oldAttr.nodeName); + }, + //get real attribute name,and remove it by removeAttributeNode + removeAttributeNS : function(namespaceURI, localName){ + var old = this.getAttributeNodeNS(namespaceURI, localName); + old && this.removeAttributeNode(old); + }, + + hasAttributeNS : function(namespaceURI, localName){ + return this.getAttributeNodeNS(namespaceURI, localName)!=null; + }, + getAttributeNS : function(namespaceURI, localName){ + var attr = this.getAttributeNodeNS(namespaceURI, localName); + return attr && attr.value || ''; + }, + setAttributeNS : function(namespaceURI, qualifiedName, value){ + var attr = this.ownerDocument.createAttributeNS(namespaceURI, qualifiedName); + attr.value = attr.nodeValue = "" + value; + this.setAttributeNode(attr) + }, + getAttributeNodeNS : function(namespaceURI, localName){ + return this.attributes.getNamedItemNS(namespaceURI, localName); + }, + + getElementsByTagName : function(tagName){ + return new LiveNodeList(this,function(base){ + var ls = []; + _visitNode(base,function(node){ + if(node !== base && node.nodeType == ELEMENT_NODE && (tagName === '*' || node.tagName == tagName)){ + ls.push(node); + } + }); + return ls; + }); + }, + getElementsByTagNameNS : function(namespaceURI, localName){ + return new LiveNodeList(this,function(base){ + var ls = []; + _visitNode(base,function(node){ + if(node !== base && node.nodeType === ELEMENT_NODE && (namespaceURI === '*' || node.namespaceURI === namespaceURI) && (localName === '*' || node.localName == localName)){ + ls.push(node); + } + }); + return ls; + + }); + } +}; +Document.prototype.getElementsByTagName = Element.prototype.getElementsByTagName; +Document.prototype.getElementsByTagNameNS = Element.prototype.getElementsByTagNameNS; + + +_extends(Element,Node); +function Attr() { +}; +Attr.prototype.nodeType = ATTRIBUTE_NODE; +_extends(Attr,Node); + + +function CharacterData() { +}; +CharacterData.prototype = { + data : '', + substringData : function(offset, count) { + return this.data.substring(offset, offset+count); + }, + appendData: function(text) { + text = this.data+text; + this.nodeValue = this.data = text; + this.length = text.length; + }, + insertData: function(offset,text) { + this.replaceData(offset,0,text); + + }, + appendChild:function(newChild){ + throw new Error(ExceptionMessage[HIERARCHY_REQUEST_ERR]) + }, + deleteData: function(offset, count) { + this.replaceData(offset,count,""); + }, + replaceData: function(offset, count, text) { + var start = this.data.substring(0,offset); + var end = this.data.substring(offset+count); + text = start + text + end; + this.nodeValue = this.data = text; + this.length = text.length; + } +} +_extends(CharacterData,Node); +function Text() { +}; +Text.prototype = { + nodeName : "#text", + nodeType : TEXT_NODE, + splitText : function(offset) { + var text = this.data; + var newText = text.substring(offset); + text = text.substring(0, offset); + this.data = this.nodeValue = text; + this.length = text.length; + var newNode = this.ownerDocument.createTextNode(newText); + if(this.parentNode){ + this.parentNode.insertBefore(newNode, this.nextSibling); + } + return newNode; + } +} +_extends(Text,CharacterData); +function Comment() { +}; +Comment.prototype = { + nodeName : "#comment", + nodeType : COMMENT_NODE +} +_extends(Comment,CharacterData); + +function CDATASection() { +}; +CDATASection.prototype = { + nodeName : "#cdata-section", + nodeType : CDATA_SECTION_NODE +} +_extends(CDATASection,CharacterData); + + +function DocumentType() { +}; +DocumentType.prototype.nodeType = DOCUMENT_TYPE_NODE; +_extends(DocumentType,Node); + +function Notation() { +}; +Notation.prototype.nodeType = NOTATION_NODE; +_extends(Notation,Node); + +function Entity() { +}; +Entity.prototype.nodeType = ENTITY_NODE; +_extends(Entity,Node); + +function EntityReference() { +}; +EntityReference.prototype.nodeType = ENTITY_REFERENCE_NODE; +_extends(EntityReference,Node); + +function DocumentFragment() { +}; +DocumentFragment.prototype.nodeName = "#document-fragment"; +DocumentFragment.prototype.nodeType = DOCUMENT_FRAGMENT_NODE; +_extends(DocumentFragment,Node); + + +function ProcessingInstruction() { +} +ProcessingInstruction.prototype.nodeType = PROCESSING_INSTRUCTION_NODE; +_extends(ProcessingInstruction,Node); +function XMLSerializer(){} +XMLSerializer.prototype.serializeToString = function(node,isHtml,nodeFilter){ + return nodeSerializeToString.call(node,isHtml,nodeFilter); +} +Node.prototype.toString = nodeSerializeToString; +function nodeSerializeToString(isHtml,nodeFilter){ + var buf = []; + var refNode = this.nodeType == 9?this.documentElement:this; + var prefix = refNode.prefix; + var uri = refNode.namespaceURI; + + if(uri && prefix == null){ + //console.log(prefix) + var prefix = refNode.lookupPrefix(uri); + if(prefix == null){ + //isHTML = true; + var visibleNamespaces=[ + {namespace:uri,prefix:null} + //{namespace:uri,prefix:''} + ] + } + } + serializeToString(this,buf,isHtml,nodeFilter,visibleNamespaces); + //console.log('###',this.nodeType,uri,prefix,buf.join('')) + return buf.join(''); +} +function needNamespaceDefine(node,isHTML, visibleNamespaces) { + var prefix = node.prefix||''; + var uri = node.namespaceURI; + if (!prefix && !uri){ + return false; + } + if (prefix === "xml" && uri === "http://www.w3.org/XML/1998/namespace" + || uri == 'http://www.w3.org/2000/xmlns/'){ + return false; + } + + var i = visibleNamespaces.length + //console.log('@@@@',node.tagName,prefix,uri,visibleNamespaces) + while (i--) { + var ns = visibleNamespaces[i]; + // get namespace prefix + //console.log(node.nodeType,node.tagName,ns.prefix,prefix) + if (ns.prefix == prefix){ + return ns.namespace != uri; + } + } + //console.log(isHTML,uri,prefix=='') + //if(isHTML && prefix ==null && uri == 'http://www.w3.org/1999/xhtml'){ + // return false; + //} + //node.flag = '11111' + //console.error(3,true,node.flag,node.prefix,node.namespaceURI) + return true; +} +function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){ + if(nodeFilter){ + node = nodeFilter(node); + if(node){ + if(typeof node == 'string'){ + buf.push(node); + return; + } + }else{ + return; + } + //buf.sort.apply(attrs, attributeSorter); + } + switch(node.nodeType){ + case ELEMENT_NODE: + if (!visibleNamespaces) visibleNamespaces = []; + var startVisibleNamespaces = visibleNamespaces.length; + var attrs = node.attributes; + var len = attrs.length; + var child = node.firstChild; + var nodeName = node.tagName; + + isHTML = (htmlns === node.namespaceURI) ||isHTML + buf.push('<',nodeName); + + + + for(var i=0;i'); + //if is cdata child node + if(isHTML && /^script$/i.test(nodeName)){ + while(child){ + if(child.data){ + buf.push(child.data); + }else{ + serializeToString(child,buf,isHTML,nodeFilter,visibleNamespaces); + } + child = child.nextSibling; + } + }else + { + while(child){ + serializeToString(child,buf,isHTML,nodeFilter,visibleNamespaces); + child = child.nextSibling; + } + } + buf.push(''); + }else{ + buf.push('/>'); + } + // remove added visible namespaces + //visibleNamespaces.length = startVisibleNamespaces; + return; + case DOCUMENT_NODE: + case DOCUMENT_FRAGMENT_NODE: + var child = node.firstChild; + while(child){ + serializeToString(child,buf,isHTML,nodeFilter,visibleNamespaces); + child = child.nextSibling; + } + return; + case ATTRIBUTE_NODE: + return buf.push(' ',node.name,'="',node.value.replace(/[<&"]/g,_xmlEncoder),'"'); + case TEXT_NODE: + return buf.push(node.data.replace(/[<&]/g,_xmlEncoder)); + case CDATA_SECTION_NODE: + return buf.push( ''); + case COMMENT_NODE: + return buf.push( ""); + case DOCUMENT_TYPE_NODE: + var pubid = node.publicId; + var sysid = node.systemId; + buf.push(''); + }else if(sysid && sysid!='.'){ + buf.push(' SYSTEM "',sysid,'">'); + }else{ + var sub = node.internalSubset; + if(sub){ + buf.push(" [",sub,"]"); + } + buf.push(">"); + } + return; + case PROCESSING_INSTRUCTION_NODE: + return buf.push( ""); + case ENTITY_REFERENCE_NODE: + return buf.push( '&',node.nodeName,';'); + //case ENTITY_NODE: + //case NOTATION_NODE: + default: + buf.push('??',node.nodeName); + } +} +function importNode(doc,node,deep){ + var node2; + switch (node.nodeType) { + case ELEMENT_NODE: + node2 = node.cloneNode(false); + node2.ownerDocument = doc; + //var attrs = node2.attributes; + //var len = attrs.length; + //for(var i=0;i 0) && (this.settings.height === 0 || this.settings.height > 0)) { + // viewport = "width="+this.settings.width+", height="+this.settings.height+""; + } + + properties = { + layout: layout, + spread: spread, + orientation: orientation, + flow: flow, + viewport: viewport, + minSpreadWidth: minSpreadWidth, + direction: direction + }; + + return properties; + } + + /** + * Adjust the flow of the rendition to paginated or scrolled + * (scrolled-continuous vs scrolled-doc are handled by different view managers) + * @param {string} flow + */ + + }, { + key: "flow", + value: function flow(_flow2) { + var _flow = _flow2; + if (_flow2 === "scrolled" || _flow2 === "scrolled-doc" || _flow2 === "scrolled-continuous") { + _flow = "scrolled"; + } + + if (_flow2 === "auto" || _flow2 === "paginated") { + _flow = "paginated"; + } + + this.settings.flow = _flow2; + + if (this._layout) { + this._layout.flow(_flow); + } + + if (this.manager && this._layout) { + this.manager.applyLayout(this._layout); + } + + if (this.manager) { + this.manager.updateFlow(_flow); + } + + if (this.manager && this.manager.isRendered() && this.location) { + this.manager.clear(); + this.display(this.location.start.cfi); + } + } + + /** + * Adjust the layout of the rendition to reflowable or pre-paginated + * @param {object} settings + */ + + }, { + key: "layout", + value: function layout(settings) { + var _this4 = this; + + if (settings) { + this._layout = new _layout2.default(settings); + this._layout.spread(settings.spread, this.settings.minSpreadWidth); + + // this.mapping = new Mapping(this._layout.props); + + this._layout.on(_constants.EVENTS.LAYOUT.UPDATED, function (props, changed) { + _this4.emit(_constants.EVENTS.RENDITION.LAYOUT, props, changed); + }); + } + + if (this.manager && this._layout) { + this.manager.applyLayout(this._layout); + } + + return this._layout; + } + + /** + * Adjust if the rendition uses spreads + * @param {string} spread none | auto (TODO: implement landscape, portrait, both) + * @param {int} [min] min width to use spreads at + */ + + }, { + key: "spread", + value: function spread(_spread, min) { + + this.settings.spread = _spread; + + if (min) { + this.settings.minSpreadWidth = min; + } + + if (this._layout) { + this._layout.spread(_spread, min); + } + + if (this.manager && this.manager.isRendered()) { + this.manager.updateLayout(); + } + } + + /** + * Adjust the direction of the rendition + * @param {string} dir + */ + + }, { + key: "direction", + value: function direction(dir) { + + this.settings.direction = dir || "ltr"; + + if (this.manager) { + this.manager.direction(this.settings.direction); + } + + if (this.manager && this.manager.isRendered() && this.location) { + this.manager.clear(); + this.display(this.location.start.cfi); + } + } + + /** + * Report the current location + * @fires relocated + * @fires locationChanged + */ + + }, { + key: "reportLocation", + value: function reportLocation() { + return this.q.enqueue(function reportedLocation() { + requestAnimationFrame(function reportedLocationAfterRAF() { + var location = this.manager.currentLocation(); + if (location && location.then && typeof location.then === "function") { + location.then(function (result) { + var located = this.located(result); + + if (!located || !located.start || !located.end) { + return; + } + + this.location = located; + + this.emit(_constants.EVENTS.RENDITION.LOCATION_CHANGED, { + index: this.location.start.index, + href: this.location.start.href, + start: this.location.start.cfi, + end: this.location.end.cfi, + percentage: this.location.start.percentage + }); + + this.emit(_constants.EVENTS.RENDITION.RELOCATED, this.location); + }.bind(this)); + } else if (location) { + var located = this.located(location); + + if (!located || !located.start || !located.end) { + return; + } + + this.location = located; + + /** + * @event locationChanged + * @deprecated + * @type {object} + * @property {number} index + * @property {string} href + * @property {EpubCFI} start + * @property {EpubCFI} end + * @property {number} percentage + * @memberof Rendition + */ + this.emit(_constants.EVENTS.RENDITION.LOCATION_CHANGED, { + index: this.location.start.index, + href: this.location.start.href, + start: this.location.start.cfi, + end: this.location.end.cfi, + percentage: this.location.start.percentage + }); + + /** + * @event relocated + * @type {displayedLocation} + * @memberof Rendition + */ + this.emit(_constants.EVENTS.RENDITION.RELOCATED, this.location); + } + }.bind(this)); + }.bind(this)); + } + + /** + * Get the Current Location object + * @return {displayedLocation | promise} location (may be a promise) + */ + + }, { + key: "currentLocation", + value: function currentLocation() { + var location = this.manager.currentLocation(); + if (location && location.then && typeof location.then === "function") { + location.then(function (result) { + var located = this.located(result); + return located; + }.bind(this)); + } else if (location) { + var located = this.located(location); + return located; + } + } + + /** + * Creates a Rendition#locationRange from location + * passed by the Manager + * @returns {displayedLocation} + * @private + */ + + }, { + key: "located", + value: function located(location) { + if (!location.length) { + return {}; + } + var start = location[0]; + var end = location[location.length - 1]; + + var located = { + start: { + index: start.index, + href: start.href, + cfi: start.mapping.start, + displayed: { + page: start.pages[0] || 1, + total: start.totalPages + } + }, + end: { + index: end.index, + href: end.href, + cfi: end.mapping.end, + displayed: { + page: end.pages[end.pages.length - 1] || 1, + total: end.totalPages + } + } + }; + + var locationStart = this.book.locations.locationFromCfi(start.mapping.start); + var locationEnd = this.book.locations.locationFromCfi(end.mapping.end); + + if (locationStart != null) { + located.start.location = locationStart; + located.start.percentage = this.book.locations.percentageFromLocation(locationStart); + } + if (locationEnd != null) { + located.end.location = locationEnd; + located.end.percentage = this.book.locations.percentageFromLocation(locationEnd); + } + + var pageStart = this.book.pageList.pageFromCfi(start.mapping.start); + var pageEnd = this.book.pageList.pageFromCfi(end.mapping.end); + + if (pageStart != -1) { + located.start.page = pageStart; + } + if (pageEnd != -1) { + located.end.page = pageEnd; + } + + if (end.index === this.book.spine.last().index && located.end.displayed.page >= located.end.displayed.total) { + located.atEnd = true; + } + + if (start.index === this.book.spine.first().index && located.start.displayed.page === 1) { + located.atStart = true; + } + + return located; + } + + /** + * Remove and Clean Up the Rendition + */ + + }, { + key: "destroy", + value: function destroy() { + // Clear the queue + // this.q.clear(); + // this.q = undefined; + + this.manager && this.manager.destroy(); + + this.book = undefined; + + // this.views = null; + + // this.hooks.display.clear(); + // this.hooks.serialize.clear(); + // this.hooks.content.clear(); + // this.hooks.layout.clear(); + // this.hooks.render.clear(); + // this.hooks.show.clear(); + // this.hooks = {}; + + // this.themes.destroy(); + // this.themes = undefined; + + // this.epubcfi = undefined; + + // this.starting = undefined; + // this.started = undefined; + + } + + /** + * Pass the events from a view's Contents + * @private + * @param {Contents} view contents + */ + + }, { + key: "passEvents", + value: function passEvents(contents) { + var _this5 = this; + + _constants.DOM_EVENTS.forEach(function (e) { + contents.on(e, function (ev) { + return _this5.triggerViewEvent(ev, contents); + }); + }); + + contents.on(_constants.EVENTS.CONTENTS.SELECTED, function (e) { + return _this5.triggerSelectedEvent(e, contents); + }); + } + + /** + * Emit events passed by a view + * @private + * @param {event} e + */ + + }, { + key: "triggerViewEvent", + value: function triggerViewEvent(e, contents) { + this.emit(e.type, e, contents); + } + + /** + * Emit a selection event's CFI Range passed from a a view + * @private + * @param {EpubCFI} cfirange + */ + + }, { + key: "triggerSelectedEvent", + value: function triggerSelectedEvent(cfirange, contents) { + /** + * Emit that a text selection has occured + * @event selected + * @param {EpubCFI} cfirange + * @param {Contents} contents + * @memberof Rendition + */ + this.emit(_constants.EVENTS.RENDITION.SELECTED, cfirange, contents); + } + + /** + * Emit a markClicked event with the cfiRange and data from a mark + * @private + * @param {EpubCFI} cfirange + */ + + }, { + key: "triggerMarkEvent", + value: function triggerMarkEvent(cfiRange, data, contents) { + /** + * Emit that a mark was clicked + * @event markClicked + * @param {EpubCFI} cfirange + * @param {object} data + * @param {Contents} contents + * @memberof Rendition + */ + this.emit(_constants.EVENTS.RENDITION.MARK_CLICKED, cfiRange, data, contents); + } + + /** + * Get a Range from a Visible CFI + * @param {string} cfi EpubCfi String + * @param {string} ignoreClass + * @return {range} + */ + + }, { + key: "getRange", + value: function getRange(cfi, ignoreClass) { + var _cfi = new _epubcfi2.default(cfi); + var found = this.manager.visible().filter(function (view) { + if (_cfi.spinePos === view.index) return true; + }); + + // Should only every return 1 item + if (found.length) { + return found[0].contents.range(_cfi, ignoreClass); + } + } + + /** + * Hook to adjust images to fit in columns + * @param {Contents} contents + * @private + */ + + }, { + key: "adjustImages", + value: function adjustImages(contents) { + + if (this._layout.name === "pre-paginated") { + return new Promise(function (resolve) { + resolve(); + }); + } + + var computed = contents.window.getComputedStyle(contents.content, null); + var height = (contents.content.offsetHeight - (parseFloat(computed.paddingTop) + parseFloat(computed.paddingBottom))) * .95; + var verticalPadding = parseFloat(computed.verticalPadding); + + contents.addStylesheetRules({ + "img": { + "max-width": (this._layout.columnWidth ? this._layout.columnWidth - verticalPadding + "px" : "100%") + "!important", + "max-height": height + "px" + "!important", + "object-fit": "contain", + "page-break-inside": "avoid", + "break-inside": "avoid", + "box-sizing": "border-box" + }, + "svg": { + "max-width": (this._layout.columnWidth ? this._layout.columnWidth - verticalPadding + "px" : "100%") + "!important", + "max-height": height + "px" + "!important", + "page-break-inside": "avoid", + "break-inside": "avoid" + } + }); + + return new Promise(function (resolve, reject) { + // Wait to apply + setTimeout(function () { + resolve(); + }, 1); + }); + } + + /** + * Get the Contents object of each rendered view + * @returns {Contents[]} + */ + + }, { + key: "getContents", + value: function getContents() { + return this.manager ? this.manager.getContents() : []; + } + + /** + * Get the views member from the manager + * @returns {Views} + */ + + }, { + key: "views", + value: function views() { + var views = this.manager ? this.manager.views : undefined; + return views || []; + } + + /** + * Hook to handle link clicks in rendered content + * @param {Contents} contents + * @private + */ + + }, { + key: "handleLinks", + value: function handleLinks(contents) { + var _this6 = this; + + if (contents) { + contents.on(_constants.EVENTS.CONTENTS.LINK_CLICKED, function (href) { + var relative = _this6.book.path.relative(href); + _this6.display(relative); + }); + } + } + + /** + * Hook to handle injecting stylesheet before + * a Section is serialized + * @param {document} doc + * @param {Section} section + * @private + */ + + }, { + key: "injectStylesheet", + value: function injectStylesheet(doc, section) { + var style = doc.createElement("link"); + style.setAttribute("type", "text/css"); + style.setAttribute("rel", "stylesheet"); + style.setAttribute("href", this.settings.stylesheet); + doc.getElementsByTagName("head")[0].appendChild(style); + } + + /** + * Hook to handle injecting scripts before + * a Section is serialized + * @param {document} doc + * @param {Section} section + * @private + */ + + }, { + key: "injectScript", + value: function injectScript(doc, section) { + var script = doc.createElement("script"); + script.setAttribute("type", "text/javascript"); + script.setAttribute("src", this.settings.script); + script.textContent = " "; // Needed to prevent self closing tag + doc.getElementsByTagName("head")[0].appendChild(script); + } + + /** + * Hook to handle the document identifier before + * a Section is serialized + * @param {document} doc + * @param {Section} section + * @private + */ + + }, { + key: "injectIdentifier", + value: function injectIdentifier(doc, section) { + var ident = this.book.packaging.metadata.identifier; + var meta = doc.createElement("meta"); + meta.setAttribute("name", "dc.relation.ispartof"); + if (ident) { + meta.setAttribute("content", ident); + } + doc.getElementsByTagName("head")[0].appendChild(meta); + } + }]); + + return Rendition; +}(); + +//-- Enable binding events to Renderer + + +(0, _eventEmitter2.default)(Rendition.prototype); + +exports.default = Rendition; +module.exports = exports["default"]; + +/***/ }), +/* 19 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _epubcfi = __webpack_require__(1); + +var _epubcfi2 = _interopRequireDefault(_epubcfi); + +var _core = __webpack_require__(0); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * Map text locations to CFI ranges + * @class + * @param {Layout} layout Layout to apply + * @param {string} [direction="ltr"] Text direction + * @param {string} [axis="horizontal"] vertical or horizontal axis + * @param {boolean} [dev] toggle developer highlighting + */ +var Mapping = function () { + function Mapping(layout, direction, axis, dev) { + _classCallCheck(this, Mapping); + + this.layout = layout; + this.horizontal = axis === "horizontal" ? true : false; + this.direction = direction || "ltr"; + this._dev = dev; + } + + /** + * Find CFI pairs for entire section at once + */ + + + _createClass(Mapping, [{ + key: "section", + value: function section(view) { + var ranges = this.findRanges(view); + var map = this.rangeListToCfiList(view.section.cfiBase, ranges); + + return map; + } + + /** + * Find CFI pairs for a page + * @param {Contents} contents Contents from view + * @param {string} cfiBase string of the base for a cfi + * @param {number} start position to start at + * @param {number} end position to end at + */ + + }, { + key: "page", + value: function page(contents, cfiBase, start, end) { + var root = contents && contents.document ? contents.document.body : false; + var result; + + if (!root) { + return; + } + + result = this.rangePairToCfiPair(cfiBase, { + start: this.findStart(root, start, end), + end: this.findEnd(root, start, end) + }); + + if (this._dev === true) { + var doc = contents.document; + var startRange = new _epubcfi2.default(result.start).toRange(doc); + var endRange = new _epubcfi2.default(result.end).toRange(doc); + + var selection = doc.defaultView.getSelection(); + var r = doc.createRange(); + selection.removeAllRanges(); + r.setStart(startRange.startContainer, startRange.startOffset); + r.setEnd(endRange.endContainer, endRange.endOffset); + selection.addRange(r); + } + + return result; + } + + /** + * Walk a node, preforming a function on each node it finds + * @private + * @param {Node} root Node to walkToNode + * @param {function} func walk function + * @return {*} returns the result of the walk function + */ + + }, { + key: "walk", + value: function walk(root, func) { + // IE11 has strange issue, if root is text node IE throws exception on + // calling treeWalker.nextNode(), saying + // Unexpected call to method or property access instead of returing null value + if (root && root.nodeType === Node.TEXT_NODE) { + return; + } + // safeFilter is required so that it can work in IE as filter is a function for IE + // and for other browser filter is an object. + var filter = { + acceptNode: function acceptNode(node) { + if (node.data.trim().length > 0) { + return NodeFilter.FILTER_ACCEPT; + } else { + return NodeFilter.FILTER_REJECT; + } + } + }; + var safeFilter = filter.acceptNode; + safeFilter.acceptNode = filter.acceptNode; + + var treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, safeFilter, false); + var node; + var result; + while (node = treeWalker.nextNode()) { + result = func(node); + if (result) break; + } + + return result; + } + }, { + key: "findRanges", + value: function findRanges(view) { + var columns = []; + var scrollWidth = view.contents.scrollWidth(); + var spreads = Math.ceil(scrollWidth / this.layout.spreadWidth); + var count = spreads * this.layout.divisor; + var columnWidth = this.layout.columnWidth; + var gap = this.layout.gap; + var start, end; + + for (var i = 0; i < count.pages; i++) { + start = (columnWidth + gap) * i; + end = columnWidth * (i + 1) + gap * i; + columns.push({ + start: this.findStart(view.document.body, start, end), + end: this.findEnd(view.document.body, start, end) + }); + } + + return columns; + } + + /** + * Find Start Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + + }, { + key: "findStart", + value: function findStart(root, start, end) { + var _this = this; + + var stack = [root]; + var $el; + var found; + var $prev = root; + + while (stack.length) { + + $el = stack.shift(); + + found = this.walk($el, function (node) { + var left, right, top, bottom; + var elPos; + var elRange; + + elPos = (0, _core.nodeBounds)(node); + + if (_this.horizontal && _this.direction === "ltr") { + + left = _this.horizontal ? elPos.left : elPos.top; + right = _this.horizontal ? elPos.right : elPos.bottom; + + if (left >= start && left <= end) { + return node; + } else if (right > start) { + return node; + } else { + $prev = node; + stack.push(node); + } + } else if (_this.horizontal && _this.direction === "rtl") { + + left = elPos.left; + right = elPos.right; + + if (right <= end && right >= start) { + return node; + } else if (left < end) { + return node; + } else { + $prev = node; + stack.push(node); + } + } else { + + top = elPos.top; + bottom = elPos.bottom; + + if (top >= start && top <= end) { + return node; + } else if (bottom > start) { + return node; + } else { + $prev = node; + stack.push(node); + } + } + }); + + if (found) { + return this.findTextStartRange(found, start, end); + } + } + + // Return last element + return this.findTextStartRange($prev, start, end); + } + + /** + * Find End Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + + }, { + key: "findEnd", + value: function findEnd(root, start, end) { + var _this2 = this; + + var stack = [root]; + var $el; + var $prev = root; + var found; + + while (stack.length) { + + $el = stack.shift(); + + found = this.walk($el, function (node) { + + var left, right, top, bottom; + var elPos; + var elRange; + + elPos = (0, _core.nodeBounds)(node); + + if (_this2.horizontal && _this2.direction === "ltr") { + + left = Math.round(elPos.left); + right = Math.round(elPos.right); + + if (left > end && $prev) { + return $prev; + } else if (right > end) { + return node; + } else { + $prev = node; + stack.push(node); + } + } else if (_this2.horizontal && _this2.direction === "rtl") { + + left = Math.round(_this2.horizontal ? elPos.left : elPos.top); + right = Math.round(_this2.horizontal ? elPos.right : elPos.bottom); + + if (right < start && $prev) { + return $prev; + } else if (left < start) { + return node; + } else { + $prev = node; + stack.push(node); + } + } else { + + top = Math.round(elPos.top); + bottom = Math.round(elPos.bottom); + + if (top > end && $prev) { + return $prev; + } else if (bottom > end) { + return node; + } else { + $prev = node; + stack.push(node); + } + } + }); + + if (found) { + return this.findTextEndRange(found, start, end); + } + } + + // end of chapter + return this.findTextEndRange($prev, start, end); + } + + /** + * Find Text Start Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + + }, { + key: "findTextStartRange", + value: function findTextStartRange(node, start, end) { + var ranges = this.splitTextNodeIntoRanges(node); + var range; + var pos; + var left, top, right; + + for (var i = 0; i < ranges.length; i++) { + range = ranges[i]; + + pos = range.getBoundingClientRect(); + + if (this.horizontal && this.direction === "ltr") { + + left = pos.left; + if (left >= start) { + return range; + } + } else if (this.horizontal && this.direction === "rtl") { + + right = pos.right; + if (right <= end) { + return range; + } + } else { + + top = pos.top; + if (top >= start) { + return range; + } + } + + // prev = range; + } + + return ranges[0]; + } + + /** + * Find Text End Range + * @private + * @param {Node} root root node + * @param {number} start position to start at + * @param {number} end position to end at + * @return {Range} + */ + + }, { + key: "findTextEndRange", + value: function findTextEndRange(node, start, end) { + var ranges = this.splitTextNodeIntoRanges(node); + var prev; + var range; + var pos; + var left, right, top, bottom; + + for (var i = 0; i < ranges.length; i++) { + range = ranges[i]; + + pos = range.getBoundingClientRect(); + + if (this.horizontal && this.direction === "ltr") { + + left = pos.left; + right = pos.right; + + if (left > end && prev) { + return prev; + } else if (right > end) { + return range; + } + } else if (this.horizontal && this.direction === "rtl") { + + left = pos.left; + right = pos.right; + + if (right < start && prev) { + return prev; + } else if (left < start) { + return range; + } + } else { + + top = pos.top; + bottom = pos.bottom; + + if (top > end && prev) { + return prev; + } else if (bottom > end) { + return range; + } + } + + prev = range; + } + + // Ends before limit + return ranges[ranges.length - 1]; + } + + /** + * Split up a text node into ranges for each word + * @private + * @param {Node} root root node + * @param {string} [_splitter] what to split on + * @return {Range[]} + */ + + }, { + key: "splitTextNodeIntoRanges", + value: function splitTextNodeIntoRanges(node, _splitter) { + var ranges = []; + var textContent = node.textContent || ""; + var text = textContent.trim(); + var range; + var doc = node.ownerDocument; + var splitter = _splitter || " "; + + var pos = text.indexOf(splitter); + + if (pos === -1 || node.nodeType != Node.TEXT_NODE) { + range = doc.createRange(); + range.selectNodeContents(node); + return [range]; + } + + range = doc.createRange(); + range.setStart(node, 0); + range.setEnd(node, pos); + ranges.push(range); + range = false; + + while (pos != -1) { + + pos = text.indexOf(splitter, pos + 1); + if (pos > 0) { + + if (range) { + range.setEnd(node, pos); + ranges.push(range); + } + + range = doc.createRange(); + range.setStart(node, pos + 1); + } + } + + if (range) { + range.setEnd(node, text.length); + ranges.push(range); + } + + return ranges; + } + + /** + * Turn a pair of ranges into a pair of CFIs + * @private + * @param {string} cfiBase base string for an EpubCFI + * @param {object} rangePair { start: Range, end: Range } + * @return {object} { start: "epubcfi(...)", end: "epubcfi(...)" } + */ + + }, { + key: "rangePairToCfiPair", + value: function rangePairToCfiPair(cfiBase, rangePair) { + + var startRange = rangePair.start; + var endRange = rangePair.end; + + startRange.collapse(true); + endRange.collapse(false); + + var startCfi = new _epubcfi2.default(startRange, cfiBase).toString(); + var endCfi = new _epubcfi2.default(endRange, cfiBase).toString(); + + return { + start: startCfi, + end: endCfi + }; + } + }, { + key: "rangeListToCfiList", + value: function rangeListToCfiList(cfiBase, columns) { + var map = []; + var cifPair; + + for (var i = 0; i < columns.length; i++) { + cifPair = this.rangePairToCfiPair(cfiBase, columns[i]); + + map.push(cifPair); + } + + return map; + } + + /** + * Set the axis for mapping + * @param {string} axis horizontal | vertical + * @return {boolean} is it horizontal? + */ + + }, { + key: "axis", + value: function axis(_axis) { + if (_axis) { + this.horizontal = _axis === "horizontal" ? true : false; + } + return this.horizontal; + } + }]); + + return Mapping; +}(); + +exports.default = Mapping; +module.exports = exports["default"]; + +/***/ }), +/* 20 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _eventEmitter = __webpack_require__(3); + +var _eventEmitter2 = _interopRequireDefault(_eventEmitter); + +var _core = __webpack_require__(0); + +var _epubcfi = __webpack_require__(1); + +var _epubcfi2 = _interopRequireDefault(_epubcfi); + +var _contents = __webpack_require__(14); + +var _contents2 = _interopRequireDefault(_contents); + +var _constants = __webpack_require__(2); + +var _marksPane = __webpack_require__(56); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var IframeView = function () { + function IframeView(section, options) { + _classCallCheck(this, IframeView); + + this.settings = (0, _core.extend)({ + ignoreClass: "", + axis: options.layout && options.layout.props.flow === "scrolled" ? "vertical" : "horizontal", + direction: undefined, + width: 0, + height: 0, + layout: undefined, + globalLayoutProperties: {}, + method: undefined + }, options || {}); + + this.id = "epubjs-view-" + (0, _core.uuid)(); + this.section = section; + this.index = section.index; + + this.element = this.container(this.settings.axis); + + this.added = false; + this.displayed = false; + this.rendered = false; + + // this.width = this.settings.width; + // this.height = this.settings.height; + + this.fixedWidth = 0; + this.fixedHeight = 0; + + // Blank Cfi for Parsing + this.epubcfi = new _epubcfi2.default(); + + this.layout = this.settings.layout; + // Dom events to listen for + // this.listenedEvents = ["keydown", "keyup", "keypressed", "mouseup", "mousedown", "click", "touchend", "touchstart"]; + + this.pane = undefined; + this.highlights = {}; + this.underlines = {}; + this.marks = {}; + } + + _createClass(IframeView, [{ + key: "container", + value: function container(axis) { + var element = document.createElement("div"); + + element.classList.add("epub-view"); + + // this.element.style.minHeight = "100px"; + element.style.height = "0px"; + element.style.width = "0px"; + element.style.overflow = "hidden"; + element.style.position = "relative"; + element.style.display = "block"; + + if (axis && axis == "horizontal") { + element.style.flex = "none"; + } else { + element.style.flex = "initial"; + } + + return element; + } + }, { + key: "create", + value: function create() { + + if (this.iframe) { + return this.iframe; + } + + if (!this.element) { + this.element = this.createContainer(); + } + + this.iframe = document.createElement("iframe"); + this.iframe.id = this.id; + this.iframe.scrolling = "no"; // Might need to be removed: breaks ios width calculations + this.iframe.style.overflow = "hidden"; + this.iframe.seamless = "seamless"; + // Back up if seamless isn't supported + this.iframe.style.border = "none"; + + this.iframe.setAttribute("enable-annotation", "true"); + + this.resizing = true; + + // this.iframe.style.display = "none"; + this.element.style.visibility = "hidden"; + this.iframe.style.visibility = "hidden"; + + this.iframe.style.width = "0"; + this.iframe.style.height = "0"; + this._width = 0; + this._height = 0; + + this.element.setAttribute("ref", this.index); + + this.added = true; + + this.elementBounds = (0, _core.bounds)(this.element); + + // if(width || height){ + // this.resize(width, height); + // } else if(this.width && this.height){ + // this.resize(this.width, this.height); + // } else { + // this.iframeBounds = bounds(this.iframe); + // } + + + if ("srcdoc" in this.iframe) { + this.supportsSrcdoc = true; + } else { + this.supportsSrcdoc = false; + } + + if (!this.settings.method) { + this.settings.method = this.supportsSrcdoc ? "srcdoc" : "write"; + } + + return this.iframe; + } + }, { + key: "render", + value: function render(request, show) { + + // view.onLayout = this.layout.format.bind(this.layout); + this.create(); + + // Fit to size of the container, apply padding + this.size(); + + if (!this.sectionRender) { + this.sectionRender = this.section.render(request); + } + + // Render Chain + return this.sectionRender.then(function (contents) { + return this.load(contents); + }.bind(this)).then(function () { + var _this = this; + + // apply the layout function to the contents + this.layout.format(this.contents); + + // find and report the writingMode axis + var writingMode = this.contents.writingMode(); + var axis = writingMode.indexOf("vertical") === 0 ? "vertical" : "horizontal"; + + this.setAxis(axis); + this.emit(_constants.EVENTS.VIEWS.AXIS, axis); + + // Listen for events that require an expansion of the iframe + this.addListeners(); + + return new Promise(function (resolve, reject) { + // Expand the iframe to the full size of the content + _this.expand(); + resolve(); + }); + }.bind(this), function (e) { + this.emit(_constants.EVENTS.VIEWS.LOAD_ERROR, e); + return new Promise(function (resolve, reject) { + reject(e); + }); + }.bind(this)).then(function () { + this.emit(_constants.EVENTS.VIEWS.RENDERED, this.section); + }.bind(this)); + } + }, { + key: "reset", + value: function reset() { + if (this.iframe) { + this.iframe.style.width = "0"; + this.iframe.style.height = "0"; + this._width = 0; + this._height = 0; + this._textWidth = undefined; + this._contentWidth = undefined; + this._textHeight = undefined; + this._contentHeight = undefined; + } + this._needsReframe = true; + } + + // Determine locks base on settings + + }, { + key: "size", + value: function size(_width, _height) { + var width = _width || this.settings.width; + var height = _height || this.settings.height; + + if (this.layout.name === "pre-paginated") { + this.lock("both", width, height); + } else if (this.settings.axis === "horizontal") { + this.lock("height", width, height); + } else { + this.lock("width", width, height); + } + + this.settings.width = width; + this.settings.height = height; + } + + // Lock an axis to element dimensions, taking borders into account + + }, { + key: "lock", + value: function lock(what, width, height) { + var elBorders = (0, _core.borders)(this.element); + var iframeBorders; + + if (this.iframe) { + iframeBorders = (0, _core.borders)(this.iframe); + } else { + iframeBorders = { width: 0, height: 0 }; + } + + if (what == "width" && (0, _core.isNumber)(width)) { + this.lockedWidth = width - elBorders.width - iframeBorders.width; + // this.resize(this.lockedWidth, width); // width keeps ratio correct + } + + if (what == "height" && (0, _core.isNumber)(height)) { + this.lockedHeight = height - elBorders.height - iframeBorders.height; + // this.resize(width, this.lockedHeight); + } + + if (what === "both" && (0, _core.isNumber)(width) && (0, _core.isNumber)(height)) { + + this.lockedWidth = width - elBorders.width - iframeBorders.width; + this.lockedHeight = height - elBorders.height - iframeBorders.height; + // this.resize(this.lockedWidth, this.lockedHeight); + } + + if (this.displayed && this.iframe) { + + // this.contents.layout(); + this.expand(); + } + } + + // Resize a single axis based on content dimensions + + }, { + key: "expand", + value: function expand(force) { + var width = this.lockedWidth; + var height = this.lockedHeight; + var columns; + + var textWidth, textHeight; + + if (!this.iframe || this._expanding) return; + + this._expanding = true; + + if (this.layout.name === "pre-paginated") { + width = this.layout.columnWidth; + height = this.layout.height; + } + // Expand Horizontally + else if (this.settings.axis === "horizontal") { + // Get the width of the text + width = this.contents.textWidth(); + + if (width % this.layout.pageWidth > 0) { + width = Math.ceil(width / this.layout.pageWidth) * this.layout.pageWidth; + } + + if (this.settings.forceEvenPages) { + columns = width / this.layout.pageWidth; + if (this.layout.divisor > 1 && this.layout.name === "reflowable" && columns % 2 > 0) { + // add a blank page + width += this.layout.pageWidth; + } + } + } // Expand Vertically + else if (this.settings.axis === "vertical") { + height = this.contents.textHeight(); + } + + // Only Resize if dimensions have changed or + // if Frame is still hidden, so needs reframing + if (this._needsReframe || width != this._width || height != this._height) { + this.reframe(width, height); + } + + this._expanding = false; + } + }, { + key: "reframe", + value: function reframe(width, height) { + var _this2 = this; + + var size; + + if ((0, _core.isNumber)(width)) { + this.element.style.width = width + "px"; + this.iframe.style.width = width + "px"; + this._width = width; + } + + if ((0, _core.isNumber)(height)) { + this.element.style.height = height + "px"; + this.iframe.style.height = height + "px"; + this._height = height; + } + + var widthDelta = this.prevBounds ? width - this.prevBounds.width : width; + var heightDelta = this.prevBounds ? height - this.prevBounds.height : height; + + size = { + width: width, + height: height, + widthDelta: widthDelta, + heightDelta: heightDelta + }; + + this.pane && this.pane.render(); + + requestAnimationFrame(function () { + var mark = void 0; + for (var m in _this2.marks) { + if (_this2.marks.hasOwnProperty(m)) { + mark = _this2.marks[m]; + _this2.placeMark(mark.element, mark.range); + } + } + }); + + this.onResize(this, size); + + this.emit(_constants.EVENTS.VIEWS.RESIZED, size); + + this.prevBounds = size; + + this.elementBounds = (0, _core.bounds)(this.element); + } + }, { + key: "load", + value: function load(contents) { + var loading = new _core.defer(); + var loaded = loading.promise; + + if (!this.iframe) { + loading.reject(new Error("No Iframe Available")); + return loaded; + } + + this.iframe.onload = function (event) { + + this.onLoad(event, loading); + }.bind(this); + + if (this.settings.method === "blobUrl") { + this.blobUrl = (0, _core.createBlobUrl)(contents, "application/xhtml+xml"); + this.iframe.src = this.blobUrl; + this.element.appendChild(this.iframe); + } else if (this.settings.method === "srcdoc") { + this.iframe.srcdoc = contents; + this.element.appendChild(this.iframe); + } else { + + this.element.appendChild(this.iframe); + + this.document = this.iframe.contentDocument; + + if (!this.document) { + loading.reject(new Error("No Document Available")); + return loaded; + } + + this.iframe.contentDocument.open(); + this.iframe.contentDocument.write(contents); + this.iframe.contentDocument.close(); + } + + return loaded; + } + }, { + key: "onLoad", + value: function onLoad(event, promise) { + var _this3 = this; + + this.window = this.iframe.contentWindow; + this.document = this.iframe.contentDocument; + + this.contents = new _contents2.default(this.document, this.document.body, this.section.cfiBase, this.section.index); + + this.rendering = false; + + var link = this.document.querySelector("link[rel='canonical']"); + if (link) { + link.setAttribute("href", this.section.canonical); + } else { + link = this.document.createElement("link"); + link.setAttribute("rel", "canonical"); + link.setAttribute("href", this.section.canonical); + this.document.querySelector("head").appendChild(link); + } + + this.contents.on(_constants.EVENTS.CONTENTS.EXPAND, function () { + if (_this3.displayed && _this3.iframe) { + _this3.expand(); + if (_this3.contents) { + _this3.layout.format(_this3.contents); + } + } + }); + + this.contents.on(_constants.EVENTS.CONTENTS.RESIZE, function (e) { + if (_this3.displayed && _this3.iframe) { + _this3.expand(); + if (_this3.contents) { + _this3.layout.format(_this3.contents); + } + } + }); + + promise.resolve(this.contents); + } + }, { + key: "setLayout", + value: function setLayout(layout) { + this.layout = layout; + + if (this.contents) { + this.layout.format(this.contents); + this.expand(); + } + } + }, { + key: "setAxis", + value: function setAxis(axis) { + + // Force vertical for scrolled + if (this.layout.props.flow === "scrolled") { + axis = "vertical"; + } + + this.settings.axis = axis; + + if (axis == "horizontal") { + this.element.style.flex = "none"; + } else { + this.element.style.flex = "initial"; + } + + this.size(); + } + }, { + key: "addListeners", + value: function addListeners() { + //TODO: Add content listeners for expanding + } + }, { + key: "removeListeners", + value: function removeListeners(layoutFunc) { + //TODO: remove content listeners for expanding + } + }, { + key: "display", + value: function display(request) { + var displayed = new _core.defer(); + + if (!this.displayed) { + + this.render(request).then(function () { + + this.emit(_constants.EVENTS.VIEWS.DISPLAYED, this); + this.onDisplayed(this); + + this.displayed = true; + displayed.resolve(this); + }.bind(this), function (err) { + displayed.reject(err, this); + }); + } else { + displayed.resolve(this); + } + + return displayed.promise; + } + }, { + key: "show", + value: function show() { + + this.element.style.visibility = "visible"; + + if (this.iframe) { + this.iframe.style.visibility = "visible"; + + // Remind Safari to redraw the iframe + this.iframe.style.transform = "translateZ(0)"; + this.iframe.offsetWidth; + this.iframe.style.transform = null; + } + + this.emit(_constants.EVENTS.VIEWS.SHOWN, this); + } + }, { + key: "hide", + value: function hide() { + // this.iframe.style.display = "none"; + this.element.style.visibility = "hidden"; + this.iframe.style.visibility = "hidden"; + + this.stopExpanding = true; + this.emit(_constants.EVENTS.VIEWS.HIDDEN, this); + } + }, { + key: "offset", + value: function offset() { + return { + top: this.element.offsetTop, + left: this.element.offsetLeft + }; + } + }, { + key: "width", + value: function width() { + return this._width; + } + }, { + key: "height", + value: function height() { + return this._height; + } + }, { + key: "position", + value: function position() { + return this.element.getBoundingClientRect(); + } + }, { + key: "locationOf", + value: function locationOf(target) { + var parentPos = this.iframe.getBoundingClientRect(); + var targetPos = this.contents.locationOf(target, this.settings.ignoreClass); + + return { + "left": targetPos.left, + "top": targetPos.top + }; + } + }, { + key: "onDisplayed", + value: function onDisplayed(view) { + // Stub, override with a custom functions + } + }, { + key: "onResize", + value: function onResize(view, e) { + // Stub, override with a custom functions + } + }, { + key: "bounds", + value: function bounds(force) { + if (force || !this.elementBounds) { + this.elementBounds = (0, _core.bounds)(this.element); + } + + return this.elementBounds; + } + }, { + key: "highlight", + value: function highlight(cfiRange) { + var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var cb = arguments[2]; + + var _this4 = this; + + var className = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "epubjs-hl"; + var styles = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; + + if (!this.contents) { + return; + } + var attributes = Object.assign({ "fill": "yellow", "fill-opacity": "0.3", "mix-blend-mode": "multiply" }, styles); + var range = this.contents.range(cfiRange); + + var emitter = function emitter() { + _this4.emit(_constants.EVENTS.VIEWS.MARK_CLICKED, cfiRange, data); + }; + + data["epubcfi"] = cfiRange; + + if (!this.pane) { + this.pane = new _marksPane.Pane(this.iframe, this.element); + } + + var m = new _marksPane.Highlight(range, className, data, attributes); + var h = this.pane.addMark(m); + + this.highlights[cfiRange] = { "mark": h, "element": h.element, "listeners": [emitter, cb] }; + + h.element.setAttribute("ref", className); + h.element.addEventListener("click", emitter); + h.element.addEventListener("touchstart", emitter); + + if (cb) { + h.element.addEventListener("click", cb); + h.element.addEventListener("touchstart", cb); + } + return h; + } + }, { + key: "underline", + value: function underline(cfiRange) { + var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var cb = arguments[2]; + + var _this5 = this; + + var className = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "epubjs-ul"; + var styles = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; + + if (!this.contents) { + return; + } + var attributes = Object.assign({ "stroke": "black", "stroke-opacity": "0.3", "mix-blend-mode": "multiply" }, styles); + var range = this.contents.range(cfiRange); + var emitter = function emitter() { + _this5.emit(_constants.EVENTS.VIEWS.MARK_CLICKED, cfiRange, data); + }; + + data["epubcfi"] = cfiRange; + + if (!this.pane) { + this.pane = new _marksPane.Pane(this.iframe, this.element); + } + + var m = new _marksPane.Underline(range, className, data, attributes); + var h = this.pane.addMark(m); + + this.underlines[cfiRange] = { "mark": h, "element": h.element, "listeners": [emitter, cb] }; + + h.element.setAttribute("ref", className); + h.element.addEventListener("click", emitter); + h.element.addEventListener("touchstart", emitter); + + if (cb) { + h.element.addEventListener("click", cb); + h.element.addEventListener("touchstart", cb); + } + return h; + } + }, { + key: "mark", + value: function mark(cfiRange) { + var _this6 = this; + + var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var cb = arguments[2]; + + if (!this.contents) { + return; + } + + if (cfiRange in this.marks) { + var item = this.marks[cfiRange]; + return item; + } + + var range = this.contents.range(cfiRange); + if (!range) { + return; + } + var container = range.commonAncestorContainer; + var parent = container.nodeType === 1 ? container : container.parentNode; + + var emitter = function emitter(e) { + _this6.emit(_constants.EVENTS.VIEWS.MARK_CLICKED, cfiRange, data); + }; + + if (range.collapsed && container.nodeType === 1) { + range = new Range(); + range.selectNodeContents(container); + } else if (range.collapsed) { + // Webkit doesn't like collapsed ranges + range = new Range(); + range.selectNodeContents(parent); + } + + var mark = this.document.createElement("a"); + mark.setAttribute("ref", "epubjs-mk"); + mark.style.position = "absolute"; + + mark.dataset["epubcfi"] = cfiRange; + + if (data) { + Object.keys(data).forEach(function (key) { + mark.dataset[key] = data[key]; + }); + } + + if (cb) { + mark.addEventListener("click", cb); + mark.addEventListener("touchstart", cb); + } + + mark.addEventListener("click", emitter); + mark.addEventListener("touchstart", emitter); + + this.placeMark(mark, range); + + this.element.appendChild(mark); + + this.marks[cfiRange] = { "element": mark, "range": range, "listeners": [emitter, cb] }; + + return parent; + } + }, { + key: "placeMark", + value: function placeMark(element, range) { + var top = void 0, + right = void 0, + left = void 0; + + if (this.layout.name === "pre-paginated" || this.settings.axis !== "horizontal") { + var pos = range.getBoundingClientRect(); + top = pos.top; + right = pos.right; + } else { + // Element might break columns, so find the left most element + var rects = range.getClientRects(); + + var rect = void 0; + for (var i = 0; i != rects.length; i++) { + rect = rects[i]; + if (!left || rect.left < left) { + left = rect.left; + // right = rect.right; + right = Math.ceil(left / this.layout.props.pageWidth) * this.layout.props.pageWidth - this.layout.gap / 2; + top = rect.top; + } + } + } + + element.style.top = top + "px"; + element.style.left = right + "px"; + } + }, { + key: "unhighlight", + value: function unhighlight(cfiRange) { + var item = void 0; + if (cfiRange in this.highlights) { + item = this.highlights[cfiRange]; + + this.pane.removeMark(item.mark); + item.listeners.forEach(function (l) { + if (l) { + item.element.removeEventListener("click", l); + item.element.removeEventListener("touchstart", l); + }; + }); + delete this.highlights[cfiRange]; + } + } + }, { + key: "ununderline", + value: function ununderline(cfiRange) { + var item = void 0; + if (cfiRange in this.underlines) { + item = this.underlines[cfiRange]; + this.pane.removeMark(item.mark); + item.listeners.forEach(function (l) { + if (l) { + item.element.removeEventListener("click", l); + item.element.removeEventListener("touchstart", l); + }; + }); + delete this.underlines[cfiRange]; + } + } + }, { + key: "unmark", + value: function unmark(cfiRange) { + var item = void 0; + if (cfiRange in this.marks) { + item = this.marks[cfiRange]; + this.element.removeChild(item.element); + item.listeners.forEach(function (l) { + if (l) { + item.element.removeEventListener("click", l); + item.element.removeEventListener("touchstart", l); + }; + }); + delete this.marks[cfiRange]; + } + } + }, { + key: "destroy", + value: function destroy() { + + for (var cfiRange in this.highlights) { + this.unhighlight(cfiRange); + } + + for (var _cfiRange in this.underlines) { + this.ununderline(_cfiRange); + } + + for (var _cfiRange2 in this.marks) { + this.unmark(_cfiRange2); + } + + if (this.blobUrl) { + (0, _core.revokeBlobUrl)(this.blobUrl); + } + + if (this.displayed) { + this.displayed = false; + + this.removeListeners(); + this.contents.destroy(); + + this.stopExpanding = true; + this.element.removeChild(this.iframe); + + this.iframe = undefined; + this.contents = undefined; + + this._textWidth = null; + this._textHeight = null; + this._width = null; + this._height = null; + } + + // this.element.style.height = "0px"; + // this.element.style.width = "0px"; + } + }]); + + return IframeView; +}(); + +(0, _eventEmitter2.default)(IframeView.prototype); + +exports.default = IframeView; +module.exports = exports["default"]; + +/***/ }), +/* 21 */ +/***/ (function(module, exports, __webpack_require__) { + +var isObject = __webpack_require__(16), + now = __webpack_require__(61), + toNumber = __webpack_require__(63); + +/** Error message constants. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeMax = Math.max, + nativeMin = Math.min; + +/** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ +function debounce(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = toNumber(wait) || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + timeWaiting = wait - timeSinceLastCall; + + return maxing + ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) + : timeWaiting; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now()); + } + + function debounced() { + var time = now(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; +} + +module.exports = debounce; + + +/***/ }), +/* 22 */ +/***/ (function(module, exports, __webpack_require__) { + +var freeGlobal = __webpack_require__(62); + +/** Detect free variable `self`. */ +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +var root = freeGlobal || freeSelf || Function('return this')(); + +module.exports = root; + + +/***/ }), +/* 23 */ +/***/ (function(module, exports, __webpack_require__) { + +var root = __webpack_require__(22); + +/** Built-in value references. */ +var Symbol = root.Symbol; + +module.exports = Symbol; + + +/***/ }), +/* 24 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _core = __webpack_require__(0); + +var _default = __webpack_require__(15); + +var _default2 = _interopRequireDefault(_default); + +var _snap = __webpack_require__(70); + +var _snap2 = _interopRequireDefault(_snap); + +var _constants = __webpack_require__(2); + +var _debounce = __webpack_require__(21); + +var _debounce2 = _interopRequireDefault(_debounce); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var ContinuousViewManager = function (_DefaultViewManager) { + _inherits(ContinuousViewManager, _DefaultViewManager); + + function ContinuousViewManager(options) { + _classCallCheck(this, ContinuousViewManager); + + var _this = _possibleConstructorReturn(this, (ContinuousViewManager.__proto__ || Object.getPrototypeOf(ContinuousViewManager)).call(this, options)); + + _this.name = "continuous"; + + _this.settings = (0, _core.extend)(_this.settings || {}, { + infinite: true, + overflow: undefined, + axis: undefined, + flow: "scrolled", + offset: 500, + offsetDelta: 250, + width: undefined, + height: undefined, + snap: false, + afterScrolledTimeout: 10 + }); + + (0, _core.extend)(_this.settings, options.settings || {}); + + // Gap can be 0, but defaults doesn't handle that + if (options.settings.gap != "undefined" && options.settings.gap === 0) { + _this.settings.gap = options.settings.gap; + } + + _this.viewSettings = { + ignoreClass: _this.settings.ignoreClass, + axis: _this.settings.axis, + flow: _this.settings.flow, + layout: _this.layout, + width: 0, + height: 0, + forceEvenPages: false + }; + + _this.scrollTop = 0; + _this.scrollLeft = 0; + return _this; + } + + _createClass(ContinuousViewManager, [{ + key: "display", + value: function display(section, target) { + return _default2.default.prototype.display.call(this, section, target).then(function () { + return this.fill(); + }.bind(this)); + } + }, { + key: "fill", + value: function fill(_full) { + var _this2 = this; + + var full = _full || new _core.defer(); + + this.q.enqueue(function () { + return _this2.check(); + }).then(function (result) { + if (result) { + _this2.fill(full); + } else { + full.resolve(); + } + }); + + return full.promise; + } + }, { + key: "moveTo", + value: function moveTo(offset) { + // var bounds = this.stage.bounds(); + // var dist = Math.floor(offset.top / bounds.height) * bounds.height; + var distX = 0, + distY = 0; + + var offsetX = 0, + offsetY = 0; + + if (!this.isPaginated) { + distY = offset.top; + offsetY = offset.top + this.settings.offsetDelta; + } else { + distX = Math.floor(offset.left / this.layout.delta) * this.layout.delta; + offsetX = distX + this.settings.offsetDelta; + } + + if (distX > 0 || distY > 0) { + this.scrollBy(distX, distY, true); + } + } + }, { + key: "afterResized", + value: function afterResized(view) { + this.emit(_constants.EVENTS.MANAGERS.RESIZE, view.section); + } + + // Remove Previous Listeners if present + + }, { + key: "removeShownListeners", + value: function removeShownListeners(view) { + + // view.off("shown", this.afterDisplayed); + // view.off("shown", this.afterDisplayedAbove); + view.onDisplayed = function () {}; + } + }, { + key: "add", + value: function add(section) { + var _this3 = this; + + var view = this.createView(section); + + this.views.append(view); + + view.on(_constants.EVENTS.VIEWS.RESIZED, function (bounds) { + view.expanded = true; + }); + + view.on(_constants.EVENTS.VIEWS.AXIS, function (axis) { + _this3.updateAxis(axis); + }); + + // view.on(EVENTS.VIEWS.SHOWN, this.afterDisplayed.bind(this)); + view.onDisplayed = this.afterDisplayed.bind(this); + view.onResize = this.afterResized.bind(this); + + return view.display(this.request); + } + }, { + key: "append", + value: function append(section) { + var _this4 = this; + + var view = this.createView(section); + + view.on(_constants.EVENTS.VIEWS.RESIZED, function (bounds) { + view.expanded = true; + }); + + view.on(_constants.EVENTS.VIEWS.AXIS, function (axis) { + _this4.updateAxis(axis); + }); + + this.views.append(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + + return view; + } + }, { + key: "prepend", + value: function prepend(section) { + var _this5 = this; + + var view = this.createView(section); + + view.on(_constants.EVENTS.VIEWS.RESIZED, function (bounds) { + _this5.counter(bounds); + view.expanded = true; + }); + + view.on(_constants.EVENTS.VIEWS.AXIS, function (axis) { + _this5.updateAxis(axis); + }); + + this.views.prepend(view); + + view.onDisplayed = this.afterDisplayed.bind(this); + + return view; + } + }, { + key: "counter", + value: function counter(bounds) { + if (this.settings.axis === "vertical") { + this.scrollBy(0, bounds.heightDelta, true); + } else { + this.scrollBy(bounds.widthDelta, 0, true); + } + } + }, { + key: "update", + value: function update(_offset) { + var container = this.bounds(); + var views = this.views.all(); + var viewsLength = views.length; + var visible = []; + var offset = typeof _offset != "undefined" ? _offset : this.settings.offset || 0; + var isVisible; + var view; + + var updating = new _core.defer(); + var promises = []; + for (var i = 0; i < viewsLength; i++) { + view = views[i]; + + isVisible = this.isVisible(view, offset, offset, container); + + if (isVisible === true) { + // console.log("visible " + view.index); + + if (!view.displayed) { + var displayed = view.display(this.request).then(function (view) { + view.show(); + }, function (err) { + view.hide(); + }); + promises.push(displayed); + } else { + view.show(); + } + visible.push(view); + } else { + this.q.enqueue(view.destroy.bind(view)); + // console.log("hidden " + view.index); + + clearTimeout(this.trimTimeout); + this.trimTimeout = setTimeout(function () { + this.q.enqueue(this.trim.bind(this)); + }.bind(this), 250); + } + } + + if (promises.length) { + return Promise.all(promises).catch(function (err) { + updating.reject(err); + }); + } else { + updating.resolve(); + return updating.promise; + } + } + }, { + key: "check", + value: function check(_offsetLeft, _offsetTop) { + var _this6 = this; + + var checking = new _core.defer(); + var newViews = []; + + var horizontal = this.settings.axis === "horizontal"; + var delta = this.settings.offset || 0; + + if (_offsetLeft && horizontal) { + delta = _offsetLeft; + } + + if (_offsetTop && !horizontal) { + delta = _offsetTop; + } + + var bounds = this._bounds; // bounds saved this until resize + + var rtl = this.settings.direction === "rtl"; + var dir = horizontal && rtl ? -1 : 1; //RTL reverses scrollTop + + var offset = horizontal ? this.scrollLeft : this.scrollTop * dir; + var visibleLength = horizontal ? Math.floor(bounds.width) : bounds.height; + var contentLength = horizontal ? this.container.scrollWidth : this.container.scrollHeight; + + var prepend = function prepend() { + var first = _this6.views.first(); + var prev = first && first.section.prev(); + + if (prev) { + newViews.push(_this6.prepend(prev)); + } + }; + + var append = function append() { + var last = _this6.views.last(); + var next = last && last.section.next(); + + if (next) { + newViews.push(_this6.append(next)); + } + }; + + if (offset + visibleLength + delta >= contentLength) { + if (horizontal && rtl) { + prepend(); + } else { + append(); + } + } + + if (offset - delta < 0) { + if (horizontal && rtl) { + append(); + } else { + prepend(); + } + } + + var promises = newViews.map(function (view) { + return view.displayed; + }); + + if (newViews.length) { + return Promise.all(promises).then(function () { + if (_this6.layout.name === "pre-paginated" && _this6.layout.props.spread) { + return _this6.check(); + } + }).then(function () { + // Check to see if anything new is on screen after rendering + return _this6.update(delta); + }, function (err) { + return err; + }); + } else { + this.q.enqueue(function () { + this.update(); + }.bind(this)); + checking.resolve(false); + return checking.promise; + } + } + }, { + key: "trim", + value: function trim() { + var task = new _core.defer(); + var displayed = this.views.displayed(); + var first = displayed[0]; + var last = displayed[displayed.length - 1]; + var firstIndex = this.views.indexOf(first); + var lastIndex = this.views.indexOf(last); + var above = this.views.slice(0, firstIndex); + var below = this.views.slice(lastIndex + 1); + + // Erase all but last above + for (var i = 0; i < above.length - 1; i++) { + this.erase(above[i], above); + } + + // Erase all except first below + for (var j = 1; j < below.length; j++) { + this.erase(below[j]); + } + + task.resolve(); + return task.promise; + } + }, { + key: "erase", + value: function erase(view, above) { + //Trim + + var prevTop; + var prevLeft; + + if (!this.settings.fullsize) { + prevTop = this.container.scrollTop; + prevLeft = this.container.scrollLeft; + } else { + prevTop = window.scrollY; + prevLeft = window.scrollX; + } + + var bounds = view.bounds(); + + this.views.remove(view); + + if (above) { + if (this.settings.axis === "vertical") { + this.scrollTo(0, prevTop - bounds.height, true); + } else { + this.scrollTo(prevLeft - Math.floor(bounds.width), 0, true); + } + } + } + }, { + key: "addEventListeners", + value: function addEventListeners(stage) { + + window.addEventListener("unload", function (e) { + this.ignore = true; + // this.scrollTo(0,0); + this.destroy(); + }.bind(this)); + + this.addScrollListeners(); + + if (this.isPaginated && this.settings.snap) { + this.snapper = new _snap2.default(this, this.settings.snap && _typeof(this.settings.snap) === "object" && this.settings.snap); + } + } + }, { + key: "addScrollListeners", + value: function addScrollListeners() { + var scroller; + + this.tick = _core.requestAnimationFrame; + + if (!this.settings.fullsize) { + this.prevScrollTop = this.container.scrollTop; + this.prevScrollLeft = this.container.scrollLeft; + } else { + this.prevScrollTop = window.scrollY; + this.prevScrollLeft = window.scrollX; + } + + this.scrollDeltaVert = 0; + this.scrollDeltaHorz = 0; + + if (!this.settings.fullsize) { + scroller = this.container; + this.scrollTop = this.container.scrollTop; + this.scrollLeft = this.container.scrollLeft; + } else { + scroller = window; + this.scrollTop = window.scrollY; + this.scrollLeft = window.scrollX; + } + + this._onScroll = this.onScroll.bind(this); + scroller.addEventListener("scroll", this._onScroll); + this._scrolled = (0, _debounce2.default)(this.scrolled.bind(this), 30); + // this.tick.call(window, this.onScroll.bind(this)); + + this.didScroll = false; + } + }, { + key: "removeEventListeners", + value: function removeEventListeners() { + var scroller; + + if (!this.settings.fullsize) { + scroller = this.container; + } else { + scroller = window; + } + + scroller.removeEventListener("scroll", this._onScroll); + this._onScroll = undefined; + } + }, { + key: "onScroll", + value: function onScroll() { + var scrollTop = void 0; + var scrollLeft = void 0; + var dir = this.settings.direction === "rtl" ? -1 : 1; + + if (!this.settings.fullsize) { + scrollTop = this.container.scrollTop; + scrollLeft = this.container.scrollLeft; + } else { + scrollTop = window.scrollY * dir; + scrollLeft = window.scrollX * dir; + } + + this.scrollTop = scrollTop; + this.scrollLeft = scrollLeft; + + if (!this.ignore) { + + this._scrolled(); + } else { + this.ignore = false; + } + + this.scrollDeltaVert += Math.abs(scrollTop - this.prevScrollTop); + this.scrollDeltaHorz += Math.abs(scrollLeft - this.prevScrollLeft); + + this.prevScrollTop = scrollTop; + this.prevScrollLeft = scrollLeft; + + clearTimeout(this.scrollTimeout); + this.scrollTimeout = setTimeout(function () { + this.scrollDeltaVert = 0; + this.scrollDeltaHorz = 0; + }.bind(this), 150); + + clearTimeout(this.afterScrolled); + + this.didScroll = false; + } + }, { + key: "scrolled", + value: function scrolled() { + + this.q.enqueue(function () { + this.check(); + }.bind(this)); + + this.emit(_constants.EVENTS.MANAGERS.SCROLL, { + top: this.scrollTop, + left: this.scrollLeft + }); + + clearTimeout(this.afterScrolled); + this.afterScrolled = setTimeout(function () { + + // Don't report scroll if we are about the snap + if (this.snapper && this.snapper.supportsTouch && this.snapper.needsSnap()) { + return; + } + + this.emit(_constants.EVENTS.MANAGERS.SCROLLED, { + top: this.scrollTop, + left: this.scrollLeft + }); + }.bind(this), this.settings.afterScrolledTimeout); + } + }, { + key: "next", + value: function next() { + + var dir = this.settings.direction; + var delta = this.layout.props.name === "pre-paginated" && this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta; + + if (!this.views.length) return; + + if (this.isPaginated && this.settings.axis === "horizontal") { + + this.scrollBy(delta, 0, true); + } else { + + this.scrollBy(0, this.layout.height, true); + } + + this.q.enqueue(function () { + this.check(); + }.bind(this)); + } + }, { + key: "prev", + value: function prev() { + + var dir = this.settings.direction; + var delta = this.layout.props.name === "pre-paginated" && this.layout.props.spread ? this.layout.props.delta * 2 : this.layout.props.delta; + + if (!this.views.length) return; + + if (this.isPaginated && this.settings.axis === "horizontal") { + + this.scrollBy(-delta, 0, true); + } else { + + this.scrollBy(0, -this.layout.height, true); + } + + this.q.enqueue(function () { + this.check(); + }.bind(this)); + } + + // updateAxis(axis, forceUpdate){ + // + // super.updateAxis(axis, forceUpdate); + // + // if (axis === "vertical") { + // this.settings.infinite = true; + // } else { + // this.settings.infinite = false; + // } + // } + + }, { + key: "updateFlow", + value: function updateFlow(flow) { + if (this.rendered && this.snapper) { + this.snapper.destroy(); + this.snapper = undefined; + } + + _get(ContinuousViewManager.prototype.__proto__ || Object.getPrototypeOf(ContinuousViewManager.prototype), "updateFlow", this).call(this, flow, "scroll"); + + if (this.rendered && this.isPaginated && this.settings.snap) { + this.snapper = new _snap2.default(this, this.settings.snap && _typeof(this.settings.snap) === "object" && this.settings.snap); + } + } + }, { + key: "destroy", + value: function destroy() { + _get(ContinuousViewManager.prototype.__proto__ || Object.getPrototypeOf(ContinuousViewManager.prototype), "destroy", this).call(this); + + if (this.snapper) { + this.snapper.destroy(); + } + } + }]); + + return ContinuousViewManager; +}(_default2.default); + +exports.default = ContinuousViewManager; +module.exports = exports["default"]; + +/***/ }), +/* 25 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/* WEBPACK VAR INJECTION */(function(global) { + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _book = __webpack_require__(26); + +var _book2 = _interopRequireDefault(_book); + +var _rendition = __webpack_require__(18); + +var _rendition2 = _interopRequireDefault(_rendition); + +var _epubcfi = __webpack_require__(1); + +var _epubcfi2 = _interopRequireDefault(_epubcfi); + +var _contents = __webpack_require__(14); + +var _contents2 = _interopRequireDefault(_contents); + +var _core = __webpack_require__(0); + +var utils = _interopRequireWildcard(_core); + +var _constants = __webpack_require__(2); + +var _urlPolyfill = __webpack_require__(76); + +var URLpolyfill = _interopRequireWildcard(_urlPolyfill); + +var _iframe = __webpack_require__(20); + +var _iframe2 = _interopRequireDefault(_iframe); + +var _default = __webpack_require__(15); + +var _default2 = _interopRequireDefault(_default); + +var _continuous = __webpack_require__(24); + +var _continuous2 = _interopRequireDefault(_continuous); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Creates a new Book + * @param {string|ArrayBuffer} url URL, Path or ArrayBuffer + * @param {object} options to pass to the book + * @returns {Book} a new Book object + * @example ePub("/path/to/book.epub", {}) + */ +function ePub(url, options) { + return new _book2.default(url, options); +} + +ePub.VERSION = _constants.EPUBJS_VERSION; + +if (typeof global !== "undefined") { + global.EPUBJS_VERSION = _constants.EPUBJS_VERSION; +} + +ePub.Book = _book2.default; +ePub.Rendition = _rendition2.default; +ePub.Contents = _contents2.default; +ePub.CFI = _epubcfi2.default; +ePub.utils = utils; + +exports.default = ePub; +module.exports = exports["default"]; +/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(5))) + +/***/ }), +/* 26 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _eventEmitter = __webpack_require__(3); + +var _eventEmitter2 = _interopRequireDefault(_eventEmitter); + +var _core = __webpack_require__(0); + +var _url = __webpack_require__(6); + +var _url2 = _interopRequireDefault(_url); + +var _path = __webpack_require__(4); + +var _path2 = _interopRequireDefault(_path); + +var _spine = __webpack_require__(43); + +var _spine2 = _interopRequireDefault(_spine); + +var _locations = __webpack_require__(47); + +var _locations2 = _interopRequireDefault(_locations); + +var _container = __webpack_require__(48); + +var _container2 = _interopRequireDefault(_container); + +var _packaging = __webpack_require__(49); + +var _packaging2 = _interopRequireDefault(_packaging); + +var _navigation = __webpack_require__(50); + +var _navigation2 = _interopRequireDefault(_navigation); + +var _resources = __webpack_require__(51); + +var _resources2 = _interopRequireDefault(_resources); + +var _pagelist = __webpack_require__(52); + +var _pagelist2 = _interopRequireDefault(_pagelist); + +var _rendition = __webpack_require__(18); + +var _rendition2 = _interopRequireDefault(_rendition); + +var _archive = __webpack_require__(71); + +var _archive2 = _interopRequireDefault(_archive); + +var _request2 = __webpack_require__(9); + +var _request3 = _interopRequireDefault(_request2); + +var _epubcfi = __webpack_require__(1); + +var _epubcfi2 = _interopRequireDefault(_epubcfi); + +var _store = __webpack_require__(73); + +var _store2 = _interopRequireDefault(_store); + +var _displayoptions = __webpack_require__(75); + +var _displayoptions2 = _interopRequireDefault(_displayoptions); + +var _constants = __webpack_require__(2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var CONTAINER_PATH = "META-INF/container.xml"; +var IBOOKS_DISPLAY_OPTIONS_PATH = "META-INF/com.apple.ibooks.display-options.xml"; + +var INPUT_TYPE = { + BINARY: "binary", + BASE64: "base64", + EPUB: "epub", + OPF: "opf", + MANIFEST: "json", + DIRECTORY: "directory" +}; + +/** + * An Epub representation with methods for the loading, parsing and manipulation + * of its contents. + * @class + * @param {string} [url] + * @param {object} [options] + * @param {method} [options.requestMethod] a request function to use instead of the default + * @param {boolean} [options.requestCredentials=undefined] send the xhr request withCredentials + * @param {object} [options.requestHeaders=undefined] send the xhr request headers + * @param {string} [options.encoding=binary] optional to pass 'binary' or base64' for archived Epubs + * @param {string} [options.replacements=none] use base64, blobUrl, or none for replacing assets in archived Epubs + * @param {method} [options.canonical] optional function to determine canonical urls for a path + * @param {string} [options.openAs] optional string to determine the input type + * @param {string} [options.store=false] cache the contents in local storage, value should be the name of the reader + * @returns {Book} + * @example new Book("/path/to/book.epub", {}) + * @example new Book({ replacements: "blobUrl" }) + */ + +var Book = function () { + function Book(url, options) { + var _this = this; + + _classCallCheck(this, Book); + + // Allow passing just options to the Book + if (typeof options === "undefined" && typeof url !== "string" && url instanceof Blob === false) { + options = url; + url = undefined; + } + + this.settings = (0, _core.extend)(this.settings || {}, { + requestMethod: undefined, + requestCredentials: undefined, + requestHeaders: undefined, + encoding: undefined, + replacements: undefined, + canonical: undefined, + openAs: undefined, + store: undefined + }); + + (0, _core.extend)(this.settings, options); + + // Promises + this.opening = new _core.defer(); + /** + * @member {promise} opened returns after the book is loaded + * @memberof Book + */ + this.opened = this.opening.promise; + this.isOpen = false; + + this.loading = { + manifest: new _core.defer(), + spine: new _core.defer(), + metadata: new _core.defer(), + cover: new _core.defer(), + navigation: new _core.defer(), + pageList: new _core.defer(), + resources: new _core.defer(), + displayOptions: new _core.defer() + }; + + this.loaded = { + manifest: this.loading.manifest.promise, + spine: this.loading.spine.promise, + metadata: this.loading.metadata.promise, + cover: this.loading.cover.promise, + navigation: this.loading.navigation.promise, + pageList: this.loading.pageList.promise, + resources: this.loading.resources.promise, + displayOptions: this.loading.displayOptions.promise + }; + + /** + * @member {promise} ready returns after the book is loaded and parsed + * @memberof Book + * @private + */ + this.ready = Promise.all([this.loaded.manifest, this.loaded.spine, this.loaded.metadata, this.loaded.cover, this.loaded.navigation, this.loaded.resources, this.loaded.displayOptions]); + + // Queue for methods used before opening + this.isRendered = false; + // this._q = queue(this); + + /** + * @member {method} request + * @memberof Book + * @private + */ + this.request = this.settings.requestMethod || _request3.default; + + /** + * @member {Spine} spine + * @memberof Book + */ + this.spine = new _spine2.default(); + + /** + * @member {Locations} locations + * @memberof Book + */ + this.locations = new _locations2.default(this.spine, this.load.bind(this)); + + /** + * @member {Navigation} navigation + * @memberof Book + */ + this.navigation = undefined; + + /** + * @member {PageList} pagelist + * @memberof Book + */ + this.pageList = undefined; + + /** + * @member {Url} url + * @memberof Book + * @private + */ + this.url = undefined; + + /** + * @member {Path} path + * @memberof Book + * @private + */ + this.path = undefined; + + /** + * @member {boolean} archived + * @memberof Book + * @private + */ + this.archived = false; + + /** + * @member {Archive} archive + * @memberof Book + * @private + */ + this.archive = undefined; + + /** + * @member {Store} storage + * @memberof Book + * @private + */ + this.storage = undefined; + + /** + * @member {Resources} resources + * @memberof Book + * @private + */ + this.resources = undefined; + + /** + * @member {Rendition} rendition + * @memberof Book + * @private + */ + this.rendition = undefined; + + /** + * @member {Container} container + * @memberof Book + * @private + */ + this.container = undefined; + + /** + * @member {Packaging} packaging + * @memberof Book + * @private + */ + this.packaging = undefined; + + /** + * @member {DisplayOptions} displayOptions + * @memberof DisplayOptions + * @private + */ + this.displayOptions = undefined; + + // this.toc = undefined; + if (this.settings.store) { + this.store(this.settings.store); + } + + if (url) { + this.open(url, this.settings.openAs).catch(function (error) { + var err = new Error("Cannot load book at " + url); + _this.emit(_constants.EVENTS.BOOK.OPEN_FAILED, err); + }); + } + } + + /** + * Open a epub or url + * @param {string | ArrayBuffer} input Url, Path or ArrayBuffer + * @param {string} [what="binary", "base64", "epub", "opf", "json", "directory"] force opening as a certain type + * @returns {Promise} of when the book has been loaded + * @example book.open("/path/to/book.epub") + */ + + + _createClass(Book, [{ + key: "open", + value: function open(input, what) { + var opening; + var type = what || this.determineType(input); + + if (type === INPUT_TYPE.BINARY) { + this.archived = true; + this.url = new _url2.default("/", ""); + opening = this.openEpub(input); + } else if (type === INPUT_TYPE.BASE64) { + this.archived = true; + this.url = new _url2.default("/", ""); + opening = this.openEpub(input, type); + } else if (type === INPUT_TYPE.EPUB) { + this.archived = true; + this.url = new _url2.default("/", ""); + opening = this.request(input, "binary", this.settings.requestCredentials).then(this.openEpub.bind(this)); + } else if (type == INPUT_TYPE.OPF) { + this.url = new _url2.default(input); + opening = this.openPackaging(this.url.Path.toString()); + } else if (type == INPUT_TYPE.MANIFEST) { + this.url = new _url2.default(input); + opening = this.openManifest(this.url.Path.toString()); + } else { + this.url = new _url2.default(input); + opening = this.openContainer(CONTAINER_PATH).then(this.openPackaging.bind(this)); + } + + return opening; + } + + /** + * Open an archived epub + * @private + * @param {binary} data + * @param {string} [encoding] + * @return {Promise} + */ + + }, { + key: "openEpub", + value: function openEpub(data, encoding) { + var _this2 = this; + + return this.unarchive(data, encoding || this.settings.encoding).then(function () { + return _this2.openContainer(CONTAINER_PATH); + }).then(function (packagePath) { + return _this2.openPackaging(packagePath); + }); + } + + /** + * Open the epub container + * @private + * @param {string} url + * @return {string} packagePath + */ + + }, { + key: "openContainer", + value: function openContainer(url) { + var _this3 = this; + + return this.load(url).then(function (xml) { + _this3.container = new _container2.default(xml); + return _this3.resolve(_this3.container.packagePath); + }); + } + + /** + * Open the Open Packaging Format Xml + * @private + * @param {string} url + * @return {Promise} + */ + + }, { + key: "openPackaging", + value: function openPackaging(url) { + var _this4 = this; + + this.path = new _path2.default(url); + return this.load(url).then(function (xml) { + _this4.packaging = new _packaging2.default(xml); + return _this4.unpack(_this4.packaging); + }); + } + + /** + * Open the manifest JSON + * @private + * @param {string} url + * @return {Promise} + */ + + }, { + key: "openManifest", + value: function openManifest(url) { + var _this5 = this; + + this.path = new _path2.default(url); + return this.load(url).then(function (json) { + _this5.packaging = new _packaging2.default(); + _this5.packaging.load(json); + return _this5.unpack(_this5.packaging); + }); + } + + /** + * Load a resource from the Book + * @param {string} path path to the resource to load + * @return {Promise} returns a promise with the requested resource + */ + + }, { + key: "load", + value: function load(path) { + var resolved = this.resolve(path); + if (this.archived) { + return this.archive.request(resolved); + } else { + return this.request(resolved, null, this.settings.requestCredentials, this.settings.requestHeaders); + } + } + + /** + * Resolve a path to it's absolute position in the Book + * @param {string} path + * @param {boolean} [absolute] force resolving the full URL + * @return {string} the resolved path string + */ + + }, { + key: "resolve", + value: function resolve(path, absolute) { + if (!path) { + return; + } + var resolved = path; + var isAbsolute = path.indexOf("://") > -1; + + if (isAbsolute) { + return path; + } + + if (this.path) { + resolved = this.path.resolve(path); + } + + if (absolute != false && this.url) { + resolved = this.url.resolve(resolved); + } + + return resolved; + } + + /** + * Get a canonical link to a path + * @param {string} path + * @return {string} the canonical path string + */ + + }, { + key: "canonical", + value: function canonical(path) { + var url = path; + + if (!path) { + return ""; + } + + if (this.settings.canonical) { + url = this.settings.canonical(path); + } else { + url = this.resolve(path, true); + } + + return url; + } + + /** + * Determine the type of they input passed to open + * @private + * @param {string} input + * @return {string} binary | directory | epub | opf + */ + + }, { + key: "determineType", + value: function determineType(input) { + var url; + var path; + var extension; + + if (this.settings.encoding === "base64") { + return INPUT_TYPE.BASE64; + } + + if (typeof input != "string") { + return INPUT_TYPE.BINARY; + } + + url = new _url2.default(input); + path = url.path(); + extension = path.extension; + + if (!extension) { + return INPUT_TYPE.DIRECTORY; + } + + if (extension === "epub") { + return INPUT_TYPE.EPUB; + } + + if (extension === "opf") { + return INPUT_TYPE.OPF; + } + + if (extension === "json") { + return INPUT_TYPE.MANIFEST; + } + } + + /** + * unpack the contents of the Books packaging + * @private + * @param {Packaging} packaging object + */ + + }, { + key: "unpack", + value: function unpack(packaging) { + var _this6 = this; + + this.package = packaging; //TODO: deprecated this + + if (this.packaging.metadata.layout === "") { + // rendition:layout not set - check display options if book is pre-paginated + this.load(this.url.resolve(IBOOKS_DISPLAY_OPTIONS_PATH)).then(function (xml) { + _this6.displayOptions = new _displayoptions2.default(xml); + _this6.loading.displayOptions.resolve(_this6.displayOptions); + }).catch(function (err) { + _this6.displayOptions = new _displayoptions2.default(); + _this6.loading.displayOptions.resolve(_this6.displayOptions); + }); + } else { + this.displayOptions = new _displayoptions2.default(); + this.loading.displayOptions.resolve(this.displayOptions); + } + + this.spine.unpack(this.packaging, this.resolve.bind(this), this.canonical.bind(this)); + + this.resources = new _resources2.default(this.packaging.manifest, { + archive: this.archive, + resolver: this.resolve.bind(this), + request: this.request.bind(this), + replacements: this.settings.replacements || (this.archived ? "blobUrl" : "base64") + }); + + this.loadNavigation(this.packaging).then(function () { + // this.toc = this.navigation.toc; + _this6.loading.navigation.resolve(_this6.navigation); + }); + + if (this.packaging.coverPath) { + this.cover = this.resolve(this.packaging.coverPath); + } + // Resolve promises + this.loading.manifest.resolve(this.packaging.manifest); + this.loading.metadata.resolve(this.packaging.metadata); + this.loading.spine.resolve(this.spine); + this.loading.cover.resolve(this.cover); + this.loading.resources.resolve(this.resources); + this.loading.pageList.resolve(this.pageList); + + this.isOpen = true; + + if (this.archived || this.settings.replacements && this.settings.replacements != "none") { + this.replacements().then(function () { + _this6.loaded.displayOptions.then(function () { + _this6.opening.resolve(_this6); + }); + }).catch(function (err) { + console.error(err); + }); + } else { + // Resolve book opened promise + this.loaded.displayOptions.then(function () { + _this6.opening.resolve(_this6); + }); + } + } + + /** + * Load Navigation and PageList from package + * @private + * @param {Packaging} packaging + */ + + }, { + key: "loadNavigation", + value: function loadNavigation(packaging) { + var _this7 = this; + + var navPath = packaging.navPath || packaging.ncxPath; + var toc = packaging.toc; + + // From json manifest + if (toc) { + return new Promise(function (resolve, reject) { + _this7.navigation = new _navigation2.default(toc); + + if (packaging.pageList) { + _this7.pageList = new _pagelist2.default(packaging.pageList); // TODO: handle page lists from Manifest + } + + resolve(_this7.navigation); + }); + } + + if (!navPath) { + return new Promise(function (resolve, reject) { + _this7.navigation = new _navigation2.default(); + _this7.pageList = new _pagelist2.default(); + + resolve(_this7.navigation); + }); + } + + return this.load(navPath, "xml").then(function (xml) { + _this7.navigation = new _navigation2.default(xml); + _this7.pageList = new _pagelist2.default(xml); + return _this7.navigation; + }); + } + + /** + * Gets a Section of the Book from the Spine + * Alias for `book.spine.get` + * @param {string} target + * @return {Section} + */ + + }, { + key: "section", + value: function section(target) { + return this.spine.get(target); + } + + /** + * Sugar to render a book to an element + * @param {element | string} element element or string to add a rendition to + * @param {object} [options] + * @return {Rendition} + */ + + }, { + key: "renderTo", + value: function renderTo(element, options) { + this.rendition = new _rendition2.default(this, options); + this.rendition.attachTo(element); + + return this.rendition; + } + + /** + * Set if request should use withCredentials + * @param {boolean} credentials + */ + + }, { + key: "setRequestCredentials", + value: function setRequestCredentials(credentials) { + this.settings.requestCredentials = credentials; + } + + /** + * Set headers request should use + * @param {object} headers + */ + + }, { + key: "setRequestHeaders", + value: function setRequestHeaders(headers) { + this.settings.requestHeaders = headers; + } + + /** + * Unarchive a zipped epub + * @private + * @param {binary} input epub data + * @param {string} [encoding] + * @return {Archive} + */ + + }, { + key: "unarchive", + value: function unarchive(input, encoding) { + this.archive = new _archive2.default(); + return this.archive.open(input, encoding); + } + + /** + * Store the epubs contents + * @private + * @param {binary} input epub data + * @param {string} [encoding] + * @return {Store} + */ + + }, { + key: "store", + value: function store(name) { + var _this8 = this; + + // Use "blobUrl" or "base64" for replacements + var replacementsSetting = this.settings.replacements && this.settings.replacements !== "none"; + // Save original url + var originalUrl = this.url; + // Save original request method + var requester = this.settings.requestMethod || _request3.default.bind(this); + // Create new Store + this.storage = new _store2.default(name, requester, this.resolve.bind(this)); + // Replace request method to go through store + this.request = this.storage.request.bind(this.storage); + + this.opened.then(function () { + if (_this8.archived) { + _this8.storage.requester = _this8.archive.request.bind(_this8.archive); + } + // Substitute hook + var substituteResources = function substituteResources(output, section) { + section.output = _this8.resources.substitute(output, section.url); + }; + + // Set to use replacements + _this8.resources.settings.replacements = replacementsSetting || "blobUrl"; + // Create replacement urls + _this8.resources.replacements().then(function () { + return _this8.resources.replaceCss(); + }); + + _this8.storage.on("offline", function () { + // Remove url to use relative resolving for hrefs + _this8.url = new _url2.default("/", ""); + // Add hook to replace resources in contents + _this8.spine.hooks.serialize.register(substituteResources); + }); + + _this8.storage.on("online", function () { + // Restore original url + _this8.url = originalUrl; + // Remove hook + _this8.spine.hooks.serialize.deregister(substituteResources); + }); + }); + + return this.storage; + } + + /** + * Get the cover url + * @return {string} coverUrl + */ + + }, { + key: "coverUrl", + value: function coverUrl() { + var _this9 = this; + + var retrieved = this.loaded.cover.then(function (url) { + if (_this9.archived) { + // return this.archive.createUrl(this.cover); + return _this9.resources.get(_this9.cover); + } else { + return _this9.cover; + } + }); + + return retrieved; + } + + /** + * Load replacement urls + * @private + * @return {Promise} completed loading urls + */ + + }, { + key: "replacements", + value: function replacements() { + var _this10 = this; + + this.spine.hooks.serialize.register(function (output, section) { + section.output = _this10.resources.substitute(output, section.url); + }); + + return this.resources.replacements().then(function () { + return _this10.resources.replaceCss(); + }); + } + + /** + * Find a DOM Range for a given CFI Range + * @param {EpubCFI} cfiRange a epub cfi range + * @return {Range} + */ + + }, { + key: "getRange", + value: function getRange(cfiRange) { + var cfi = new _epubcfi2.default(cfiRange); + var item = this.spine.get(cfi.spinePos); + var _request = this.load.bind(this); + if (!item) { + return new Promise(function (resolve, reject) { + reject("CFI could not be found"); + }); + } + return item.load(_request).then(function (contents) { + var range = cfi.toRange(item.document); + return range; + }); + } + + /** + * Generates the Book Key using the identifer in the manifest or other string provided + * @param {string} [identifier] to use instead of metadata identifier + * @return {string} key + */ + + }, { + key: "key", + value: function key(identifier) { + var ident = identifier || this.packaging.metadata.identifier || this.url.filename; + return "epubjs:" + _constants.EPUBJS_VERSION + ":" + ident; + } + + /** + * Destroy the Book and all associated objects + */ + + }, { + key: "destroy", + value: function destroy() { + this.opened = undefined; + this.loading = undefined; + this.loaded = undefined; + this.ready = undefined; + + this.isOpen = false; + this.isRendered = false; + + this.spine && this.spine.destroy(); + this.locations && this.locations.destroy(); + this.pageList && this.pageList.destroy(); + this.archive && this.archive.destroy(); + this.resources && this.resources.destroy(); + this.container && this.container.destroy(); + this.packaging && this.packaging.destroy(); + this.rendition && this.rendition.destroy(); + this.displayOptions && this.displayOptions.destroy(); + + this.spine = undefined; + this.locations = undefined; + this.pageList = undefined; + this.archive = undefined; + this.resources = undefined; + this.container = undefined; + this.packaging = undefined; + this.rendition = undefined; + + this.navigation = undefined; + this.url = undefined; + this.path = undefined; + this.archived = false; + } + }]); + + return Book; +}(); + +//-- Enable binding events to book + + +(0, _eventEmitter2.default)(Book.prototype); + +exports.default = Book; +module.exports = exports["default"]; + +/***/ }), +/* 27 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var assign = __webpack_require__(28) + , normalizeOpts = __webpack_require__(36) + , isCallable = __webpack_require__(37) + , contains = __webpack_require__(38) + + , d; + +d = module.exports = function (dscr, value/*, options*/) { + var c, e, w, options, desc; + if ((arguments.length < 2) || (typeof dscr !== 'string')) { + options = value; + value = dscr; + dscr = null; + } else { + options = arguments[2]; + } + if (dscr == null) { + c = w = true; + e = false; + } else { + c = contains.call(dscr, 'c'); + e = contains.call(dscr, 'e'); + w = contains.call(dscr, 'w'); + } + + desc = { value: value, configurable: c, enumerable: e, writable: w }; + return !options ? desc : assign(normalizeOpts(options), desc); +}; + +d.gs = function (dscr, get, set/*, options*/) { + var c, e, options, desc; + if (typeof dscr !== 'string') { + options = set; + set = get; + get = dscr; + dscr = null; + } else { + options = arguments[3]; + } + if (get == null) { + get = undefined; + } else if (!isCallable(get)) { + options = get; + get = set = undefined; + } else if (set == null) { + set = undefined; + } else if (!isCallable(set)) { + options = set; + set = undefined; + } + if (dscr == null) { + c = true; + e = false; + } else { + c = contains.call(dscr, 'c'); + e = contains.call(dscr, 'e'); + } + + desc = { get: get, set: set, configurable: c, enumerable: e }; + return !options ? desc : assign(normalizeOpts(options), desc); +}; + + +/***/ }), +/* 28 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = __webpack_require__(29)() + ? Object.assign + : __webpack_require__(30); + + +/***/ }), +/* 29 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = function () { + var assign = Object.assign, obj; + if (typeof assign !== "function") return false; + obj = { foo: "raz" }; + assign(obj, { bar: "dwa" }, { trzy: "trzy" }); + return (obj.foo + obj.bar + obj.trzy) === "razdwatrzy"; +}; + + +/***/ }), +/* 30 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var keys = __webpack_require__(31) + , value = __webpack_require__(35) + , max = Math.max; + +module.exports = function (dest, src /*, …srcn*/) { + var error, i, length = max(arguments.length, 2), assign; + dest = Object(value(dest)); + assign = function (key) { + try { + dest[key] = src[key]; + } catch (e) { + if (!error) error = e; + } + }; + for (i = 1; i < length; ++i) { + src = arguments[i]; + keys(src).forEach(assign); + } + if (error !== undefined) throw error; + return dest; +}; + + +/***/ }), +/* 31 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = __webpack_require__(32)() ? Object.keys : __webpack_require__(33); + + +/***/ }), +/* 32 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = function () { + try { + Object.keys("primitive"); + return true; + } catch (e) { + return false; + } +}; + + +/***/ }), +/* 33 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var isValue = __webpack_require__(10); + +var keys = Object.keys; + +module.exports = function (object) { return keys(isValue(object) ? Object(object) : object); }; + + +/***/ }), +/* 34 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +// eslint-disable-next-line no-empty-function +module.exports = function () {}; + + +/***/ }), +/* 35 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var isValue = __webpack_require__(10); + +module.exports = function (value) { + if (!isValue(value)) throw new TypeError("Cannot use null or undefined"); + return value; +}; + + +/***/ }), +/* 36 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var isValue = __webpack_require__(10); + +var forEach = Array.prototype.forEach, create = Object.create; + +var process = function (src, obj) { + var key; + for (key in src) obj[key] = src[key]; +}; + +// eslint-disable-next-line no-unused-vars +module.exports = function (opts1 /*, …options*/) { + var result = create(null); + forEach.call(arguments, function (options) { + if (!isValue(options)) return; + process(Object(options), result); + }); + return result; +}; + + +/***/ }), +/* 37 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +// Deprecated + + + +module.exports = function (obj) { + return typeof obj === "function"; +}; + + +/***/ }), +/* 38 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = __webpack_require__(39)() + ? String.prototype.contains + : __webpack_require__(40); + + +/***/ }), +/* 39 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var str = "razdwatrzy"; + +module.exports = function () { + if (typeof str.contains !== "function") return false; + return (str.contains("dwa") === true) && (str.contains("foo") === false); +}; + + +/***/ }), +/* 40 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var indexOf = String.prototype.indexOf; + +module.exports = function (searchString/*, position*/) { + return indexOf.call(this, searchString, arguments[1]) > -1; +}; + + +/***/ }), +/* 41 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +module.exports = function (fn) { + if (typeof fn !== "function") throw new TypeError(fn + " is not a function"); + return fn; +}; + + +/***/ }), +/* 42 */ +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE_42__; + +/***/ }), +/* 43 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _epubcfi = __webpack_require__(1); + +var _epubcfi2 = _interopRequireDefault(_epubcfi); + +var _hook = __webpack_require__(11); + +var _hook2 = _interopRequireDefault(_hook); + +var _section = __webpack_require__(44); + +var _section2 = _interopRequireDefault(_section); + +var _replacements = __webpack_require__(8); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * A collection of Spine Items + */ +var Spine = function () { + function Spine() { + _classCallCheck(this, Spine); + + this.spineItems = []; + this.spineByHref = {}; + this.spineById = {}; + + this.hooks = {}; + this.hooks.serialize = new _hook2.default(); + this.hooks.content = new _hook2.default(); + + // Register replacements + this.hooks.content.register(_replacements.replaceBase); + this.hooks.content.register(_replacements.replaceCanonical); + this.hooks.content.register(_replacements.replaceMeta); + + this.epubcfi = new _epubcfi2.default(); + + this.loaded = false; + + this.items = undefined; + this.manifest = undefined; + this.spineNodeIndex = undefined; + this.baseUrl = undefined; + this.length = undefined; + } + + /** + * Unpack items from a opf into spine items + * @param {Packaging} _package + * @param {method} resolver URL resolver + * @param {method} canonical Resolve canonical url + */ + + + _createClass(Spine, [{ + key: "unpack", + value: function unpack(_package, resolver, canonical) { + var _this = this; + + this.items = _package.spine; + this.manifest = _package.manifest; + this.spineNodeIndex = _package.spineNodeIndex; + this.baseUrl = _package.baseUrl || _package.basePath || ""; + this.length = this.items.length; + + this.items.forEach(function (item, index) { + var manifestItem = _this.manifest[item.idref]; + var spineItem; + + item.index = index; + item.cfiBase = _this.epubcfi.generateChapterComponent(_this.spineNodeIndex, item.index, item.idref); + + if (item.href) { + item.url = resolver(item.href, true); + item.canonical = canonical(item.href); + } + + if (manifestItem) { + item.href = manifestItem.href; + item.url = resolver(item.href, true); + item.canonical = canonical(item.href); + + if (manifestItem.properties.length) { + item.properties.push.apply(item.properties, manifestItem.properties); + } + } + + if (item.linear === "yes") { + item.prev = function () { + var prevIndex = item.index; + while (prevIndex > 0) { + var prev = this.get(prevIndex - 1); + if (prev && prev.linear) { + return prev; + } + prevIndex -= 1; + } + return; + }.bind(_this); + item.next = function () { + var nextIndex = item.index; + while (nextIndex < this.spineItems.length - 1) { + var next = this.get(nextIndex + 1); + if (next && next.linear) { + return next; + } + nextIndex += 1; + } + return; + }.bind(_this); + } else { + item.prev = function () { + return; + }; + item.next = function () { + return; + }; + } + + spineItem = new _section2.default(item, _this.hooks); + + _this.append(spineItem); + }); + + this.loaded = true; + } + + /** + * Get an item from the spine + * @param {string|number} [target] + * @return {Section} section + * @example spine.get(); + * @example spine.get(1); + * @example spine.get("chap1.html"); + * @example spine.get("#id1234"); + */ + + }, { + key: "get", + value: function get(target) { + var index = 0; + + if (typeof target === "undefined") { + while (index < this.spineItems.length) { + var next = this.spineItems[index]; + if (next && next.linear) { + break; + } + index += 1; + } + } else if (this.epubcfi.isCfiString(target)) { + var cfi = new _epubcfi2.default(target); + index = cfi.spinePos; + } else if (typeof target === "number" || isNaN(target) === false) { + index = target; + } else if (typeof target === "string" && target.indexOf("#") === 0) { + index = this.spineById[target.substring(1)]; + } else if (typeof target === "string") { + // Remove fragments + target = target.split("#")[0]; + index = this.spineByHref[target] || this.spineByHref[encodeURI(target)]; + } + + return this.spineItems[index] || null; + } + + /** + * Append a Section to the Spine + * @private + * @param {Section} section + */ + + }, { + key: "append", + value: function append(section) { + var index = this.spineItems.length; + section.index = index; + + this.spineItems.push(section); + + // Encode and Decode href lookups + // see pr for details: https://github.com/futurepress/epub.js/pull/358 + this.spineByHref[decodeURI(section.href)] = index; + this.spineByHref[encodeURI(section.href)] = index; + this.spineByHref[section.href] = index; + + this.spineById[section.idref] = index; + + return index; + } + + /** + * Prepend a Section to the Spine + * @private + * @param {Section} section + */ + + }, { + key: "prepend", + value: function prepend(section) { + // var index = this.spineItems.unshift(section); + this.spineByHref[section.href] = 0; + this.spineById[section.idref] = 0; + + // Re-index + this.spineItems.forEach(function (item, index) { + item.index = index; + }); + + return 0; + } + + // insert(section, index) { + // + // }; + + /** + * Remove a Section from the Spine + * @private + * @param {Section} section + */ + + }, { + key: "remove", + value: function remove(section) { + var index = this.spineItems.indexOf(section); + + if (index > -1) { + delete this.spineByHref[section.href]; + delete this.spineById[section.idref]; + + return this.spineItems.splice(index, 1); + } + } + + /** + * Loop over the Sections in the Spine + * @return {method} forEach + */ + + }, { + key: "each", + value: function each() { + return this.spineItems.forEach.apply(this.spineItems, arguments); + } + + /** + * Find the first Section in the Spine + * @return {Section} first section + */ + + }, { + key: "first", + value: function first() { + var index = 0; + + do { + var next = this.get(index); + + if (next && next.linear) { + return next; + } + index += 1; + } while (index < this.spineItems.length); + } + + /** + * Find the last Section in the Spine + * @return {Section} last section + */ + + }, { + key: "last", + value: function last() { + var index = this.spineItems.length - 1; + + do { + var prev = this.get(index); + if (prev && prev.linear) { + return prev; + } + index -= 1; + } while (index >= 0); + } + }, { + key: "destroy", + value: function destroy() { + this.each(function (section) { + return section.destroy(); + }); + + this.spineItems = undefined; + this.spineByHref = undefined; + this.spineById = undefined; + + this.hooks.serialize.clear(); + this.hooks.content.clear(); + this.hooks = undefined; + + this.epubcfi = undefined; + + this.loaded = false; + + this.items = undefined; + this.manifest = undefined; + this.spineNodeIndex = undefined; + this.baseUrl = undefined; + this.length = undefined; + } + }]); + + return Spine; +}(); + +exports.default = Spine; +module.exports = exports["default"]; + +/***/ }), +/* 44 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _core = __webpack_require__(0); + +var _epubcfi = __webpack_require__(1); + +var _epubcfi2 = _interopRequireDefault(_epubcfi); + +var _hook = __webpack_require__(11); + +var _hook2 = _interopRequireDefault(_hook); + +var _replacements = __webpack_require__(8); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * Represents a Section of the Book + * + * In most books this is equivelent to a Chapter + * @param {object} item The spine item representing the section + * @param {object} hooks hooks for serialize and content + */ +var Section = function () { + function Section(item, hooks) { + _classCallCheck(this, Section); + + this.idref = item.idref; + this.linear = item.linear === "yes"; + this.properties = item.properties; + this.index = item.index; + this.href = item.href; + this.url = item.url; + this.canonical = item.canonical; + this.next = item.next; + this.prev = item.prev; + + this.cfiBase = item.cfiBase; + + if (hooks) { + this.hooks = hooks; + } else { + this.hooks = {}; + this.hooks.serialize = new _hook2.default(this); + this.hooks.content = new _hook2.default(this); + } + + this.document = undefined; + this.contents = undefined; + this.output = undefined; + } + + /** + * Load the section from its url + * @param {method} [_request] a request method to use for loading + * @return {document} a promise with the xml document + */ + + + _createClass(Section, [{ + key: "load", + value: function load(_request) { + var request = _request || this.request || __webpack_require__(9); + var loading = new _core.defer(); + var loaded = loading.promise; + + if (this.contents) { + loading.resolve(this.contents); + } else { + request(this.url).then(function (xml) { + // var directory = new Url(this.url).directory; + + this.document = xml; + this.contents = xml.documentElement; + + return this.hooks.content.trigger(this.document, this); + }.bind(this)).then(function () { + loading.resolve(this.contents); + }.bind(this)).catch(function (error) { + loading.reject(error); + }); + } + + return loaded; + } + + /** + * Adds a base tag for resolving urls in the section + * @private + */ + + }, { + key: "base", + value: function base() { + return (0, _replacements.replaceBase)(this.document, this); + } + + /** + * Render the contents of a section + * @param {method} [_request] a request method to use for loading + * @return {string} output a serialized XML Document + */ + + }, { + key: "render", + value: function render(_request) { + var rendering = new _core.defer(); + var rendered = rendering.promise; + this.output; // TODO: better way to return this from hooks? + + this.load(_request).then(function (contents) { + var userAgent = typeof navigator !== 'undefined' && navigator.userAgent || ''; + var isIE = userAgent.indexOf('Trident') >= 0; + var Serializer; + if (typeof XMLSerializer === "undefined" || isIE) { + Serializer = __webpack_require__(45).XMLSerializer; + } else { + Serializer = XMLSerializer; + } + var serializer = new Serializer(); + this.output = serializer.serializeToString(contents); + return this.output; + }.bind(this)).then(function () { + return this.hooks.serialize.trigger(this.output, this); + }.bind(this)).then(function () { + rendering.resolve(this.output); + }.bind(this)).catch(function (error) { + rendering.reject(error); + }); + + return rendered; + } + + /** + * Find a string in a section + * @param {string} _query The query string to find + * @return {object[]} A list of matches, with form {cfi, excerpt} + */ + + }, { + key: "find", + value: function find(_query) { + var section = this; + var matches = []; + var query = _query.toLowerCase(); + var find = function find(node) { + var text = node.textContent.toLowerCase(); + var range = section.document.createRange(); + var cfi; + var pos; + var last = -1; + var excerpt; + var limit = 150; + + while (pos != -1) { + // Search for the query + pos = text.indexOf(query, last + 1); + + if (pos != -1) { + // We found it! Generate a CFI + range = section.document.createRange(); + range.setStart(node, pos); + range.setEnd(node, pos + query.length); + + cfi = section.cfiFromRange(range); + + // Generate the excerpt + if (node.textContent.length < limit) { + excerpt = node.textContent; + } else { + excerpt = node.textContent.substring(pos - limit / 2, pos + limit / 2); + excerpt = "..." + excerpt + "..."; + } + + // Add the CFI to the matches list + matches.push({ + cfi: cfi, + excerpt: excerpt + }); + } + + last = pos; + } + }; + + (0, _core.sprint)(section.document, function (node) { + find(node); + }); + + return matches; + } + }, { + key: "reconcileLayoutSettings", + + + /** + * Reconciles the current chapters layout properies with + * the global layout properities. + * @param {object} globalLayout The global layout settings object, chapter properties string + * @return {object} layoutProperties Object with layout properties + */ + value: function reconcileLayoutSettings(globalLayout) { + //-- Get the global defaults + var settings = { + layout: globalLayout.layout, + spread: globalLayout.spread, + orientation: globalLayout.orientation + }; + + //-- Get the chapter's display type + this.properties.forEach(function (prop) { + var rendition = prop.replace("rendition:", ""); + var split = rendition.indexOf("-"); + var property, value; + + if (split != -1) { + property = rendition.slice(0, split); + value = rendition.slice(split + 1); + + settings[property] = value; + } + }); + return settings; + } + + /** + * Get a CFI from a Range in the Section + * @param {range} _range + * @return {string} cfi an EpubCFI string + */ + + }, { + key: "cfiFromRange", + value: function cfiFromRange(_range) { + return new _epubcfi2.default(_range, this.cfiBase).toString(); + } + + /** + * Get a CFI from an Element in the Section + * @param {element} el + * @return {string} cfi an EpubCFI string + */ + + }, { + key: "cfiFromElement", + value: function cfiFromElement(el) { + return new _epubcfi2.default(el, this.cfiBase).toString(); + } + + /** + * Unload the section document + */ + + }, { + key: "unload", + value: function unload() { + this.document = undefined; + this.contents = undefined; + this.output = undefined; + } + }, { + key: "destroy", + value: function destroy() { + this.unload(); + this.hooks.serialize.clear(); + this.hooks.content.clear(); + + this.hooks = undefined; + this.idref = undefined; + this.linear = undefined; + this.properties = undefined; + this.index = undefined; + this.href = undefined; + this.url = undefined; + this.next = undefined; + this.prev = undefined; + + this.cfiBase = undefined; + } + }]); + + return Section; +}(); + +exports.default = Section; +module.exports = exports["default"]; + +/***/ }), +/* 45 */ +/***/ (function(module, exports, __webpack_require__) { + +function DOMParser(options){ + this.options = options ||{locator:{}}; + +} +DOMParser.prototype.parseFromString = function(source,mimeType){ + var options = this.options; + var sax = new XMLReader(); + var domBuilder = options.domBuilder || new DOMHandler();//contentHandler and LexicalHandler + var errorHandler = options.errorHandler; + var locator = options.locator; + var defaultNSMap = options.xmlns||{}; + var entityMap = {'lt':'<','gt':'>','amp':'&','quot':'"','apos':"'"} + if(locator){ + domBuilder.setDocumentLocator(locator) + } + + sax.errorHandler = buildErrorHandler(errorHandler,domBuilder,locator); + sax.domBuilder = options.domBuilder || domBuilder; + if(/\/x?html?$/.test(mimeType)){ + entityMap.nbsp = '\xa0'; + entityMap.copy = '\xa9'; + defaultNSMap['']= 'http://www.w3.org/1999/xhtml'; + } + defaultNSMap.xml = defaultNSMap.xml || 'http://www.w3.org/XML/1998/namespace'; + if(source){ + sax.parse(source,defaultNSMap,entityMap); + }else{ + sax.errorHandler.error("invalid doc source"); + } + return domBuilder.doc; +} +function buildErrorHandler(errorImpl,domBuilder,locator){ + if(!errorImpl){ + if(domBuilder instanceof DOMHandler){ + return domBuilder; + } + errorImpl = domBuilder ; + } + var errorHandler = {} + var isCallback = errorImpl instanceof Function; + locator = locator||{} + function build(key){ + var fn = errorImpl[key]; + if(!fn && isCallback){ + fn = errorImpl.length == 2?function(msg){errorImpl(key,msg)}:errorImpl; + } + errorHandler[key] = fn && function(msg){ + fn('[xmldom '+key+']\t'+msg+_locator(locator)); + }||function(){}; + } + build('warning'); + build('error'); + build('fatalError'); + return errorHandler; +} + +//console.log('#\n\n\n\n\n\n\n####') +/** + * +ContentHandler+ErrorHandler + * +LexicalHandler+EntityResolver2 + * -DeclHandler-DTDHandler + * + * DefaultHandler:EntityResolver, DTDHandler, ContentHandler, ErrorHandler + * DefaultHandler2:DefaultHandler,LexicalHandler, DeclHandler, EntityResolver2 + * @link http://www.saxproject.org/apidoc/org/xml/sax/helpers/DefaultHandler.html + */ +function DOMHandler() { + this.cdata = false; +} +function position(locator,node){ + node.lineNumber = locator.lineNumber; + node.columnNumber = locator.columnNumber; +} +/** + * @see org.xml.sax.ContentHandler#startDocument + * @link http://www.saxproject.org/apidoc/org/xml/sax/ContentHandler.html + */ +DOMHandler.prototype = { + startDocument : function() { + this.doc = new DOMImplementation().createDocument(null, null, null); + if (this.locator) { + this.doc.documentURI = this.locator.systemId; + } + }, + startElement:function(namespaceURI, localName, qName, attrs) { + var doc = this.doc; + var el = doc.createElementNS(namespaceURI, qName||localName); + var len = attrs.length; + appendElement(this, el); + this.currentElement = el; + + this.locator && position(this.locator,el) + for (var i = 0 ; i < len; i++) { + var namespaceURI = attrs.getURI(i); + var value = attrs.getValue(i); + var qName = attrs.getQName(i); + var attr = doc.createAttributeNS(namespaceURI, qName); + this.locator &&position(attrs.getLocator(i),attr); + attr.value = attr.nodeValue = value; + el.setAttributeNode(attr) + } + }, + endElement:function(namespaceURI, localName, qName) { + var current = this.currentElement + var tagName = current.tagName; + this.currentElement = current.parentNode; + }, + startPrefixMapping:function(prefix, uri) { + }, + endPrefixMapping:function(prefix) { + }, + processingInstruction:function(target, data) { + var ins = this.doc.createProcessingInstruction(target, data); + this.locator && position(this.locator,ins) + appendElement(this, ins); + }, + ignorableWhitespace:function(ch, start, length) { + }, + characters:function(chars, start, length) { + chars = _toString.apply(this,arguments) + //console.log(chars) + if(chars){ + if (this.cdata) { + var charNode = this.doc.createCDATASection(chars); + } else { + var charNode = this.doc.createTextNode(chars); + } + if(this.currentElement){ + this.currentElement.appendChild(charNode); + }else if(/^\s*$/.test(chars)){ + this.doc.appendChild(charNode); + //process xml + } + this.locator && position(this.locator,charNode) + } + }, + skippedEntity:function(name) { + }, + endDocument:function() { + this.doc.normalize(); + }, + setDocumentLocator:function (locator) { + if(this.locator = locator){// && !('lineNumber' in locator)){ + locator.lineNumber = 0; + } + }, + //LexicalHandler + comment:function(chars, start, length) { + chars = _toString.apply(this,arguments) + var comm = this.doc.createComment(chars); + this.locator && position(this.locator,comm) + appendElement(this, comm); + }, + + startCDATA:function() { + //used in characters() methods + this.cdata = true; + }, + endCDATA:function() { + this.cdata = false; + }, + + startDTD:function(name, publicId, systemId) { + var impl = this.doc.implementation; + if (impl && impl.createDocumentType) { + var dt = impl.createDocumentType(name, publicId, systemId); + this.locator && position(this.locator,dt) + appendElement(this, dt); + } + }, + /** + * @see org.xml.sax.ErrorHandler + * @link http://www.saxproject.org/apidoc/org/xml/sax/ErrorHandler.html + */ + warning:function(error) { + console.warn('[xmldom warning]\t'+error,_locator(this.locator)); + }, + error:function(error) { + console.error('[xmldom error]\t'+error,_locator(this.locator)); + }, + fatalError:function(error) { + console.error('[xmldom fatalError]\t'+error,_locator(this.locator)); + throw error; + } +} +function _locator(l){ + if(l){ + return '\n@'+(l.systemId ||'')+'#[line:'+l.lineNumber+',col:'+l.columnNumber+']' + } +} +function _toString(chars,start,length){ + if(typeof chars == 'string'){ + return chars.substr(start,length) + }else{//java sax connect width xmldom on rhino(what about: "? && !(chars instanceof String)") + if(chars.length >= start+length || start){ + return new java.lang.String(chars,start,length)+''; + } + return chars; + } +} + +/* + * @link http://www.saxproject.org/apidoc/org/xml/sax/ext/LexicalHandler.html + * used method of org.xml.sax.ext.LexicalHandler: + * #comment(chars, start, length) + * #startCDATA() + * #endCDATA() + * #startDTD(name, publicId, systemId) + * + * + * IGNORED method of org.xml.sax.ext.LexicalHandler: + * #endDTD() + * #startEntity(name) + * #endEntity(name) + * + * + * @link http://www.saxproject.org/apidoc/org/xml/sax/ext/DeclHandler.html + * IGNORED method of org.xml.sax.ext.DeclHandler + * #attributeDecl(eName, aName, type, mode, value) + * #elementDecl(name, model) + * #externalEntityDecl(name, publicId, systemId) + * #internalEntityDecl(name, value) + * @link http://www.saxproject.org/apidoc/org/xml/sax/ext/EntityResolver2.html + * IGNORED method of org.xml.sax.EntityResolver2 + * #resolveEntity(String name,String publicId,String baseURI,String systemId) + * #resolveEntity(publicId, systemId) + * #getExternalSubset(name, baseURI) + * @link http://www.saxproject.org/apidoc/org/xml/sax/DTDHandler.html + * IGNORED method of org.xml.sax.DTDHandler + * #notationDecl(name, publicId, systemId) {}; + * #unparsedEntityDecl(name, publicId, systemId, notationName) {}; + */ +"endDTD,startEntity,endEntity,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,resolveEntity,getExternalSubset,notationDecl,unparsedEntityDecl".replace(/\w+/g,function(key){ + DOMHandler.prototype[key] = function(){return null} +}) + +/* Private static helpers treated below as private instance methods, so don't need to add these to the public API; we might use a Relator to also get rid of non-standard public properties */ +function appendElement (hander,node) { + if (!hander.currentElement) { + hander.doc.appendChild(node); + } else { + hander.currentElement.appendChild(node); + } +}//appendChild and setAttributeNS are preformance key + +//if(typeof require == 'function'){ + var XMLReader = __webpack_require__(46).XMLReader; + var DOMImplementation = exports.DOMImplementation = __webpack_require__(17).DOMImplementation; + exports.XMLSerializer = __webpack_require__(17).XMLSerializer ; + exports.DOMParser = DOMParser; +//} + + +/***/ }), +/* 46 */ +/***/ (function(module, exports) { + +//[4] NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] +//[4a] NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] +//[5] Name ::= NameStartChar (NameChar)* +var nameStartChar = /[A-Z_a-z\xC0-\xD6\xD8-\xF6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]///\u10000-\uEFFFF +var nameChar = new RegExp("[\\-\\.0-9"+nameStartChar.source.slice(1,-1)+"\\u00B7\\u0300-\\u036F\\u203F-\\u2040]"); +var tagNamePattern = new RegExp('^'+nameStartChar.source+nameChar.source+'*(?:\:'+nameStartChar.source+nameChar.source+'*)?$'); +//var tagNamePattern = /^[a-zA-Z_][\w\-\.]*(?:\:[a-zA-Z_][\w\-\.]*)?$/ +//var handlers = 'resolveEntity,getExternalSubset,characters,endDocument,endElement,endPrefixMapping,ignorableWhitespace,processingInstruction,setDocumentLocator,skippedEntity,startDocument,startElement,startPrefixMapping,notationDecl,unparsedEntityDecl,error,fatalError,warning,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,comment,endCDATA,endDTD,endEntity,startCDATA,startDTD,startEntity'.split(',') + +//S_TAG, S_ATTR, S_EQ, S_ATTR_NOQUOT_VALUE +//S_ATTR_SPACE, S_ATTR_END, S_TAG_SPACE, S_TAG_CLOSE +var S_TAG = 0;//tag name offerring +var S_ATTR = 1;//attr name offerring +var S_ATTR_SPACE=2;//attr name end and space offer +var S_EQ = 3;//=space? +var S_ATTR_NOQUOT_VALUE = 4;//attr value(no quot value only) +var S_ATTR_END = 5;//attr value end and no space(quot end) +var S_TAG_SPACE = 6;//(attr value end || tag end ) && (space offer) +var S_TAG_CLOSE = 7;//closed el + +function XMLReader(){ + +} + +XMLReader.prototype = { + parse:function(source,defaultNSMap,entityMap){ + var domBuilder = this.domBuilder; + domBuilder.startDocument(); + _copy(defaultNSMap ,defaultNSMap = {}) + parse(source,defaultNSMap,entityMap, + domBuilder,this.errorHandler); + domBuilder.endDocument(); + } +} +function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ + function fixedFromCharCode(code) { + // String.prototype.fromCharCode does not supports + // > 2 bytes unicode chars directly + if (code > 0xffff) { + code -= 0x10000; + var surrogate1 = 0xd800 + (code >> 10) + , surrogate2 = 0xdc00 + (code & 0x3ff); + + return String.fromCharCode(surrogate1, surrogate2); + } else { + return String.fromCharCode(code); + } + } + function entityReplacer(a){ + var k = a.slice(1,-1); + if(k in entityMap){ + return entityMap[k]; + }else if(k.charAt(0) === '#'){ + return fixedFromCharCode(parseInt(k.substr(1).replace('x','0x'))) + }else{ + errorHandler.error('entity not found:'+a); + return a; + } + } + function appendText(end){//has some bugs + if(end>start){ + var xt = source.substring(start,end).replace(/&#?\w+;/g,entityReplacer); + locator&&position(start); + domBuilder.characters(xt,0,end-start); + start = end + } + } + function position(p,m){ + while(p>=lineEnd && (m = linePattern.exec(source))){ + lineStart = m.index; + lineEnd = lineStart + m[0].length; + locator.lineNumber++; + //console.log('line++:',locator,startPos,endPos) + } + locator.columnNumber = p-lineStart+1; + } + var lineStart = 0; + var lineEnd = 0; + var linePattern = /.*(?:\r\n?|\n)|.*$/g + var locator = domBuilder.locator; + + var parseStack = [{currentNSMap:defaultNSMapCopy}] + var closeMap = {}; + var start = 0; + while(true){ + try{ + var tagStart = source.indexOf('<',start); + if(tagStart<0){ + if(!source.substr(start).match(/^\s*$/)){ + var doc = domBuilder.doc; + var text = doc.createTextNode(source.substr(start)); + doc.appendChild(text); + domBuilder.currentElement = text; + } + return; + } + if(tagStart>start){ + appendText(tagStart); + } + switch(source.charAt(tagStart+1)){ + case '/': + var end = source.indexOf('>',tagStart+3); + var tagName = source.substring(tagStart+2,end); + var config = parseStack.pop(); + if(end<0){ + + tagName = source.substring(tagStart+2).replace(/[\s<].*/,''); + //console.error('#@@@@@@'+tagName) + errorHandler.error("end tag name: "+tagName+' is not complete:'+config.tagName); + end = tagStart+1+tagName.length; + }else if(tagName.match(/\s + locator&&position(tagStart); + end = parseInstruction(source,tagStart,domBuilder); + break; + case '!':// start){ + start = end; + }else{ + //TODO: 这里有可能sax回退,有位置错误风险 + appendText(Math.max(tagStart,start)+1); + } + } +} +function copyLocator(f,t){ + t.lineNumber = f.lineNumber; + t.columnNumber = f.columnNumber; + return t; +} + +/** + * @see #appendElement(source,elStartEnd,el,selfClosed,entityReplacer,domBuilder,parseStack); + * @return end of the elementStartPart(end of elementEndPart for selfClosed el) + */ +function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,errorHandler){ + var attrName; + var value; + var p = ++start; + var s = S_TAG;//status + while(true){ + var c = source.charAt(p); + switch(c){ + case '=': + if(s === S_ATTR){//attrName + attrName = source.slice(start,p); + s = S_EQ; + }else if(s === S_ATTR_SPACE){ + s = S_EQ; + }else{ + //fatalError: equal must after attrName or space after attrName + throw new Error('attribute equal must after attrName'); + } + break; + case '\'': + case '"': + if(s === S_EQ || s === S_ATTR //|| s == S_ATTR_SPACE + ){//equal + if(s === S_ATTR){ + errorHandler.warning('attribute value must after "="') + attrName = source.slice(start,p) + } + start = p+1; + p = source.indexOf(c,start) + if(p>0){ + value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer); + el.add(attrName,value,start-1); + s = S_ATTR_END; + }else{ + //fatalError: no end quot match + throw new Error('attribute value no end \''+c+'\' match'); + } + }else if(s == S_ATTR_NOQUOT_VALUE){ + value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer); + //console.log(attrName,value,start,p) + el.add(attrName,value,start); + //console.dir(el) + errorHandler.warning('attribute "'+attrName+'" missed start quot('+c+')!!'); + start = p+1; + s = S_ATTR_END + }else{ + //fatalError: no equal before + throw new Error('attribute value must after "="'); + } + break; + case '/': + switch(s){ + case S_TAG: + el.setTagName(source.slice(start,p)); + case S_ATTR_END: + case S_TAG_SPACE: + case S_TAG_CLOSE: + s =S_TAG_CLOSE; + el.closed = true; + case S_ATTR_NOQUOT_VALUE: + case S_ATTR: + case S_ATTR_SPACE: + break; + //case S_EQ: + default: + throw new Error("attribute invalid close char('/')") + } + break; + case ''://end document + //throw new Error('unexpected end of input') + errorHandler.error('unexpected end of input'); + if(s == S_TAG){ + el.setTagName(source.slice(start,p)); + } + return p; + case '>': + switch(s){ + case S_TAG: + el.setTagName(source.slice(start,p)); + case S_ATTR_END: + case S_TAG_SPACE: + case S_TAG_CLOSE: + break;//normal + case S_ATTR_NOQUOT_VALUE://Compatible state + case S_ATTR: + value = source.slice(start,p); + if(value.slice(-1) === '/'){ + el.closed = true; + value = value.slice(0,-1) + } + case S_ATTR_SPACE: + if(s === S_ATTR_SPACE){ + value = attrName; + } + if(s == S_ATTR_NOQUOT_VALUE){ + errorHandler.warning('attribute "'+value+'" missed quot(")!!'); + el.add(attrName,value.replace(/&#?\w+;/g,entityReplacer),start) + }else{ + if(currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !value.match(/^(?:disabled|checked|selected)$/i)){ + errorHandler.warning('attribute "'+value+'" missed value!! "'+value+'" instead!!') + } + el.add(value,value,start) + } + break; + case S_EQ: + throw new Error('attribute value missed!!'); + } +// console.log(tagName,tagNamePattern,tagNamePattern.test(tagName)) + return p; + /*xml space '\x20' | #x9 | #xD | #xA; */ + case '\u0080': + c = ' '; + default: + if(c<= ' '){//space + switch(s){ + case S_TAG: + el.setTagName(source.slice(start,p));//tagName + s = S_TAG_SPACE; + break; + case S_ATTR: + attrName = source.slice(start,p) + s = S_ATTR_SPACE; + break; + case S_ATTR_NOQUOT_VALUE: + var value = source.slice(start,p).replace(/&#?\w+;/g,entityReplacer); + errorHandler.warning('attribute "'+value+'" missed quot(")!!'); + el.add(attrName,value,start) + case S_ATTR_END: + s = S_TAG_SPACE; + break; + //case S_TAG_SPACE: + //case S_EQ: + //case S_ATTR_SPACE: + // void();break; + //case S_TAG_CLOSE: + //ignore warning + } + }else{//not space +//S_TAG, S_ATTR, S_EQ, S_ATTR_NOQUOT_VALUE +//S_ATTR_SPACE, S_ATTR_END, S_TAG_SPACE, S_TAG_CLOSE + switch(s){ + //case S_TAG:void();break; + //case S_ATTR:void();break; + //case S_ATTR_NOQUOT_VALUE:void();break; + case S_ATTR_SPACE: + var tagName = el.tagName; + if(currentNSMap[''] !== 'http://www.w3.org/1999/xhtml' || !attrName.match(/^(?:disabled|checked|selected)$/i)){ + errorHandler.warning('attribute "'+attrName+'" missed value!! "'+attrName+'" instead2!!') + } + el.add(attrName,attrName,start); + start = p; + s = S_ATTR; + break; + case S_ATTR_END: + errorHandler.warning('attribute space is required"'+attrName+'"!!') + case S_TAG_SPACE: + s = S_ATTR; + start = p; + break; + case S_EQ: + s = S_ATTR_NOQUOT_VALUE; + start = p; + break; + case S_TAG_CLOSE: + throw new Error("elements closed character '/' and '>' must be connected to"); + } + } + }//end outer switch + //console.log('p++',p) + p++; + } +} +/** + * @return true if has new namespace define + */ +function appendElement(el,domBuilder,currentNSMap){ + var tagName = el.tagName; + var localNSMap = null; + //var currentNSMap = parseStack[parseStack.length-1].currentNSMap; + var i = el.length; + while(i--){ + var a = el[i]; + var qName = a.qName; + var value = a.value; + var nsp = qName.indexOf(':'); + if(nsp>0){ + var prefix = a.prefix = qName.slice(0,nsp); + var localName = qName.slice(nsp+1); + var nsPrefix = prefix === 'xmlns' && localName + }else{ + localName = qName; + prefix = null + nsPrefix = qName === 'xmlns' && '' + } + //can not set prefix,because prefix !== '' + a.localName = localName ; + //prefix == null for no ns prefix attribute + if(nsPrefix !== false){//hack!! + if(localNSMap == null){ + localNSMap = {} + //console.log(currentNSMap,0) + _copy(currentNSMap,currentNSMap={}) + //console.log(currentNSMap,1) + } + currentNSMap[nsPrefix] = localNSMap[nsPrefix] = value; + a.uri = 'http://www.w3.org/2000/xmlns/' + domBuilder.startPrefixMapping(nsPrefix, value) + } + } + var i = el.length; + while(i--){ + a = el[i]; + var prefix = a.prefix; + if(prefix){//no prefix attribute has no namespace + if(prefix === 'xml'){ + a.uri = 'http://www.w3.org/XML/1998/namespace'; + }if(prefix !== 'xmlns'){ + a.uri = currentNSMap[prefix || ''] + + //{console.log('###'+a.qName,domBuilder.locator.systemId+'',currentNSMap,a.uri)} + } + } + } + var nsp = tagName.indexOf(':'); + if(nsp>0){ + prefix = el.prefix = tagName.slice(0,nsp); + localName = el.localName = tagName.slice(nsp+1); + }else{ + prefix = null;//important!! + localName = el.localName = tagName; + } + //no prefix element has default namespace + var ns = el.uri = currentNSMap[prefix || '']; + domBuilder.startElement(ns,localName,tagName,el); + //endPrefixMapping and startPrefixMapping have not any help for dom builder + //localNSMap = null + if(el.closed){ + domBuilder.endElement(ns,localName,tagName); + if(localNSMap){ + for(prefix in localNSMap){ + domBuilder.endPrefixMapping(prefix) + } + } + }else{ + el.currentNSMap = currentNSMap; + el.localNSMap = localNSMap; + //parseStack.push(el); + return true; + } +} +function parseHtmlSpecialContent(source,elStartEnd,tagName,entityReplacer,domBuilder){ + if(/^(?:script|textarea)$/i.test(tagName)){ + var elEndStart = source.indexOf('',elStartEnd); + var text = source.substring(elStartEnd+1,elEndStart); + if(/[&<]/.test(text)){ + if(/^script$/i.test(tagName)){ + //if(!/\]\]>/.test(text)){ + //lexHandler.startCDATA(); + domBuilder.characters(text,0,text.length); + //lexHandler.endCDATA(); + return elEndStart; + //} + }//}else{//text area + text = text.replace(/&#?\w+;/g,entityReplacer); + domBuilder.characters(text,0,text.length); + return elEndStart; + //} + + } + } + return elStartEnd+1; +} +function fixSelfClosed(source,elStartEnd,tagName,closeMap){ + //if(tagName in closeMap){ + var pos = closeMap[tagName]; + if(pos == null){ + //console.log(tagName) + pos = source.lastIndexOf('') + if(pos',start+4); + //append comment source.substring(4,end)// https://davidwalsh.name/add-rules-stylesheets + style.appendChild(document.createTextNode("")); + + document.head.appendChild(style); + + return style.sheet; + } + }, { + key: "addStyleRules", + value: function addStyleRules(selector, rulesArray) { + var scope = "#" + this.id + " "; + var rules = ""; + + if (!this.sheet) { + this.sheet = this.getSheet(); + } + + rulesArray.forEach(function (set) { + for (var prop in set) { + if (set.hasOwnProperty(prop)) { + rules += prop + ":" + set[prop] + ";"; + } + } + }); + + this.sheet.insertRule(scope + selector + " {" + rules + "}", 0); + } + }, { + key: "axis", + value: function axis(_axis) { + if (_axis === "horizontal") { + this.container.style.display = "flex"; + this.container.style.flexDirection = "row"; + this.container.style.flexWrap = "nowrap"; + } else { + this.container.style.display = "block"; + } + this.settings.axis = _axis; + } + + // orientation(orientation) { + // if (orientation === "landscape") { + // + // } else { + // + // } + // + // this.orientation = orientation; + // } + + }, { + key: "direction", + value: function direction(dir) { + if (this.container) { + this.container.dir = dir; + this.container.style["direction"] = dir; + } + + if (this.settings.fullsize) { + document.body.style["direction"] = dir; + } + this.settings.dir = dir; + } + }, { + key: "overflow", + value: function overflow(_overflow) { + if (this.container) { + if (_overflow === "scroll" && this.settings.axis === "vertical") { + this.container.style["overflow-y"] = _overflow; + this.container.style["overflow-x"] = "hidden"; + } else if (_overflow === "scroll" && this.settings.axis === "horizontal") { + this.container.style["overflow-y"] = "hidden"; + this.container.style["overflow-x"] = _overflow; + } else { + this.container.style["overflow"] = _overflow; + } + } + this.settings.overflow = _overflow; + } + }, { + key: "destroy", + value: function destroy() { + var base; + + if (this.element) { + + if (this.settings.hidden) { + base = this.wrapper; + } else { + base = this.container; + } + + if (this.element.contains(this.container)) { + this.element.removeChild(this.container); + } + + window.removeEventListener("resize", this.resizeFunc); + window.removeEventListener("orientationChange", this.orientationChangeFunc); + } + } + }]); + + return Stage; +}(); + +exports.default = Stage; +module.exports = exports["default"]; + +/***/ }), +/* 60 */ +/***/ (function(module, exports, __webpack_require__) { + +var debounce = __webpack_require__(21), + isObject = __webpack_require__(16); + +/** Error message constants. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed `func` invocations and a `flush` method to + * immediately invoke them. Provide `options` to indicate whether `func` + * should be invoked on the leading and/or trailing edge of the `wait` + * timeout. The `func` is invoked with the last arguments provided to the + * throttled function. Subsequent calls to the throttled function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the throttled function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=true] + * Specify invoking on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // Avoid excessively updating the position while scrolling. + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. + * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); + * jQuery(element).on('click', throttled); + * + * // Cancel the trailing throttled invocation. + * jQuery(window).on('popstate', throttled.cancel); + */ +function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (isObject(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce(func, wait, { + 'leading': leading, + 'maxWait': wait, + 'trailing': trailing + }); +} + +module.exports = throttle; + + +/***/ }), +/* 61 */ +/***/ (function(module, exports, __webpack_require__) { + +var root = __webpack_require__(22); + +/** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ +var now = function() { + return root.Date.now(); +}; + +module.exports = now; + + +/***/ }), +/* 62 */ +/***/ (function(module, exports, __webpack_require__) { + +/* WEBPACK VAR INJECTION */(function(global) {/** Detect free variable `global` from Node.js. */ +var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + +module.exports = freeGlobal; + +/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(5))) + +/***/ }), +/* 63 */ +/***/ (function(module, exports, __webpack_require__) { + +var isObject = __webpack_require__(16), + isSymbol = __webpack_require__(64); + +/** Used as references for various `Number` constants. */ +var NAN = 0 / 0; + +/** Used to match leading and trailing whitespace. */ +var reTrim = /^\s+|\s+$/g; + +/** Used to detect bad signed hexadecimal string values. */ +var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; + +/** Used to detect binary string values. */ +var reIsBinary = /^0b[01]+$/i; + +/** Used to detect octal string values. */ +var reIsOctal = /^0o[0-7]+$/i; + +/** Built-in method references without a dependency on `root`. */ +var freeParseInt = parseInt; + +/** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ +function toNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + if (isObject(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = value.replace(reTrim, ''); + var isBinary = reIsBinary.test(value); + return (isBinary || reIsOctal.test(value)) + ? freeParseInt(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex.test(value) ? NAN : +value); +} + +module.exports = toNumber; + + +/***/ }), +/* 64 */ +/***/ (function(module, exports, __webpack_require__) { + +var baseGetTag = __webpack_require__(65), + isObjectLike = __webpack_require__(68); + +/** `Object#toString` result references. */ +var symbolTag = '[object Symbol]'; + +/** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ +function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && baseGetTag(value) == symbolTag); +} + +module.exports = isSymbol; + + +/***/ }), +/* 65 */ +/***/ (function(module, exports, __webpack_require__) { + +var Symbol = __webpack_require__(23), + getRawTag = __webpack_require__(66), + objectToString = __webpack_require__(67); + +/** `Object#toString` result references. */ +var nullTag = '[object Null]', + undefinedTag = '[object Undefined]'; + +/** Built-in value references. */ +var symToStringTag = Symbol ? Symbol.toStringTag : undefined; + +/** + * The base implementation of `getTag` without fallbacks for buggy environments. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +function baseGetTag(value) { + if (value == null) { + return value === undefined ? undefinedTag : nullTag; + } + return (symToStringTag && symToStringTag in Object(value)) + ? getRawTag(value) + : objectToString(value); +} + +module.exports = baseGetTag; + + +/***/ }), +/* 66 */ +/***/ (function(module, exports, __webpack_require__) { + +var Symbol = __webpack_require__(23); + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString = objectProto.toString; + +/** Built-in value references. */ +var symToStringTag = Symbol ? Symbol.toStringTag : undefined; + +/** + * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the raw `toStringTag`. + */ +function getRawTag(value) { + var isOwn = hasOwnProperty.call(value, symToStringTag), + tag = value[symToStringTag]; + + try { + value[symToStringTag] = undefined; + var unmasked = true; + } catch (e) {} + + var result = nativeObjectToString.call(value); + if (unmasked) { + if (isOwn) { + value[symToStringTag] = tag; + } else { + delete value[symToStringTag]; + } + } + return result; +} + +module.exports = getRawTag; + + +/***/ }), +/* 67 */ +/***/ (function(module, exports) { + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString = objectProto.toString; + +/** + * Converts `value` to a string using `Object.prototype.toString`. + * + * @private + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + */ +function objectToString(value) { + return nativeObjectToString.call(value); +} + +module.exports = objectToString; + + +/***/ }), +/* 68 */ +/***/ (function(module, exports) { + +/** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ +function isObjectLike(value) { + return value != null && typeof value == 'object'; +} + +module.exports = isObjectLike; + + +/***/ }), +/* 69 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Views = function () { + function Views(container) { + _classCallCheck(this, Views); + + this.container = container; + this._views = []; + this.length = 0; + this.hidden = false; + } + + _createClass(Views, [{ + key: "all", + value: function all() { + return this._views; + } + }, { + key: "first", + value: function first() { + return this._views[0]; + } + }, { + key: "last", + value: function last() { + return this._views[this._views.length - 1]; + } + }, { + key: "indexOf", + value: function indexOf(view) { + return this._views.indexOf(view); + } + }, { + key: "slice", + value: function slice() { + return this._views.slice.apply(this._views, arguments); + } + }, { + key: "get", + value: function get(i) { + return this._views[i]; + } + }, { + key: "append", + value: function append(view) { + this._views.push(view); + if (this.container) { + this.container.appendChild(view.element); + } + this.length++; + return view; + } + }, { + key: "prepend", + value: function prepend(view) { + this._views.unshift(view); + if (this.container) { + this.container.insertBefore(view.element, this.container.firstChild); + } + this.length++; + return view; + } + }, { + key: "insert", + value: function insert(view, index) { + this._views.splice(index, 0, view); + + if (this.container) { + if (index < this.container.children.length) { + this.container.insertBefore(view.element, this.container.children[index]); + } else { + this.container.appendChild(view.element); + } + } + + this.length++; + return view; + } + }, { + key: "remove", + value: function remove(view) { + var index = this._views.indexOf(view); + + if (index > -1) { + this._views.splice(index, 1); + } + + this.destroy(view); + + this.length--; + } + }, { + key: "destroy", + value: function destroy(view) { + if (view.displayed) { + view.destroy(); + } + + if (this.container) { + this.container.removeChild(view.element); + } + view = null; + } + + // Iterators + + }, { + key: "forEach", + value: function forEach() { + return this._views.forEach.apply(this._views, arguments); + } + }, { + key: "clear", + value: function clear() { + // Remove all views + var view; + var len = this.length; + + if (!this.length) return; + + for (var i = 0; i < len; i++) { + view = this._views[i]; + this.destroy(view); + } + + this._views = []; + this.length = 0; + } + }, { + key: "find", + value: function find(section) { + + var view; + var len = this.length; + + for (var i = 0; i < len; i++) { + view = this._views[i]; + if (view.displayed && view.section.index == section.index) { + return view; + } + } + } + }, { + key: "displayed", + value: function displayed() { + var displayed = []; + var view; + var len = this.length; + + for (var i = 0; i < len; i++) { + view = this._views[i]; + if (view.displayed) { + displayed.push(view); + } + } + return displayed; + } + }, { + key: "show", + value: function show() { + var view; + var len = this.length; + + for (var i = 0; i < len; i++) { + view = this._views[i]; + if (view.displayed) { + view.show(); + } + } + this.hidden = false; + } + }, { + key: "hide", + value: function hide() { + var view; + var len = this.length; + + for (var i = 0; i < len; i++) { + view = this._views[i]; + if (view.displayed) { + view.hide(); + } + } + this.hidden = true; + } + }]); + + return Views; +}(); + +exports.default = Views; +module.exports = exports["default"]; + +/***/ }), +/* 70 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _core = __webpack_require__(0); + +var _constants = __webpack_require__(2); + +var _eventEmitter = __webpack_require__(3); + +var _eventEmitter2 = _interopRequireDefault(_eventEmitter); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +// easing equations from https://github.com/danro/easing-js/blob/master/easing.js +var PI_D2 = Math.PI / 2; +var EASING_EQUATIONS = { + easeOutSine: function easeOutSine(pos) { + return Math.sin(pos * PI_D2); + }, + easeInOutSine: function easeInOutSine(pos) { + return -0.5 * (Math.cos(Math.PI * pos) - 1); + }, + easeInOutQuint: function easeInOutQuint(pos) { + if ((pos /= 0.5) < 1) { + return 0.5 * Math.pow(pos, 5); + } + return 0.5 * (Math.pow(pos - 2, 5) + 2); + }, + easeInCubic: function easeInCubic(pos) { + return Math.pow(pos, 3); + } +}; + +var Snap = function () { + function Snap(manager, options) { + _classCallCheck(this, Snap); + + this.settings = (0, _core.extend)({ + duration: 80, + minVelocity: 0.2, + minDistance: 10, + easing: EASING_EQUATIONS['easeInCubic'] + }, options || {}); + + this.supportsTouch = this.supportsTouch(); + + if (this.supportsTouch) { + this.setup(manager); + } + } + + _createClass(Snap, [{ + key: "setup", + value: function setup(manager) { + this.manager = manager; + + this.layout = this.manager.layout; + + this.fullsize = this.manager.settings.fullsize; + if (this.fullsize) { + this.element = this.manager.stage.element; + this.scroller = window; + this.disableScroll(); + } else { + this.element = this.manager.stage.container; + this.scroller = this.element; + this.element.style["WebkitOverflowScrolling"] = "touch"; + } + + // this.overflow = this.manager.overflow; + + // set lookahead offset to page width + this.manager.settings.offset = this.layout.width; + this.manager.settings.afterScrolledTimeout = this.settings.duration * 2; + + this.isVertical = this.manager.settings.axis === "vertical"; + + // disable snapping if not paginated or axis in not horizontal + if (!this.manager.isPaginated || this.isVertical) { + return; + } + + this.touchCanceler = false; + this.resizeCanceler = false; + this.snapping = false; + + this.scrollLeft; + this.scrollTop; + + this.startTouchX = undefined; + this.startTouchY = undefined; + this.startTime = undefined; + this.endTouchX = undefined; + this.endTouchY = undefined; + this.endTime = undefined; + + this.addListeners(); + } + }, { + key: "supportsTouch", + value: function supportsTouch() { + if ('ontouchstart' in window || window.DocumentTouch && document instanceof DocumentTouch) { + return true; + } + + return false; + } + }, { + key: "disableScroll", + value: function disableScroll() { + this.element.style.overflow = "hidden"; + } + }, { + key: "enableScroll", + value: function enableScroll() { + this.element.style.overflow = ""; + } + }, { + key: "addListeners", + value: function addListeners() { + this._onResize = this.onResize.bind(this); + window.addEventListener('resize', this._onResize); + + this._onScroll = this.onScroll.bind(this); + this.scroller.addEventListener('scroll', this._onScroll); + + this._onTouchStart = this.onTouchStart.bind(this); + this.scroller.addEventListener('touchstart', this._onTouchStart, { passive: true }); + this.on('touchstart', this._onTouchStart); + + this._onTouchMove = this.onTouchMove.bind(this); + this.scroller.addEventListener('touchmove', this._onTouchMove, { passive: true }); + this.on('touchmove', this._onTouchMove); + + this._onTouchEnd = this.onTouchEnd.bind(this); + this.scroller.addEventListener('touchend', this._onTouchEnd, { passive: true }); + this.on('touchend', this._onTouchEnd); + + this._afterDisplayed = this.afterDisplayed.bind(this); + this.manager.on(_constants.EVENTS.MANAGERS.ADDED, this._afterDisplayed); + } + }, { + key: "removeListeners", + value: function removeListeners() { + window.removeEventListener('resize', this._onResize); + this._onResize = undefined; + + this.scroller.removeEventListener('scroll', this._onScroll); + this._onScroll = undefined; + + this.scroller.removeEventListener('touchstart', this._onTouchStart, { passive: true }); + this.off('touchstart', this._onTouchStart); + this._onTouchStart = undefined; + + this.scroller.removeEventListener('touchmove', this._onTouchMove, { passive: true }); + this.off('touchmove', this._onTouchMove); + this._onTouchMove = undefined; + + this.scroller.removeEventListener('touchend', this._onTouchEnd, { passive: true }); + this.off('touchend', this._onTouchEnd); + this._onTouchEnd = undefined; + + this.manager.off(_constants.EVENTS.MANAGERS.ADDED, this._afterDisplayed); + this._afterDisplayed = undefined; + } + }, { + key: "afterDisplayed", + value: function afterDisplayed(view) { + var _this = this; + + var contents = view.contents; + ["touchstart", "touchmove", "touchend"].forEach(function (e) { + contents.on(e, function (ev) { + return _this.triggerViewEvent(ev, contents); + }); + }); + } + }, { + key: "triggerViewEvent", + value: function triggerViewEvent(e, contents) { + this.emit(e.type, e, contents); + } + }, { + key: "onScroll", + value: function onScroll(e) { + this.scrollLeft = this.fullsize ? window.scrollX : this.scroller.scrollLeft; + this.scrollTop = this.fullsize ? window.scrollY : this.scroller.scrollTop; + } + }, { + key: "onResize", + value: function onResize(e) { + this.resizeCanceler = true; + } + }, { + key: "onTouchStart", + value: function onTouchStart(e) { + var _e$touches$ = e.touches[0], + screenX = _e$touches$.screenX, + screenY = _e$touches$.screenY; + + + if (this.fullsize) { + this.enableScroll(); + } + + this.touchCanceler = true; + + if (!this.startTouchX) { + this.startTouchX = screenX; + this.startTouchY = screenY; + this.startTime = this.now(); + } + + this.endTouchX = screenX; + this.endTouchY = screenY; + this.endTime = this.now(); + } + }, { + key: "onTouchMove", + value: function onTouchMove(e) { + var _e$touches$2 = e.touches[0], + screenX = _e$touches$2.screenX, + screenY = _e$touches$2.screenY; + + var deltaY = Math.abs(screenY - this.endTouchY); + + this.touchCanceler = true; + + if (!this.fullsize && deltaY < 10) { + this.element.scrollLeft -= screenX - this.endTouchX; + } + + this.endTouchX = screenX; + this.endTouchY = screenY; + this.endTime = this.now(); + } + }, { + key: "onTouchEnd", + value: function onTouchEnd(e) { + if (this.fullsize) { + this.disableScroll(); + } + + this.touchCanceler = false; + + var swipped = this.wasSwiped(); + + if (swipped !== 0) { + this.snap(swipped); + } else { + this.snap(); + } + + this.startTouchX = undefined; + this.startTouchY = undefined; + this.startTime = undefined; + this.endTouchX = undefined; + this.endTouchY = undefined; + this.endTime = undefined; + } + }, { + key: "wasSwiped", + value: function wasSwiped() { + var snapWidth = this.layout.pageWidth * this.layout.divisor; + var distance = this.endTouchX - this.startTouchX; + var absolute = Math.abs(distance); + var time = this.endTime - this.startTime; + var velocity = distance / time; + var minVelocity = this.settings.minVelocity; + + if (absolute <= this.settings.minDistance || absolute >= snapWidth) { + return 0; + } + + if (velocity > minVelocity) { + // previous + return -1; + } else if (velocity < -minVelocity) { + // next + return 1; + } + } + }, { + key: "needsSnap", + value: function needsSnap() { + var left = this.scrollLeft; + var snapWidth = this.layout.pageWidth * this.layout.divisor; + return left % snapWidth !== 0; + } + }, { + key: "snap", + value: function snap() { + var howMany = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + + var left = this.scrollLeft; + var snapWidth = this.layout.pageWidth * this.layout.divisor; + var snapTo = Math.round(left / snapWidth) * snapWidth; + + if (howMany) { + snapTo += howMany * snapWidth; + } + + return this.smoothScrollTo(snapTo); + } + }, { + key: "smoothScrollTo", + value: function smoothScrollTo(destination) { + var deferred = new _core.defer(); + var start = this.scrollLeft; + var startTime = this.now(); + + var duration = this.settings.duration; + var easing = this.settings.easing; + + this.snapping = true; + + // add animation loop + function tick() { + var now = this.now(); + var time = Math.min(1, (now - startTime) / duration); + var timeFunction = easing(time); + + if (this.touchCanceler || this.resizeCanceler) { + this.resizeCanceler = false; + this.snapping = false; + deferred.resolve(); + return; + } + + if (time < 1) { + window.requestAnimationFrame(tick.bind(this)); + this.scrollTo(start + (destination - start) * time, 0); + } else { + this.scrollTo(destination, 0); + this.snapping = false; + deferred.resolve(); + } + } + + tick.call(this); + + return deferred.promise; + } + }, { + key: "scrollTo", + value: function scrollTo() { + var left = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + var top = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + + if (this.fullsize) { + window.scroll(left, top); + } else { + this.scroller.scrollLeft = left; + this.scroller.scrollTop = top; + } + } + }, { + key: "now", + value: function now() { + return 'now' in window.performance ? performance.now() : new Date().getTime(); + } + }, { + key: "destroy", + value: function destroy() { + if (!this.scroller) { + return; + } + + if (this.fullsize) { + this.enableScroll(); + } + + this.removeListeners(); + + this.scroller = undefined; + } + }]); + + return Snap; +}(); + +(0, _eventEmitter2.default)(Snap.prototype); + +exports.default = Snap; +module.exports = exports["default"]; + +/***/ }), +/* 71 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _core = __webpack_require__(0); + +var _request = __webpack_require__(9); + +var _request2 = _interopRequireDefault(_request); + +var _mime = __webpack_require__(13); + +var _mime2 = _interopRequireDefault(_mime); + +var _path = __webpack_require__(4); + +var _path2 = _interopRequireDefault(_path); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * Handles Unzipping a requesting files from an Epub Archive + * @class + */ +var Archive = function () { + function Archive() { + _classCallCheck(this, Archive); + + this.zip = undefined; + this.urlCache = {}; + + this.checkRequirements(); + } + + /** + * Checks to see if JSZip exists in global namspace, + * Requires JSZip if it isn't there + * @private + */ + + + _createClass(Archive, [{ + key: "checkRequirements", + value: function checkRequirements() { + try { + if (typeof JSZip === "undefined") { + var _JSZip = __webpack_require__(72); + this.zip = new _JSZip(); + } else { + this.zip = new JSZip(); + } + } catch (e) { + throw new Error("JSZip lib not loaded"); + } + } + + /** + * Open an archive + * @param {binary} input + * @param {boolean} [isBase64] tells JSZip if the input data is base64 encoded + * @return {Promise} zipfile + */ + + }, { + key: "open", + value: function open(input, isBase64) { + return this.zip.loadAsync(input, { "base64": isBase64 }); + } + + /** + * Load and Open an archive + * @param {string} zipUrl + * @param {boolean} [isBase64] tells JSZip if the input data is base64 encoded + * @return {Promise} zipfile + */ + + }, { + key: "openUrl", + value: function openUrl(zipUrl, isBase64) { + return (0, _request2.default)(zipUrl, "binary").then(function (data) { + return this.zip.loadAsync(data, { "base64": isBase64 }); + }.bind(this)); + } + + /** + * Request a url from the archive + * @param {string} url a url to request from the archive + * @param {string} [type] specify the type of the returned result + * @return {Promise} + */ + + }, { + key: "request", + value: function request(url, type) { + var deferred = new _core.defer(); + var response; + var path = new _path2.default(url); + + // If type isn't set, determine it from the file extension + if (!type) { + type = path.extension; + } + + if (type == "blob") { + response = this.getBlob(url); + } else { + response = this.getText(url); + } + + if (response) { + response.then(function (r) { + var result = this.handleResponse(r, type); + deferred.resolve(result); + }.bind(this)); + } else { + deferred.reject({ + message: "File not found in the epub: " + url, + stack: new Error().stack + }); + } + return deferred.promise; + } + + /** + * Handle the response from request + * @private + * @param {any} response + * @param {string} [type] + * @return {any} the parsed result + */ + + }, { + key: "handleResponse", + value: function handleResponse(response, type) { + var r; + + if (type == "json") { + r = JSON.parse(response); + } else if ((0, _core.isXml)(type)) { + r = (0, _core.parse)(response, "text/xml"); + } else if (type == "xhtml") { + r = (0, _core.parse)(response, "application/xhtml+xml"); + } else if (type == "html" || type == "htm") { + r = (0, _core.parse)(response, "text/html"); + } else { + r = response; + } + + return r; + } + + /** + * Get a Blob from Archive by Url + * @param {string} url + * @param {string} [mimeType] + * @return {Blob} + */ + + }, { + key: "getBlob", + value: function getBlob(url, mimeType) { + var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash + var entry = this.zip.file(decodededUrl); + + if (entry) { + mimeType = mimeType || _mime2.default.lookup(entry.name); + return entry.async("uint8array").then(function (uint8array) { + return new Blob([uint8array], { type: mimeType }); + }); + } + } + + /** + * Get Text from Archive by Url + * @param {string} url + * @param {string} [encoding] + * @return {string} + */ + + }, { + key: "getText", + value: function getText(url, encoding) { + var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash + var entry = this.zip.file(decodededUrl); + + if (entry) { + return entry.async("string").then(function (text) { + return text; + }); + } + } + + /** + * Get a base64 encoded result from Archive by Url + * @param {string} url + * @param {string} [mimeType] + * @return {string} base64 encoded + */ + + }, { + key: "getBase64", + value: function getBase64(url, mimeType) { + var decodededUrl = window.decodeURIComponent(url.substr(1)); // Remove first slash + var entry = this.zip.file(decodededUrl); + + if (entry) { + mimeType = mimeType || _mime2.default.lookup(entry.name); + return entry.async("base64").then(function (data) { + return "data:" + mimeType + ";base64," + data; + }); + } + } + + /** + * Create a Url from an unarchived item + * @param {string} url + * @param {object} [options.base64] use base64 encoding or blob url + * @return {Promise} url promise with Url string + */ + + }, { + key: "createUrl", + value: function createUrl(url, options) { + var deferred = new _core.defer(); + var _URL = window.URL || window.webkitURL || window.mozURL; + var tempUrl; + var response; + var useBase64 = options && options.base64; + + if (url in this.urlCache) { + deferred.resolve(this.urlCache[url]); + return deferred.promise; + } + + if (useBase64) { + response = this.getBase64(url); + + if (response) { + response.then(function (tempUrl) { + + this.urlCache[url] = tempUrl; + deferred.resolve(tempUrl); + }.bind(this)); + } + } else { + + response = this.getBlob(url); + + if (response) { + response.then(function (blob) { + + tempUrl = _URL.createObjectURL(blob); + this.urlCache[url] = tempUrl; + deferred.resolve(tempUrl); + }.bind(this)); + } + } + + if (!response) { + deferred.reject({ + message: "File not found in the epub: " + url, + stack: new Error().stack + }); + } + + return deferred.promise; + } + + /** + * Revoke Temp Url for a achive item + * @param {string} url url of the item in the archive + */ + + }, { + key: "revokeUrl", + value: function revokeUrl(url) { + var _URL = window.URL || window.webkitURL || window.mozURL; + var fromCache = this.urlCache[url]; + if (fromCache) _URL.revokeObjectURL(fromCache); + } + }, { + key: "destroy", + value: function destroy() { + var _URL = window.URL || window.webkitURL || window.mozURL; + for (var fromCache in this.urlCache) { + _URL.revokeObjectURL(fromCache); + } + this.zip = undefined; + this.urlCache = {}; + } + }]); + + return Archive; +}(); + +exports.default = Archive; +module.exports = exports["default"]; + +/***/ }), +/* 72 */ +/***/ (function(module, exports) { + +if(typeof __WEBPACK_EXTERNAL_MODULE_72__ === 'undefined') {var e = new Error("Cannot find module \"jszip\""); e.code = 'MODULE_NOT_FOUND'; throw e;} +module.exports = __WEBPACK_EXTERNAL_MODULE_72__; + +/***/ }), +/* 73 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _core = __webpack_require__(0); + +var _request = __webpack_require__(9); + +var _request2 = _interopRequireDefault(_request); + +var _mime = __webpack_require__(13); + +var _mime2 = _interopRequireDefault(_mime); + +var _path = __webpack_require__(4); + +var _path2 = _interopRequireDefault(_path); + +var _eventEmitter = __webpack_require__(3); + +var _eventEmitter2 = _interopRequireDefault(_eventEmitter); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * Handles saving and requesting files from local storage + * @class + * @param {string} name This should be the name of the application for modals + * @param {function} [requester] + * @param {function} [resolver] + */ +var Store = function () { + function Store(name, requester, resolver) { + _classCallCheck(this, Store); + + this.urlCache = {}; + + this.storage = undefined; + + this.name = name; + this.requester = requester || _request2.default; + this.resolver = resolver; + + this.online = true; + + this.checkRequirements(); + + this.addListeners(); + } + + /** + * Checks to see if localForage exists in global namspace, + * Requires localForage if it isn't there + * @private + */ + + + _createClass(Store, [{ + key: "checkRequirements", + value: function checkRequirements() { + try { + var store = void 0; + if (typeof localforage === "undefined") { + store = __webpack_require__(74); + } else { + store = localforage; + } + this.storage = store.createInstance({ + name: this.name + }); + } catch (e) { + throw new Error("localForage lib not loaded"); + } + } + + /** + * Add online and offline event listeners + * @private + */ + + }, { + key: "addListeners", + value: function addListeners() { + this._status = this.status.bind(this); + window.addEventListener('online', this._status); + window.addEventListener('offline', this._status); + } + + /** + * Remove online and offline event listeners + * @private + */ + + }, { + key: "removeListeners", + value: function removeListeners() { + window.removeEventListener('online', this._status); + window.removeEventListener('offline', this._status); + this._status = undefined; + } + + /** + * Update the online / offline status + * @private + */ + + }, { + key: "status", + value: function status(event) { + var online = navigator.onLine; + this.online = online; + if (online) { + this.emit("online", this); + } else { + this.emit("offline", this); + } + } + + /** + * Add all of a book resources to the store + * @param {Resources} resources book resources + * @param {boolean} [force] force resaving resources + * @return {Promise} store objects + */ + + }, { + key: "add", + value: function add(resources, force) { + var _this = this; + + var mapped = resources.resources.map(function (item) { + var href = item.href; + + var url = _this.resolver(href); + var encodedUrl = window.encodeURIComponent(url); + + return _this.storage.getItem(encodedUrl).then(function (item) { + if (!item || force) { + return _this.requester(url, "binary").then(function (data) { + return _this.storage.setItem(encodedUrl, data); + }); + } else { + return item; + } + }); + }); + return Promise.all(mapped); + } + + /** + * Put binary data from a url to storage + * @param {string} url a url to request from storage + * @param {boolean} [withCredentials] + * @param {object} [headers] + * @return {Promise} + */ + + }, { + key: "put", + value: function put(url, withCredentials, headers) { + var _this2 = this; + + var encodedUrl = window.encodeURIComponent(url); + + return this.storage.getItem(encodedUrl).then(function (result) { + if (!result) { + return _this2.requester(url, "binary", withCredentials, headers).then(function (data) { + return _this2.storage.setItem(encodedUrl, data); + }); + } + return result; + }); + } + + /** + * Request a url + * @param {string} url a url to request from storage + * @param {string} [type] specify the type of the returned result + * @param {boolean} [withCredentials] + * @param {object} [headers] + * @return {Promise} + */ + + }, { + key: "request", + value: function request(url, type, withCredentials, headers) { + var _this3 = this; + + if (this.online) { + // From network + return this.requester(url, type, withCredentials, headers).then(function (data) { + // save to store if not present + _this3.put(url); + return data; + }); + } else { + // From store + return this.retrieve(url, type); + } + } + + /** + * Request a url from storage + * @param {string} url a url to request from storage + * @param {string} [type] specify the type of the returned result + * @return {Promise} + */ + + }, { + key: "retrieve", + value: function retrieve(url, type) { + var _this4 = this; + + var deferred = new _core.defer(); + var response; + var path = new _path2.default(url); + + // If type isn't set, determine it from the file extension + if (!type) { + type = path.extension; + } + + if (type == "blob") { + response = this.getBlob(url); + } else { + response = this.getText(url); + } + + return response.then(function (r) { + var deferred = new _core.defer(); + var result; + if (r) { + result = _this4.handleResponse(r, type); + deferred.resolve(result); + } else { + deferred.reject({ + message: "File not found in storage: " + url, + stack: new Error().stack + }); + } + return deferred.promise; + }); + } + + /** + * Handle the response from request + * @private + * @param {any} response + * @param {string} [type] + * @return {any} the parsed result + */ + + }, { + key: "handleResponse", + value: function handleResponse(response, type) { + var r; + + if (type == "json") { + r = JSON.parse(response); + } else if ((0, _core.isXml)(type)) { + r = (0, _core.parse)(response, "text/xml"); + } else if (type == "xhtml") { + r = (0, _core.parse)(response, "application/xhtml+xml"); + } else if (type == "html" || type == "htm") { + r = (0, _core.parse)(response, "text/html"); + } else { + r = response; + } + + return r; + } + + /** + * Get a Blob from Storage by Url + * @param {string} url + * @param {string} [mimeType] + * @return {Blob} + */ + + }, { + key: "getBlob", + value: function getBlob(url, mimeType) { + var encodedUrl = window.encodeURIComponent(url); + + return this.storage.getItem(encodedUrl).then(function (uint8array) { + if (!uint8array) return; + + mimeType = mimeType || _mime2.default.lookup(url); + + return new Blob([uint8array], { type: mimeType }); + }); + } + + /** + * Get Text from Storage by Url + * @param {string} url + * @param {string} [mimeType] + * @return {string} + */ + + }, { + key: "getText", + value: function getText(url, mimeType) { + var encodedUrl = window.encodeURIComponent(url); + + mimeType = mimeType || _mime2.default.lookup(url); + + return this.storage.getItem(encodedUrl).then(function (uint8array) { + var deferred = new _core.defer(); + var reader = new FileReader(); + var blob; + + if (!uint8array) return; + + blob = new Blob([uint8array], { type: mimeType }); + + reader.addEventListener("loadend", function () { + deferred.resolve(reader.result); + }); + + reader.readAsText(blob, mimeType); + + return deferred.promise; + }); + } + + /** + * Get a base64 encoded result from Storage by Url + * @param {string} url + * @param {string} [mimeType] + * @return {string} base64 encoded + */ + + }, { + key: "getBase64", + value: function getBase64(url, mimeType) { + var encodedUrl = window.encodeURIComponent(url); + + mimeType = mimeType || _mime2.default.lookup(url); + + return this.storage.getItem(encodedUrl).then(function (uint8array) { + var deferred = new _core.defer(); + var reader = new FileReader(); + var blob; + + if (!uint8array) return; + + blob = new Blob([uint8array], { type: mimeType }); + + reader.addEventListener("loadend", function () { + deferred.resolve(reader.result); + }); + reader.readAsDataURL(blob, mimeType); + + return deferred.promise; + }); + } + + /** + * Create a Url from a stored item + * @param {string} url + * @param {object} [options.base64] use base64 encoding or blob url + * @return {Promise} url promise with Url string + */ + + }, { + key: "createUrl", + value: function createUrl(url, options) { + var deferred = new _core.defer(); + var _URL = window.URL || window.webkitURL || window.mozURL; + var tempUrl; + var response; + var useBase64 = options && options.base64; + + if (url in this.urlCache) { + deferred.resolve(this.urlCache[url]); + return deferred.promise; + } + + if (useBase64) { + response = this.getBase64(url); + + if (response) { + response.then(function (tempUrl) { + + this.urlCache[url] = tempUrl; + deferred.resolve(tempUrl); + }.bind(this)); + } + } else { + + response = this.getBlob(url); + + if (response) { + response.then(function (blob) { + + tempUrl = _URL.createObjectURL(blob); + this.urlCache[url] = tempUrl; + deferred.resolve(tempUrl); + }.bind(this)); + } + } + + if (!response) { + deferred.reject({ + message: "File not found in storage: " + url, + stack: new Error().stack + }); + } + + return deferred.promise; + } + + /** + * Revoke Temp Url for a achive item + * @param {string} url url of the item in the store + */ + + }, { + key: "revokeUrl", + value: function revokeUrl(url) { + var _URL = window.URL || window.webkitURL || window.mozURL; + var fromCache = this.urlCache[url]; + if (fromCache) _URL.revokeObjectURL(fromCache); + } + }, { + key: "destroy", + value: function destroy() { + var _URL = window.URL || window.webkitURL || window.mozURL; + for (var fromCache in this.urlCache) { + _URL.revokeObjectURL(fromCache); + } + this.urlCache = {}; + this.removeListeners(); + } + }]); + + return Store; +}(); + +(0, _eventEmitter2.default)(Store.prototype); + +exports.default = Store; +module.exports = exports["default"]; + +/***/ }), +/* 74 */ +/***/ (function(module, exports, __webpack_require__) { + +/* WEBPACK VAR INJECTION */(function(global) {var require;var require;/*! + localForage -- Offline Storage, Improved + Version 1.7.3 + https://localforage.github.io/localForage + (c) 2013-2017 Mozilla, Apache License 2.0 +*/ +(function(f){if(true){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.localforage = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return require(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw (f.code="MODULE_NOT_FOUND", f)}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o element; its readystatechange event will be fired asynchronously once it is inserted + // into the document. Do so, thus queuing up the task. Remember to clean up once it's been called. + var scriptEl = global.document.createElement('script'); + scriptEl.onreadystatechange = function () { + nextTick(); + + scriptEl.onreadystatechange = null; + scriptEl.parentNode.removeChild(scriptEl); + scriptEl = null; + }; + global.document.documentElement.appendChild(scriptEl); + }; + } else { + scheduleDrain = function () { + setTimeout(nextTick, 0); + }; + } +} + +var draining; +var queue = []; +//named nextTick for less confusing stack traces +function nextTick() { + draining = true; + var i, oldQueue; + var len = queue.length; + while (len) { + oldQueue = queue; + queue = []; + i = -1; + while (++i < len) { + oldQueue[i](); + } + len = queue.length; + } + draining = false; +} + +module.exports = immediate; +function immediate(task) { + if (queue.push(task) === 1 && !draining) { + scheduleDrain(); + } +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],2:[function(_dereq_,module,exports){ +'use strict'; +var immediate = _dereq_(1); + +/* istanbul ignore next */ +function INTERNAL() {} + +var handlers = {}; + +var REJECTED = ['REJECTED']; +var FULFILLED = ['FULFILLED']; +var PENDING = ['PENDING']; + +module.exports = Promise; + +function Promise(resolver) { + if (typeof resolver !== 'function') { + throw new TypeError('resolver must be a function'); + } + this.state = PENDING; + this.queue = []; + this.outcome = void 0; + if (resolver !== INTERNAL) { + safelyResolveThenable(this, resolver); + } +} + +Promise.prototype["catch"] = function (onRejected) { + return this.then(null, onRejected); +}; +Promise.prototype.then = function (onFulfilled, onRejected) { + if (typeof onFulfilled !== 'function' && this.state === FULFILLED || + typeof onRejected !== 'function' && this.state === REJECTED) { + return this; + } + var promise = new this.constructor(INTERNAL); + if (this.state !== PENDING) { + var resolver = this.state === FULFILLED ? onFulfilled : onRejected; + unwrap(promise, resolver, this.outcome); + } else { + this.queue.push(new QueueItem(promise, onFulfilled, onRejected)); + } + + return promise; +}; +function QueueItem(promise, onFulfilled, onRejected) { + this.promise = promise; + if (typeof onFulfilled === 'function') { + this.onFulfilled = onFulfilled; + this.callFulfilled = this.otherCallFulfilled; + } + if (typeof onRejected === 'function') { + this.onRejected = onRejected; + this.callRejected = this.otherCallRejected; + } +} +QueueItem.prototype.callFulfilled = function (value) { + handlers.resolve(this.promise, value); +}; +QueueItem.prototype.otherCallFulfilled = function (value) { + unwrap(this.promise, this.onFulfilled, value); +}; +QueueItem.prototype.callRejected = function (value) { + handlers.reject(this.promise, value); +}; +QueueItem.prototype.otherCallRejected = function (value) { + unwrap(this.promise, this.onRejected, value); +}; + +function unwrap(promise, func, value) { + immediate(function () { + var returnValue; + try { + returnValue = func(value); + } catch (e) { + return handlers.reject(promise, e); + } + if (returnValue === promise) { + handlers.reject(promise, new TypeError('Cannot resolve promise with itself')); + } else { + handlers.resolve(promise, returnValue); + } + }); +} + +handlers.resolve = function (self, value) { + var result = tryCatch(getThen, value); + if (result.status === 'error') { + return handlers.reject(self, result.value); + } + var thenable = result.value; + + if (thenable) { + safelyResolveThenable(self, thenable); + } else { + self.state = FULFILLED; + self.outcome = value; + var i = -1; + var len = self.queue.length; + while (++i < len) { + self.queue[i].callFulfilled(value); + } + } + return self; +}; +handlers.reject = function (self, error) { + self.state = REJECTED; + self.outcome = error; + var i = -1; + var len = self.queue.length; + while (++i < len) { + self.queue[i].callRejected(error); + } + return self; +}; + +function getThen(obj) { + // Make sure we only access the accessor once as required by the spec + var then = obj && obj.then; + if (obj && (typeof obj === 'object' || typeof obj === 'function') && typeof then === 'function') { + return function appyThen() { + then.apply(obj, arguments); + }; + } +} + +function safelyResolveThenable(self, thenable) { + // Either fulfill, reject or reject with error + var called = false; + function onError(value) { + if (called) { + return; + } + called = true; + handlers.reject(self, value); + } + + function onSuccess(value) { + if (called) { + return; + } + called = true; + handlers.resolve(self, value); + } + + function tryToUnwrap() { + thenable(onSuccess, onError); + } + + var result = tryCatch(tryToUnwrap); + if (result.status === 'error') { + onError(result.value); + } +} + +function tryCatch(func, value) { + var out = {}; + try { + out.value = func(value); + out.status = 'success'; + } catch (e) { + out.status = 'error'; + out.value = e; + } + return out; +} + +Promise.resolve = resolve; +function resolve(value) { + if (value instanceof this) { + return value; + } + return handlers.resolve(new this(INTERNAL), value); +} + +Promise.reject = reject; +function reject(reason) { + var promise = new this(INTERNAL); + return handlers.reject(promise, reason); +} + +Promise.all = all; +function all(iterable) { + var self = this; + if (Object.prototype.toString.call(iterable) !== '[object Array]') { + return this.reject(new TypeError('must be an array')); + } + + var len = iterable.length; + var called = false; + if (!len) { + return this.resolve([]); + } + + var values = new Array(len); + var resolved = 0; + var i = -1; + var promise = new this(INTERNAL); + + while (++i < len) { + allResolver(iterable[i], i); + } + return promise; + function allResolver(value, i) { + self.resolve(value).then(resolveFromAll, function (error) { + if (!called) { + called = true; + handlers.reject(promise, error); + } + }); + function resolveFromAll(outValue) { + values[i] = outValue; + if (++resolved === len && !called) { + called = true; + handlers.resolve(promise, values); + } + } + } +} + +Promise.race = race; +function race(iterable) { + var self = this; + if (Object.prototype.toString.call(iterable) !== '[object Array]') { + return this.reject(new TypeError('must be an array')); + } + + var len = iterable.length; + var called = false; + if (!len) { + return this.resolve([]); + } + + var i = -1; + var promise = new this(INTERNAL); + + while (++i < len) { + resolver(iterable[i]); + } + return promise; + function resolver(value) { + self.resolve(value).then(function (response) { + if (!called) { + called = true; + handlers.resolve(promise, response); + } + }, function (error) { + if (!called) { + called = true; + handlers.reject(promise, error); + } + }); + } +} + +},{"1":1}],3:[function(_dereq_,module,exports){ +(function (global){ +'use strict'; +if (typeof global.Promise !== 'function') { + global.Promise = _dereq_(2); +} + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"2":2}],4:[function(_dereq_,module,exports){ +'use strict'; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function getIDB() { + /* global indexedDB,webkitIndexedDB,mozIndexedDB,OIndexedDB,msIndexedDB */ + try { + if (typeof indexedDB !== 'undefined') { + return indexedDB; + } + if (typeof webkitIndexedDB !== 'undefined') { + return webkitIndexedDB; + } + if (typeof mozIndexedDB !== 'undefined') { + return mozIndexedDB; + } + if (typeof OIndexedDB !== 'undefined') { + return OIndexedDB; + } + if (typeof msIndexedDB !== 'undefined') { + return msIndexedDB; + } + } catch (e) { + return; + } +} + +var idb = getIDB(); + +function isIndexedDBValid() { + try { + // Initialize IndexedDB; fall back to vendor-prefixed versions + // if needed. + if (!idb) { + return false; + } + // We mimic PouchDB here; + // + // We test for openDatabase because IE Mobile identifies itself + // as Safari. Oh the lulz... + var isSafari = typeof openDatabase !== 'undefined' && /(Safari|iPhone|iPad|iPod)/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent) && !/BlackBerry/.test(navigator.platform); + + var hasFetch = typeof fetch === 'function' && fetch.toString().indexOf('[native code') !== -1; + + // Safari <10.1 does not meet our requirements for IDB support (#5572) + // since Safari 10.1 shipped with fetch, we can use that to detect it + return (!isSafari || hasFetch) && typeof indexedDB !== 'undefined' && + // some outdated implementations of IDB that appear on Samsung + // and HTC Android devices <4.4 are missing IDBKeyRange + // See: https://github.com/mozilla/localForage/issues/128 + // See: https://github.com/mozilla/localForage/issues/272 + typeof IDBKeyRange !== 'undefined'; + } catch (e) { + return false; + } +} + +// Abstracts constructing a Blob object, so it also works in older +// browsers that don't support the native Blob constructor. (i.e. +// old QtWebKit versions, at least). +// Abstracts constructing a Blob object, so it also works in older +// browsers that don't support the native Blob constructor. (i.e. +// old QtWebKit versions, at least). +function createBlob(parts, properties) { + /* global BlobBuilder,MSBlobBuilder,MozBlobBuilder,WebKitBlobBuilder */ + parts = parts || []; + properties = properties || {}; + try { + return new Blob(parts, properties); + } catch (e) { + if (e.name !== 'TypeError') { + throw e; + } + var Builder = typeof BlobBuilder !== 'undefined' ? BlobBuilder : typeof MSBlobBuilder !== 'undefined' ? MSBlobBuilder : typeof MozBlobBuilder !== 'undefined' ? MozBlobBuilder : WebKitBlobBuilder; + var builder = new Builder(); + for (var i = 0; i < parts.length; i += 1) { + builder.append(parts[i]); + } + return builder.getBlob(properties.type); + } +} + +// This is CommonJS because lie is an external dependency, so Rollup +// can just ignore it. +if (typeof Promise === 'undefined') { + // In the "nopromises" build this will just throw if you don't have + // a global promise object, but it would throw anyway later. + _dereq_(3); +} +var Promise$1 = Promise; + +function executeCallback(promise, callback) { + if (callback) { + promise.then(function (result) { + callback(null, result); + }, function (error) { + callback(error); + }); + } +} + +function executeTwoCallbacks(promise, callback, errorCallback) { + if (typeof callback === 'function') { + promise.then(callback); + } + + if (typeof errorCallback === 'function') { + promise["catch"](errorCallback); + } +} + +function normalizeKey(key) { + // Cast the key to a string, as that's all we can set as a key. + if (typeof key !== 'string') { + console.warn(key + ' used as a key, but it is not a string.'); + key = String(key); + } + + return key; +} + +function getCallback() { + if (arguments.length && typeof arguments[arguments.length - 1] === 'function') { + return arguments[arguments.length - 1]; + } +} + +// Some code originally from async_storage.js in +// [Gaia](https://github.com/mozilla-b2g/gaia). + +var DETECT_BLOB_SUPPORT_STORE = 'local-forage-detect-blob-support'; +var supportsBlobs = void 0; +var dbContexts = {}; +var toString = Object.prototype.toString; + +// Transaction Modes +var READ_ONLY = 'readonly'; +var READ_WRITE = 'readwrite'; + +// Transform a binary string to an array buffer, because otherwise +// weird stuff happens when you try to work with the binary string directly. +// It is known. +// From http://stackoverflow.com/questions/14967647/ (continues on next line) +// encode-decode-image-with-base64-breaks-image (2013-04-21) +function _binStringToArrayBuffer(bin) { + var length = bin.length; + var buf = new ArrayBuffer(length); + var arr = new Uint8Array(buf); + for (var i = 0; i < length; i++) { + arr[i] = bin.charCodeAt(i); + } + return buf; +} + +// +// Blobs are not supported in all versions of IndexedDB, notably +// Chrome <37 and Android <5. In those versions, storing a blob will throw. +// +// Various other blob bugs exist in Chrome v37-42 (inclusive). +// Detecting them is expensive and confusing to users, and Chrome 37-42 +// is at very low usage worldwide, so we do a hacky userAgent check instead. +// +// content-type bug: https://code.google.com/p/chromium/issues/detail?id=408120 +// 404 bug: https://code.google.com/p/chromium/issues/detail?id=447916 +// FileReader bug: https://code.google.com/p/chromium/issues/detail?id=447836 +// +// Code borrowed from PouchDB. See: +// https://github.com/pouchdb/pouchdb/blob/master/packages/node_modules/pouchdb-adapter-idb/src/blobSupport.js +// +function _checkBlobSupportWithoutCaching(idb) { + return new Promise$1(function (resolve) { + var txn = idb.transaction(DETECT_BLOB_SUPPORT_STORE, READ_WRITE); + var blob = createBlob(['']); + txn.objectStore(DETECT_BLOB_SUPPORT_STORE).put(blob, 'key'); + + txn.onabort = function (e) { + // If the transaction aborts now its due to not being able to + // write to the database, likely due to the disk being full + e.preventDefault(); + e.stopPropagation(); + resolve(false); + }; + + txn.oncomplete = function () { + var matchedChrome = navigator.userAgent.match(/Chrome\/(\d+)/); + var matchedEdge = navigator.userAgent.match(/Edge\//); + // MS Edge pretends to be Chrome 42: + // https://msdn.microsoft.com/en-us/library/hh869301%28v=vs.85%29.aspx + resolve(matchedEdge || !matchedChrome || parseInt(matchedChrome[1], 10) >= 43); + }; + })["catch"](function () { + return false; // error, so assume unsupported + }); +} + +function _checkBlobSupport(idb) { + if (typeof supportsBlobs === 'boolean') { + return Promise$1.resolve(supportsBlobs); + } + return _checkBlobSupportWithoutCaching(idb).then(function (value) { + supportsBlobs = value; + return supportsBlobs; + }); +} + +function _deferReadiness(dbInfo) { + var dbContext = dbContexts[dbInfo.name]; + + // Create a deferred object representing the current database operation. + var deferredOperation = {}; + + deferredOperation.promise = new Promise$1(function (resolve, reject) { + deferredOperation.resolve = resolve; + deferredOperation.reject = reject; + }); + + // Enqueue the deferred operation. + dbContext.deferredOperations.push(deferredOperation); + + // Chain its promise to the database readiness. + if (!dbContext.dbReady) { + dbContext.dbReady = deferredOperation.promise; + } else { + dbContext.dbReady = dbContext.dbReady.then(function () { + return deferredOperation.promise; + }); + } +} + +function _advanceReadiness(dbInfo) { + var dbContext = dbContexts[dbInfo.name]; + + // Dequeue a deferred operation. + var deferredOperation = dbContext.deferredOperations.pop(); + + // Resolve its promise (which is part of the database readiness + // chain of promises). + if (deferredOperation) { + deferredOperation.resolve(); + return deferredOperation.promise; + } +} + +function _rejectReadiness(dbInfo, err) { + var dbContext = dbContexts[dbInfo.name]; + + // Dequeue a deferred operation. + var deferredOperation = dbContext.deferredOperations.pop(); + + // Reject its promise (which is part of the database readiness + // chain of promises). + if (deferredOperation) { + deferredOperation.reject(err); + return deferredOperation.promise; + } +} + +function _getConnection(dbInfo, upgradeNeeded) { + return new Promise$1(function (resolve, reject) { + dbContexts[dbInfo.name] = dbContexts[dbInfo.name] || createDbContext(); + + if (dbInfo.db) { + if (upgradeNeeded) { + _deferReadiness(dbInfo); + dbInfo.db.close(); + } else { + return resolve(dbInfo.db); + } + } + + var dbArgs = [dbInfo.name]; + + if (upgradeNeeded) { + dbArgs.push(dbInfo.version); + } + + var openreq = idb.open.apply(idb, dbArgs); + + if (upgradeNeeded) { + openreq.onupgradeneeded = function (e) { + var db = openreq.result; + try { + db.createObjectStore(dbInfo.storeName); + if (e.oldVersion <= 1) { + // Added when support for blob shims was added + db.createObjectStore(DETECT_BLOB_SUPPORT_STORE); + } + } catch (ex) { + if (ex.name === 'ConstraintError') { + console.warn('The database "' + dbInfo.name + '"' + ' has been upgraded from version ' + e.oldVersion + ' to version ' + e.newVersion + ', but the storage "' + dbInfo.storeName + '" already exists.'); + } else { + throw ex; + } + } + }; + } + + openreq.onerror = function (e) { + e.preventDefault(); + reject(openreq.error); + }; + + openreq.onsuccess = function () { + resolve(openreq.result); + _advanceReadiness(dbInfo); + }; + }); +} + +function _getOriginalConnection(dbInfo) { + return _getConnection(dbInfo, false); +} + +function _getUpgradedConnection(dbInfo) { + return _getConnection(dbInfo, true); +} + +function _isUpgradeNeeded(dbInfo, defaultVersion) { + if (!dbInfo.db) { + return true; + } + + var isNewStore = !dbInfo.db.objectStoreNames.contains(dbInfo.storeName); + var isDowngrade = dbInfo.version < dbInfo.db.version; + var isUpgrade = dbInfo.version > dbInfo.db.version; + + if (isDowngrade) { + // If the version is not the default one + // then warn for impossible downgrade. + if (dbInfo.version !== defaultVersion) { + console.warn('The database "' + dbInfo.name + '"' + " can't be downgraded from version " + dbInfo.db.version + ' to version ' + dbInfo.version + '.'); + } + // Align the versions to prevent errors. + dbInfo.version = dbInfo.db.version; + } + + if (isUpgrade || isNewStore) { + // If the store is new then increment the version (if needed). + // This will trigger an "upgradeneeded" event which is required + // for creating a store. + if (isNewStore) { + var incVersion = dbInfo.db.version + 1; + if (incVersion > dbInfo.version) { + dbInfo.version = incVersion; + } + } + + return true; + } + + return false; +} + +// encode a blob for indexeddb engines that don't support blobs +function _encodeBlob(blob) { + return new Promise$1(function (resolve, reject) { + var reader = new FileReader(); + reader.onerror = reject; + reader.onloadend = function (e) { + var base64 = btoa(e.target.result || ''); + resolve({ + __local_forage_encoded_blob: true, + data: base64, + type: blob.type + }); + }; + reader.readAsBinaryString(blob); + }); +} + +// decode an encoded blob +function _decodeBlob(encodedBlob) { + var arrayBuff = _binStringToArrayBuffer(atob(encodedBlob.data)); + return createBlob([arrayBuff], { type: encodedBlob.type }); +} + +// is this one of our fancy encoded blobs? +function _isEncodedBlob(value) { + return value && value.__local_forage_encoded_blob; +} + +// Specialize the default `ready()` function by making it dependent +// on the current database operations. Thus, the driver will be actually +// ready when it's been initialized (default) *and* there are no pending +// operations on the database (initiated by some other instances). +function _fullyReady(callback) { + var self = this; + + var promise = self._initReady().then(function () { + var dbContext = dbContexts[self._dbInfo.name]; + + if (dbContext && dbContext.dbReady) { + return dbContext.dbReady; + } + }); + + executeTwoCallbacks(promise, callback, callback); + return promise; +} + +// Try to establish a new db connection to replace the +// current one which is broken (i.e. experiencing +// InvalidStateError while creating a transaction). +function _tryReconnect(dbInfo) { + _deferReadiness(dbInfo); + + var dbContext = dbContexts[dbInfo.name]; + var forages = dbContext.forages; + + for (var i = 0; i < forages.length; i++) { + var forage = forages[i]; + if (forage._dbInfo.db) { + forage._dbInfo.db.close(); + forage._dbInfo.db = null; + } + } + dbInfo.db = null; + + return _getOriginalConnection(dbInfo).then(function (db) { + dbInfo.db = db; + if (_isUpgradeNeeded(dbInfo)) { + // Reopen the database for upgrading. + return _getUpgradedConnection(dbInfo); + } + return db; + }).then(function (db) { + // store the latest db reference + // in case the db was upgraded + dbInfo.db = dbContext.db = db; + for (var i = 0; i < forages.length; i++) { + forages[i]._dbInfo.db = db; + } + })["catch"](function (err) { + _rejectReadiness(dbInfo, err); + throw err; + }); +} + +// FF doesn't like Promises (micro-tasks) and IDDB store operations, +// so we have to do it with callbacks +function createTransaction(dbInfo, mode, callback, retries) { + if (retries === undefined) { + retries = 1; + } + + try { + var tx = dbInfo.db.transaction(dbInfo.storeName, mode); + callback(null, tx); + } catch (err) { + if (retries > 0 && (!dbInfo.db || err.name === 'InvalidStateError' || err.name === 'NotFoundError')) { + return Promise$1.resolve().then(function () { + if (!dbInfo.db || err.name === 'NotFoundError' && !dbInfo.db.objectStoreNames.contains(dbInfo.storeName) && dbInfo.version <= dbInfo.db.version) { + // increase the db version, to create the new ObjectStore + if (dbInfo.db) { + dbInfo.version = dbInfo.db.version + 1; + } + // Reopen the database for upgrading. + return _getUpgradedConnection(dbInfo); + } + }).then(function () { + return _tryReconnect(dbInfo).then(function () { + createTransaction(dbInfo, mode, callback, retries - 1); + }); + })["catch"](callback); + } + + callback(err); + } +} + +function createDbContext() { + return { + // Running localForages sharing a database. + forages: [], + // Shared database. + db: null, + // Database readiness (promise). + dbReady: null, + // Deferred operations on the database. + deferredOperations: [] + }; +} + +// Open the IndexedDB database (automatically creates one if one didn't +// previously exist), using any options set in the config. +function _initStorage(options) { + var self = this; + var dbInfo = { + db: null + }; + + if (options) { + for (var i in options) { + dbInfo[i] = options[i]; + } + } + + // Get the current context of the database; + var dbContext = dbContexts[dbInfo.name]; + + // ...or create a new context. + if (!dbContext) { + dbContext = createDbContext(); + // Register the new context in the global container. + dbContexts[dbInfo.name] = dbContext; + } + + // Register itself as a running localForage in the current context. + dbContext.forages.push(self); + + // Replace the default `ready()` function with the specialized one. + if (!self._initReady) { + self._initReady = self.ready; + self.ready = _fullyReady; + } + + // Create an array of initialization states of the related localForages. + var initPromises = []; + + function ignoreErrors() { + // Don't handle errors here, + // just makes sure related localForages aren't pending. + return Promise$1.resolve(); + } + + for (var j = 0; j < dbContext.forages.length; j++) { + var forage = dbContext.forages[j]; + if (forage !== self) { + // Don't wait for itself... + initPromises.push(forage._initReady()["catch"](ignoreErrors)); + } + } + + // Take a snapshot of the related localForages. + var forages = dbContext.forages.slice(0); + + // Initialize the connection process only when + // all the related localForages aren't pending. + return Promise$1.all(initPromises).then(function () { + dbInfo.db = dbContext.db; + // Get the connection or open a new one without upgrade. + return _getOriginalConnection(dbInfo); + }).then(function (db) { + dbInfo.db = db; + if (_isUpgradeNeeded(dbInfo, self._defaultConfig.version)) { + // Reopen the database for upgrading. + return _getUpgradedConnection(dbInfo); + } + return db; + }).then(function (db) { + dbInfo.db = dbContext.db = db; + self._dbInfo = dbInfo; + // Share the final connection amongst related localForages. + for (var k = 0; k < forages.length; k++) { + var forage = forages[k]; + if (forage !== self) { + // Self is already up-to-date. + forage._dbInfo.db = dbInfo.db; + forage._dbInfo.version = dbInfo.version; + } + } + }); +} + +function getItem(key, callback) { + var self = this; + + key = normalizeKey(key); + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) { + if (err) { + return reject(err); + } + + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.get(key); + + req.onsuccess = function () { + var value = req.result; + if (value === undefined) { + value = null; + } + if (_isEncodedBlob(value)) { + value = _decodeBlob(value); + } + resolve(value); + }; + + req.onerror = function () { + reject(req.error); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +// Iterate over all items stored in database. +function iterate(iterator, callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) { + if (err) { + return reject(err); + } + + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.openCursor(); + var iterationNumber = 1; + + req.onsuccess = function () { + var cursor = req.result; + + if (cursor) { + var value = cursor.value; + if (_isEncodedBlob(value)) { + value = _decodeBlob(value); + } + var result = iterator(value, cursor.key, iterationNumber++); + + // when the iterator callback retuns any + // (non-`undefined`) value, then we stop + // the iteration immediately + if (result !== void 0) { + resolve(result); + } else { + cursor["continue"](); + } + } else { + resolve(); + } + }; + + req.onerror = function () { + reject(req.error); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + + return promise; +} + +function setItem(key, value, callback) { + var self = this; + + key = normalizeKey(key); + + var promise = new Promise$1(function (resolve, reject) { + var dbInfo; + self.ready().then(function () { + dbInfo = self._dbInfo; + if (toString.call(value) === '[object Blob]') { + return _checkBlobSupport(dbInfo.db).then(function (blobSupport) { + if (blobSupport) { + return value; + } + return _encodeBlob(value); + }); + } + return value; + }).then(function (value) { + createTransaction(self._dbInfo, READ_WRITE, function (err, transaction) { + if (err) { + return reject(err); + } + + try { + var store = transaction.objectStore(self._dbInfo.storeName); + + // The reason we don't _save_ null is because IE 10 does + // not support saving the `null` type in IndexedDB. How + // ironic, given the bug below! + // See: https://github.com/mozilla/localForage/issues/161 + if (value === null) { + value = undefined; + } + + var req = store.put(value, key); + + transaction.oncomplete = function () { + // Cast to undefined so the value passed to + // callback/promise is the same as what one would get out + // of `getItem()` later. This leads to some weirdness + // (setItem('foo', undefined) will return `null`), but + // it's not my fault localStorage is our baseline and that + // it's weird. + if (value === undefined) { + value = null; + } + + resolve(value); + }; + transaction.onabort = transaction.onerror = function () { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +function removeItem(key, callback) { + var self = this; + + key = normalizeKey(key); + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_WRITE, function (err, transaction) { + if (err) { + return reject(err); + } + + try { + var store = transaction.objectStore(self._dbInfo.storeName); + // We use a Grunt task to make this safe for IE and some + // versions of Android (including those used by Cordova). + // Normally IE won't like `.delete()` and will insist on + // using `['delete']()`, but we have a build step that + // fixes this for us now. + var req = store["delete"](key); + transaction.oncomplete = function () { + resolve(); + }; + + transaction.onerror = function () { + reject(req.error); + }; + + // The request will be also be aborted if we've exceeded our storage + // space. + transaction.onabort = function () { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +function clear(callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_WRITE, function (err, transaction) { + if (err) { + return reject(err); + } + + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.clear(); + + transaction.oncomplete = function () { + resolve(); + }; + + transaction.onabort = transaction.onerror = function () { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +function length(callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) { + if (err) { + return reject(err); + } + + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.count(); + + req.onsuccess = function () { + resolve(req.result); + }; + + req.onerror = function () { + reject(req.error); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +function key(n, callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + if (n < 0) { + resolve(null); + + return; + } + + self.ready().then(function () { + createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) { + if (err) { + return reject(err); + } + + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var advanced = false; + var req = store.openCursor(); + + req.onsuccess = function () { + var cursor = req.result; + if (!cursor) { + // this means there weren't enough keys + resolve(null); + + return; + } + + if (n === 0) { + // We have the first key, return it if that's what they + // wanted. + resolve(cursor.key); + } else { + if (!advanced) { + // Otherwise, ask the cursor to skip ahead n + // records. + advanced = true; + cursor.advance(n); + } else { + // When we get here, we've got the nth key. + resolve(cursor.key); + } + } + }; + + req.onerror = function () { + reject(req.error); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +function keys(callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + createTransaction(self._dbInfo, READ_ONLY, function (err, transaction) { + if (err) { + return reject(err); + } + + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.openCursor(); + var keys = []; + + req.onsuccess = function () { + var cursor = req.result; + + if (!cursor) { + resolve(keys); + return; + } + + keys.push(cursor.key); + cursor["continue"](); + }; + + req.onerror = function () { + reject(req.error); + }; + } catch (e) { + reject(e); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +function dropInstance(options, callback) { + callback = getCallback.apply(this, arguments); + + var currentConfig = this.config(); + options = typeof options !== 'function' && options || {}; + if (!options.name) { + options.name = options.name || currentConfig.name; + options.storeName = options.storeName || currentConfig.storeName; + } + + var self = this; + var promise; + if (!options.name) { + promise = Promise$1.reject('Invalid arguments'); + } else { + var isCurrentDb = options.name === currentConfig.name && self._dbInfo.db; + + var dbPromise = isCurrentDb ? Promise$1.resolve(self._dbInfo.db) : _getOriginalConnection(options).then(function (db) { + var dbContext = dbContexts[options.name]; + var forages = dbContext.forages; + dbContext.db = db; + for (var i = 0; i < forages.length; i++) { + forages[i]._dbInfo.db = db; + } + return db; + }); + + if (!options.storeName) { + promise = dbPromise.then(function (db) { + _deferReadiness(options); + + var dbContext = dbContexts[options.name]; + var forages = dbContext.forages; + + db.close(); + for (var i = 0; i < forages.length; i++) { + var forage = forages[i]; + forage._dbInfo.db = null; + } + + var dropDBPromise = new Promise$1(function (resolve, reject) { + var req = idb.deleteDatabase(options.name); + + req.onerror = req.onblocked = function (err) { + var db = req.result; + if (db) { + db.close(); + } + reject(err); + }; + + req.onsuccess = function () { + var db = req.result; + if (db) { + db.close(); + } + resolve(db); + }; + }); + + return dropDBPromise.then(function (db) { + dbContext.db = db; + for (var i = 0; i < forages.length; i++) { + var _forage = forages[i]; + _advanceReadiness(_forage._dbInfo); + } + })["catch"](function (err) { + (_rejectReadiness(options, err) || Promise$1.resolve())["catch"](function () {}); + throw err; + }); + }); + } else { + promise = dbPromise.then(function (db) { + if (!db.objectStoreNames.contains(options.storeName)) { + return; + } + + var newVersion = db.version + 1; + + _deferReadiness(options); + + var dbContext = dbContexts[options.name]; + var forages = dbContext.forages; + + db.close(); + for (var i = 0; i < forages.length; i++) { + var forage = forages[i]; + forage._dbInfo.db = null; + forage._dbInfo.version = newVersion; + } + + var dropObjectPromise = new Promise$1(function (resolve, reject) { + var req = idb.open(options.name, newVersion); + + req.onerror = function (err) { + var db = req.result; + db.close(); + reject(err); + }; + + req.onupgradeneeded = function () { + var db = req.result; + db.deleteObjectStore(options.storeName); + }; + + req.onsuccess = function () { + var db = req.result; + db.close(); + resolve(db); + }; + }); + + return dropObjectPromise.then(function (db) { + dbContext.db = db; + for (var j = 0; j < forages.length; j++) { + var _forage2 = forages[j]; + _forage2._dbInfo.db = db; + _advanceReadiness(_forage2._dbInfo); + } + })["catch"](function (err) { + (_rejectReadiness(options, err) || Promise$1.resolve())["catch"](function () {}); + throw err; + }); + }); + } + } + + executeCallback(promise, callback); + return promise; +} + +var asyncStorage = { + _driver: 'asyncStorage', + _initStorage: _initStorage, + _support: isIndexedDBValid(), + iterate: iterate, + getItem: getItem, + setItem: setItem, + removeItem: removeItem, + clear: clear, + length: length, + key: key, + keys: keys, + dropInstance: dropInstance +}; + +function isWebSQLValid() { + return typeof openDatabase === 'function'; +} + +// Sadly, the best way to save binary data in WebSQL/localStorage is serializing +// it to Base64, so this is how we store it to prevent very strange errors with less +// verbose ways of binary <-> string data storage. +var BASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + +var BLOB_TYPE_PREFIX = '~~local_forage_type~'; +var BLOB_TYPE_PREFIX_REGEX = /^~~local_forage_type~([^~]+)~/; + +var SERIALIZED_MARKER = '__lfsc__:'; +var SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER.length; + +// OMG the serializations! +var TYPE_ARRAYBUFFER = 'arbf'; +var TYPE_BLOB = 'blob'; +var TYPE_INT8ARRAY = 'si08'; +var TYPE_UINT8ARRAY = 'ui08'; +var TYPE_UINT8CLAMPEDARRAY = 'uic8'; +var TYPE_INT16ARRAY = 'si16'; +var TYPE_INT32ARRAY = 'si32'; +var TYPE_UINT16ARRAY = 'ur16'; +var TYPE_UINT32ARRAY = 'ui32'; +var TYPE_FLOAT32ARRAY = 'fl32'; +var TYPE_FLOAT64ARRAY = 'fl64'; +var TYPE_SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER_LENGTH + TYPE_ARRAYBUFFER.length; + +var toString$1 = Object.prototype.toString; + +function stringToBuffer(serializedString) { + // Fill the string into a ArrayBuffer. + var bufferLength = serializedString.length * 0.75; + var len = serializedString.length; + var i; + var p = 0; + var encoded1, encoded2, encoded3, encoded4; + + if (serializedString[serializedString.length - 1] === '=') { + bufferLength--; + if (serializedString[serializedString.length - 2] === '=') { + bufferLength--; + } + } + + var buffer = new ArrayBuffer(bufferLength); + var bytes = new Uint8Array(buffer); + + for (i = 0; i < len; i += 4) { + encoded1 = BASE_CHARS.indexOf(serializedString[i]); + encoded2 = BASE_CHARS.indexOf(serializedString[i + 1]); + encoded3 = BASE_CHARS.indexOf(serializedString[i + 2]); + encoded4 = BASE_CHARS.indexOf(serializedString[i + 3]); + + /*jslint bitwise: true */ + bytes[p++] = encoded1 << 2 | encoded2 >> 4; + bytes[p++] = (encoded2 & 15) << 4 | encoded3 >> 2; + bytes[p++] = (encoded3 & 3) << 6 | encoded4 & 63; + } + return buffer; +} + +// Converts a buffer to a string to store, serialized, in the backend +// storage library. +function bufferToString(buffer) { + // base64-arraybuffer + var bytes = new Uint8Array(buffer); + var base64String = ''; + var i; + + for (i = 0; i < bytes.length; i += 3) { + /*jslint bitwise: true */ + base64String += BASE_CHARS[bytes[i] >> 2]; + base64String += BASE_CHARS[(bytes[i] & 3) << 4 | bytes[i + 1] >> 4]; + base64String += BASE_CHARS[(bytes[i + 1] & 15) << 2 | bytes[i + 2] >> 6]; + base64String += BASE_CHARS[bytes[i + 2] & 63]; + } + + if (bytes.length % 3 === 2) { + base64String = base64String.substring(0, base64String.length - 1) + '='; + } else if (bytes.length % 3 === 1) { + base64String = base64String.substring(0, base64String.length - 2) + '=='; + } + + return base64String; +} + +// Serialize a value, afterwards executing a callback (which usually +// instructs the `setItem()` callback/promise to be executed). This is how +// we store binary data with localStorage. +function serialize(value, callback) { + var valueType = ''; + if (value) { + valueType = toString$1.call(value); + } + + // Cannot use `value instanceof ArrayBuffer` or such here, as these + // checks fail when running the tests using casper.js... + // + // TODO: See why those tests fail and use a better solution. + if (value && (valueType === '[object ArrayBuffer]' || value.buffer && toString$1.call(value.buffer) === '[object ArrayBuffer]')) { + // Convert binary arrays to a string and prefix the string with + // a special marker. + var buffer; + var marker = SERIALIZED_MARKER; + + if (value instanceof ArrayBuffer) { + buffer = value; + marker += TYPE_ARRAYBUFFER; + } else { + buffer = value.buffer; + + if (valueType === '[object Int8Array]') { + marker += TYPE_INT8ARRAY; + } else if (valueType === '[object Uint8Array]') { + marker += TYPE_UINT8ARRAY; + } else if (valueType === '[object Uint8ClampedArray]') { + marker += TYPE_UINT8CLAMPEDARRAY; + } else if (valueType === '[object Int16Array]') { + marker += TYPE_INT16ARRAY; + } else if (valueType === '[object Uint16Array]') { + marker += TYPE_UINT16ARRAY; + } else if (valueType === '[object Int32Array]') { + marker += TYPE_INT32ARRAY; + } else if (valueType === '[object Uint32Array]') { + marker += TYPE_UINT32ARRAY; + } else if (valueType === '[object Float32Array]') { + marker += TYPE_FLOAT32ARRAY; + } else if (valueType === '[object Float64Array]') { + marker += TYPE_FLOAT64ARRAY; + } else { + callback(new Error('Failed to get type for BinaryArray')); + } + } + + callback(marker + bufferToString(buffer)); + } else if (valueType === '[object Blob]') { + // Conver the blob to a binaryArray and then to a string. + var fileReader = new FileReader(); + + fileReader.onload = function () { + // Backwards-compatible prefix for the blob type. + var str = BLOB_TYPE_PREFIX + value.type + '~' + bufferToString(this.result); + + callback(SERIALIZED_MARKER + TYPE_BLOB + str); + }; + + fileReader.readAsArrayBuffer(value); + } else { + try { + callback(JSON.stringify(value)); + } catch (e) { + console.error("Couldn't convert value into a JSON string: ", value); + + callback(null, e); + } + } +} + +// Deserialize data we've inserted into a value column/field. We place +// special markers into our strings to mark them as encoded; this isn't +// as nice as a meta field, but it's the only sane thing we can do whilst +// keeping localStorage support intact. +// +// Oftentimes this will just deserialize JSON content, but if we have a +// special marker (SERIALIZED_MARKER, defined above), we will extract +// some kind of arraybuffer/binary data/typed array out of the string. +function deserialize(value) { + // If we haven't marked this string as being specially serialized (i.e. + // something other than serialized JSON), we can just return it and be + // done with it. + if (value.substring(0, SERIALIZED_MARKER_LENGTH) !== SERIALIZED_MARKER) { + return JSON.parse(value); + } + + // The following code deals with deserializing some kind of Blob or + // TypedArray. First we separate out the type of data we're dealing + // with from the data itself. + var serializedString = value.substring(TYPE_SERIALIZED_MARKER_LENGTH); + var type = value.substring(SERIALIZED_MARKER_LENGTH, TYPE_SERIALIZED_MARKER_LENGTH); + + var blobType; + // Backwards-compatible blob type serialization strategy. + // DBs created with older versions of localForage will simply not have the blob type. + if (type === TYPE_BLOB && BLOB_TYPE_PREFIX_REGEX.test(serializedString)) { + var matcher = serializedString.match(BLOB_TYPE_PREFIX_REGEX); + blobType = matcher[1]; + serializedString = serializedString.substring(matcher[0].length); + } + var buffer = stringToBuffer(serializedString); + + // Return the right type based on the code/type set during + // serialization. + switch (type) { + case TYPE_ARRAYBUFFER: + return buffer; + case TYPE_BLOB: + return createBlob([buffer], { type: blobType }); + case TYPE_INT8ARRAY: + return new Int8Array(buffer); + case TYPE_UINT8ARRAY: + return new Uint8Array(buffer); + case TYPE_UINT8CLAMPEDARRAY: + return new Uint8ClampedArray(buffer); + case TYPE_INT16ARRAY: + return new Int16Array(buffer); + case TYPE_UINT16ARRAY: + return new Uint16Array(buffer); + case TYPE_INT32ARRAY: + return new Int32Array(buffer); + case TYPE_UINT32ARRAY: + return new Uint32Array(buffer); + case TYPE_FLOAT32ARRAY: + return new Float32Array(buffer); + case TYPE_FLOAT64ARRAY: + return new Float64Array(buffer); + default: + throw new Error('Unkown type: ' + type); + } +} + +var localforageSerializer = { + serialize: serialize, + deserialize: deserialize, + stringToBuffer: stringToBuffer, + bufferToString: bufferToString +}; + +/* + * Includes code from: + * + * base64-arraybuffer + * https://github.com/niklasvh/base64-arraybuffer + * + * Copyright (c) 2012 Niklas von Hertzen + * Licensed under the MIT license. + */ + +function createDbTable(t, dbInfo, callback, errorCallback) { + t.executeSql('CREATE TABLE IF NOT EXISTS ' + dbInfo.storeName + ' ' + '(id INTEGER PRIMARY KEY, key unique, value)', [], callback, errorCallback); +} + +// Open the WebSQL database (automatically creates one if one didn't +// previously exist), using any options set in the config. +function _initStorage$1(options) { + var self = this; + var dbInfo = { + db: null + }; + + if (options) { + for (var i in options) { + dbInfo[i] = typeof options[i] !== 'string' ? options[i].toString() : options[i]; + } + } + + var dbInfoPromise = new Promise$1(function (resolve, reject) { + // Open the database; the openDatabase API will automatically + // create it for us if it doesn't exist. + try { + dbInfo.db = openDatabase(dbInfo.name, String(dbInfo.version), dbInfo.description, dbInfo.size); + } catch (e) { + return reject(e); + } + + // Create our key/value table if it doesn't exist. + dbInfo.db.transaction(function (t) { + createDbTable(t, dbInfo, function () { + self._dbInfo = dbInfo; + resolve(); + }, function (t, error) { + reject(error); + }); + }, reject); + }); + + dbInfo.serializer = localforageSerializer; + return dbInfoPromise; +} + +function tryExecuteSql(t, dbInfo, sqlStatement, args, callback, errorCallback) { + t.executeSql(sqlStatement, args, callback, function (t, error) { + if (error.code === error.SYNTAX_ERR) { + t.executeSql('SELECT name FROM sqlite_master ' + "WHERE type='table' AND name = ?", [dbInfo.storeName], function (t, results) { + if (!results.rows.length) { + // if the table is missing (was deleted) + // re-create it table and retry + createDbTable(t, dbInfo, function () { + t.executeSql(sqlStatement, args, callback, errorCallback); + }, errorCallback); + } else { + errorCallback(t, error); + } + }, errorCallback); + } else { + errorCallback(t, error); + } + }, errorCallback); +} + +function getItem$1(key, callback) { + var self = this; + + key = normalizeKey(key); + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'SELECT * FROM ' + dbInfo.storeName + ' WHERE key = ? LIMIT 1', [key], function (t, results) { + var result = results.rows.length ? results.rows.item(0).value : null; + + // Check to see if this is serialized content we need to + // unpack. + if (result) { + result = dbInfo.serializer.deserialize(result); + } + + resolve(result); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +function iterate$1(iterator, callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'SELECT * FROM ' + dbInfo.storeName, [], function (t, results) { + var rows = results.rows; + var length = rows.length; + + for (var i = 0; i < length; i++) { + var item = rows.item(i); + var result = item.value; + + // Check to see if this is serialized content + // we need to unpack. + if (result) { + result = dbInfo.serializer.deserialize(result); + } + + result = iterator(result, item.key, i + 1); + + // void(0) prevents problems with redefinition + // of `undefined`. + if (result !== void 0) { + resolve(result); + return; + } + } + + resolve(); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +function _setItem(key, value, callback, retriesLeft) { + var self = this; + + key = normalizeKey(key); + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + // The localStorage API doesn't return undefined values in an + // "expected" way, so undefined is always cast to null in all + // drivers. See: https://github.com/mozilla/localForage/pull/42 + if (value === undefined) { + value = null; + } + + // Save the original value to pass to the callback. + var originalValue = value; + + var dbInfo = self._dbInfo; + dbInfo.serializer.serialize(value, function (value, error) { + if (error) { + reject(error); + } else { + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'INSERT OR REPLACE INTO ' + dbInfo.storeName + ' ' + '(key, value) VALUES (?, ?)', [key, value], function () { + resolve(originalValue); + }, function (t, error) { + reject(error); + }); + }, function (sqlError) { + // The transaction failed; check + // to see if it's a quota error. + if (sqlError.code === sqlError.QUOTA_ERR) { + // We reject the callback outright for now, but + // it's worth trying to re-run the transaction. + // Even if the user accepts the prompt to use + // more storage on Safari, this error will + // be called. + // + // Try to re-run the transaction. + if (retriesLeft > 0) { + resolve(_setItem.apply(self, [key, originalValue, callback, retriesLeft - 1])); + return; + } + reject(sqlError); + } + }); + } + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +function setItem$1(key, value, callback) { + return _setItem.apply(this, [key, value, callback, 1]); +} + +function removeItem$1(key, callback) { + var self = this; + + key = normalizeKey(key); + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'DELETE FROM ' + dbInfo.storeName + ' WHERE key = ?', [key], function () { + resolve(); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +// Deletes every item in the table. +// TODO: Find out if this resets the AUTO_INCREMENT number. +function clear$1(callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'DELETE FROM ' + dbInfo.storeName, [], function () { + resolve(); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +// Does a simple `COUNT(key)` to get the number of items stored in +// localForage. +function length$1(callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + // Ahhh, SQL makes this one soooooo easy. + tryExecuteSql(t, dbInfo, 'SELECT COUNT(key) as c FROM ' + dbInfo.storeName, [], function (t, results) { + var result = results.rows.item(0).c; + resolve(result); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +// Return the key located at key index X; essentially gets the key from a +// `WHERE id = ?`. This is the most efficient way I can think to implement +// this rarely-used (in my experience) part of the API, but it can seem +// inconsistent, because we do `INSERT OR REPLACE INTO` on `setItem()`, so +// the ID of each key will change every time it's updated. Perhaps a stored +// procedure for the `setItem()` SQL would solve this problem? +// TODO: Don't change ID on `setItem()`. +function key$1(n, callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'SELECT key FROM ' + dbInfo.storeName + ' WHERE id = ? LIMIT 1', [n + 1], function (t, results) { + var result = results.rows.length ? results.rows.item(0).key : null; + resolve(result); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +function keys$1(callback) { + var self = this; + + var promise = new Promise$1(function (resolve, reject) { + self.ready().then(function () { + var dbInfo = self._dbInfo; + dbInfo.db.transaction(function (t) { + tryExecuteSql(t, dbInfo, 'SELECT key FROM ' + dbInfo.storeName, [], function (t, results) { + var keys = []; + + for (var i = 0; i < results.rows.length; i++) { + keys.push(results.rows.item(i).key); + } + + resolve(keys); + }, function (t, error) { + reject(error); + }); + }); + })["catch"](reject); + }); + + executeCallback(promise, callback); + return promise; +} + +// https://www.w3.org/TR/webdatabase/#databases +// > There is no way to enumerate or delete the databases available for an origin from this API. +function getAllStoreNames(db) { + return new Promise$1(function (resolve, reject) { + db.transaction(function (t) { + t.executeSql('SELECT name FROM sqlite_master ' + "WHERE type='table' AND name <> '__WebKitDatabaseInfoTable__'", [], function (t, results) { + var storeNames = []; + + for (var i = 0; i < results.rows.length; i++) { + storeNames.push(results.rows.item(i).name); + } + + resolve({ + db: db, + storeNames: storeNames + }); + }, function (t, error) { + reject(error); + }); + }, function (sqlError) { + reject(sqlError); + }); + }); +} + +function dropInstance$1(options, callback) { + callback = getCallback.apply(this, arguments); + + var currentConfig = this.config(); + options = typeof options !== 'function' && options || {}; + if (!options.name) { + options.name = options.name || currentConfig.name; + options.storeName = options.storeName || currentConfig.storeName; + } + + var self = this; + var promise; + if (!options.name) { + promise = Promise$1.reject('Invalid arguments'); + } else { + promise = new Promise$1(function (resolve) { + var db; + if (options.name === currentConfig.name) { + // use the db reference of the current instance + db = self._dbInfo.db; + } else { + db = openDatabase(options.name, '', '', 0); + } + + if (!options.storeName) { + // drop all database tables + resolve(getAllStoreNames(db)); + } else { + resolve({ + db: db, + storeNames: [options.storeName] + }); + } + }).then(function (operationInfo) { + return new Promise$1(function (resolve, reject) { + operationInfo.db.transaction(function (t) { + function dropTable(storeName) { + return new Promise$1(function (resolve, reject) { + t.executeSql('DROP TABLE IF EXISTS ' + storeName, [], function () { + resolve(); + }, function (t, error) { + reject(error); + }); + }); + } + + var operations = []; + for (var i = 0, len = operationInfo.storeNames.length; i < len; i++) { + operations.push(dropTable(operationInfo.storeNames[i])); + } + + Promise$1.all(operations).then(function () { + resolve(); + })["catch"](function (e) { + reject(e); + }); + }, function (sqlError) { + reject(sqlError); + }); + }); + }); + } + + executeCallback(promise, callback); + return promise; +} + +var webSQLStorage = { + _driver: 'webSQLStorage', + _initStorage: _initStorage$1, + _support: isWebSQLValid(), + iterate: iterate$1, + getItem: getItem$1, + setItem: setItem$1, + removeItem: removeItem$1, + clear: clear$1, + length: length$1, + key: key$1, + keys: keys$1, + dropInstance: dropInstance$1 +}; + +function isLocalStorageValid() { + try { + return typeof localStorage !== 'undefined' && 'setItem' in localStorage && + // in IE8 typeof localStorage.setItem === 'object' + !!localStorage.setItem; + } catch (e) { + return false; + } +} + +function _getKeyPrefix(options, defaultConfig) { + var keyPrefix = options.name + '/'; + + if (options.storeName !== defaultConfig.storeName) { + keyPrefix += options.storeName + '/'; + } + return keyPrefix; +} + +// Check if localStorage throws when saving an item +function checkIfLocalStorageThrows() { + var localStorageTestKey = '_localforage_support_test'; + + try { + localStorage.setItem(localStorageTestKey, true); + localStorage.removeItem(localStorageTestKey); + + return false; + } catch (e) { + return true; + } +} + +// Check if localStorage is usable and allows to save an item +// This method checks if localStorage is usable in Safari Private Browsing +// mode, or in any other case where the available quota for localStorage +// is 0 and there wasn't any saved items yet. +function _isLocalStorageUsable() { + return !checkIfLocalStorageThrows() || localStorage.length > 0; +} + +// Config the localStorage backend, using options set in the config. +function _initStorage$2(options) { + var self = this; + var dbInfo = {}; + if (options) { + for (var i in options) { + dbInfo[i] = options[i]; + } + } + + dbInfo.keyPrefix = _getKeyPrefix(options, self._defaultConfig); + + if (!_isLocalStorageUsable()) { + return Promise$1.reject(); + } + + self._dbInfo = dbInfo; + dbInfo.serializer = localforageSerializer; + + return Promise$1.resolve(); +} + +// Remove all keys from the datastore, effectively destroying all data in +// the app's key/value store! +function clear$2(callback) { + var self = this; + var promise = self.ready().then(function () { + var keyPrefix = self._dbInfo.keyPrefix; + + for (var i = localStorage.length - 1; i >= 0; i--) { + var key = localStorage.key(i); + + if (key.indexOf(keyPrefix) === 0) { + localStorage.removeItem(key); + } + } + }); + + executeCallback(promise, callback); + return promise; +} + +// Retrieve an item from the store. Unlike the original async_storage +// library in Gaia, we don't modify return values at all. If a key's value +// is `undefined`, we pass that value to the callback function. +function getItem$2(key, callback) { + var self = this; + + key = normalizeKey(key); + + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var result = localStorage.getItem(dbInfo.keyPrefix + key); + + // If a result was found, parse it from the serialized + // string into a JS object. If result isn't truthy, the key + // is likely undefined and we'll pass it straight to the + // callback. + if (result) { + result = dbInfo.serializer.deserialize(result); + } + + return result; + }); + + executeCallback(promise, callback); + return promise; +} + +// Iterate over all items in the store. +function iterate$2(iterator, callback) { + var self = this; + + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var keyPrefix = dbInfo.keyPrefix; + var keyPrefixLength = keyPrefix.length; + var length = localStorage.length; + + // We use a dedicated iterator instead of the `i` variable below + // so other keys we fetch in localStorage aren't counted in + // the `iterationNumber` argument passed to the `iterate()` + // callback. + // + // See: github.com/mozilla/localForage/pull/435#discussion_r38061530 + var iterationNumber = 1; + + for (var i = 0; i < length; i++) { + var key = localStorage.key(i); + if (key.indexOf(keyPrefix) !== 0) { + continue; + } + var value = localStorage.getItem(key); + + // If a result was found, parse it from the serialized + // string into a JS object. If result isn't truthy, the + // key is likely undefined and we'll pass it straight + // to the iterator. + if (value) { + value = dbInfo.serializer.deserialize(value); + } + + value = iterator(value, key.substring(keyPrefixLength), iterationNumber++); + + if (value !== void 0) { + return value; + } + } + }); + + executeCallback(promise, callback); + return promise; +} + +// Same as localStorage's key() method, except takes a callback. +function key$2(n, callback) { + var self = this; + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var result; + try { + result = localStorage.key(n); + } catch (error) { + result = null; + } + + // Remove the prefix from the key, if a key is found. + if (result) { + result = result.substring(dbInfo.keyPrefix.length); + } + + return result; + }); + + executeCallback(promise, callback); + return promise; +} + +function keys$2(callback) { + var self = this; + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + var length = localStorage.length; + var keys = []; + + for (var i = 0; i < length; i++) { + var itemKey = localStorage.key(i); + if (itemKey.indexOf(dbInfo.keyPrefix) === 0) { + keys.push(itemKey.substring(dbInfo.keyPrefix.length)); + } + } + + return keys; + }); + + executeCallback(promise, callback); + return promise; +} + +// Supply the number of keys in the datastore to the callback function. +function length$2(callback) { + var self = this; + var promise = self.keys().then(function (keys) { + return keys.length; + }); + + executeCallback(promise, callback); + return promise; +} + +// Remove an item from the store, nice and simple. +function removeItem$2(key, callback) { + var self = this; + + key = normalizeKey(key); + + var promise = self.ready().then(function () { + var dbInfo = self._dbInfo; + localStorage.removeItem(dbInfo.keyPrefix + key); + }); + + executeCallback(promise, callback); + return promise; +} + +// Set a key's value and run an optional callback once the value is set. +// Unlike Gaia's implementation, the callback function is passed the value, +// in case you want to operate on that value only after you're sure it +// saved, or something like that. +function setItem$2(key, value, callback) { + var self = this; + + key = normalizeKey(key); + + var promise = self.ready().then(function () { + // Convert undefined values to null. + // https://github.com/mozilla/localForage/pull/42 + if (value === undefined) { + value = null; + } + + // Save the original value to pass to the callback. + var originalValue = value; + + return new Promise$1(function (resolve, reject) { + var dbInfo = self._dbInfo; + dbInfo.serializer.serialize(value, function (value, error) { + if (error) { + reject(error); + } else { + try { + localStorage.setItem(dbInfo.keyPrefix + key, value); + resolve(originalValue); + } catch (e) { + // localStorage capacity exceeded. + // TODO: Make this a specific error/event. + if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') { + reject(e); + } + reject(e); + } + } + }); + }); + }); + + executeCallback(promise, callback); + return promise; +} + +function dropInstance$2(options, callback) { + callback = getCallback.apply(this, arguments); + + options = typeof options !== 'function' && options || {}; + if (!options.name) { + var currentConfig = this.config(); + options.name = options.name || currentConfig.name; + options.storeName = options.storeName || currentConfig.storeName; + } + + var self = this; + var promise; + if (!options.name) { + promise = Promise$1.reject('Invalid arguments'); + } else { + promise = new Promise$1(function (resolve) { + if (!options.storeName) { + resolve(options.name + '/'); + } else { + resolve(_getKeyPrefix(options, self._defaultConfig)); + } + }).then(function (keyPrefix) { + for (var i = localStorage.length - 1; i >= 0; i--) { + var key = localStorage.key(i); + + if (key.indexOf(keyPrefix) === 0) { + localStorage.removeItem(key); + } + } + }); + } + + executeCallback(promise, callback); + return promise; +} + +var localStorageWrapper = { + _driver: 'localStorageWrapper', + _initStorage: _initStorage$2, + _support: isLocalStorageValid(), + iterate: iterate$2, + getItem: getItem$2, + setItem: setItem$2, + removeItem: removeItem$2, + clear: clear$2, + length: length$2, + key: key$2, + keys: keys$2, + dropInstance: dropInstance$2 +}; + +var sameValue = function sameValue(x, y) { + return x === y || typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y); +}; + +var includes = function includes(array, searchElement) { + var len = array.length; + var i = 0; + while (i < len) { + if (sameValue(array[i], searchElement)) { + return true; + } + i++; + } + + return false; +}; + +var isArray = Array.isArray || function (arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; +}; + +// Drivers are stored here when `defineDriver()` is called. +// They are shared across all instances of localForage. +var DefinedDrivers = {}; + +var DriverSupport = {}; + +var DefaultDrivers = { + INDEXEDDB: asyncStorage, + WEBSQL: webSQLStorage, + LOCALSTORAGE: localStorageWrapper +}; + +var DefaultDriverOrder = [DefaultDrivers.INDEXEDDB._driver, DefaultDrivers.WEBSQL._driver, DefaultDrivers.LOCALSTORAGE._driver]; + +var OptionalDriverMethods = ['dropInstance']; + +var LibraryMethods = ['clear', 'getItem', 'iterate', 'key', 'keys', 'length', 'removeItem', 'setItem'].concat(OptionalDriverMethods); + +var DefaultConfig = { + description: '', + driver: DefaultDriverOrder.slice(), + name: 'localforage', + // Default DB size is _JUST UNDER_ 5MB, as it's the highest size + // we can use without a prompt. + size: 4980736, + storeName: 'keyvaluepairs', + version: 1.0 +}; + +function callWhenReady(localForageInstance, libraryMethod) { + localForageInstance[libraryMethod] = function () { + var _args = arguments; + return localForageInstance.ready().then(function () { + return localForageInstance[libraryMethod].apply(localForageInstance, _args); + }); + }; +} + +function extend() { + for (var i = 1; i < arguments.length; i++) { + var arg = arguments[i]; + + if (arg) { + for (var _key in arg) { + if (arg.hasOwnProperty(_key)) { + if (isArray(arg[_key])) { + arguments[0][_key] = arg[_key].slice(); + } else { + arguments[0][_key] = arg[_key]; + } + } + } + } + } + + return arguments[0]; +} + +var LocalForage = function () { + function LocalForage(options) { + _classCallCheck(this, LocalForage); + + for (var driverTypeKey in DefaultDrivers) { + if (DefaultDrivers.hasOwnProperty(driverTypeKey)) { + var driver = DefaultDrivers[driverTypeKey]; + var driverName = driver._driver; + this[driverTypeKey] = driverName; + + if (!DefinedDrivers[driverName]) { + // we don't need to wait for the promise, + // since the default drivers can be defined + // in a blocking manner + this.defineDriver(driver); + } + } + } + + this._defaultConfig = extend({}, DefaultConfig); + this._config = extend({}, this._defaultConfig, options); + this._driverSet = null; + this._initDriver = null; + this._ready = false; + this._dbInfo = null; + + this._wrapLibraryMethodsWithReady(); + this.setDriver(this._config.driver)["catch"](function () {}); + } + + // Set any config values for localForage; can be called anytime before + // the first API call (e.g. `getItem`, `setItem`). + // We loop through options so we don't overwrite existing config + // values. + + + LocalForage.prototype.config = function config(options) { + // If the options argument is an object, we use it to set values. + // Otherwise, we return either a specified config value or all + // config values. + if ((typeof options === 'undefined' ? 'undefined' : _typeof(options)) === 'object') { + // If localforage is ready and fully initialized, we can't set + // any new configuration values. Instead, we return an error. + if (this._ready) { + return new Error("Can't call config() after localforage " + 'has been used.'); + } + + for (var i in options) { + if (i === 'storeName') { + options[i] = options[i].replace(/\W/g, '_'); + } + + if (i === 'version' && typeof options[i] !== 'number') { + return new Error('Database version must be a number.'); + } + + this._config[i] = options[i]; + } + + // after all config options are set and + // the driver option is used, try setting it + if ('driver' in options && options.driver) { + return this.setDriver(this._config.driver); + } + + return true; + } else if (typeof options === 'string') { + return this._config[options]; + } else { + return this._config; + } + }; + + // Used to define a custom driver, shared across all instances of + // localForage. + + + LocalForage.prototype.defineDriver = function defineDriver(driverObject, callback, errorCallback) { + var promise = new Promise$1(function (resolve, reject) { + try { + var driverName = driverObject._driver; + var complianceError = new Error('Custom driver not compliant; see ' + 'https://mozilla.github.io/localForage/#definedriver'); + + // A driver name should be defined and not overlap with the + // library-defined, default drivers. + if (!driverObject._driver) { + reject(complianceError); + return; + } + + var driverMethods = LibraryMethods.concat('_initStorage'); + for (var i = 0, len = driverMethods.length; i < len; i++) { + var driverMethodName = driverMethods[i]; + + // when the property is there, + // it should be a method even when optional + var isRequired = !includes(OptionalDriverMethods, driverMethodName); + if ((isRequired || driverObject[driverMethodName]) && typeof driverObject[driverMethodName] !== 'function') { + reject(complianceError); + return; + } + } + + var configureMissingMethods = function configureMissingMethods() { + var methodNotImplementedFactory = function methodNotImplementedFactory(methodName) { + return function () { + var error = new Error('Method ' + methodName + ' is not implemented by the current driver'); + var promise = Promise$1.reject(error); + executeCallback(promise, arguments[arguments.length - 1]); + return promise; + }; + }; + + for (var _i = 0, _len = OptionalDriverMethods.length; _i < _len; _i++) { + var optionalDriverMethod = OptionalDriverMethods[_i]; + if (!driverObject[optionalDriverMethod]) { + driverObject[optionalDriverMethod] = methodNotImplementedFactory(optionalDriverMethod); + } + } + }; + + configureMissingMethods(); + + var setDriverSupport = function setDriverSupport(support) { + if (DefinedDrivers[driverName]) { + console.info('Redefining LocalForage driver: ' + driverName); + } + DefinedDrivers[driverName] = driverObject; + DriverSupport[driverName] = support; + // don't use a then, so that we can define + // drivers that have simple _support methods + // in a blocking manner + resolve(); + }; + + if ('_support' in driverObject) { + if (driverObject._support && typeof driverObject._support === 'function') { + driverObject._support().then(setDriverSupport, reject); + } else { + setDriverSupport(!!driverObject._support); + } + } else { + setDriverSupport(true); + } + } catch (e) { + reject(e); + } + }); + + executeTwoCallbacks(promise, callback, errorCallback); + return promise; + }; + + LocalForage.prototype.driver = function driver() { + return this._driver || null; + }; + + LocalForage.prototype.getDriver = function getDriver(driverName, callback, errorCallback) { + var getDriverPromise = DefinedDrivers[driverName] ? Promise$1.resolve(DefinedDrivers[driverName]) : Promise$1.reject(new Error('Driver not found.')); + + executeTwoCallbacks(getDriverPromise, callback, errorCallback); + return getDriverPromise; + }; + + LocalForage.prototype.getSerializer = function getSerializer(callback) { + var serializerPromise = Promise$1.resolve(localforageSerializer); + executeTwoCallbacks(serializerPromise, callback); + return serializerPromise; + }; + + LocalForage.prototype.ready = function ready(callback) { + var self = this; + + var promise = self._driverSet.then(function () { + if (self._ready === null) { + self._ready = self._initDriver(); + } + + return self._ready; + }); + + executeTwoCallbacks(promise, callback, callback); + return promise; + }; + + LocalForage.prototype.setDriver = function setDriver(drivers, callback, errorCallback) { + var self = this; + + if (!isArray(drivers)) { + drivers = [drivers]; + } + + var supportedDrivers = this._getSupportedDrivers(drivers); + + function setDriverToConfig() { + self._config.driver = self.driver(); + } + + function extendSelfWithDriver(driver) { + self._extend(driver); + setDriverToConfig(); + + self._ready = self._initStorage(self._config); + return self._ready; + } + + function initDriver(supportedDrivers) { + return function () { + var currentDriverIndex = 0; + + function driverPromiseLoop() { + while (currentDriverIndex < supportedDrivers.length) { + var driverName = supportedDrivers[currentDriverIndex]; + currentDriverIndex++; + + self._dbInfo = null; + self._ready = null; + + return self.getDriver(driverName).then(extendSelfWithDriver)["catch"](driverPromiseLoop); + } + + setDriverToConfig(); + var error = new Error('No available storage method found.'); + self._driverSet = Promise$1.reject(error); + return self._driverSet; + } + + return driverPromiseLoop(); + }; + } + + // There might be a driver initialization in progress + // so wait for it to finish in order to avoid a possible + // race condition to set _dbInfo + var oldDriverSetDone = this._driverSet !== null ? this._driverSet["catch"](function () { + return Promise$1.resolve(); + }) : Promise$1.resolve(); + + this._driverSet = oldDriverSetDone.then(function () { + var driverName = supportedDrivers[0]; + self._dbInfo = null; + self._ready = null; + + return self.getDriver(driverName).then(function (driver) { + self._driver = driver._driver; + setDriverToConfig(); + self._wrapLibraryMethodsWithReady(); + self._initDriver = initDriver(supportedDrivers); + }); + })["catch"](function () { + setDriverToConfig(); + var error = new Error('No available storage method found.'); + self._driverSet = Promise$1.reject(error); + return self._driverSet; + }); + + executeTwoCallbacks(this._driverSet, callback, errorCallback); + return this._driverSet; + }; + + LocalForage.prototype.supports = function supports(driverName) { + return !!DriverSupport[driverName]; + }; + + LocalForage.prototype._extend = function _extend(libraryMethodsAndProperties) { + extend(this, libraryMethodsAndProperties); + }; + + LocalForage.prototype._getSupportedDrivers = function _getSupportedDrivers(drivers) { + var supportedDrivers = []; + for (var i = 0, len = drivers.length; i < len; i++) { + var driverName = drivers[i]; + if (this.supports(driverName)) { + supportedDrivers.push(driverName); + } + } + return supportedDrivers; + }; + + LocalForage.prototype._wrapLibraryMethodsWithReady = function _wrapLibraryMethodsWithReady() { + // Add a stub for each driver API method that delays the call to the + // corresponding driver method until localForage is ready. These stubs + // will be replaced by the driver methods as soon as the driver is + // loaded, so there is no performance impact. + for (var i = 0, len = LibraryMethods.length; i < len; i++) { + callWhenReady(this, LibraryMethods[i]); + } + }; + + LocalForage.prototype.createInstance = function createInstance(options) { + return new LocalForage(options); + }; + + return LocalForage; +}(); + +// The actual localForage object that we expose as a module or via a +// global. It's extended by pulling in one of our other libraries. + + +var localforage_js = new LocalForage(); + +module.exports = localforage_js; + +},{"3":3}]},{},[4])(4) +}); + +/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(5))) + +/***/ }), +/* 75 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _core = __webpack_require__(0); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * Open DisplayOptions Format Parser + * @class + * @param {document} displayOptionsDocument XML + */ +var DisplayOptions = function () { + function DisplayOptions(displayOptionsDocument) { + _classCallCheck(this, DisplayOptions); + + this.interactive = ""; + this.fixedLayout = ""; + this.openToSpread = ""; + this.orientationLock = ""; + + if (displayOptionsDocument) { + this.parse(displayOptionsDocument); + } + } + + /** + * Parse XML + * @param {document} displayOptionsDocument XML + * @return {DisplayOptions} self + */ + + + _createClass(DisplayOptions, [{ + key: "parse", + value: function parse(displayOptionsDocument) { + var _this = this; + + if (!displayOptionsDocument) { + return this; + } + + var displayOptionsNode = (0, _core.qs)(displayOptionsDocument, "display_options"); + if (!displayOptionsNode) { + return this; + } + + var options = (0, _core.qsa)(displayOptionsNode, "option"); + options.forEach(function (el) { + var value = ""; + + if (el.childNodes.length) { + value = el.childNodes[0].nodeValue; + } + + switch (el.attributes.name.value) { + case "interactive": + _this.interactive = value; + break; + case "fixed-layout": + _this.fixedLayout = value; + break; + case "open-to-spread": + _this.openToSpread = value; + break; + case "orientation-lock": + _this.orientationLock = value; + break; + } + }); + + return this; + } + }, { + key: "destroy", + value: function destroy() { + this.interactive = undefined; + this.fixedLayout = undefined; + this.openToSpread = undefined; + this.orientationLock = undefined; + } + }]); + + return DisplayOptions; +}(); + +exports.default = DisplayOptions; +module.exports = exports["default"]; + +/***/ }), +/* 76 */ +/***/ (function(module, exports, __webpack_require__) { + +/* WEBPACK VAR INJECTION */(function(global) {(function(global) { + /** + * Polyfill URLSearchParams + * + * Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js + */ + + var checkIfIteratorIsSupported = function() { + try { + return !!Symbol.iterator; + } catch (error) { + return false; + } + }; + + + var iteratorSupported = checkIfIteratorIsSupported(); + + var createIterator = function(items) { + var iterator = { + next: function() { + var value = items.shift(); + return { done: value === void 0, value: value }; + } + }; + + if (iteratorSupported) { + iterator[Symbol.iterator] = function() { + return iterator; + }; + } + + return iterator; + }; + + /** + * Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing + * encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`. + */ + var serializeParam = function(value) { + return encodeURIComponent(value).replace(/%20/g, '+'); + }; + + var deserializeParam = function(value) { + return decodeURIComponent(value).replace(/\+/g, ' '); + }; + + var polyfillURLSearchParams = function() { + + var URLSearchParams = function(searchString) { + Object.defineProperty(this, '_entries', { writable: true, value: {} }); + var typeofSearchString = typeof searchString; + + if (typeofSearchString === 'undefined') { + // do nothing + } else if (typeofSearchString === 'string') { + if (searchString !== '') { + this._fromString(searchString); + } + } else if (searchString instanceof URLSearchParams) { + var _this = this; + searchString.forEach(function(value, name) { + _this.append(name, value); + }); + } else if ((searchString !== null) && (typeofSearchString === 'object')) { + if (Object.prototype.toString.call(searchString) === '[object Array]') { + for (var i = 0; i < searchString.length; i++) { + var entry = searchString[i]; + if ((Object.prototype.toString.call(entry) === '[object Array]') || (entry.length !== 2)) { + this.append(entry[0], entry[1]); + } else { + throw new TypeError('Expected [string, any] as entry at index ' + i + ' of URLSearchParams\'s input'); + } + } + } else { + for (var key in searchString) { + if (searchString.hasOwnProperty(key)) { + this.append(key, searchString[key]); + } + } + } + } else { + throw new TypeError('Unsupported input\'s type for URLSearchParams'); + } + }; + + var proto = URLSearchParams.prototype; + + proto.append = function(name, value) { + if (name in this._entries) { + this._entries[name].push(String(value)); + } else { + this._entries[name] = [String(value)]; + } + }; + + proto.delete = function(name) { + delete this._entries[name]; + }; + + proto.get = function(name) { + return (name in this._entries) ? this._entries[name][0] : null; + }; + + proto.getAll = function(name) { + return (name in this._entries) ? this._entries[name].slice(0) : []; + }; + + proto.has = function(name) { + return (name in this._entries); + }; + + proto.set = function(name, value) { + this._entries[name] = [String(value)]; + }; + + proto.forEach = function(callback, thisArg) { + var entries; + for (var name in this._entries) { + if (this._entries.hasOwnProperty(name)) { + entries = this._entries[name]; + for (var i = 0; i < entries.length; i++) { + callback.call(thisArg, entries[i], name, this); + } + } + } + }; + + proto.keys = function() { + var items = []; + this.forEach(function(value, name) { + items.push(name); + }); + return createIterator(items); + }; + + proto.values = function() { + var items = []; + this.forEach(function(value) { + items.push(value); + }); + return createIterator(items); + }; + + proto.entries = function() { + var items = []; + this.forEach(function(value, name) { + items.push([name, value]); + }); + return createIterator(items); + }; + + if (iteratorSupported) { + proto[Symbol.iterator] = proto.entries; + } + + proto.toString = function() { + var searchArray = []; + this.forEach(function(value, name) { + searchArray.push(serializeParam(name) + '=' + serializeParam(value)); + }); + return searchArray.join('&'); + }; + + + global.URLSearchParams = URLSearchParams; + }; + + if (!('URLSearchParams' in global) || (new URLSearchParams('?a=1').toString() !== 'a=1')) { + polyfillURLSearchParams(); + } + + var proto = URLSearchParams.prototype; + + if (typeof proto.sort !== 'function') { + proto.sort = function() { + var _this = this; + var items = []; + this.forEach(function(value, name) { + items.push([name, value]); + if (!_this._entries) { + _this.delete(name); + } + }); + items.sort(function(a, b) { + if (a[0] < b[0]) { + return -1; + } else if (a[0] > b[0]) { + return +1; + } else { + return 0; + } + }); + if (_this._entries) { // force reset because IE keeps keys index + _this._entries = {}; + } + for (var i = 0; i < items.length; i++) { + this.append(items[i][0], items[i][1]); + } + }; + } + + if (typeof proto._fromString !== 'function') { + Object.defineProperty(proto, '_fromString', { + enumerable: false, + configurable: false, + writable: false, + value: function(searchString) { + if (this._entries) { + this._entries = {}; + } else { + var keys = []; + this.forEach(function(value, name) { + keys.push(name); + }); + for (var i = 0; i < keys.length; i++) { + this.delete(keys[i]); + } + } + + searchString = searchString.replace(/^\?/, ''); + var attributes = searchString.split('&'); + var attribute; + for (var i = 0; i < attributes.length; i++) { + attribute = attributes[i].split('='); + this.append( + deserializeParam(attribute[0]), + (attribute.length > 1) ? deserializeParam(attribute[1]) : '' + ); + } + } + }); + } + + // HTMLAnchorElement + +})( + (typeof global !== 'undefined') ? global + : ((typeof window !== 'undefined') ? window + : ((typeof self !== 'undefined') ? self : this)) +); + +(function(global) { + /** + * Polyfill URL + * + * Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js + */ + + var checkIfURLIsSupported = function() { + try { + var u = new URL('b', 'http://a'); + u.pathname = 'c%20d'; + return (u.href === 'http://a/c%20d') && u.searchParams; + } catch (e) { + return false; + } + }; + + + var polyfillURL = function() { + var _URL = global.URL; + + var URL = function(url, base) { + if (typeof url !== 'string') url = String(url); + + // Only create another document if the base is different from current location. + var doc = document, baseElement; + if (base && (global.location === void 0 || base !== global.location.href)) { + doc = document.implementation.createHTMLDocument(''); + baseElement = doc.createElement('base'); + baseElement.href = base; + doc.head.appendChild(baseElement); + try { + if (baseElement.href.indexOf(base) !== 0) throw new Error(baseElement.href); + } catch (err) { + throw new Error('URL unable to set base ' + base + ' due to ' + err); + } + } + + var anchorElement = doc.createElement('a'); + anchorElement.href = url; + if (baseElement) { + doc.body.appendChild(anchorElement); + anchorElement.href = anchorElement.href; // force href to refresh + } + + if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href)) { + throw new TypeError('Invalid URL'); + } + + Object.defineProperty(this, '_anchorElement', { + value: anchorElement + }); + + + // create a linked searchParams which reflect its changes on URL + var searchParams = new URLSearchParams(this.search); + var enableSearchUpdate = true; + var enableSearchParamsUpdate = true; + var _this = this; + ['append', 'delete', 'set'].forEach(function(methodName) { + var method = searchParams[methodName]; + searchParams[methodName] = function() { + method.apply(searchParams, arguments); + if (enableSearchUpdate) { + enableSearchParamsUpdate = false; + _this.search = searchParams.toString(); + enableSearchParamsUpdate = true; + } + }; + }); + + Object.defineProperty(this, 'searchParams', { + value: searchParams, + enumerable: true + }); + + var search = void 0; + Object.defineProperty(this, '_updateSearchParams', { + enumerable: false, + configurable: false, + writable: false, + value: function() { + if (this.search !== search) { + search = this.search; + if (enableSearchParamsUpdate) { + enableSearchUpdate = false; + this.searchParams._fromString(this.search); + enableSearchUpdate = true; + } + } + } + }); + }; + + var proto = URL.prototype; + + var linkURLWithAnchorAttribute = function(attributeName) { + Object.defineProperty(proto, attributeName, { + get: function() { + return this._anchorElement[attributeName]; + }, + set: function(value) { + this._anchorElement[attributeName] = value; + }, + enumerable: true + }); + }; + + ['hash', 'host', 'hostname', 'port', 'protocol'] + .forEach(function(attributeName) { + linkURLWithAnchorAttribute(attributeName); + }); + + Object.defineProperty(proto, 'search', { + get: function() { + return this._anchorElement['search']; + }, + set: function(value) { + this._anchorElement['search'] = value; + this._updateSearchParams(); + }, + enumerable: true + }); + + Object.defineProperties(proto, { + + 'toString': { + get: function() { + var _this = this; + return function() { + return _this.href; + }; + } + }, + + 'href': { + get: function() { + return this._anchorElement.href.replace(/\?$/, ''); + }, + set: function(value) { + this._anchorElement.href = value; + this._updateSearchParams(); + }, + enumerable: true + }, + + 'pathname': { + get: function() { + return this._anchorElement.pathname.replace(/(^\/?)/, '/'); + }, + set: function(value) { + this._anchorElement.pathname = value; + }, + enumerable: true + }, + + 'origin': { + get: function() { + // get expected port from protocol + var expectedPort = { 'http:': 80, 'https:': 443, 'ftp:': 21 }[this._anchorElement.protocol]; + // add port to origin if, expected port is different than actual port + // and it is not empty f.e http://foo:8080 + // 8080 != 80 && 8080 != '' + var addPortToOrigin = this._anchorElement.port != expectedPort && + this._anchorElement.port !== ''; + + return this._anchorElement.protocol + + '//' + + this._anchorElement.hostname + + (addPortToOrigin ? (':' + this._anchorElement.port) : ''); + }, + enumerable: true + }, + + 'password': { // TODO + get: function() { + return ''; + }, + set: function(value) { + }, + enumerable: true + }, + + 'username': { // TODO + get: function() { + return ''; + }, + set: function(value) { + }, + enumerable: true + }, + }); + + URL.createObjectURL = function(blob) { + return _URL.createObjectURL.apply(_URL, arguments); + }; + + URL.revokeObjectURL = function(url) { + return _URL.revokeObjectURL.apply(_URL, arguments); + }; + + global.URL = URL; + + }; + + if (!checkIfURLIsSupported()) { + polyfillURL(); + } + + if ((global.location !== void 0) && !('origin' in global.location)) { + var getOrigin = function() { + return global.location.protocol + '//' + global.location.hostname + (global.location.port ? (':' + global.location.port) : ''); + }; + + try { + Object.defineProperty(global.location, 'origin', { + get: getOrigin, + enumerable: true + }); + } catch (e) { + setInterval(function() { + global.location.origin = getOrigin(); + }, 100); + } + } + +})( + (typeof global !== 'undefined') ? global + : ((typeof window !== 'undefined') ? window + : ((typeof self !== 'undefined') ? self : this)) +); + +/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(5))) + +/***/ }) +/******/ ]); +}); diff --git a/static/epub.js/js/epub.min.js b/static/epub.js/js/epub.min.js new file mode 100644 index 000000000..708d146a7 --- /dev/null +++ b/static/epub.js/js/epub.min.js @@ -0,0 +1 @@ +(function(e,t){'object'==typeof exports&&'object'==typeof module?module.exports=t(require('xmldom'),function(){try{return require('jszip')}catch(t){}}()):'function'==typeof define&&define.amd?define(['xmldom','jszip'],t):'object'==typeof exports?exports.ePub=t(require('xmldom'),function(){try{return require('jszip')}catch(t){}}()):e.ePub=t(e.xmldom,e.jszip)})('undefined'==typeof self?this:self,function(e,t){var n=String.prototype,a=Math.abs,i=Math.min,o=Math.ceil,r=Math.round,s=Math.max,l=Math.floor;return function(e){function t(a){if(n[a])return n[a].exports;var i=n[a]={i:a,l:!1,exports:{}};return e[a].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,a){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:a})},t.n=function(e){var n=e&&e.__esModule?function(){return e['default']}:function(){return e};return t.d(n,'a',n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p='/dist/',t(t.s=25)}([function(e,t,n){'use strict';function a(e,t){if(!(e instanceof t))throw new TypeError('Cannot call a class as a function')}function i(){var e=new Date().getTime(),t='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(t){var n=0|(e+16*Math.random())%16;return e=l(e/16),('x'==t?n:8|7&n).toString(16)});return t}function o(e){return!isNaN(parseFloat(e))&&isFinite(e)}function r(e,t,n,a,i){var o=a||0,s=i||t.length,l=parseInt(o+(s-o)/2),d;return(n||(n=function(e,t){return e>t?1:e=s-o)?l:(d=n(t[l],e),1==s-o?0<=d?l:l+1:0===d?l:-1===d?r(e,t,n,l,s):r(e,t,n,o,l))}function d(e,t,n,a,i){var o=a||0,r=i||t.length,s=parseInt(o+(r-o)/2),l;return(n||(n=function(e,t){return e>t?1:e=r-o)?-1:(l=n(t[s],e),1==r-o?0===l?s:-1:0===l?s:-1===l?d(e,t,n,s,r):d(e,t,n,o,s))}function u(e,t){for(var n=e.parentNode,a=n.childNodes,o=-1,r=0,i;rn.spinePos)return 1;if(t.spinePoso[d].index)return 1;if(a[d].indexs.offset?1:r.offset')}},{key:'textNodes',value:function(e,t){return Array.prototype.slice.call(e.childNodes).filter(function(e){return e.nodeType===l||t&&e.classList.contains(t)})}},{key:'walkToNode',value:function(e,t,n){var a=t||document,o=a.documentElement,s=e.length,l,d,u;for(u=0;uc)t-=c;else{i=u.nodeType===s?u.childNodes[0]:u;break}}return{container:i,offset:t}}},{key:'toRange',value:function(e,t){var n=e||document,a=this,i=!!t&&null!=n.querySelector('.'+t),o,s,l,d,u,c,p,g;if(o='undefined'==typeof n.createRange?new r.RangeObject:n.createRange(),a.range?(s=a.start,c=a.path.steps.concat(s.steps),d=this.findNode(c,n,i?t:null),l=a.end,p=a.path.steps.concat(l.steps),u=this.findNode(p,n,i?t:null)):(s=a.path,c=a.path.steps,d=this.findNode(a.path.steps,n,i?t:null)),d)try{null==s.terminal.offset?o.setStart(d,0):o.setStart(d,s.terminal.offset)}catch(a){g=this.fixMiss(c,s.terminal.offset,n,i?t:null),o.setStart(g.container,g.offset)}else return console.log('No startContainer found for',this.toString()),null;if(u)try{null==l.terminal.offset?o.setEnd(u,0):o.setEnd(u,l.terminal.offset)}catch(r){g=this.fixMiss(p,a.end.terminal.offset,n,i?t:null),o.setEnd(g.container,g.offset)}return o}},{key:'isCfiString',value:function(e){return'string'==typeof e&&0===e.indexOf('epubcfi(')&&')'===e[e.length-1]}},{key:'generateChapterComponent',value:function(e,t,n){var a=parseInt(t),i='/'+2*(e+1)+'/';return i+=2*(a+1),n&&(i+='['+n+']'),i}},{key:'collapse',value:function(e){this.range&&(this.range=!1,e?(this.path.steps=this.path.steps.concat(this.start.steps),this.path.terminal=this.start.terminal):(this.path.steps=this.path.steps.concat(this.end.steps),this.path.terminal=this.end.terminal))}}]),e}();t.default=d,e.exports=t['default']},function(e,t){'use strict';Object.defineProperty(t,'__esModule',{value:!0});var n=t.EPUBJS_VERSION='0.3',a=t.DOM_EVENTS=['keydown','keyup','keypressed','mouseup','mousedown','click','touchend','touchstart','touchmove'],i=t.EVENTS={BOOK:{OPEN_FAILED:'openFailed'},CONTENTS:{EXPAND:'expand',RESIZE:'resize',SELECTED:'selected',SELECTED_RANGE:'selectedRange',LINK_CLICKED:'linkClicked'},LOCATIONS:{CHANGED:'changed'},MANAGERS:{RESIZE:'resize',RESIZED:'resized',ORIENTATION_CHANGE:'orientationchange',ADDED:'added',SCROLL:'scroll',SCROLLED:'scrolled',REMOVED:'removed'},VIEWS:{AXIS:'axis',LOAD_ERROR:'loaderror',RENDERED:'rendered',RESIZED:'resized',DISPLAYED:'displayed',SHOWN:'shown',HIDDEN:'hidden',MARK_CLICKED:'markClicked'},RENDITION:{STARTED:'started',ATTACHED:'attached',DISPLAYED:'displayed',DISPLAY_ERROR:'displayerror',RENDERED:'rendered',REMOVED:'removed',RESIZED:'resized',ORIENTATION_CHANGE:'orientationchange',LOCATION_CHANGED:'locationChanged',RELOCATED:'relocated',MARK_CLICKED:'markClicked',SELECTED:'selected',LAYOUT:'layout'},LAYOUT:{UPDATED:'updated'},ANNOTATION:{ATTACH:'attach',DETACH:'detach'}}},function(e,t,n){'use strict';var a=n(27),o=n(41),r=Function.prototype.apply,s=Function.prototype.call,i=Object.create,l=Object.defineProperty,d=Object.defineProperties,u=Object.prototype.hasOwnProperty,c={configurable:!0,enumerable:!1,writable:!0},p,g,h,m,f,y,v;p=function(e,t){var n;return o(t),u.call(this,'__ee__')?n=this.__ee__:(n=c.value=i(null),l(this,'__ee__',c),c.value=null),n[e]?'object'==typeof n[e]?n[e].push(t):n[e]=[n[e],t]:n[e]=t,this},g=function(e,t){var n,a;return o(t),a=this,p.call(this,e,n=function(){h.call(a,e,n),r.call(t,this,arguments)}),n.__eeOnceListener__=t,this},h=function(e,t){var n,a,r,s;if(o(t),!u.call(this,'__ee__'))return this;if(n=this.__ee__,!n[e])return this;if(a=n[e],'object'==typeof a)for(s=0;r=a[s];++s)(r===t||r.__eeOnceListener__===t)&&(2===a.length?n[e]=a[s?0:1]:a.splice(s,1));else(a===t||a.__eeOnceListener__===t)&&delete n[e];return this},m=function(e){var t,n,a,i,o;if(u.call(this,'__ee__')&&(i=this.__ee__[e],!!i))if('object'==typeof i){for(n=arguments.length,o=Array(n-1),t=1;tn.length||46!==n.charCodeAt(n.length-1)||46!==n.charCodeAt(n.length-2))if(2c){if(47===n.charCodeAt(l+g))return n.slice(l+g+1);if(0==g)return n.slice(l+g)}else s>c&&(47===e.charCodeAt(a+g)?p=g:0==g&&(p=0));break}var i=e.charCodeAt(a+g),h=n.charCodeAt(l+g);if(i!==h)break;else 47===i&&(p=g)}var m='';for(g=a+p+1;g<=o;++g)(g===o||47===e.charCodeAt(g))&&(m+=0===m.length?'..':'/..');return 0=r;--c){if(a=e.charCodeAt(c),47===a){if(!u){l=c+1;break}continue}-1==d&&(u=!1,d=c+1),46===a?-1==s?s=c:1!=i&&(i=1):-1!=s&&(i=-1)}return-1==s||-1==d||0==i||1==i&&s==d-1&&s==l+1?-1!=d&&(0==l&&o?n.base=n.name=e.slice(1,d):n.base=n.name=e.slice(l,d)):(0==l&&o?(n.name=e.slice(1,s),n.base=e.slice(1,d)):(n.name=e.slice(l,s),n.base=e.slice(l,d)),n.ext=e.slice(s,d)),0this.container.scrollWidth&&(t=this.container.scrollWidth-this.layout.delta)):n=e.top,this.scrollTo(t,n,!0)}},{key:'add',value:function(e){var t=this,n=this.createView(e);return this.views.append(n),n.onDisplayed=this.afterDisplayed.bind(this),n.onResize=this.afterResized.bind(this),n.on(b.EVENTS.VIEWS.AXIS,function(e){t.updateAxis(e)}),n.display(this.request)}},{key:'append',value:function(e){var t=this,n=this.createView(e);return this.views.append(n),n.onDisplayed=this.afterDisplayed.bind(this),n.onResize=this.afterResized.bind(this),n.on(b.EVENTS.VIEWS.AXIS,function(e){t.updateAxis(e)}),n.display(this.request)}},{key:'prepend',value:function(e){var t=this,n=this.createView(e);return n.on(b.EVENTS.VIEWS.RESIZED,function(e){t.counter(e)}),this.views.prepend(n),n.onDisplayed=this.afterDisplayed.bind(this),n.onResize=this.afterResized.bind(this),n.on(b.EVENTS.VIEWS.AXIS,function(e){t.updateAxis(e)}),n.display(this.request)}},{key:'counter',value:function(e){'vertical'===this.settings.axis?this.scrollBy(0,e.heightDelta,!0):this.scrollBy(e.widthDelta,0,!0)}},{key:'next',value:function(){var e=this.settings.direction,t,n;if(this.views.length){if(this.isPaginated&&'horizontal'===this.settings.axis&&(!e||'ltr'===e))this.scrollLeft=this.container.scrollLeft,n=this.container.scrollLeft+this.container.offsetWidth+this.layout.delta,n<=this.container.scrollWidth?this.scrollBy(this.layout.delta,0,!0):t=this.views.last().section.next();else if(this.isPaginated&&'horizontal'===this.settings.axis&&'rtl'===e)this.scrollLeft=this.container.scrollLeft,n=this.container.scrollLeft,0p&&(h=p,s=h-g);var m=e.layout.count(p,a).pages,f=o(g/a),y=[],v=o(h/a);y=[];for(var b=f,i;b<=v;b++)i=b+1,y.push(i);var k=e.mapping.page(t.contents,t.section.cfiBase,g,h);return{index:d,href:u,pages:y,totalPages:m,mapping:k}});return i}},{key:'paginatedLocation',value:function(){var e=this,t=this.visible(),n=this.container.getBoundingClientRect(),a=0,o=0;this.settings.fullsize&&(a=window.scrollX);var i=t.map(function(t){var r=t.section,s=r.index,d=r.href,u=t.offset().left,c=t.position().left,p=t.width(),g=a+n.left-c+o,h=g+e.layout.width-o,m=e.mapping.page(t.contents,t.section.cfiBase,g,h),f=e.layout.count(p).pages,y=l(g/e.layout.pageWidth),v=[],b=l(h/e.layout.pageWidth);if(0>y&&(y=0,++b),'rtl'===e.settings.direction){var k=y;y=f-b,b=f-k}for(var x=y+1,i;x<=b;x++)i=x,v.push(i);return{index:s,href:d,pages:v,totalPages:f,mapping:m}});return i}},{key:'isVisible',value:function(e,t,n,a){var i=e.position(),o=a||this.bounds();return'horizontal'===this.settings.axis&&i.right>o.left-t&&i.lefto.top-t&&i.top'==e&&'>'||'&'==e&&'&'||'"'==e&&'"'||'&#'+e.charCodeAt()+';'}function m(e,t){if(t(e))return!0;if(e=e.firstChild)do if(m(e,t))return!0;while(e=e.nextSibling)}function f(){}function y(e,t,n){e&&e._inc++;var a=n.namespaceURI;'http://www.w3.org/2000/xmlns/'==a&&(t._nsMap[n.prefix?n.localName:'']=n.value)}function v(e,t,n){e&&e._inc++;var a=n.namespaceURI;'http://www.w3.org/2000/xmlns/'==a&&delete t._nsMap[n.prefix?n.localName:'']}function b(e,t,n){if(e&&e._inc){e._inc++;var a=t.childNodes;if(n)a[a.length++]=n;else{for(var o=t.firstChild,r=0;o;)a[r++]=o,o=o.nextSibling;a.length=r}}}function k(e,t){var n=t.previousSibling,a=t.nextSibling;return n?n.nextSibling=a:e.firstChild=a,a?a.previousSibling=n:e.lastChild=n,b(e.ownerDocument,e),t}function x(e,t,n){var a=t.parentNode;if(a&&a.removeChild(t),t.nodeType===ee){var i=t.firstChild;if(null==i)return t;var o=t.lastChild}else i=o=t;var r=n?n.previousSibling:e.lastChild;i.previousSibling=r,o.nextSibling=n,r?r.nextSibling=i:e.firstChild=i,null==n?e.lastChild=o:n.previousSibling=o;do i.parentNode=e;while(i!==o&&(i=i.nextSibling));return b(e.ownerDocument||e,e),t.nodeType==ee&&(t.firstChild=t.lastChild=null),t}function E(e,t){var n=t.parentNode;if(n){var a=e.lastChild;n.removeChild(t);var a=e.lastChild}var a=e.lastChild;return t.parentNode=e,t.previousSibling=a,t.nextSibling=null,a?a.nextSibling=t:e.firstChild=t,e.lastChild=t,b(e.ownerDocument,e,t),t}function _(){this._nsMap={}}function N(){}function S(){}function w(){}function T(){}function C(){}function R(){}function I(){}function A(){}function L(){}function O(){}function D(){}function P(){}function z(e,t){var n=[],a=9==this.nodeType?this.documentElement:this,i=a.prefix,o=a.namespaceURI;if(o&&null==i){var i=a.lookupPrefix(o);if(null==i)var r=[{namespace:o,prefix:null}]}return B(this,n,e,t,r),n.join('')}function M(e,t,n){var a=e.prefix||'',o=e.namespaceURI;if(!a&&!o)return!1;if('xml'===a&&'http://www.w3.org/XML/1998/namespace'===o||'http://www.w3.org/2000/xmlns/'==o)return!1;for(var r=n.length;r--;){var i=n[r];if(i.prefix==a)return i.namespace!=o}return!0}function B(e,t,n,a,o){if(a){if(e=a(e),!e)return;if('string'==typeof e)return void t.push(e)}switch(e.nodeType){case F:o||(o=[]);var r=o.length,s=e.attributes,l=s.length,d=e.firstChild,u=e.tagName;n=V===e.namespaceURI||n,t.push('<',u);for(var c=0,i;c'),n&&/^script$/i.test(u))for(;d;)d.data?t.push(d.data):B(d,t,n,a,o),d=d.nextSibling;else for(;d;)B(d,t,n,a,o),d=d.nextSibling;t.push('')}else t.push('/>');return;case J:case ee:for(var d=e.firstChild;d;)B(d,t,n,a,o),d=d.nextSibling;return;case H:return t.push(' ',e.name,'="',e.value.replace(/[<&"]/g,h),'"');case X:return t.push(e.data.replace(/[<&]/g,h));case Y:return t.push('');case Q:return t.push('');case $:var f=e.publicId,y=e.systemId;if(t.push('');else if(y&&'.'!=y)t.push(' SYSTEM "',y,'">');else{var v=e.internalSubset;v&&t.push(' [',v,']'),t.push('>')}return;case Z:return t.push('');case G:return t.push('&',e.nodeName,';');default:t.push('??',e.nodeName);}}function j(e,t,n){var a;switch(t.nodeType){case F:a=t.cloneNode(!1),a.ownerDocument=e;case ee:break;case H:n=!0;}if(a||(a=t.cloneNode(!1)),a.ownerDocument=e,a.parentNode=null,n)for(var i=t.firstChild;i;)a.appendChild(j(e,i,n)),i=i.nextSibling;return a}function q(e,t,a){var o=new t.constructor;for(var s in t){var n=t[s];'object'!=typeof n&&n!=o[s]&&(o[s]=n)}switch(t.childNodes&&(o.childNodes=new r),o.ownerDocument=e,o.nodeType){case F:var d=t.attributes,u=o.attributes=new l,c=d.length;u._ownerElement=o;for(var p=0;p=a.end.displayed.total&&(a.atEnd=!0),t.index===this.book.spine.first().index&&1===a.start.displayed.page&&(a.atStart=!0),a}},{key:'destroy',value:function(){this.manager&&this.manager.destroy(),this.book=void 0}},{key:'passEvents',value:function(t){var n=this;N.DOM_EVENTS.forEach(function(a){t.on(a,function(e){return n.triggerViewEvent(e,t)})}),t.on(N.EVENTS.CONTENTS.SELECTED,function(a){return n.triggerSelectedEvent(a,t)})}},{key:'triggerViewEvent',value:function(t,e){this.emit(t.type,t,e)}},{key:'triggerSelectedEvent',value:function(e,t){this.emit(N.EVENTS.RENDITION.SELECTED,e,t)}},{key:'triggerMarkEvent',value:function(e,t,n){this.emit(N.EVENTS.RENDITION.MARK_CLICKED,e,t,n)}},{key:'getRange',value:function(e,t){var n=new g.default(e),a=this.manager.visible().filter(function(e){if(n.spinePos===e.index)return!0});if(a.length)return a[0].contents.range(n,t)}},{key:'adjustImages',value:function(e){if('pre-paginated'===this._layout.name)return new Promise(function(e){e()});var t=e.window.getComputedStyle(e.content,null),n=.95*(e.content.offsetHeight-(parseFloat(t.paddingTop)+parseFloat(t.paddingBottom))),a=parseFloat(t.verticalPadding);return e.addStylesheetRules({img:{"max-width":(this._layout.columnWidth?this._layout.columnWidth-a+'px':'100%')+'!important',"max-height":n+'px!important',"object-fit":'contain',"page-break-inside":'avoid',"break-inside":'avoid',"box-sizing":'border-box'},svg:{"max-width":(this._layout.columnWidth?this._layout.columnWidth-a+'px':'100%')+'!important',"max-height":n+'px!important',"page-break-inside":'avoid',"break-inside":'avoid'}}),new Promise(function(e){setTimeout(function(){e()},1)})}},{key:'getContents',value:function(){return this.manager?this.manager.getContents():[]}},{key:'views',value:function(){var e=this.manager?this.manager.views:void 0;return e||[]}},{key:'handleLinks',value:function(e){var t=this;e&&e.on(N.EVENTS.CONTENTS.LINK_CLICKED,function(e){var n=t.book.path.relative(e);t.display(n)})}},{key:'injectStylesheet',value:function(e){var t=e.createElement('link');t.setAttribute('type','text/css'),t.setAttribute('rel','stylesheet'),t.setAttribute('href',this.settings.stylesheet),e.getElementsByTagName('head')[0].appendChild(t)}},{key:'injectScript',value:function(e){var t=e.createElement('script');t.setAttribute('type','text/javascript'),t.setAttribute('src',this.settings.script),t.textContent=' ',e.getElementsByTagName('head')[0].appendChild(t)}},{key:'injectIdentifier',value:function(e){var t=this.book.packaging.metadata.identifier,n=e.createElement('meta');n.setAttribute('name','dc.relation.ispartof'),t&&n.setAttribute('content',t),e.getElementsByTagName('head')[0].appendChild(n)}}]),e}();(0,l.default)(A.prototype),t.default=A,e.exports=t['default']},function(e,t,n){'use strict';function a(e,t){if(!(e instanceof t))throw new TypeError('Cannot call a class as a function')}Object.defineProperty(t,'__esModule',{value:!0});var i=function(){function e(e,t){for(var n=0,a;n=t&&r<=n)return e;if(s>t)return e;o=e,i.push(e)}else if(a.horizontal&&'rtl'===a.direction){if(r=c.left,s=c.right,s<=n&&s>=t)return e;if(r=t&&l<=n)return e;if(u>t)return e;o=e,i.push(e)}}),s)return this.findTextStartRange(s,t,n);return this.findTextStartRange(o,t,n)}},{key:'findEnd',value:function(e,t,n){for(var a=this,i=[e],o=e,s,l;i.length;)if(s=i.shift(),l=this.walk(s,function(e){var s,l,u,c,p;if(p=(0,d.nodeBounds)(e),a.horizontal&&'ltr'===a.direction){if(s=r(p.left),l=r(p.right),s>n&&o)return o;if(l>n)return e;o=e,i.push(e)}else if(a.horizontal&&'rtl'===a.direction){if(s=r(a.horizontal?p.left:p.top),l=r(a.horizontal?p.right:p.bottom),ln&&o)return o;if(c>n)return e;o=e,i.push(e)}}),l)return this.findTextEndRange(l,t,n);return this.findTextEndRange(o,t,n)}},{key:'findTextStartRange',value:function(e,t,n){for(var a=this.splitTextNodeIntoRanges(e),o=0,i,r,s,l,d;o=t)return i;}else if(this.horizontal&&'rtl'===this.direction){if(d=r.right,d<=n)return i;}else if(l=r.top,l>=t)return i;return a[0]}},{key:'findTextEndRange',value:function(e,t,n){for(var a=this.splitTextNodeIntoRanges(e),o=0,i,r,s,l,d,u,c;on&&i)return i;if(d>n)return r}else if(this.horizontal&&'rtl'===this.direction){if(l=s.left,d=s.right,dn&&i)return i;if(c>n)return r}i=r}return a[a.length-1]}},{key:'splitTextNodeIntoRanges',value:function(e,t){var n=[],a=e.textContent||'',i=a.trim(),o=e.ownerDocument,r=t||' ',s=i.indexOf(r),l;if(-1===s||e.nodeType!=Node.TEXT_NODE)return l=o.createRange(),l.selectNodeContents(e),[l];for(l=o.createRange(),l.setStart(e,0),l.setEnd(e,s),n.push(l),l=!1;-1!=s;)s=i.indexOf(r,s+1),0=t||0>n||y&&a>=x}function p(){var e=o();return c(e)?g(e):void(_=setTimeout(p,u(e)))}function g(e){return(_=void 0,v&&b)?l(e):(b=k=void 0,E)}function h(){var e=o(),n=c(e);if(b=arguments,k=this,N=e,n){if(void 0===_)return d(N);if(y)return _=setTimeout(p,t),l(N)}return void 0===_&&(_=setTimeout(p,t)),E}var m=0,f=!1,y=!1,v=!0,b,k,x,E,_,N;if('function'!=typeof e)throw new TypeError('Expected a function');return t=r(t)||0,a(n)&&(f=!!n.leading,y='maxWait'in n,x=y?s(r(n.maxWait)||0,t):x,v='trailing'in n?!!n.trailing:v),h.cancel=function(){void 0!==_&&clearTimeout(_),m=0,b=N=k=_=void 0},h.flush=function(){return void 0===_?E:g(o())},h}},function(e,t,n){var a=n(62),i='object'==typeof self&&self&&self.Object===Object&&self,o=a||i||Function('return this')();e.exports=o},function(e,t,n){var a=n(22),i=a.Symbol;e.exports=i},function(e,t,n){'use strict';function i(e){return e&&e.__esModule?e:{default:e}}function o(e,t){if(!(e instanceof t))throw new TypeError('Cannot call a class as a function')}function r(e,t){if(!e)throw new ReferenceError('this hasn\'t been initialised - super() hasn\'t been called');return t&&('object'==typeof t||'function'==typeof t)?t:e}function s(e,t){if('function'!=typeof t&&null!==t)throw new TypeError('Super expression must either be null or a function, not '+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,'__esModule',{value:!0});var d='function'==typeof Symbol&&'symbol'==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&'function'==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?'symbol':typeof e},u=function(){function e(e,t){for(var n=0,a;n=h&&(o&&d?m():f()),0>c-r&&(o&&d?f():m());var y=i.map(function(e){return e.displayed});return i.length?Promise.all(y).then(function(){if('pre-paginated'===n.layout.name&&n.layout.props.spread)return n.check()}).then(function(){return n.update(r)},function(e){return e}):(this.q.enqueue(function(){this.update()}.bind(this)),a.resolve(!1),a.promise)}},{key:'trim',value:function(){for(var e=new p.defer,t=this.views.displayed(),n=t[0],a=t[t.length-1],o=this.views.indexOf(n),r=this.views.indexOf(a),s=this.views.slice(0,o),l=this.views.slice(r+1),d=0;darguments.length||'string'!=typeof t?(d=n,n=t,t=null):d=arguments[2],null==t?(o=l=!0,s=!1):(o=r.call(t,'c'),s=r.call(t,'e'),l=r.call(t,'w')),u={value:n,configurable:o,enumerable:s,writable:l},d?a(i(d),u):u},s.gs=function(t,n,s){var l,d,u,p;return'string'==typeof t?u=arguments[3]:(u=s,s=n,n=t,t=null),null==n?n=void 0:o(n)?null==s?s=void 0:!o(s)&&(u=s,s=void 0):(u=n,n=s=void 0),null==t?(l=!0,d=!1):(l=r.call(t,'c'),d=r.call(t,'e')),p={get:n,set:s,configurable:l,enumerable:d},u?a(i(u),p):p}},function(e,t,n){'use strict';e.exports=n(29)()?Object.assign:n(30)},function(e){'use strict';e.exports=function(){var e=Object.assign,t;return!('function'!=typeof e)&&(t={foo:'raz'},e(t,{bar:'dwa'},{trzy:'trzy'}),'razdwatrzy'===t.foo+t.bar+t.trzy)}},function(e,t,n){'use strict';var a=n(31),o=n(35);e.exports=function(e,t){var n=s(arguments.length,2),r,l,i;for(e=Object(o(e)),i=function(n){try{e[n]=t[n]}catch(t){r||(r=t)}},l=1;l=t+n||t?new java.lang.String(e,t,n)+'':e}function d(e,t){e.currentElement?e.currentElement.appendChild(t):e.doc.appendChild(t)}a.prototype.parseFromString=function(e,t){var n=this.options,a=new u,r=n.domBuilder||new o,s=n.errorHandler,l=n.locator,d=n.xmlns||{},c={lt:'<',gt:'>',amp:'&',quot:'"',apos:'\''};return l&&r.setDocumentLocator(l),a.errorHandler=i(s,r,l),a.domBuilder=n.domBuilder||r,/\/x?html?$/.test(t)&&(c.nbsp='\xA0',c.copy='\xA9',d['']='http://www.w3.org/1999/xhtml'),d.xml=d.xml||'http://www.w3.org/XML/1998/namespace',e?a.parse(e,d,c):a.errorHandler.error('invalid doc source'),r.doc},o.prototype={startDocument:function(){this.doc=new c().createDocument(null,null,null),this.locator&&(this.doc.documentURI=this.locator.systemId)},startElement:function(e,t,n,a){var o=this.doc,s=o.createElementNS(e,n||t),l=a.length;d(this,s),this.currentElement=s,this.locator&&r(this.locator,s);for(var u=0;u>10),a=56320+(1023&e);return t(n,a)}return t(e)}function y(e){var t=e.slice(1,-1);return t in n?n[t]:'#'===t.charAt(0)?f(parseInt(t.substr(1).replace('x','0x'))):(m.error('entity not found:'+e),e)}function v(t){if(t>w){var n=e.substring(w,t).replace(/&#?\w+;/g,y);_&&b(w),c.characters(n,0,t-w),w=t}}function b(t,n){for(;t>=x&&(n=E.exec(e));)k=n.index,x=k+n[0].length,_.lineNumber++;_.columnNumber=t-k+1}for(var k=0,x=0,E=/.*(?:\r\n?|\n)|.*$/g,_=c.locator,N=[{currentNSMap:t}],S={},w=0;;){try{var T=e.indexOf('<',w);if(0>T){if(!e.substr(w).match(/^\s*$/)){var C=c.doc,R=C.createTextNode(e.substr(w));C.appendChild(R),c.currentElement=R}return}switch(T>w&&v(T),e.charAt(T+1)){case'/':var I=e.indexOf('>',T+3),A=e.substring(T+2,I),L=N.pop();0>I?(A=e.substring(T+2).replace(/[\s<].*/,''),m.error('end tag name: '+A+' is not complete:'+L.tagName),I=T+1+A.length):A.match(/\sw?w=I:v(s(T,w)+1)}}function o(e,n){return n.lineNumber=e.lineNumber,n.columnNumber=e.columnNumber,n}function r(e,t,n,a,i,o){for(var r=++t,l=b,s,d;;){var u=e.charAt(r);switch(u){case'=':if(l==k)s=e.slice(t,r),l=E;else if(l==x)l=E;else throw new Error('attribute equal must after attrName');break;case'\'':case'"':if(l==E||l==k){if(l==k&&(o.warning('attribute value must after "="'),s=e.slice(t,r)),t=r+1,r=e.indexOf(u,t),0=u)switch(l){case b:n.setTagName(e.slice(t,r)),l=S;break;case k:s=e.slice(t,r),l=x;break;case _:var d=e.slice(t,r).replace(/&#?\w+;/g,i);o.warning('attribute "'+d+'" missed quot(")!!'),n.add(s,d,t);case N:l=S;}else switch(l){case x:n.tagName;'http://www.w3.org/1999/xhtml'===a['']&&s.match(/^(?:disabled|checked|selected)$/i)||o.warning('attribute "'+s+'" missed value!! "'+s+'" instead2!!'),n.add(s,s,t),t=r,l=k;break;case N:o.warning('attribute space is required"'+s+'"!!');case S:l=k,t=r;break;case E:l=_,t=r;break;case w:throw new Error('elements closed character \'/\' and \'>\' must be connected to');}}r++}}function l(e,t,n){for(var o=e.tagName,r=null,s=e.length;s--;){var i=e[s],a=i.qName,l=i.value,d=a.indexOf(':');if(0',t),r=e.substring(t+1,o);if(/[&<]/.test(r))return /^script$/i.test(n)?(i.characters(r,0,r.length),o):(r=r.replace(/&#?\w+;/g,a),i.characters(r,0,r.length),o)}return t+1}function u(e,t,n,a){var i=a[n];return null==i&&(i=e.lastIndexOf(''),i',t+4);return o>t?(n.comment(e,t+4,o-t-4),o+3):(a.error('Unclosed comment'),-1)}return-1;default:if('CDATA['==e.substr(t+3,6)){var o=e.indexOf(']]>',t+9);return n.startCDATA(),n.characters(e,t+9,o-t-9),n.endCDATA(),o+3}var r=m(e,t),s=r.length;if(1',t);if(a){var i=e.substring(t,a).match(/^<\?(\S*)\s*([\s\S]*?)\s*$/);if(i){i[0].length;return n.processingInstruction(i[1],i[2]),a+2}return-1}return-1}function h(){}function i(e,t){return e.__proto__=t,e}function m(e,t){var n=[],a=/'[^']+'|"[^"]+"|[^\s<>\/=]+=?|(\/?\s*>|<)/g,i;for(a.lastIndex=t,a.exec(e);i=a.exec(e);)if(n.push(i),i[1])return n}var f=/[A-Z_a-z\xC0-\xD6\xD8-\xF6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/,y=new RegExp('[\\-\\.0-9'+f.source.slice(1,-1)+'\\u00B7\\u0300-\\u036F\\u203F-\\u2040]'),v=new RegExp('^'+f.source+y.source+'*(?::'+f.source+y.source+'*)?$'),b=0,k=1,x=2,E=3,_=4,N=5,S=6,w=7;n.prototype={parse:function(e,t,n){var i=this.domBuilder;i.startDocument(),c(t,t={}),a(e,t,n,i,this.errorHandler),i.endDocument()}},h.prototype={setTagName:function(e){if(!v.test(e))throw new Error('invalid tagName:'+e);this.tagName=e},add:function(e,t,n){if(!v.test(e))throw new Error('invalid attribute:'+e);this[this.length++]={qName:e,value:t,offset:n}},length:0,getLocalName:function(e){return this[e].localName},getLocator:function(e){return this[e].locator},getQName:function(e){return this[e].qName},getURI:function(e){return this[e].uri},getValue:function(e){return this[e].value}},i({},i.prototype)instanceof i||(i=function(e,t){function n(){}for(t in n.prototype=t,n=new n,e)n[t]=e[t];return n}),t.XMLReader=n},function(e,t,n){'use strict';function a(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError('Cannot call a class as a function')}Object.defineProperty(t,'__esModule',{value:!0});var r=function(){function e(e,t){for(var n=0,a;nn&&(r+=n,i=n);i=n)r+=n-i,i=n;else{i+=o,d.endContainer=e,d.endOffset=i;var s=new c.default(d,t).toString();a.push(s),r=0}u=e}.bind(this)),d&&d.startContainer&&u){d.endContainer=u,d.endOffset=u.length;var p=new c.default(d,t).toString();a.push(p),r=0}return a}},{key:'locationFromCfi',value:function(e){var t;return(c.default.prototype.isCfiString(e)&&(e=new c.default(e)),0===this._locations.length)?-1:(t=(0,s.locationOf)(e,this._locations,this.epubcfi.compare),t>this.total?this.total:t)}},{key:'percentageFromCfi',value:function(e){if(0===this._locations.length)return null;var t=this.locationFromCfi(e);return this.percentageFromLocation(t)}},{key:'percentageFromLocation',value:function(e){return e&&this.total?e/this.total:0}},{key:'cfiFromLocation',value:function(e){var t=-1;return'number'!=typeof e&&(e=parseInt(e)),0<=e&&e=this._minSpreadWidth?2:1,'reflowable'!==this.name||'paginated'!==this._flow||0<=n||(i=0==s%2?s:s-1),'pre-paginated'===this.name&&(i=0),1=e.left&&t.top>=e.top&&t.bottom<=e.bottom}var u='function'==typeof Symbol&&'symbol'==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&'function'==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?'symbol':typeof e};Object.defineProperty(t,'__esModule',{value:!0}),t.Underline=t.Highlight=t.Mark=t.Pane=void 0;var c=function e(t,n,a){null===t&&(t=Function.prototype);var i=Object.getOwnPropertyDescriptor(t,n);if(i===void 0){var o=Object.getPrototypeOf(t);return null===o?void 0:e(o,n,a)}if('value'in i)return i.value;var r=i.get;return void 0===r?void 0:r.call(a)},p=function(){function e(e,t){for(var n=0,a;nn&&r>t}var s=t.getBoundingClientRect(),r=e.getBoundingClientRect();if(!o(r,n,a))return!1;for(var l=e.getClientRects(),d=0,i=l.length;d(e/=0.5)?0.5*s(e,5):0.5*(s(e-2,5)+2)},easeInCubic:function(e){return s(e,3)}},m=function(){function e(t,n){o(this,e),this.settings=(0,u.extend)({duration:80,minVelocity:0.2,minDistance:10,easing:h.easeInCubic},n||{}),this.supportsTouch=this.supportsTouch(),this.supportsTouch&&this.setup(t)}return d(e,[{key:'setup',value:function(e){this.manager=e,this.layout=this.manager.layout,this.fullsize=this.manager.settings.fullsize,this.fullsize?(this.element=this.manager.stage.element,this.scroller=window,this.disableScroll()):(this.element=this.manager.stage.container,this.scroller=this.element,this.element.style.WebkitOverflowScrolling='touch'),this.manager.settings.offset=this.layout.width,this.manager.settings.afterScrolledTimeout=2*this.settings.duration,this.isVertical='vertical'===this.manager.settings.axis,!this.manager.isPaginated||this.isVertical||(this.touchCanceler=!1,this.resizeCanceler=!1,this.snapping=!1,this.scrollLeft,this.scrollTop,this.startTouchX=void 0,this.startTouchY=void 0,this.startTime=void 0,this.endTouchX=void 0,this.endTouchY=void 0,this.endTime=void 0,this.addListeners())}},{key:'supportsTouch',value:function(){return'ontouchstart'in window||window.DocumentTouch&&document instanceof DocumentTouch}},{key:'disableScroll',value:function(){this.element.style.overflow='hidden'}},{key:'enableScroll',value:function(){this.element.style.overflow=''}},{key:'addListeners',value:function(){this._onResize=this.onResize.bind(this),window.addEventListener('resize',this._onResize),this._onScroll=this.onScroll.bind(this),this.scroller.addEventListener('scroll',this._onScroll),this._onTouchStart=this.onTouchStart.bind(this),this.scroller.addEventListener('touchstart',this._onTouchStart,{passive:!0}),this.on('touchstart',this._onTouchStart),this._onTouchMove=this.onTouchMove.bind(this),this.scroller.addEventListener('touchmove',this._onTouchMove,{passive:!0}),this.on('touchmove',this._onTouchMove),this._onTouchEnd=this.onTouchEnd.bind(this),this.scroller.addEventListener('touchend',this._onTouchEnd,{passive:!0}),this.on('touchend',this._onTouchEnd),this._afterDisplayed=this.afterDisplayed.bind(this),this.manager.on(c.EVENTS.MANAGERS.ADDED,this._afterDisplayed)}},{key:'removeListeners',value:function(){window.removeEventListener('resize',this._onResize),this._onResize=void 0,this.scroller.removeEventListener('scroll',this._onScroll),this._onScroll=void 0,this.scroller.removeEventListener('touchstart',this._onTouchStart,{passive:!0}),this.off('touchstart',this._onTouchStart),this._onTouchStart=void 0,this.scroller.removeEventListener('touchmove',this._onTouchMove,{passive:!0}),this.off('touchmove',this._onTouchMove),this._onTouchMove=void 0,this.scroller.removeEventListener('touchend',this._onTouchEnd,{passive:!0}),this.off('touchend',this._onTouchEnd),this._onTouchEnd=void 0,this.manager.off(c.EVENTS.MANAGERS.ADDED,this._afterDisplayed),this._afterDisplayed=void 0}},{key:'afterDisplayed',value:function(e){var t=this,n=e.contents;['touchstart','touchmove','touchend'].forEach(function(a){n.on(a,function(e){return t.triggerViewEvent(e,n)})})}},{key:'triggerViewEvent',value:function(t,e){this.emit(t.type,t,e)}},{key:'onScroll',value:function(){this.scrollLeft=this.fullsize?window.scrollX:this.scroller.scrollLeft,this.scrollTop=this.fullsize?window.scrollY:this.scroller.scrollTop}},{key:'onResize',value:function(){this.resizeCanceler=!0}},{key:'onTouchStart',value:function(t){var e=t.touches[0],n=e.screenX,a=e.screenY;this.fullsize&&this.enableScroll(),this.touchCanceler=!0,this.startTouchX||(this.startTouchX=n,this.startTouchY=a,this.startTime=this.now()),this.endTouchX=n,this.endTouchY=a,this.endTime=this.now()}},{key:'onTouchMove',value:function(t){var e=t.touches[0],n=e.screenX,i=e.screenY,o=a(i-this.endTouchY);this.touchCanceler=!0,!this.fullsize&&10>o&&(this.element.scrollLeft-=n-this.endTouchX),this.endTouchX=n,this.endTouchY=i,this.endTime=this.now()}},{key:'onTouchEnd',value:function(){this.fullsize&&this.disableScroll(),this.touchCanceler=!1;var e=this.wasSwiped();0===e?this.snap():this.snap(e),this.startTouchX=void 0,this.startTouchY=void 0,this.startTime=void 0,this.endTouchX=void 0,this.endTouchY=void 0,this.endTime=void 0}},{key:'wasSwiped',value:function(){var e=this.layout.pageWidth*this.layout.divisor,t=this.endTouchX-this.startTouchX,n=a(t),i=this.endTime-this.startTime,o=t/i,r=this.settings.minVelocity;return n<=this.settings.minDistance||n>=e?0:o>r?-1:o<-r?1:void 0}},{key:'needsSnap',value:function(){var e=this.scrollLeft,t=this.layout.pageWidth*this.layout.divisor;return 0!=e%t}},{key:'snap',value:function(){var e=0d?(window.requestAnimationFrame(t.bind(this)),this.scrollTo(a+(e-a)*d,0)):(this.scrollTo(e,0),this.snapping=!1,n.resolve()))}var n=new u.defer,a=this.scrollLeft,o=this.now(),r=this.settings.duration,s=this.settings.easing;return this.snapping=!0,t.call(this),n.promise}},{key:'scrollTo',value:function(){var e=0=n.oldVersion&&e.createObjectStore(j)}catch(e){if('ConstraintError'===e.name)console.warn('The database "'+t.name+'" has been upgraded from version '+n.oldVersion+' to version '+n.newVersion+', but the storage "'+t.storeName+'" already exists.');else throw e}}),o.onerror=function(t){t.preventDefault(),a(o.error)},o.onsuccess=function(){n(o.result),p(t)}})}function m(e){return h(e,!1)}function f(e){return h(e,!0)}function y(e,t){if(!e.db)return!0;var n=!e.db.objectStoreNames.contains(e.storeName),a=e.versione.db.version;if(a&&(e.version!==t&&console.warn('The database "'+e.name+'" can\'t be downgraded from version '+e.db.version+' to version '+e.version+'.'),e.version=e.db.version),i||n){if(n){var o=e.db.version+1;o>e.version&&(e.version=o)}return!0}return!1}function v(t){return new B(function(n,e){var a=new FileReader;a.onerror=e,a.onloadend=function(a){var e=btoa(a.target.result||'');n({__local_forage_encoded_blob:!0,data:e,type:t.type})},a.readAsBinaryString(t)})}function b(e){var t=l(atob(e.data));return a([t],{type:e.type})}function k(e){return e&&e.__local_forage_encoded_blob}function x(e){var t=this,n=t._initReady().then(function(){var e=U[t._dbInfo.name];if(e&&e.dbReady)return e.dbReady});return o(n,e,e),n}function E(e){c(e);for(var t=U[e.name],n=t.forages,a=0,i;a>4,u[a++]=(15&r)<<4|s>>2,u[a++]=(3&s)<<6|63&l;return d}function w(e){var t=new Uint8Array(e),n='',a;for(a=0;a>2],n+=X[(3&t[a])<<4|t[a+1]>>4],n+=X[(15&t[a+1])<<2|t[a+2]>>6],n+=X[63&t[a+2]];return 2==t.length%3?n=n.substring(0,n.length-1)+'=':1==t.length%3&&(n=n.substring(0,n.length-2)+'=='),n}function T(e,t,n,a){e.executeSql('CREATE TABLE IF NOT EXISTS '+t.storeName+' (id INTEGER PRIMARY KEY, key unique, value)',[],n,a)}function C(e,n,a,i,o,r){e.executeSql(a,i,o,function(e,s){s.code===s.SYNTAX_ERR?e.executeSql('SELECT name FROM sqlite_master WHERE type=\'table\' AND name = ?',[n.storeName],function(e,t){t.rows.length?r(e,s):T(e,n,function(){e.executeSql(a,i,o,r)},r)},r):r(e,s)},r)}function R(e,t,n,a){var o=this;e=r(e);var s=new B(function(i,r){o.ready().then(function(){void 0===t&&(t=null);var s=t,l=o._dbInfo;l.serializer.serialize(t,function(d,t){t?r(t):l.db.transaction(function(n){C(n,l,'INSERT OR REPLACE INTO '+l.storeName+' (key, value) VALUES (?, ?)',[e,d],function(){i(s)},function(e,t){r(t)})},function(t){if(t.code===t.QUOTA_ERR){if(0 \'__WebKitDatabaseInfoTable__\'',[],function(a,t){for(var o=[],r=0;re?void t(null):void n.ready().then(function(){_(n._dbInfo,W,function(i,o){if(i)return a(i);try{var r=o.objectStore(n._dbInfo.storeName),s=!1,l=r.openCursor();l.onsuccess=function(){var n=l.result;return n?void(0===e?t(n.key):s?t(n.key):(s=!0,n.advance(e))):void t(null)},l.onerror=function(){a(l.error)}}catch(t){a(t)}})})['catch'](a)});return i(a,t),a},keys:function(e){var t=this,n=new B(function(e,n){t.ready().then(function(){_(t._dbInfo,W,function(a,i){if(a)return n(a);try{var o=i.objectStore(t._dbInfo.storeName),r=o.openCursor(),s=[];r.onsuccess=function(){var t=r.result;return t?void(s.push(t.key),t['continue']()):void e(s)},r.onerror=function(){n(r.error)}}catch(t){n(t)}})})['catch'](n)});return i(n,e),n},dropInstance:function(e,t){t=s.apply(this,arguments);var n=this.config();e='function'!=typeof e&&e||{},e.name||(e.name=e.name||n.name,e.storeName=e.storeName||n.storeName);var a=this,o;if(!e.name)o=B.reject('Invalid arguments');else{var r=e.name===n.name&&a._dbInfo.db,l=r?B.resolve(a._dbInfo.db):m(e).then(function(t){var n=U[e.name],a=n.forages;n.db=t;for(var o=0;ot[0]?1:0}),e._entries&&(e._entries={});for(var n=0;nn/2.5&&(p=n/2.5,pop_content.style.maxHeight=p+"px"),popRect.height+l>=n-25?(c.style.top=l-popRect.height+"px",c.classList.add("above")):c.classList.remove("above"),k-popRect.width<=0?(c.style.left=k+"px",c.classList.add("left")):c.classList.remove("left"),k+popRect.width/2>=o?(c.style.left=k-300+"px",popRect=c.getBoundingClientRect(),c.style.left=k-popRect.width+"px",popRect.height+l>=n-25?(c.style.top=l-popRect.height+"px",c.classList.add("above")):c.classList.remove("above"),c.classList.add("right")):c.classList.remove("right")}function d(){f[i].classList.add("on")}function e(){f[i].classList.remove("on")}function g(){setTimeout(function(){f[i].classList.remove("show")},100)}var h,i,j,k,l,m;"noteref"==a.getAttribute("epub:type")&&(h=a.getAttribute("href"),i=h.replace("#",""),j=b.render.document.getElementById(i),a.addEventListener("mouseover",c,!1),a.addEventListener("mouseout",g,!1))}),a&&a()},EPUBJS.Hooks.register("beforeChapterDisplay").mathml=function(a,b){if(b.currentChapter.manifestProperties.indexOf("mathml")!==-1){b.render.iframe.contentWindow.mathmlCallback=a;var c=document.createElement("script");c.type="text/x-mathjax-config",c.innerHTML=' MathJax.Hub.Register.StartupHook("End",function () { window.mathmlCallback(); }); MathJax.Hub.Config({jax: ["input/TeX","input/MathML","output/SVG"],extensions: ["tex2jax.js","mml2jax.js","MathEvents.js"],TeX: {extensions: ["noErrors.js","noUndefined.js","autoload-all.js"]},MathMenu: {showRenderer: false},menuSettings: {zoom: "Click"},messageStyle: "none"}); ',b.doc.body.appendChild(c),EPUBJS.core.addScript("http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML",null,b.doc.head)}else a&&a()},EPUBJS.Hooks.register("beforeChapterDisplay").smartimages=function(a,b){var c=b.contents.querySelectorAll("img"),d=Array.prototype.slice.call(c),e=b.height;if("reflowable"!=b.layoutSettings.layout)return void a();d.forEach(function(a){var c=function(){var c,d=a.getBoundingClientRect(),f=d.height,g=d.top,h=a.getAttribute("data-height"),i=h||f,j=Number(getComputedStyle(a,"").fontSize.match(/(\d*(\.\d*)?)px/)[1]),k=j?j/2:0;e=b.contents.clientHeight,g<0&&(g=0),a.style.maxWidth="100%",i+g>=e?(ge&&(a.style.maxHeight=e+"px",a.style.width="auto",d=a.getBoundingClientRect(),i=d.height),a.style.display="block",a.style.WebkitColumnBreakBefore="always",a.style.breakBefore="column"),a.setAttribute("data-height",c)):(a.style.removeProperty("max-height"),a.style.removeProperty("margin-top"))},d=function(){b.off("renderer:resized",c),b.off("renderer:chapterUnload",this)};a.addEventListener("load",c,!1),b.on("renderer:resized",c),b.on("renderer:chapterUnload",d),c()}),a&&a()},EPUBJS.Hooks.register("beforeChapterDisplay").transculsions=function(a,b){var c=b.contents.querySelectorAll("[transclusion]");Array.prototype.slice.call(c).forEach(function(a){function c(){j=g,k=h,j>chapter.colWidth&&(d=chapter.colWidth/j,j=chapter.colWidth,k*=d),f.width=j,f.height=k}var d,e=a.getAttribute("ref"),f=document.createElement("iframe"),g=a.getAttribute("width"),h=a.getAttribute("height"),i=a.parentNode,j=g,k=h;c(),b.listenUntil("renderer:resized","renderer:chapterUnloaded",c),f.src=e,i.replaceChild(f,a)}),a&&a()}; \ No newline at end of file diff --git a/static/epub.js/js/hooks.min.map b/static/epub.js/js/hooks.min.map new file mode 100644 index 000000000..5da22bee6 --- /dev/null +++ b/static/epub.js/js/hooks.min.map @@ -0,0 +1 @@ +{"version":3,"file":"hooks.min.js","sources":["../../hooks/default/endnotes.js","../../hooks/default/mathml.js","../../hooks/default/smartimages.js","../../hooks/default/transculsions.js"],"names":["EPUBJS","Hooks","register","endnotes","callback","renderer","notes","contents","querySelectorAll","items","Array","prototype","slice","call","attr","type","folder","core","location","pathname","popups","cssPath","addCss","render","document","head","forEach","item","showPop","pop","itemRect","iheight","height","iwidth","width","maxHeight","txt","el","cloneNode","querySelector","id","createElement","setAttribute","pop_content","appendChild","body","addEventListener","onPop","offPop","on","hidePop","this","getBoundingClientRect","left","top","classList","add","popRect","style","remove","setTimeout","href","epubType","getAttribute","replace","getElementById","mathml","currentChapter","manifestProperties","indexOf","iframe","contentWindow","mathmlCallback","s","innerHTML","doc","addScript","smartimages","images","layoutSettings","layout","size","newHeight","rectHeight","oHeight","fontSize","Number","getComputedStyle","match","fontAdjust","clientHeight","display","removeProperty","unloaded","off","transculsions","trans","orginal_width","orginal_height","chapter","colWidth","ratio","src","parent","parentNode","listenUntil","replaceChild"],"mappings":"AAAAA,OAAOC,MAAMC,SAAS,wBAAwBC,SAAW,SAASC,EAAUC,GAE1E,GAAIC,GAAQD,EAASE,SAASC,iBAAiB,WAC9CC,EAAQC,MAAMC,UAAUC,MAAMC,KAAKP,GACnCQ,EAAO,YACPC,EAAO,UACPC,EAAShB,OAAOiB,KAAKD,OAAOE,SAASC,UAErCC,GADWJ,EAAShB,OAAOqB,SAAYL,KAGxChB,QAAOiB,KAAKK,OAAOtB,OAAOqB,QAAU,aAAa,EAAOhB,EAASkB,OAAOC,SAASC,MAGjFhB,EAAMiB,QAAQ,SAASC,GAqBtB,QAASC,KACR,GAICC,GAEAC,EALAC,EAAU1B,EAAS2B,OACnBC,EAAS5B,EAAS6B,MAGlBC,EAAY,GAGTC,KACHP,EAAMQ,EAAGC,WAAU,GACnBF,EAAMP,EAAIU,cAAc,MAKrBnB,EAAOoB,KACVpB,EAAOoB,GAAMhB,SAASiB,cAAc,OACpCrB,EAAOoB,GAAIE,aAAa,QAAS,SAEjCC,YAAcnB,SAASiB,cAAc,OAErCrB,EAAOoB,GAAII,YAAYD,aAEvBA,YAAYC,YAAYR,GACxBO,YAAYD,aAAa,QAAS,eAElCrC,EAASkB,OAAOC,SAASqB,KAAKD,YAAYxB,EAAOoB,IAGjDpB,EAAOoB,GAAIM,iBAAiB,YAAaC,GAAO,GAChD3B,EAAOoB,GAAIM,iBAAiB,WAAYE,GAAQ,GAKhD3C,EAAS4C,GAAG,uBAAwBC,EAASC,MAC7C9C,EAAS4C,GAAG,uBAAwBD,EAAQG,OAI7CtB,EAAMT,EAAOoB,GAIbV,EAAWH,EAAKyB,wBAChBC,EAAOvB,EAASuB,KAChBC,EAAMxB,EAASwB,IAGfzB,EAAI0B,UAAUC,IAAI,QAGlBC,QAAU5B,EAAIuB,wBAGdvB,EAAI6B,MAAML,KAAOA,EAAOI,QAAQvB,MAAQ,EAAI,KAC5CL,EAAI6B,MAAMJ,IAAMA,EAAM,KAInBnB,EAAYJ,EAAU,MACxBI,EAAYJ,EAAU,IACtBY,YAAYe,MAAMvB,UAAYA,EAAY,MAIxCsB,QAAQzB,OAASsB,GAAOvB,EAAU,IACpCF,EAAI6B,MAAMJ,IAAMA,EAAMG,QAAQzB,OAAU,KACxCH,EAAI0B,UAAUC,IAAI,UAElB3B,EAAI0B,UAAUI,OAAO,SAInBN,EAAOI,QAAQvB,OAAS,GAC1BL,EAAI6B,MAAML,KAAOA,EAAO,KACxBxB,EAAI0B,UAAUC,IAAI,SAElB3B,EAAI0B,UAAUI,OAAO,QAInBN,EAAOI,QAAQvB,MAAQ,GAAKD,GAE9BJ,EAAI6B,MAAML,KAAOA,EAAO,IAAM,KAE9BI,QAAU5B,EAAIuB,wBACdvB,EAAI6B,MAAML,KAAOA,EAAOI,QAAQvB,MAAQ,KAErCuB,QAAQzB,OAASsB,GAAOvB,EAAU,IACpCF,EAAI6B,MAAMJ,IAAMA,EAAMG,QAAQzB,OAAU,KACxCH,EAAI0B,UAAUC,IAAI,UAElB3B,EAAI0B,UAAUI,OAAO,SAGtB9B,EAAI0B,UAAUC,IAAI,UAElB3B,EAAI0B,UAAUI,OAAO,SAMvB,QAASZ,KACR3B,EAAOoB,GAAIe,UAAUC,IAAI,MAG1B,QAASR,KACR5B,EAAOoB,GAAIe,UAAUI,OAAO,MAG7B,QAAST,KACRU,WAAW,WACVxC,EAAOoB,GAAIe,UAAUI,OAAO,SAC1B,KAxIJ,GACCE,GACArB,EACAH,EAGAgB,EACAC,EACAlB,EARG0B,EAAWnC,EAAKoC,aAAajD,EAU9BgD,IAAY/C,IAEf8C,EAAOlC,EAAKoC,aAAa,QACzBvB,EAAKqB,EAAKG,QAAQ,IAAK,IACvB3B,EAAKhC,EAASkB,OAAOC,SAASyC,eAAezB,GAG7Cb,EAAKmB,iBAAiB,YAAalB,GAAS,GAC5CD,EAAKmB,iBAAiB,WAAYI,GAAS,MA4HzC9C,GAAUA,KC5JfJ,OAAOC,MAAMC,SAAS,wBAAwBgE,OAAS,SAAS9D,EAAUC,GAGtE,GAAoE,KAAjEA,EAAS8D,eAAeC,mBAAmBC,QAAQ,UAAkB,CAGpEhE,EAASkB,OAAO+C,OAAOC,cAAcC,eAAiBpE,CAGtD,IAAIqE,GAAIjD,SAASiB,cAAc,SAC/BgC,GAAE1D,KAAO,wBACT0D,EAAEC,UAAY,6ZAMdrE,EAASsE,IAAI9B,KAAKD,YAAY6B,GAE9BzE,OAAOiB,KAAK2D,UAAU,gFAAiF,KAAMvE,EAASsE,IAAIlD,UAGvHrB,IAAUA,KCtBrBJ,OAAOC,MAAMC,SAAS,wBAAwB2E,YAAc,SAASzE,EAAUC,GAC7E,GAAIyE,GAASzE,EAASE,SAASC,iBAAiB,OAC/CC,EAAQC,MAAMC,UAAUC,MAAMC,KAAKiE,GACnC/C,EAAU1B,EAAS2B,MAGpB,OAAqC,cAAlC3B,EAAS0E,eAAeC,WAC1B5E,MAIDK,EAAMiB,QAAQ,SAASC,GAEtB,GAAIsD,GAAO,WACV,GAKCC,GALGpD,EAAWH,EAAKyB,wBACnB+B,EAAarD,EAASE,OACtBsB,EAAMxB,EAASwB,IACf8B,EAAUzD,EAAKoC,aAAa,eAC5B/B,EAASoD,GAAWD,EAEpBE,EAAWC,OAAOC,iBAAiB5D,EAAM,IAAI0D,SAASG,MAAM,mBAAmB,IAC/EC,EAAaJ,EAAWA,EAAW,EAAI,CAExCtD,GAAU1B,EAASE,SAASmF,aACnB,EAANpC,IAASA,EAAM,GAEftB,EAASsB,GAAOvB,GAETA,EAAQ,EAAduB,GAEF4B,EAAYnD,EAAUuB,EAAMmC,EAC5B9D,EAAK+B,MAAMvB,UAAY+C,EAAY,KACnCvD,EAAK+B,MAAMxB,MAAO,SAEfF,EAASD,IACXJ,EAAK+B,MAAMvB,UAAYJ,EAAU,KACjCJ,EAAK+B,MAAMxB,MAAO,OAClBJ,EAAWH,EAAKyB,wBAChBpB,EAASF,EAASE,QAEnBL,EAAK+B,MAAMiC,QAAU,QACrBhE,EAAK+B,MAA+B,wBAAI,SACxC/B,EAAK+B,MAAmB,YAAI,UAI7B/B,EAAKe,aAAa,cAAewC,KAGjCvD,EAAK+B,MAAMkC,eAAe,cAC1BjE,EAAK+B,MAAMkC,eAAe,gBAIxBC,EAAW,WAEdxF,EAASyF,IAAI,mBAAoBb,GACjC5E,EAASyF,IAAI,yBAA0B3C,MAGxCxB,GAAKmB,iBAAiB,OAAQmC,GAAM,GAEpC5E,EAAS4C,GAAG,mBAAoBgC,GAEhC5E,EAAS4C,GAAG,yBAA0B4C,GAEtCZ,WAIE7E,GAAUA,OCtEfJ,OAAOC,MAAMC,SAAS,wBAAwB6F,cAAgB,SAAS3F,EAAUC,GAO/E,GAAI2F,GAAQ3F,EAASE,SAASC,iBAAiB,kBAC7CC,EAAQC,MAAMC,UAAUC,MAAMC,KAAKmF,EAErCvF,GAAMiB,QAAQ,SAASC,GAWtB,QAASsD,KACR/C,EAAQ+D,EACRjE,EAASkE,EAENhE,EAAQiE,QAAQC,WAClBC,EAAQF,QAAQC,SAAWlE,EAE3BA,EAAQiE,QAAQC,SAChBpE,GAAkBqE,GAGnB/B,EAAOpC,MAAQA,EACfoC,EAAOtC,OAASA,EAtBjB,GAOCqE,GAPGC,EAAM3E,EAAKoC,aAAa,OAC3BO,EAAS9C,SAASiB,cAAc,UAChCwD,EAAgBtE,EAAKoC,aAAa,SAClCmC,EAAiBvE,EAAKoC,aAAa,UACnCwC,EAAS5E,EAAK6E,WACdtE,EAAQ+D,EACRjE,EAASkE,CAoBVjB,KAKA5E,EAASoG,YAAY,mBAAoB,2BAA4BxB,GAErEX,EAAOgC,IAAMA,EAGbC,EAAOG,aAAapC,EAAQ3C,KAQ1BvB,GAAUA"} \ No newline at end of file diff --git a/static/epub.js/js/hooks/extensions/highlight.js b/static/epub.js/js/hooks/extensions/highlight.js new file mode 100644 index 000000000..1dd1c6714 --- /dev/null +++ b/static/epub.js/js/hooks/extensions/highlight.js @@ -0,0 +1,14 @@ +EPUBJS.Hooks.register("beforeChapterDisplay").highlight = function(callback, renderer){ + + // EPUBJS.core.addScript("js/libs/jquery.highlight.js", null, renderer.doc.head); + + var s = document.createElement("style"); + s.innerHTML =".highlight { background: yellow; font-weight: normal; }"; + + renderer.render.document.head.appendChild(s); + + if(callback) callback(); + +} + + diff --git a/static/epub.js/js/libs/jquery.min.js b/static/epub.js/js/libs/jquery.min.js new file mode 100644 index 000000000..4024b6622 --- /dev/null +++ b/static/epub.js/js/libs/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.2.4 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="2.2.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isPlainObject:function(a){var b;if("object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype||{},"isPrototypeOf"))return!1;for(b in a);return void 0===b||k.call(a,b)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=d.createElement("script"),b.text=a,d.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:h.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(d=e.call(arguments,2),f=function(){return a.apply(b||this,d.concat(e.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return h.call(b,a)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&f.parentNode&&(this.length=1,this[0]=f),this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?void 0!==c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?h.call(n(a),this[0]):h.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||n.uniqueSort(e),D.test(a)&&e.reverse()),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.removeEventListener("DOMContentLoaded",J),a.removeEventListener("load",J),n.ready()}n.ready.promise=function(b){return I||(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(n.ready):(d.addEventListener("DOMContentLoaded",J),a.addEventListener("load",J))),I.promise(b)},n.ready.promise();var K=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)K(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},L=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function M(){this.expando=n.expando+M.uid++}M.uid=1,M.prototype={register:function(a,b){var c=b||{};return a.nodeType?a[this.expando]=c:Object.defineProperty(a,this.expando,{value:c,writable:!0,configurable:!0}),a[this.expando]},cache:function(a){if(!L(a))return{};var b=a[this.expando];return b||(b={},L(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[b]=c;else for(d in b)e[d]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=a[this.expando];if(void 0!==f){if(void 0===b)this.register(a);else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in f?d=[b,e]:(d=e,d=d in f?[d]:d.match(G)||[])),c=d.length;while(c--)delete f[d[c]]}(void 0===b||n.isEmptyObject(f))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!n.isEmptyObject(b)}};var N=new M,O=new M,P=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Q=/[A-Z]/g;function R(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Q,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:P.test(c)?n.parseJSON(c):c; +}catch(e){}O.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return O.hasData(a)||N.hasData(a)},data:function(a,b,c){return O.access(a,b,c)},removeData:function(a,b){O.remove(a,b)},_data:function(a,b,c){return N.access(a,b,c)},_removeData:function(a,b){N.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=O.get(f),1===f.nodeType&&!N.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),R(f,d,e[d])));N.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){O.set(this,a)}):K(this,function(b){var c,d;if(f&&void 0===b){if(c=O.get(f,a)||O.get(f,a.replace(Q,"-$&").toLowerCase()),void 0!==c)return c;if(d=n.camelCase(a),c=O.get(f,d),void 0!==c)return c;if(c=R(f,d,void 0),void 0!==c)return c}else d=n.camelCase(a),this.each(function(){var c=O.get(this,d);O.set(this,d,b),a.indexOf("-")>-1&&void 0!==c&&O.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){O.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=N.get(a,b),c&&(!d||n.isArray(c)?d=N.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return N.get(a,c)||N.access(a,c,{empty:n.Callbacks("once memory").add(function(){N.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length",""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};$.optgroup=$.option,$.tbody=$.tfoot=$.colgroup=$.caption=$.thead,$.th=$.td;function _(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function aa(a,b){for(var c=0,d=a.length;d>c;c++)N.set(a[c],"globalEval",!b||N.get(b[c],"globalEval"))}var ba=/<|&#?\w+;/;function ca(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],o=0,p=a.length;p>o;o++)if(f=a[o],f||0===f)if("object"===n.type(f))n.merge(m,f.nodeType?[f]:f);else if(ba.test(f)){g=g||l.appendChild(b.createElement("div")),h=(Y.exec(f)||["",""])[1].toLowerCase(),i=$[h]||$._default,g.innerHTML=i[1]+n.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;n.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",o=0;while(f=m[o++])if(d&&n.inArray(f,d)>-1)e&&e.push(f);else if(j=n.contains(f.ownerDocument,f),g=_(l.appendChild(f),"script"),j&&aa(g),c){k=0;while(f=g[k++])Z.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var da=/^key/,ea=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,fa=/^([^.]*)(?:\.(.+)|)/;function ga(){return!0}function ha(){return!1}function ia(){try{return d.activeElement}catch(a){}}function ja(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ja(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ha;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return"undefined"!=typeof n&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(G)||[""],j=b.length;while(j--)h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.hasData(a)&&N.get(a);if(r&&(i=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&N.remove(a,"handle events")}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(N.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,la=/\s*$/g;function pa(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function qa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function ra(a){var b=na.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function sa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(N.hasData(a)&&(f=N.access(a),g=N.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}O.hasData(a)&&(h=O.access(a),i=n.extend({},h),O.set(b,i))}}function ta(a,b){var c=b.nodeName.toLowerCase();"input"===c&&X.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function ua(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&ma.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),ua(f,b,c,d)});if(o&&(e=ca(b,a[0].ownerDocument,!1,a,d),g=e.firstChild,1===e.childNodes.length&&(e=g),g||d)){for(h=n.map(_(e,"script"),qa),i=h.length;o>m;m++)j=e,m!==p&&(j=n.clone(j,!0,!0),i&&n.merge(h,_(j,"script"))),c.call(a[m],j,m);if(i)for(k=h[h.length-1].ownerDocument,n.map(h,ra),m=0;i>m;m++)j=h[m],Z.test(j.type||"")&&!N.access(j,"globalEval")&&n.contains(k,j)&&(j.src?n._evalUrl&&n._evalUrl(j.src):n.globalEval(j.textContent.replace(oa,"")))}return a}function va(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(_(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&aa(_(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(ka,"<$1>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=_(h),f=_(a),d=0,e=f.length;e>d;d++)ta(f[d],g[d]);if(b)if(c)for(f=f||_(a),g=g||_(h),d=0,e=f.length;e>d;d++)sa(f[d],g[d]);else sa(a,h);return g=_(h,"script"),g.length>0&&aa(g,!i&&_(a,"script")),h},cleanData:function(a){for(var b,c,d,e=n.event.special,f=0;void 0!==(c=a[f]);f++)if(L(c)){if(b=c[N.expando]){if(b.events)for(d in b.events)e[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);c[N.expando]=void 0}c[O.expando]&&(c[O.expando]=void 0)}}}),n.fn.extend({domManip:ua,detach:function(a){return va(this,a,!0)},remove:function(a){return va(this,a)},text:function(a){return K(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.appendChild(a)}})},prepend:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(_(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return K(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!la.test(a)&&!$[(Y.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(_(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return ua(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(_(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),f=e.length-1,h=0;f>=h;h++)c=h===f?this:this.clone(!0),n(e[h])[b](c),g.apply(d,c.get());return this.pushStack(d)}});var wa,xa={HTML:"block",BODY:"block"};function ya(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function za(a){var b=d,c=xa[a];return c||(c=ya(a,b),"none"!==c&&c||(wa=(wa||n(" + + + ` + } else if (data.document.extension == "jpg" || data.document.extension == "png") { + var img = `${pandora.proto}://${data.site}/documents/${data.document.id}/${safeDocumentName(data.document.title)}.${data.document.extension}` + var open_text = `Open on ${data.site}` + if (data.crop) { + img = `${pandora.proto}://${data.site}/documents/${data.document.id}/1024p${data.crop.join(',')}.jpg` + data.link = getLink(`documents/${data.document.id}`) + open_text = `Open image` + } + div.innerHTML = ` +

${data.document.title}

+
+ +
+ + ` + + } else { + div.innerHTML = `unsupported document type` + } + document.querySelector(".content").replaceWith(div) +} diff --git a/static/mobile/js/edits.js b/static/mobile/js/edits.js new file mode 100644 index 000000000..f691d6188 --- /dev/null +++ b/static/mobile/js/edits.js @@ -0,0 +1,235 @@ + + +const sortByKey = function(array, by) { + return array.sort(function(a, b) { + var aValue, bValue, index = 0, key, ret = 0; + while (ret == 0 && index < by.length) { + key = by[index].key; + aValue = getSortValue(a[key]) + bValue = getSortValue(b[key]) + if ((aValue === null) != (bValue === null)) { + ret = aValue === null ? 1 : -1; + } else if (aValue < bValue) { + ret = by[index].operator == '+' ? -1 : 1; + } else if (aValue > bValue) { + ret = by[index].operator == '+' ? 1 : -1; + } else { + index++; + } + } + return ret; + }); +}; + +async function sortClips(edit, sort) { + var key = sort.key, index; + if (key == 'position') { + key = 'in'; + } + if ([ + 'id', 'index', 'in', 'out', 'duration', + 'title', 'director', 'year', 'videoRatio' + ].indexOf(key) > -1) { + sortBy(sort); + index = 0; + edit.clips.forEach(function(clip) { + clip.sort = index++; + if (sort.operator == '-') { + clip.sort = -clip.sort; + } + }); + } else { + var response = await pandoraAPI('sortClips', { + edit: edit.id, + sort: [sort] + }) + edit.clips.forEach(function(clip) { + clip.sort = response.data.clips.indexOf(clip.id); + if (sort.operator == '-') { + clip.sort = -clip.sort; + } + }); + sortBy({ + key: 'sort', + operator: '+' + }); + } + function sortBy(key) { + edit.clips = sortByKey(edit.clips, [key]); + } +} + +function getClip(edit, position) { + const response = {} + let pos = 0 + edit.clips.forEach(function(clip) { + if (clip.position < position && clip.position + clip.duration > position) { + response.item = clip.item + response.position = position - clip.position + if (clip['in']) { + response.position += clip['in'] + } + } + }); + return response +} + +async function loadEdit(id, args) { + var data = window.data = {} + data.id = id + data.site = pandora.hostname + + var response = await pandoraAPI('getEdit', { + id: data.id, + keys: [ + ] + }) + if (response.status.code != 200) { + return { + site: data.site, + error: response.status + } + } + data.edit = response['data'] + if (['public', 'featured'].indexOf(data.edit.status) == -1) { + return { + site: data.site, + error: { + code: 403, + text: 'permission denied' + } + } + } + data.layers = {} + data.videos = [] + + if (args.sort) { + await sortClips(data.edit, args.sort) + } + + data.edit.duration = 0; + data.edit.clips.forEach(function(clip) { + clip.position = data.edit.duration; + data.edit.duration += clip.duration; + }); + + data.edit.clips.forEach(clip => { + var start = clip['in'] || 0, end = clip.out, position = 0; + clip.durations.forEach((duration, idx) => { + if (!duration) { + return + } + if (position + duration <= start || position > end) { + // pass + } else { + var video = {} + var oshash = clip.streams[idx] + video.src = getVideoURL(clip.item, pandora.resolution, idx+1, '', oshash) + /* + if (clip['in'] && clip.out) { + video.src += `#t=${clip['in']},${clip.out}` + } + */ + if (isNumber(clip.volume)) { + video.volume = clip.volume; + } + if ( + position <= start + && position + duration > start + ) { + video['in'] = start - position; + } + if (position + duration >= end) { + video.out = end - position; + } + if (video['in'] && video.out) { + video.duration = video.out - video['in'] + } else if (video.out) { + video.duration = video.out; + } else if (!isUndefined(video['in'])) { + video.duration = duration - video['in']; + video.out = duration; + } else { + video.duration = duration; + video['in'] = 0; + video.out = video.duration; + } + data.videos.push(video) + } + position += duration + }) + Object.keys(clip.layers).forEach(layer => { + clip.layers[layer].forEach(annotation => { + if (args.users && !args.users.includes(annotation.user)) { + return + } + if (args.layers && !args.layers.includes(layer)) { + return + } + var a = {...annotation} + a['id'] = clip['id'] + '/' + a['id']; + a['in'] = Math.max( + clip['position'], + a['in'] - clip['in'] + clip['position'] + ); + a.out = Math.min( + clip['position'] + clip['duration'], + a.out - clip['in'] + clip['position'] + ); + data.layers[layer] = data.layers[layer] || [] + data.layers[layer].push(a) + }) + }) + }) + if (data.layers[pandora.subtitleLayer]) { + var previous; + data.layers[pandora.subtitleLayer].forEach(annotation => { + if (previous) { + previous.out = annotation['in'] + } + previous = annotation + }) + } + var value = [] + pandora.layerKeys.forEach(layer => { + if (!data.layers[layer]) { + return + } + var html = [] + var layerData = getObjectById(pandora.site.layers, layer) + html.push(`

+ ${icon.down} + ${layerData.title} +

`) + data.layers[layer].forEach(annotation => { + html.push(` +
+ ${annotation.value} +
+ `) + }) + var layerClass = "" + if (layerData.isSubtitles) { + layerClass = " is-subtitles" + } + value.push('
' + html.join('\n') + '
') + }) + data.value = value.join('\n') + + data.title = data.edit.name + data.byline = data.edit.description + data.link = `${pandora.proto}://${data.site}/edits/${data.edit.id}` + let poster = data.edit.posterFrames[0] + if (args.parts[2] && args.parts[2].indexOf(':') > -1) { + poster = getClip(data.edit, parseDuration(args.parts[2])) + } + if (poster && poster.item) { + data.poster = `${pandora.proto}://${data.site}/${poster.item}/${pandora.resolution}p${poster.position.toFixed(3)}.jpg` + } else { + data.poster = data.videos[0].src.split('/48')[0] + `/${pandora.resolution}p${data.videos[0].in.toFixed(3)}.jpg` + } + data.aspectratio = data.edit.clips[0].videoRatio + data.duration = data.edit.duration + return data + +} diff --git a/static/mobile/js/icons.js b/static/mobile/js/icons.js new file mode 100644 index 000000000..501161081 --- /dev/null +++ b/static/mobile/js/icons.js @@ -0,0 +1,200 @@ +var icon = {} +icon.enterFullscreen = ` + + + + + + + + +` +icon.exitFullscreen = ` + + + + + + + + +` + +icon.mute = ` + + + + + + + +` + +icon.unmute = ` + + + + +` + +icon.play = ` + + + +` +icon.pause = ` + + + + + +` +icon.loading = ` + + + + + + +` + +icon.loading_w = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` + +icon.right = ` + + + +` +icon.left = ` + + + +` +icon.down = ` + + + +` + +icon.publishComment = ` + + + + + + +` diff --git a/static/mobile/js/item.js b/static/mobile/js/item.js new file mode 100644 index 000000000..42c4d2cd5 --- /dev/null +++ b/static/mobile/js/item.js @@ -0,0 +1,182 @@ + +async function loadData(id, args) { + var data = window.data = {} + data.id = id + data.site = pandora.hostname + + var response = await pandoraAPI('get', { + id: data.id.split('/')[0], + keys: [ + "id", + "title", + "director", + "year", + "date", + "source", + "summary", + "streams", + "duration", + "durations", + "layers", + "rightslevel", + "videoRatio" + ] + }) + + if (response.status.code != 200) { + return { + site: data.site, + error: response.status + } + } + data.item = response['data'] + if (data.item.rightslevel > pandora.site.capabilities.canPlayClips['guest']) { + return { + site: data.site, + error: { + code: 403, + text: 'permission denied' + } + } + } + if (id.split('/').length == 1 || id.split('/')[1] == 'info') { + data.view = 'info' + data.title = data.item.title + if (data.item.source) { + data.byline = data.item.source + } else { + data.byline = data.item.director ? data.item.director.join(', ') : '' + } + if (data.item.year) { + data.byline += ' (' + data.item.year + ')' + } else if (data.item.date) { + data.byline += ' (' + data.item.date.split('-')[0] + ')' + } + data.link = `${pandora.proto}://${data.site}/${data.item.id}/info` + let poster = pandora.site.user.ui.icons == 'posters' ? 'poster' : 'icon' + data.icon = `${pandora.proto}://${data.site}/${data.item.id}/${poster}.jpg` + return data + } + + if (id.includes('-') || id.includes(',')) { + var inout = id.split('/')[1].split(id.includes('-') ? '-' : ',').map(parseDuration) + data.out = inout.pop() + data['in'] = inout.pop() + } else if (args.full) { + data.out = data.item.duration + data['in'] = 0 + } else { + var annotation = await pandoraAPI('getAnnotation', { + id: data.id, + }) + if (annotation.status.code != 200) { + return { + site: data.site, + error: annotation.status + } + } + data.annotation = annotation['data'] + data['in'] = data.annotation['in'] + data.out = data.annotation['out'] + } + + data.layers = {} + + pandora.layerKeys.forEach(layer => { + data.item.layers[layer].forEach(annot => { + if (data.annotation) { + if (annot.id == data.annotation.id) { + data.layers[layer] = data.layers[layer] || [] + data["layers"][layer].push(annot) + } + } else if (annot['out'] > data['in'] && annot['in'] < data['out']) { + if (args.users && !args.users.includes(annot.user)) { + return + } + if (args.layers && !args.layers.includes(layer)) { + return + } + data.layers[layer] = data.layers[layer] || [] + //annot['in'] = Math.max([annot['in'], data['in']]) + //annot['out'] = Math.min([annot['out'], data['out']]) + data["layers"][layer].push(annot) + } + }) + }) + data.videos = [] + data.item.durations.forEach((duration, idx) => { + var oshash = data.item.streams[idx] + var url = getVideoURL(data.item.id, pandora.resolution, idx+1, '', oshash) + data.videos.push({ + src: url, + duration: duration + }) + }) + if (data.layers[pandora.subtitleLayer]) { + var previous; + data.layers[pandora.subtitleLayer].forEach(annotation => { + if (previous) { + previous.out = annotation['in'] + } + previous = annotation + }) + } + var value = [] + Object.keys(data.layers).forEach(layer => { + var html = [] + var layerData = getObjectById(pandora.site.layers, layer) + html.push(`

+ ${icon.down} + ${layerData.title} +

`) + data.layers[layer] = sortBy(data.layers[layer], [ + {key: "in", operator: "+"}, + {key: "created", operator: "+"} + ]) + data.layers[layer].forEach(annotation => { + if (pandora.url) { + annotation.value = annotation.value.replace( + /src="\//g, `src="${pandora.url.origin}/` + ).replace( + /href="\//g, `href="${pandora.url.origin}/` + ) + } + let content = annotation.value + if (!layerData.isSubtitles && layerData.type == "text" && args.show && args.show.includes("user")) { + content += `\n
— ${annotation.user}
` + } + html.push(` +
+ ${content} +
+ `) + }) + var layerClass = "" + if (layerData.isSubtitles) { + layerClass = " is-subtitles" + } + value.push('
' + html.join('\n') + '
') + }) + data.value = value.join('\n') + + data.title = data.item.title + if (data.item.source) { + data.byline = data.item.source + } else { + data.byline = data.item.director ? data.item.director.join(', ') : '' + } + if (data.item.year) { + data.byline += ' (' + data.item.year + ')' + } else if (data.item.date) { + data.byline += ' (' + data.item.date.split('-')[0] + ')' + } + data.link = `${pandora.proto}://${data.site}/${data.item.id}/${data["in"]},${data.out}` + data.poster = `${pandora.proto}://${data.site}/${data.item.id}/${pandora.resolution}p${data["in"]}.jpg` + data.aspectratio = data.item.videoRatio + if (data['in'] == data['out']) { + data['out'] += 0.04 + } + data.duration = data.out - data['in'] + return data +} + diff --git a/static/mobile/js/main.js b/static/mobile/js/main.js new file mode 100644 index 000000000..421eacc23 --- /dev/null +++ b/static/mobile/js/main.js @@ -0,0 +1,133 @@ + + +function parseURL() { + var url = pandora.url ? pandora.url : document.location, + fragment = url.hash.slice(1) + if (!fragment && url.pathname.startsWith('/m/')) { + var prefix = url.protocol + '//' + url.hostname + '/m/' + fragment = url.href.slice(prefix.length) + } + var args = fragment.split('?') + var id = args.shift() + if (args) { + args = args.join('?').split('&').map(arg => { + var kv = arg.split('=') + k = kv.shift() + v = kv.join('=') + if (['users', 'layers', 'show'].includes(k)) { + v = v.split(',') + } + return [k, v] + }).filter(kv => { + return kv[0].length + }) + if (args) { + args = Object.fromEntries(args); + } else { + args = {} + } + } else { + args = {} + } + var type = "item" + if (id.startsWith('document')) { + id = id.split('/') + id.shift() + id = id.join('/') + type = "document" + } else if (id.startsWith('edits/')) { + var parts = id.split('/') + parts.shift() + id = parts.shift().replace(/_/g, ' ').replace(/%09/g, '_') + type = "edit" + if (parts.length >= 2) { + args.sort = parts[1] + if (args.sort[0] == '-') { + args.sort = { + key: args.sort.slice(1), + operator: '-' + } + } else if (args.sort[0] == '+') { + args.sort = { + key: args.sort.slice(1), + operator: '+' + } + } else { + args.sort = { + key: args.sort, + operator: '+' + } + } + } + args.parts = parts + } else { + if (id.endsWith('/player') || id.endsWith('/editor')) { + args.full = true + } + id = id.replace('/editor/', '/').replace('/player/', '/') + type = "item" + } + //console.log(type, id, args) + return [type, id, args] +} + +function render() { + var type, id, args; + [type, id, args] = parseURL() + document.querySelector(".content").innerHTML = loadingScreen + if (type == "document") { + loadDocument(id, args).then(renderDocument) + } else if (type == "edit") { + loadEdit(id, args).then(renderItem) + } else { + loadData(id, args).then(renderItem) + } + +} +var loadingScreen = ` + +
${icon.loading}
+` + +document.querySelector(".content").innerHTML = loadingScreen +pandoraAPI("init").then(response => { + pandora = { + ...pandora, + ...response.data + } + pandora.proto = pandora.site.site.https ? 'https' : 'http' + pandora.resolution = Math.max.apply(null, pandora.site.video.resolutions) + if (pandora.site.site.videoprefix.startsWith('//')) { + pandora.site.site.videoprefix = pandora.proto + ':' + pandora.site.site.videoprefix + } + var layerKeys = [] + var subtitleLayer = pandora.site.layers.filter(layer => {return layer.isSubtitles})[0] + if (subtitleLayer) { + layerKeys.push(subtitleLayer.id) + } + pandora.subtitleLayer = subtitleLayer.id + pandora.site.layers.map(layer => { + return layer.id + }).filter(layer => { + return !subtitleLayer || layer != subtitleLayer.id + }).forEach(layer => { + layerKeys.push(layer) + }) + pandora.layerKeys = layerKeys + id = document.location.hash.slice(1) + window.addEventListener("hashchange", event => { + render() + }) + window.addEventListener("popstate", event => { + console.log("popsatte") + render() + }) + window.addEventListener('resize', event => { + }) + render() +}) diff --git a/static/mobile/js/render.js b/static/mobile/js/render.js new file mode 100644 index 000000000..40422e558 --- /dev/null +++ b/static/mobile/js/render.js @@ -0,0 +1,159 @@ + +function renderItemInfo(data) { + div = document.createElement('div') + div.className = "content" + div.innerHTML = ` +

${item.title}

+

${data.title}

+ +
+ +
+ + ` + if (!item.title) { + div.querySelector('item-title').remove() + } + document.querySelector(".content").replaceWith(div) +} + +function renderItem(data) { + window.item = window.item || {} + if (data.error) { + return renderError(data) + } + if (data.view == "info") { + return renderItemInfo(data) + } + div = document.createElement('div') + div.className = "content" + div.innerHTML = ` +

${item.title}

+

${data.title}

+ +
+
+
+
+ ${data.value} +
+
+ + ` + if (!item.title) { + div.querySelector('.item-title').remove() + } + + var comments = div.querySelector('.comments') + if (window.renderComments) { + renderComments(comments, data) + } else { + comments.remove() + } + + div.querySelectorAll('.layer a').forEach(a => { + a.addEventListener("click", clickLink) + }) + + div.querySelectorAll('.layer').forEach(layer => { + layer.querySelector('h3').addEventListener("click", event => { + var img = layer.querySelector('h3 .icon') + if (layer.classList.contains("collapsed")) { + layer.classList.remove("collapsed") + img.innerHTML = icon.down + } else { + layer.classList.add("collapsed") + img.innerHTML = icon.right + } + }) + }) + + var video = window.video = VideoPlayer({ + items: data.videos, + poster: data.poster, + "in": data["in"] || 0, + position: 0, + duration: data.duration, + aspectratio: data.aspectratio + }) + div.querySelector('.video').replaceWith(video) + video.classList.add('video') + + video.addEventListener("loadedmetadata", event => { + // + }) + + function updateAnnotations(currentTime) { + div.querySelectorAll('.annotation').forEach(annot => { + var now = currentTime + (data["in"] || 0) + var start = parseFloat(annot.dataset.in) + var end = parseFloat(annot.dataset.out) + if (now >= start && now <= end) { + annot.classList.add("active") + annot.parentElement.classList.add('active') + } else { + annot.classList.remove("active") + if (!annot.parentElement.querySelector('.active')) { + annot.parentElement.classList.remove('active') + } + } + }) + } + video.addEventListener("timeupdate", event => { + var currentTime = video.currentTime() + if ((currentTime + (data["in"] || 0)) >= data['out']) { + if (!video.paused) { + video.pause() + } + video.currentTime(0) + } + updateAnnotations(currentTime) + }) + updateAnnotations(data["position"] || 0) + if (item.next || item.previous) { + var nav = document.createElement('nav') + nav.classList.add('items') + if (item.previous) { + var a = document.createElement('a') + a.href = item.previous + a.innerText = '<< previous' + nav.appendChild(a) + } + if (item.previous && item.next) { + var e = document.createElement('span') + e.innerText = ' | ' + nav.appendChild(e) + } + if (item.next) { + var a = document.createElement('a') + a.href = item.next + a.innerText = 'next >>' + nav.appendChild(a) + } + div.appendChild(nav) + } + document.querySelector(".content").replaceWith(div) +} + +function renderError(data) { + var link = '/' + document.location.hash.slice(1) + div = document.createElement('div') + div.className = "content" + div.innerHTML = ` + +
+ Page not found
+ Open on ${data.site} +
+ ` + document.querySelector(".content").replaceWith(div) +} diff --git a/static/mobile/js/utils.js b/static/mobile/js/utils.js new file mode 100644 index 000000000..25e8a4017 --- /dev/null +++ b/static/mobile/js/utils.js @@ -0,0 +1,226 @@ + +const parseDuration = function(string) { + return string.split(':').reverse().slice(0, 4).reduce(function(p, c, i) { + return p + (parseFloat(c) || 0) * (i == 3 ? 86400 : Math.pow(60, i)); + }, 0); +}; + +const formatDuration = function(seconds) { + if (isString(seconds)) { + seconds = parseFloat(seconds) + } + seconds = seconds.toFixed(3) + var parts = [ + parseInt(seconds / 86400), + parseInt(seconds % 86400 / 3600), + parseInt(seconds % 3600 / 60), + parseInt(seconds % 60) + ] + return parts.map(p => { return p.toString().padStart(2, '0')}).join(':') +} + +const typeOf = function(value) { + return Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); +}; +const isUndefined = function(value) { + return typeOf(value) == 'undefined'; +} +const isNumber = function(value) { + return typeOf(value) == 'number'; +}; +const isObject = function(value) { + return typeOf(value) == 'object'; +}; +const isNull = function(value) { + return typeOf(value) == 'null'; +}; +const isString = function(value) { + return typeOf(value) == 'string'; +}; +const isEmpty = function(value) { + var type = typeOf(value) + if (['arguments', 'array', 'nodelist', 'string'].includes(value)) { + return value.length == 0 + } + if (['object', 'storage'].includes(type)) { + return Object.keys(value).length; + } + return false +}; +const mod = function(number, by) { + return (number % by + by) % by; +}; + +const getObjectById = function(array, id) { + return array.filter(obj => { return obj.id == id})[0] +} + +const debug = function() { + if (localStorage.debug) { + console.log.apply(null, arguments) + } +}; + +const canPlayMP4 = function() { + var video = document.createElement('video'); + if (video.canPlayType && video.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace('no', '')) { + return true + } + return false +}; + +const canPlayWebm = function() { + var video = document.createElement('video'); + if (video.canPlayType && video.canPlayType('video/webm; codecs="vp8, vorbis"').replace('no', '')) { + return true + } + return false +}; + +const getFormat = function() { + //var format = canPlayWebm() ? "webm" : "mp4" + var format = canPlayMP4() ? "mp4" : "webm" + return format +} + +const safeDocumentName = function(name) { + ['\\?', '#', '%', '/'].forEach(function(c) { + var r = new RegExp(c, 'g') + name = name.replace(r, '_'); + }) + return name; +}; + +const getVideoInfo = function() { + console.log("FIXME implement getvideoInfo") +} + +const isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); + + +const getLink = function(fragment) { + if (document.location.hash.length > 2) { + return '#' + fragment + } else { + return '/m/' + fragment + } +} + +const clickLink = function(event) { + var a = event.target + while (a && a.tagName != 'A') { + a = a.parentElement + } + if (!a) { + return + } + var href = a.attributes.href.value + var prefix = document.location.protocol + '//' + document.location.hostname + if (href.startsWith(prefix)) { + href = href.slice(prefix.length) + } + if (href.startsWith('/')) { + event.preventDefault() + event.stopPropagation() + var link = href.split('#embed')[0] + if (document.location.hash.length > 2) { + if (link.startsWith('/m')) { + link = link.slice(2) + } + document.location.hash = '#' + link.slice(1) + } else { + if (link.includes('/download/')) { + document.location.href = link + return + } else if (!link.startsWith('/m')) { + link = '/m' + link + } + history.pushState({}, '', link); + render() + } + } +} + +const getUid = (function() { + var uid = 0; + return function() { + return ++uid; + }; +}()); + + +const getVideoURLName = function(id, resolution, part, track, streamId) { + return id + '/' + resolution + 'p' + part + (track ? '.' + track : '') + + '.' + pandora.format + (streamId ? '?' + streamId : ''); +}; + +const getVideoURL = function(id, resolution, part, track, streamId) { + var uid = getUid(), + prefix = pandora.site.site.videoprefix + .replace('{id}', id) + .replace('{part}', part) + .replace('{resolution}', resolution) + .replace('{uid}', uid) + .replace('{uid42}', uid % 42); + if (!prefix) { + prefix = pandoraURL + } + return prefix + '/' + getVideoURLName(id, resolution, part, track, streamId); +}; + +const getSortValue = function(value) { + var sortValue = value; + function trim(value) { + return value.replace(/^\W+(?=\w)/, ''); + } + if ( + isEmpty(value) + || isNull(value) + || isUndefined(value) + ) { + sortValue = null; + } else if (isString(value)) { + // make lowercase and remove leading non-word characters + sortValue = trim(value.toLowerCase()); + // move leading articles to the end + // and remove leading non-word characters + ['a', 'an', 'the'].forEach(function(article) { + if (new RegExp('^' + article + ' ').test(sortValue)) { + sortValue = trim(sortValue.slice(article.length + 1)) + + ', ' + sortValue.slice(0, article.length); + return false; // break + } + }); + // remove thousand separators and pad numbers + sortValue = sortValue.replace(/(\d),(?=(\d{3}))/g, '$1') + .replace(/\d+/g, function(match) { + return match.padStart(64, '0') + }); + } + return sortValue; +}; + +function sortBy(array, by, map) { + return array.sort(function(a, b) { + var aValue, bValue, index = 0, key, ret = 0; + while (ret == 0 && index < by.length) { + key = by[index].key; + aValue = getSortValue( + map && map[key] ? map[key](a[key], a) : a[key] + ); + bValue = getSortValue( + map && map[key] ? map[key](b[key], b) : b[key] + ); + if ((aValue === null) != (bValue === null)) { + ret = aValue === null ? 1 : -1; + } else if (aValue < bValue) { + ret = by[index].operator == '+' ? -1 : 1; + } else if (aValue > bValue) { + ret = by[index].operator == '+' ? 1 : -1; + } else { + index++; + } + } + return ret; + }); +} diff --git a/static/pdf.js/compatibility.js b/static/pdf.js/compatibility.js deleted file mode 100644 index 1119a2742..000000000 --- a/static/pdf.js/compatibility.js +++ /dev/null @@ -1,593 +0,0 @@ -/* Copyright 2012 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* globals VBArray, PDFJS */ - -'use strict'; - -// Initializing PDFJS global object here, it case if we need to change/disable -// some PDF.js features, e.g. range requests -if (typeof PDFJS === 'undefined') { - (typeof window !== 'undefined' ? window : this).PDFJS = {}; -} - -// Checking if the typed arrays are supported -// Support: iOS<6.0 (subarray), IE<10, Android<4.0 -(function checkTypedArrayCompatibility() { - if (typeof Uint8Array !== 'undefined') { - // Support: iOS<6.0 - if (typeof Uint8Array.prototype.subarray === 'undefined') { - Uint8Array.prototype.subarray = function subarray(start, end) { - return new Uint8Array(this.slice(start, end)); - }; - Float32Array.prototype.subarray = function subarray(start, end) { - return new Float32Array(this.slice(start, end)); - }; - } - - // Support: Android<4.1 - if (typeof Float64Array === 'undefined') { - window.Float64Array = Float32Array; - } - return; - } - - function subarray(start, end) { - return new TypedArray(this.slice(start, end)); - } - - function setArrayOffset(array, offset) { - if (arguments.length < 2) { - offset = 0; - } - for (var i = 0, n = array.length; i < n; ++i, ++offset) { - this[offset] = array[i] & 0xFF; - } - } - - function TypedArray(arg1) { - var result, i, n; - if (typeof arg1 === 'number') { - result = []; - for (i = 0; i < arg1; ++i) { - result[i] = 0; - } - } else if ('slice' in arg1) { - result = arg1.slice(0); - } else { - result = []; - for (i = 0, n = arg1.length; i < n; ++i) { - result[i] = arg1[i]; - } - } - - result.subarray = subarray; - result.buffer = result; - result.byteLength = result.length; - result.set = setArrayOffset; - - if (typeof arg1 === 'object' && arg1.buffer) { - result.buffer = arg1.buffer; - } - return result; - } - - window.Uint8Array = TypedArray; - window.Int8Array = TypedArray; - - // we don't need support for set, byteLength for 32-bit array - // so we can use the TypedArray as well - window.Uint32Array = TypedArray; - window.Int32Array = TypedArray; - window.Uint16Array = TypedArray; - window.Float32Array = TypedArray; - window.Float64Array = TypedArray; -})(); - -// URL = URL || webkitURL -// Support: Safari<7, Android 4.2+ -(function normalizeURLObject() { - if (!window.URL) { - window.URL = window.webkitURL; - } -})(); - -// Object.defineProperty()? -// Support: Android<4.0, Safari<5.1 -(function checkObjectDefinePropertyCompatibility() { - if (typeof Object.defineProperty !== 'undefined') { - var definePropertyPossible = true; - try { - // some browsers (e.g. safari) cannot use defineProperty() on DOM objects - // and thus the native version is not sufficient - Object.defineProperty(new Image(), 'id', { value: 'test' }); - // ... another test for android gb browser for non-DOM objects - var Test = function Test() {}; - Test.prototype = { get id() { } }; - Object.defineProperty(new Test(), 'id', - { value: '', configurable: true, enumerable: true, writable: false }); - } catch (e) { - definePropertyPossible = false; - } - if (definePropertyPossible) { - return; - } - } - - Object.defineProperty = function objectDefineProperty(obj, name, def) { - delete obj[name]; - if ('get' in def) { - obj.__defineGetter__(name, def['get']); - } - if ('set' in def) { - obj.__defineSetter__(name, def['set']); - } - if ('value' in def) { - obj.__defineSetter__(name, function objectDefinePropertySetter(value) { - this.__defineGetter__(name, function objectDefinePropertyGetter() { - return value; - }); - return value; - }); - obj[name] = def.value; - } - }; -})(); - - -// No XMLHttpRequest#response? -// Support: IE<11, Android <4.0 -(function checkXMLHttpRequestResponseCompatibility() { - var xhrPrototype = XMLHttpRequest.prototype; - var xhr = new XMLHttpRequest(); - if (!('overrideMimeType' in xhr)) { - // IE10 might have response, but not overrideMimeType - // Support: IE10 - Object.defineProperty(xhrPrototype, 'overrideMimeType', { - value: function xmlHttpRequestOverrideMimeType(mimeType) {} - }); - } - if ('responseType' in xhr) { - return; - } - - // The worker will be using XHR, so we can save time and disable worker. - PDFJS.disableWorker = true; - - Object.defineProperty(xhrPrototype, 'responseType', { - get: function xmlHttpRequestGetResponseType() { - return this._responseType || 'text'; - }, - set: function xmlHttpRequestSetResponseType(value) { - if (value === 'text' || value === 'arraybuffer') { - this._responseType = value; - if (value === 'arraybuffer' && - typeof this.overrideMimeType === 'function') { - this.overrideMimeType('text/plain; charset=x-user-defined'); - } - } - } - }); - - // Support: IE9 - if (typeof VBArray !== 'undefined') { - Object.defineProperty(xhrPrototype, 'response', { - get: function xmlHttpRequestResponseGet() { - if (this.responseType === 'arraybuffer') { - return new Uint8Array(new VBArray(this.responseBody).toArray()); - } else { - return this.responseText; - } - } - }); - return; - } - - Object.defineProperty(xhrPrototype, 'response', { - get: function xmlHttpRequestResponseGet() { - if (this.responseType !== 'arraybuffer') { - return this.responseText; - } - var text = this.responseText; - var i, n = text.length; - var result = new Uint8Array(n); - for (i = 0; i < n; ++i) { - result[i] = text.charCodeAt(i) & 0xFF; - } - return result.buffer; - } - }); -})(); - -// window.btoa (base64 encode function) ? -// Support: IE<10 -(function checkWindowBtoaCompatibility() { - if ('btoa' in window) { - return; - } - - var digits = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - - window.btoa = function windowBtoa(chars) { - var buffer = ''; - var i, n; - for (i = 0, n = chars.length; i < n; i += 3) { - var b1 = chars.charCodeAt(i) & 0xFF; - var b2 = chars.charCodeAt(i + 1) & 0xFF; - var b3 = chars.charCodeAt(i + 2) & 0xFF; - var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4); - var d3 = i + 1 < n ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64; - var d4 = i + 2 < n ? (b3 & 0x3F) : 64; - buffer += (digits.charAt(d1) + digits.charAt(d2) + - digits.charAt(d3) + digits.charAt(d4)); - } - return buffer; - }; -})(); - -// window.atob (base64 encode function)? -// Support: IE<10 -(function checkWindowAtobCompatibility() { - if ('atob' in window) { - return; - } - - // https://github.com/davidchambers/Base64.js - var digits = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - window.atob = function (input) { - input = input.replace(/=+$/, ''); - if (input.length % 4 === 1) { - throw new Error('bad atob input'); - } - for ( - // initialize result and counters - var bc = 0, bs, buffer, idx = 0, output = ''; - // get next character - buffer = input.charAt(idx++); - // character found in table? - // initialize bit storage and add its ascii value - ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, - // and if not first of each 4 characters, - // convert the first 8 bits to one ascii character - bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0 - ) { - // try to find character in table (0-63, not found => -1) - buffer = digits.indexOf(buffer); - } - return output; - }; -})(); - -// Function.prototype.bind? -// Support: Android<4.0, iOS<6.0 -(function checkFunctionPrototypeBindCompatibility() { - if (typeof Function.prototype.bind !== 'undefined') { - return; - } - - Function.prototype.bind = function functionPrototypeBind(obj) { - var fn = this, headArgs = Array.prototype.slice.call(arguments, 1); - var bound = function functionPrototypeBindBound() { - var args = headArgs.concat(Array.prototype.slice.call(arguments)); - return fn.apply(obj, args); - }; - return bound; - }; -})(); - -// HTMLElement dataset property -// Support: IE<11, Safari<5.1, Android<4.0 -(function checkDatasetProperty() { - var div = document.createElement('div'); - if ('dataset' in div) { - return; // dataset property exists - } - - Object.defineProperty(HTMLElement.prototype, 'dataset', { - get: function() { - if (this._dataset) { - return this._dataset; - } - - var dataset = {}; - for (var j = 0, jj = this.attributes.length; j < jj; j++) { - var attribute = this.attributes[j]; - if (attribute.name.substring(0, 5) !== 'data-') { - continue; - } - var key = attribute.name.substring(5).replace(/\-([a-z])/g, - function(all, ch) { - return ch.toUpperCase(); - }); - dataset[key] = attribute.value; - } - - Object.defineProperty(this, '_dataset', { - value: dataset, - writable: false, - enumerable: false - }); - return dataset; - }, - enumerable: true - }); -})(); - -// HTMLElement classList property -// Support: IE<10, Android<4.0, iOS<5.0 -(function checkClassListProperty() { - var div = document.createElement('div'); - if ('classList' in div) { - return; // classList property exists - } - - function changeList(element, itemName, add, remove) { - var s = element.className || ''; - var list = s.split(/\s+/g); - if (list[0] === '') { - list.shift(); - } - var index = list.indexOf(itemName); - if (index < 0 && add) { - list.push(itemName); - } - if (index >= 0 && remove) { - list.splice(index, 1); - } - element.className = list.join(' '); - return (index >= 0); - } - - var classListPrototype = { - add: function(name) { - changeList(this.element, name, true, false); - }, - contains: function(name) { - return changeList(this.element, name, false, false); - }, - remove: function(name) { - changeList(this.element, name, false, true); - }, - toggle: function(name) { - changeList(this.element, name, true, true); - } - }; - - Object.defineProperty(HTMLElement.prototype, 'classList', { - get: function() { - if (this._classList) { - return this._classList; - } - - var classList = Object.create(classListPrototype, { - element: { - value: this, - writable: false, - enumerable: true - } - }); - Object.defineProperty(this, '_classList', { - value: classList, - writable: false, - enumerable: false - }); - return classList; - }, - enumerable: true - }); -})(); - -// Check console compatibility -// In older IE versions the console object is not available -// unless console is open. -// Support: IE<10 -(function checkConsoleCompatibility() { - if (!('console' in window)) { - window.console = { - log: function() {}, - error: function() {}, - warn: function() {} - }; - } else if (!('bind' in console.log)) { - // native functions in IE9 might not have bind - console.log = (function(fn) { - return function(msg) { return fn(msg); }; - })(console.log); - console.error = (function(fn) { - return function(msg) { return fn(msg); }; - })(console.error); - console.warn = (function(fn) { - return function(msg) { return fn(msg); }; - })(console.warn); - } -})(); - -// Check onclick compatibility in Opera -// Support: Opera<15 -(function checkOnClickCompatibility() { - // workaround for reported Opera bug DSK-354448: - // onclick fires on disabled buttons with opaque content - function ignoreIfTargetDisabled(event) { - if (isDisabled(event.target)) { - event.stopPropagation(); - } - } - function isDisabled(node) { - return node.disabled || (node.parentNode && isDisabled(node.parentNode)); - } - if (navigator.userAgent.indexOf('Opera') !== -1) { - // use browser detection since we cannot feature-check this bug - document.addEventListener('click', ignoreIfTargetDisabled, true); - } -})(); - -// Checks if possible to use URL.createObjectURL() -// Support: IE -(function checkOnBlobSupport() { - // sometimes IE loosing the data created with createObjectURL(), see #3977 - if (navigator.userAgent.indexOf('Trident') >= 0) { - PDFJS.disableCreateObjectURL = true; - } -})(); - -// Checks if navigator.language is supported -(function checkNavigatorLanguage() { - if ('language' in navigator) { - return; - } - PDFJS.locale = navigator.userLanguage || 'en-US'; -})(); - -(function checkRangeRequests() { - // Safari has issues with cached range requests see: - // https://github.com/mozilla/pdf.js/issues/3260 - // Last tested with version 6.0.4. - // Support: Safari 6.0+ - var isSafari = Object.prototype.toString.call( - window.HTMLElement).indexOf('Constructor') > 0; - - // Older versions of Android (pre 3.0) has issues with range requests, see: - // https://github.com/mozilla/pdf.js/issues/3381. - // Make sure that we only match webkit-based Android browsers, - // since Firefox/Fennec works as expected. - // Support: Android<3.0 - var regex = /Android\s[0-2][^\d]/; - var isOldAndroid = regex.test(navigator.userAgent); - - // Range requests are broken in Chrome 39 and 40, https://crbug.com/442318 - var isChromeWithRangeBug = /Chrome\/(39|40)\./.test(navigator.userAgent); - - if (isSafari || isOldAndroid || isChromeWithRangeBug) { - PDFJS.disableRange = true; - PDFJS.disableStream = true; - } -})(); - -// Check if the browser supports manipulation of the history. -// Support: IE<10, Android<4.2 -(function checkHistoryManipulation() { - // Android 2.x has so buggy pushState support that it was removed in - // Android 3.0 and restored as late as in Android 4.2. - // Support: Android 2.x - if (!history.pushState || navigator.userAgent.indexOf('Android 2.') >= 0) { - PDFJS.disableHistory = true; - } -})(); - -// Support: IE<11, Chrome<21, Android<4.4, Safari<6 -(function checkSetPresenceInImageData() { - // IE < 11 will use window.CanvasPixelArray which lacks set function. - if (window.CanvasPixelArray) { - if (typeof window.CanvasPixelArray.prototype.set !== 'function') { - window.CanvasPixelArray.prototype.set = function(arr) { - for (var i = 0, ii = this.length; i < ii; i++) { - this[i] = arr[i]; - } - }; - } - } else { - // Old Chrome and Android use an inaccessible CanvasPixelArray prototype. - // Because we cannot feature detect it, we rely on user agent parsing. - var polyfill = false, versionMatch; - if (navigator.userAgent.indexOf('Chrom') >= 0) { - versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); - // Chrome < 21 lacks the set function. - polyfill = versionMatch && parseInt(versionMatch[2]) < 21; - } else if (navigator.userAgent.indexOf('Android') >= 0) { - // Android < 4.4 lacks the set function. - // Android >= 4.4 will contain Chrome in the user agent, - // thus pass the Chrome check above and not reach this block. - polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent); - } else if (navigator.userAgent.indexOf('Safari') >= 0) { - versionMatch = navigator.userAgent. - match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//); - // Safari < 6 lacks the set function. - polyfill = versionMatch && parseInt(versionMatch[1]) < 6; - } - - if (polyfill) { - var contextPrototype = window.CanvasRenderingContext2D.prototype; - var createImageData = contextPrototype.createImageData; - contextPrototype.createImageData = function(w, h) { - var imageData = createImageData.call(this, w, h); - imageData.data.set = function(arr) { - for (var i = 0, ii = this.length; i < ii; i++) { - this[i] = arr[i]; - } - }; - return imageData; - }; - // this closure will be kept referenced, so clear its vars - contextPrototype = null; - } - } -})(); - -// Support: IE<10, Android<4.0, iOS -(function checkRequestAnimationFrame() { - function fakeRequestAnimationFrame(callback) { - window.setTimeout(callback, 20); - } - - var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); - if (isIOS) { - // requestAnimationFrame on iOS is broken, replacing with fake one. - window.requestAnimationFrame = fakeRequestAnimationFrame; - return; - } - if ('requestAnimationFrame' in window) { - return; - } - window.requestAnimationFrame = - window.mozRequestAnimationFrame || - window.webkitRequestAnimationFrame || - fakeRequestAnimationFrame; -})(); - -(function checkCanvasSizeLimitation() { - var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent); - var isAndroid = /Android/g.test(navigator.userAgent); - if (isIOS || isAndroid) { - // 5MP - PDFJS.maxCanvasPixels = 5242880; - } -})(); - -// Disable fullscreen support for certain problematic configurations. -// Support: IE11+ (when embedded). -(function checkFullscreenSupport() { - var isEmbeddedIE = (navigator.userAgent.indexOf('Trident') >= 0 && - window.parent !== window); - if (isEmbeddedIE) { - PDFJS.disableFullscreen = true; - } -})(); - -// Provides document.currentScript support -// Support: IE, Chrome<29. -(function checkCurrentScript() { - if ('currentScript' in document) { - return; - } - Object.defineProperty(document, 'currentScript', { - get: function () { - var scripts = document.getElementsByTagName('script'); - return scripts[scripts.length - 1]; - }, - enumerable: true, - configurable: true - }); -})(); diff --git a/static/pdf.js/custom/toolbarButton-crop.png b/static/pdf.js/custom/toolbarButton-crop.png new file mode 100644 index 000000000..00c56d525 Binary files /dev/null and b/static/pdf.js/custom/toolbarButton-crop.png differ diff --git a/static/pdf.js/custom/toolbarButton-crop.svg b/static/pdf.js/custom/toolbarButton-crop.svg new file mode 100644 index 000000000..618195a1e --- /dev/null +++ b/static/pdf.js/custom/toolbarButton-crop.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/pdf.js/custom/toolbarButton-crop@10.png b/static/pdf.js/custom/toolbarButton-crop@10.png new file mode 100644 index 000000000..7cc05e5d9 Binary files /dev/null and b/static/pdf.js/custom/toolbarButton-crop@10.png differ diff --git a/static/pdf.js/custom/toolbarButton-crop@2x.png b/static/pdf.js/custom/toolbarButton-crop@2x.png new file mode 100644 index 000000000..950d27580 Binary files /dev/null and b/static/pdf.js/custom/toolbarButton-crop@2x.png differ diff --git a/static/pdf.js/debugger.css b/static/pdf.js/debugger.css new file mode 100644 index 000000000..b9d9f8190 --- /dev/null +++ b/static/pdf.js/debugger.css @@ -0,0 +1,111 @@ +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:root { + --panel-width: 300px; +} + +#PDFBug, +#PDFBug :is(input, button, select) { + font: message-box; +} +#PDFBug { + background-color: rgb(255 255 255); + border: 1px solid rgb(102 102 102); + position: fixed; + top: 32px; + right: 0; + bottom: 0; + font-size: 10px; + padding: 0; + width: var(--panel-width); +} +#PDFBug .controls { + background: rgb(238 238 238); + border-bottom: 1px solid rgb(102 102 102); + padding: 3px; +} +#PDFBug .panels { + inset: 27px 0 0; + overflow: auto; + position: absolute; +} +#PDFBug .panels > div { + padding: 5px; +} +#PDFBug button.active { + font-weight: bold; +} +.debuggerShowText, +.debuggerHideText:hover { + background-color: rgb(255 255 0 / 0.25); +} +#PDFBug .stats { + font-family: courier; + font-size: 10px; + white-space: pre; +} +#PDFBug .stats .title { + font-weight: bold; +} +#PDFBug table { + font-size: 10px; + white-space: pre; +} +#PDFBug table.showText { + border-collapse: collapse; + text-align: center; +} +#PDFBug table.showText, +#PDFBug table.showText :is(tr, td) { + border: 1px solid black; + padding: 1px; +} +#PDFBug table.showText td.advance { + color: grey; +} + +#viewer.textLayer-visible .textLayer { + opacity: 1; +} + +#viewer.textLayer-visible .canvasWrapper { + background-color: rgb(128 255 128); +} + +#viewer.textLayer-visible .canvasWrapper canvas { + mix-blend-mode: screen; +} + +#viewer.textLayer-visible .textLayer span { + background-color: rgb(255 255 0 / 0.1); + color: rgb(0 0 0); + border: solid 1px rgb(255 0 0 / 0.5); + box-sizing: border-box; +} + +#viewer.textLayer-visible .textLayer span[aria-owns] { + background-color: rgb(255 0 0 / 0.3); +} + +#viewer.textLayer-hover .textLayer span:hover { + background-color: rgb(255 255 255); + color: rgb(0 0 0); +} + +#viewer.textLayer-shadow .textLayer span { + background-color: rgb(255 255 255 / 0.6); + color: rgb(0 0 0); +} diff --git a/static/pdf.js/debugger.js b/static/pdf.js/debugger.js deleted file mode 100644 index 19d29163d..000000000 --- a/static/pdf.js/debugger.js +++ /dev/null @@ -1,618 +0,0 @@ -/* Copyright 2012 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/* globals PDFJS */ - -'use strict'; - -var FontInspector = (function FontInspectorClosure() { - var fonts; - var active = false; - var fontAttribute = 'data-font-name'; - function removeSelection() { - var divs = document.querySelectorAll('div[' + fontAttribute + ']'); - for (var i = 0, ii = divs.length; i < ii; ++i) { - var div = divs[i]; - div.className = ''; - } - } - function resetSelection() { - var divs = document.querySelectorAll('div[' + fontAttribute + ']'); - for (var i = 0, ii = divs.length; i < ii; ++i) { - var div = divs[i]; - div.className = 'debuggerHideText'; - } - } - function selectFont(fontName, show) { - var divs = document.querySelectorAll('div[' + fontAttribute + '=' + - fontName + ']'); - for (var i = 0, ii = divs.length; i < ii; ++i) { - var div = divs[i]; - div.className = show ? 'debuggerShowText' : 'debuggerHideText'; - } - } - function textLayerClick(e) { - if (!e.target.dataset.fontName || - e.target.tagName.toUpperCase() !== 'DIV') { - return; - } - var fontName = e.target.dataset.fontName; - var selects = document.getElementsByTagName('input'); - for (var i = 0; i < selects.length; ++i) { - var select = selects[i]; - if (select.dataset.fontName !== fontName) { - continue; - } - select.checked = !select.checked; - selectFont(fontName, select.checked); - select.scrollIntoView(); - } - } - return { - // Properties/functions needed by PDFBug. - id: 'FontInspector', - name: 'Font Inspector', - panel: null, - manager: null, - init: function init() { - var panel = this.panel; - panel.setAttribute('style', 'padding: 5px;'); - var tmp = document.createElement('button'); - tmp.addEventListener('click', resetSelection); - tmp.textContent = 'Refresh'; - panel.appendChild(tmp); - - fonts = document.createElement('div'); - panel.appendChild(fonts); - }, - cleanup: function cleanup() { - fonts.textContent = ''; - }, - enabled: false, - get active() { - return active; - }, - set active(value) { - active = value; - if (active) { - document.body.addEventListener('click', textLayerClick, true); - resetSelection(); - } else { - document.body.removeEventListener('click', textLayerClick, true); - removeSelection(); - } - }, - // FontInspector specific functions. - fontAdded: function fontAdded(fontObj, url) { - function properties(obj, list) { - var moreInfo = document.createElement('table'); - for (var i = 0; i < list.length; i++) { - var tr = document.createElement('tr'); - var td1 = document.createElement('td'); - td1.textContent = list[i]; - tr.appendChild(td1); - var td2 = document.createElement('td'); - td2.textContent = obj[list[i]].toString(); - tr.appendChild(td2); - moreInfo.appendChild(tr); - } - return moreInfo; - } - var moreInfo = properties(fontObj, ['name', 'type']); - var fontName = fontObj.loadedName; - var font = document.createElement('div'); - var name = document.createElement('span'); - name.textContent = fontName; - var download = document.createElement('a'); - if (url) { - url = /url\(['"]?([^\)"']+)/.exec(url); - download.href = url[1]; - } else if (fontObj.data) { - url = URL.createObjectURL(new Blob([fontObj.data], { - type: fontObj.mimeType - })); - download.href = url; - } - download.textContent = 'Download'; - var logIt = document.createElement('a'); - logIt.href = ''; - logIt.textContent = 'Log'; - logIt.addEventListener('click', function(event) { - event.preventDefault(); - console.log(fontObj); - }); - var select = document.createElement('input'); - select.setAttribute('type', 'checkbox'); - select.dataset.fontName = fontName; - select.addEventListener('click', (function(select, fontName) { - return (function() { - selectFont(fontName, select.checked); - }); - })(select, fontName)); - font.appendChild(select); - font.appendChild(name); - font.appendChild(document.createTextNode(' ')); - font.appendChild(download); - font.appendChild(document.createTextNode(' ')); - font.appendChild(logIt); - font.appendChild(moreInfo); - fonts.appendChild(font); - // Somewhat of a hack, should probably add a hook for when the text layer - // is done rendering. - setTimeout(function() { - if (this.active) { - resetSelection(); - } - }.bind(this), 2000); - } - }; -})(); - -// Manages all the page steppers. -var StepperManager = (function StepperManagerClosure() { - var steppers = []; - var stepperDiv = null; - var stepperControls = null; - var stepperChooser = null; - var breakPoints = Object.create(null); - return { - // Properties/functions needed by PDFBug. - id: 'Stepper', - name: 'Stepper', - panel: null, - manager: null, - init: function init() { - var self = this; - this.panel.setAttribute('style', 'padding: 5px;'); - stepperControls = document.createElement('div'); - stepperChooser = document.createElement('select'); - stepperChooser.addEventListener('change', function(event) { - self.selectStepper(this.value); - }); - stepperControls.appendChild(stepperChooser); - stepperDiv = document.createElement('div'); - this.panel.appendChild(stepperControls); - this.panel.appendChild(stepperDiv); - if (sessionStorage.getItem('pdfjsBreakPoints')) { - breakPoints = JSON.parse(sessionStorage.getItem('pdfjsBreakPoints')); - } - }, - cleanup: function cleanup() { - stepperChooser.textContent = ''; - stepperDiv.textContent = ''; - steppers = []; - }, - enabled: false, - active: false, - // Stepper specific functions. - create: function create(pageIndex) { - var debug = document.createElement('div'); - debug.id = 'stepper' + pageIndex; - debug.setAttribute('hidden', true); - debug.className = 'stepper'; - stepperDiv.appendChild(debug); - var b = document.createElement('option'); - b.textContent = 'Page ' + (pageIndex + 1); - b.value = pageIndex; - stepperChooser.appendChild(b); - var initBreakPoints = breakPoints[pageIndex] || []; - var stepper = new Stepper(debug, pageIndex, initBreakPoints); - steppers.push(stepper); - if (steppers.length === 1) { - this.selectStepper(pageIndex, false); - } - return stepper; - }, - selectStepper: function selectStepper(pageIndex, selectPanel) { - var i; - pageIndex = pageIndex | 0; - if (selectPanel) { - this.manager.selectPanel(this); - } - for (i = 0; i < steppers.length; ++i) { - var stepper = steppers[i]; - if (stepper.pageIndex === pageIndex) { - stepper.panel.removeAttribute('hidden'); - } else { - stepper.panel.setAttribute('hidden', true); - } - } - var options = stepperChooser.options; - for (i = 0; i < options.length; ++i) { - var option = options[i]; - option.selected = (option.value | 0) === pageIndex; - } - }, - saveBreakPoints: function saveBreakPoints(pageIndex, bps) { - breakPoints[pageIndex] = bps; - sessionStorage.setItem('pdfjsBreakPoints', JSON.stringify(breakPoints)); - } - }; -})(); - -// The stepper for each page's IRQueue. -var Stepper = (function StepperClosure() { - // Shorter way to create element and optionally set textContent. - function c(tag, textContent) { - var d = document.createElement(tag); - if (textContent) { - d.textContent = textContent; - } - return d; - } - - var opMap = null; - - function simplifyArgs(args) { - if (typeof args === 'string') { - var MAX_STRING_LENGTH = 75; - return args.length <= MAX_STRING_LENGTH ? args : - args.substr(0, MAX_STRING_LENGTH) + '...'; - } - if (typeof args !== 'object' || args === null) { - return args; - } - if ('length' in args) { // array - var simpleArgs = [], i, ii; - var MAX_ITEMS = 10; - for (i = 0, ii = Math.min(MAX_ITEMS, args.length); i < ii; i++) { - simpleArgs.push(simplifyArgs(args[i])); - } - if (i < args.length) { - simpleArgs.push('...'); - } - return simpleArgs; - } - var simpleObj = {}; - for (var key in args) { - simpleObj[key] = simplifyArgs(args[key]); - } - return simpleObj; - } - - function Stepper(panel, pageIndex, initialBreakPoints) { - this.panel = panel; - this.breakPoint = 0; - this.nextBreakPoint = null; - this.pageIndex = pageIndex; - this.breakPoints = initialBreakPoints; - this.currentIdx = -1; - this.operatorListIdx = 0; - } - Stepper.prototype = { - init: function init() { - var panel = this.panel; - var content = c('div', 'c=continue, s=step'); - var table = c('table'); - content.appendChild(table); - table.cellSpacing = 0; - var headerRow = c('tr'); - table.appendChild(headerRow); - headerRow.appendChild(c('th', 'Break')); - headerRow.appendChild(c('th', 'Idx')); - headerRow.appendChild(c('th', 'fn')); - headerRow.appendChild(c('th', 'args')); - panel.appendChild(content); - this.table = table; - if (!opMap) { - opMap = Object.create(null); - for (var key in PDFJS.OPS) { - opMap[PDFJS.OPS[key]] = key; - } - } - }, - updateOperatorList: function updateOperatorList(operatorList) { - var self = this; - - function cboxOnClick() { - var x = +this.dataset.idx; - if (this.checked) { - self.breakPoints.push(x); - } else { - self.breakPoints.splice(self.breakPoints.indexOf(x), 1); - } - StepperManager.saveBreakPoints(self.pageIndex, self.breakPoints); - } - - var MAX_OPERATORS_COUNT = 15000; - if (this.operatorListIdx > MAX_OPERATORS_COUNT) { - return; - } - - var chunk = document.createDocumentFragment(); - var operatorsToDisplay = Math.min(MAX_OPERATORS_COUNT, - operatorList.fnArray.length); - for (var i = this.operatorListIdx; i < operatorsToDisplay; i++) { - var line = c('tr'); - line.className = 'line'; - line.dataset.idx = i; - chunk.appendChild(line); - var checked = this.breakPoints.indexOf(i) !== -1; - var args = operatorList.argsArray[i] || []; - - var breakCell = c('td'); - var cbox = c('input'); - cbox.type = 'checkbox'; - cbox.className = 'points'; - cbox.checked = checked; - cbox.dataset.idx = i; - cbox.onclick = cboxOnClick; - - breakCell.appendChild(cbox); - line.appendChild(breakCell); - line.appendChild(c('td', i.toString())); - var fn = opMap[operatorList.fnArray[i]]; - var decArgs = args; - if (fn === 'showText') { - var glyphs = args[0]; - var newArgs = []; - var str = []; - for (var j = 0; j < glyphs.length; j++) { - var glyph = glyphs[j]; - if (typeof glyph === 'object' && glyph !== null) { - str.push(glyph.fontChar); - } else { - if (str.length > 0) { - newArgs.push(str.join('')); - str = []; - } - newArgs.push(glyph); // null or number - } - } - if (str.length > 0) { - newArgs.push(str.join('')); - } - decArgs = [newArgs]; - } - line.appendChild(c('td', fn)); - line.appendChild(c('td', JSON.stringify(simplifyArgs(decArgs)))); - } - if (operatorsToDisplay < operatorList.fnArray.length) { - line = c('tr'); - var lastCell = c('td', '...'); - lastCell.colspan = 4; - chunk.appendChild(lastCell); - } - this.operatorListIdx = operatorList.fnArray.length; - this.table.appendChild(chunk); - }, - getNextBreakPoint: function getNextBreakPoint() { - this.breakPoints.sort(function(a, b) { return a - b; }); - for (var i = 0; i < this.breakPoints.length; i++) { - if (this.breakPoints[i] > this.currentIdx) { - return this.breakPoints[i]; - } - } - return null; - }, - breakIt: function breakIt(idx, callback) { - StepperManager.selectStepper(this.pageIndex, true); - var self = this; - var dom = document; - self.currentIdx = idx; - var listener = function(e) { - switch (e.keyCode) { - case 83: // step - dom.removeEventListener('keydown', listener, false); - self.nextBreakPoint = self.currentIdx + 1; - self.goTo(-1); - callback(); - break; - case 67: // continue - dom.removeEventListener('keydown', listener, false); - var breakPoint = self.getNextBreakPoint(); - self.nextBreakPoint = breakPoint; - self.goTo(-1); - callback(); - break; - } - }; - dom.addEventListener('keydown', listener, false); - self.goTo(idx); - }, - goTo: function goTo(idx) { - var allRows = this.panel.getElementsByClassName('line'); - for (var x = 0, xx = allRows.length; x < xx; ++x) { - var row = allRows[x]; - if ((row.dataset.idx | 0) === idx) { - row.style.backgroundColor = 'rgb(251,250,207)'; - row.scrollIntoView(); - } else { - row.style.backgroundColor = null; - } - } - } - }; - return Stepper; -})(); - -var Stats = (function Stats() { - var stats = []; - function clear(node) { - while (node.hasChildNodes()) { - node.removeChild(node.lastChild); - } - } - function getStatIndex(pageNumber) { - for (var i = 0, ii = stats.length; i < ii; ++i) { - if (stats[i].pageNumber === pageNumber) { - return i; - } - } - return false; - } - return { - // Properties/functions needed by PDFBug. - id: 'Stats', - name: 'Stats', - panel: null, - manager: null, - init: function init() { - this.panel.setAttribute('style', 'padding: 5px;'); - PDFJS.enableStats = true; - }, - enabled: false, - active: false, - // Stats specific functions. - add: function(pageNumber, stat) { - if (!stat) { - return; - } - var statsIndex = getStatIndex(pageNumber); - if (statsIndex !== false) { - var b = stats[statsIndex]; - this.panel.removeChild(b.div); - stats.splice(statsIndex, 1); - } - var wrapper = document.createElement('div'); - wrapper.className = 'stats'; - var title = document.createElement('div'); - title.className = 'title'; - title.textContent = 'Page: ' + pageNumber; - var statsDiv = document.createElement('div'); - statsDiv.textContent = stat.toString(); - wrapper.appendChild(title); - wrapper.appendChild(statsDiv); - stats.push({ pageNumber: pageNumber, div: wrapper }); - stats.sort(function(a, b) { return a.pageNumber - b.pageNumber; }); - clear(this.panel); - for (var i = 0, ii = stats.length; i < ii; ++i) { - this.panel.appendChild(stats[i].div); - } - }, - cleanup: function () { - stats = []; - clear(this.panel); - } - }; -})(); - -// Manages all the debugging tools. -var PDFBug = (function PDFBugClosure() { - var panelWidth = 300; - var buttons = []; - var activePanel = null; - - return { - tools: [ - FontInspector, - StepperManager, - Stats - ], - enable: function(ids) { - var all = false, tools = this.tools; - if (ids.length === 1 && ids[0] === 'all') { - all = true; - } - for (var i = 0; i < tools.length; ++i) { - var tool = tools[i]; - if (all || ids.indexOf(tool.id) !== -1) { - tool.enabled = true; - } - } - if (!all) { - // Sort the tools by the order they are enabled. - tools.sort(function(a, b) { - var indexA = ids.indexOf(a.id); - indexA = indexA < 0 ? tools.length : indexA; - var indexB = ids.indexOf(b.id); - indexB = indexB < 0 ? tools.length : indexB; - return indexA - indexB; - }); - } - }, - init: function init() { - /* - * Basic Layout: - * PDFBug - * Controls - * Panels - * Panel - * Panel - * ... - */ - var ui = document.createElement('div'); - ui.id = 'PDFBug'; - - var controls = document.createElement('div'); - controls.setAttribute('class', 'controls'); - ui.appendChild(controls); - - var panels = document.createElement('div'); - panels.setAttribute('class', 'panels'); - ui.appendChild(panels); - - var container = document.getElementById('viewerContainer'); - container.appendChild(ui); - container.style.right = panelWidth + 'px'; - - // Initialize all the debugging tools. - var tools = this.tools; - var self = this; - for (var i = 0; i < tools.length; ++i) { - var tool = tools[i]; - var panel = document.createElement('div'); - var panelButton = document.createElement('button'); - panelButton.textContent = tool.name; - panelButton.addEventListener('click', (function(selected) { - return function(event) { - event.preventDefault(); - self.selectPanel(selected); - }; - })(i)); - controls.appendChild(panelButton); - panels.appendChild(panel); - tool.panel = panel; - tool.manager = this; - if (tool.enabled) { - tool.init(); - } else { - panel.textContent = tool.name + ' is disabled. To enable add ' + - ' "' + tool.id + '" to the pdfBug parameter ' + - 'and refresh (seperate multiple by commas).'; - } - buttons.push(panelButton); - } - this.selectPanel(0); - }, - cleanup: function cleanup() { - for (var i = 0, ii = this.tools.length; i < ii; i++) { - if (this.tools[i].enabled) { - this.tools[i].cleanup(); - } - } - }, - selectPanel: function selectPanel(index) { - if (typeof index !== 'number') { - index = this.tools.indexOf(index); - } - if (index === activePanel) { - return; - } - activePanel = index; - var tools = this.tools; - for (var j = 0; j < tools.length; ++j) { - if (j === index) { - buttons[j].setAttribute('class', 'active'); - tools[j].active = true; - tools[j].panel.removeAttribute('hidden'); - } else { - buttons[j].setAttribute('class', ''); - tools[j].active = false; - tools[j].panel.setAttribute('hidden', 'true'); - } - } - } - }; -})(); diff --git a/static/pdf.js/debugger.mjs b/static/pdf.js/debugger.mjs new file mode 100644 index 000000000..59c1871b3 --- /dev/null +++ b/static/pdf.js/debugger.mjs @@ -0,0 +1,623 @@ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { OPS } = globalThis.pdfjsLib || (await import("pdfjs-lib")); + +const opMap = Object.create(null); +for (const key in OPS) { + opMap[OPS[key]] = key; +} + +const FontInspector = (function FontInspectorClosure() { + let fonts; + let active = false; + const fontAttribute = "data-font-name"; + function removeSelection() { + const divs = document.querySelectorAll(`span[${fontAttribute}]`); + for (const div of divs) { + div.className = ""; + } + } + function resetSelection() { + const divs = document.querySelectorAll(`span[${fontAttribute}]`); + for (const div of divs) { + div.className = "debuggerHideText"; + } + } + function selectFont(fontName, show) { + const divs = document.querySelectorAll( + `span[${fontAttribute}=${fontName}]` + ); + for (const div of divs) { + div.className = show ? "debuggerShowText" : "debuggerHideText"; + } + } + function textLayerClick(e) { + if ( + !e.target.dataset.fontName || + e.target.tagName.toUpperCase() !== "SPAN" + ) { + return; + } + const fontName = e.target.dataset.fontName; + const selects = document.getElementsByTagName("input"); + for (const select of selects) { + if (select.dataset.fontName !== fontName) { + continue; + } + select.checked = !select.checked; + selectFont(fontName, select.checked); + select.scrollIntoView(); + } + } + return { + // Properties/functions needed by PDFBug. + id: "FontInspector", + name: "Font Inspector", + panel: null, + manager: null, + init() { + const panel = this.panel; + const tmp = document.createElement("button"); + tmp.addEventListener("click", resetSelection); + tmp.textContent = "Refresh"; + panel.append(tmp); + + fonts = document.createElement("div"); + panel.append(fonts); + }, + cleanup() { + fonts.textContent = ""; + }, + enabled: false, + get active() { + return active; + }, + set active(value) { + active = value; + if (active) { + document.body.addEventListener("click", textLayerClick, true); + resetSelection(); + } else { + document.body.removeEventListener("click", textLayerClick, true); + removeSelection(); + } + }, + // FontInspector specific functions. + fontAdded(fontObj, url) { + function properties(obj, list) { + const moreInfo = document.createElement("table"); + for (const entry of list) { + const tr = document.createElement("tr"); + const td1 = document.createElement("td"); + td1.textContent = entry; + tr.append(td1); + const td2 = document.createElement("td"); + td2.textContent = obj[entry].toString(); + tr.append(td2); + moreInfo.append(tr); + } + return moreInfo; + } + + const moreInfo = fontObj.css + ? properties(fontObj, ["baseFontName"]) + : properties(fontObj, ["name", "type"]); + + const fontName = fontObj.loadedName; + const font = document.createElement("div"); + const name = document.createElement("span"); + name.textContent = fontName; + let download; + if (!fontObj.css) { + download = document.createElement("a"); + if (url) { + url = /url\(['"]?([^)"']+)/.exec(url); + download.href = url[1]; + } else if (fontObj.data) { + download.href = URL.createObjectURL( + new Blob([fontObj.data], { type: fontObj.mimetype }) + ); + } + download.textContent = "Download"; + } + + const logIt = document.createElement("a"); + logIt.href = ""; + logIt.textContent = "Log"; + logIt.addEventListener("click", function (event) { + event.preventDefault(); + console.log(fontObj); + }); + const select = document.createElement("input"); + select.setAttribute("type", "checkbox"); + select.dataset.fontName = fontName; + select.addEventListener("click", function () { + selectFont(fontName, select.checked); + }); + if (download) { + font.append(select, name, " ", download, " ", logIt, moreInfo); + } else { + font.append(select, name, " ", logIt, moreInfo); + } + fonts.append(font); + // Somewhat of a hack, should probably add a hook for when the text layer + // is done rendering. + setTimeout(() => { + if (this.active) { + resetSelection(); + } + }, 2000); + }, + }; +})(); + +// Manages all the page steppers. +const StepperManager = (function StepperManagerClosure() { + let steppers = []; + let stepperDiv = null; + let stepperControls = null; + let stepperChooser = null; + let breakPoints = Object.create(null); + return { + // Properties/functions needed by PDFBug. + id: "Stepper", + name: "Stepper", + panel: null, + manager: null, + init() { + const self = this; + stepperControls = document.createElement("div"); + stepperChooser = document.createElement("select"); + stepperChooser.addEventListener("change", function (event) { + self.selectStepper(this.value); + }); + stepperControls.append(stepperChooser); + stepperDiv = document.createElement("div"); + this.panel.append(stepperControls, stepperDiv); + if (sessionStorage.getItem("pdfjsBreakPoints")) { + breakPoints = JSON.parse(sessionStorage.getItem("pdfjsBreakPoints")); + } + }, + cleanup() { + stepperChooser.textContent = ""; + stepperDiv.textContent = ""; + steppers = []; + }, + enabled: false, + active: false, + // Stepper specific functions. + create(pageIndex) { + const debug = document.createElement("div"); + debug.id = "stepper" + pageIndex; + debug.hidden = true; + debug.className = "stepper"; + stepperDiv.append(debug); + const b = document.createElement("option"); + b.textContent = "Page " + (pageIndex + 1); + b.value = pageIndex; + stepperChooser.append(b); + const initBreakPoints = breakPoints[pageIndex] || []; + const stepper = new Stepper(debug, pageIndex, initBreakPoints); + steppers.push(stepper); + if (steppers.length === 1) { + this.selectStepper(pageIndex, false); + } + return stepper; + }, + selectStepper(pageIndex, selectPanel) { + pageIndex |= 0; + if (selectPanel) { + this.manager.selectPanel(this); + } + for (const stepper of steppers) { + stepper.panel.hidden = stepper.pageIndex !== pageIndex; + } + for (const option of stepperChooser.options) { + option.selected = (option.value | 0) === pageIndex; + } + }, + saveBreakPoints(pageIndex, bps) { + breakPoints[pageIndex] = bps; + sessionStorage.setItem("pdfjsBreakPoints", JSON.stringify(breakPoints)); + }, + }; +})(); + +// The stepper for each page's operatorList. +class Stepper { + // Shorter way to create element and optionally set textContent. + #c(tag, textContent) { + const d = document.createElement(tag); + if (textContent) { + d.textContent = textContent; + } + return d; + } + + #simplifyArgs(args) { + if (typeof args === "string") { + const MAX_STRING_LENGTH = 75; + return args.length <= MAX_STRING_LENGTH + ? args + : args.substring(0, MAX_STRING_LENGTH) + "..."; + } + if (typeof args !== "object" || args === null) { + return args; + } + if ("length" in args) { + // array + const MAX_ITEMS = 10, + simpleArgs = []; + let i, ii; + for (i = 0, ii = Math.min(MAX_ITEMS, args.length); i < ii; i++) { + simpleArgs.push(this.#simplifyArgs(args[i])); + } + if (i < args.length) { + simpleArgs.push("..."); + } + return simpleArgs; + } + const simpleObj = {}; + for (const key in args) { + simpleObj[key] = this.#simplifyArgs(args[key]); + } + return simpleObj; + } + + constructor(panel, pageIndex, initialBreakPoints) { + this.panel = panel; + this.breakPoint = 0; + this.nextBreakPoint = null; + this.pageIndex = pageIndex; + this.breakPoints = initialBreakPoints; + this.currentIdx = -1; + this.operatorListIdx = 0; + this.indentLevel = 0; + } + + init(operatorList) { + const panel = this.panel; + const content = this.#c("div", "c=continue, s=step"); + const table = this.#c("table"); + content.append(table); + table.cellSpacing = 0; + const headerRow = this.#c("tr"); + table.append(headerRow); + headerRow.append( + this.#c("th", "Break"), + this.#c("th", "Idx"), + this.#c("th", "fn"), + this.#c("th", "args") + ); + panel.append(content); + this.table = table; + this.updateOperatorList(operatorList); + } + + updateOperatorList(operatorList) { + const self = this; + + function cboxOnClick() { + const x = +this.dataset.idx; + if (this.checked) { + self.breakPoints.push(x); + } else { + self.breakPoints.splice(self.breakPoints.indexOf(x), 1); + } + StepperManager.saveBreakPoints(self.pageIndex, self.breakPoints); + } + + const MAX_OPERATORS_COUNT = 15000; + if (this.operatorListIdx > MAX_OPERATORS_COUNT) { + return; + } + + const chunk = document.createDocumentFragment(); + const operatorsToDisplay = Math.min( + MAX_OPERATORS_COUNT, + operatorList.fnArray.length + ); + for (let i = this.operatorListIdx; i < operatorsToDisplay; i++) { + const line = this.#c("tr"); + line.className = "line"; + line.dataset.idx = i; + chunk.append(line); + const checked = this.breakPoints.includes(i); + const args = operatorList.argsArray[i] || []; + + const breakCell = this.#c("td"); + const cbox = this.#c("input"); + cbox.type = "checkbox"; + cbox.className = "points"; + cbox.checked = checked; + cbox.dataset.idx = i; + cbox.onclick = cboxOnClick; + + breakCell.append(cbox); + line.append(breakCell, this.#c("td", i.toString())); + const fn = opMap[operatorList.fnArray[i]]; + let decArgs = args; + if (fn === "showText") { + const glyphs = args[0]; + const charCodeRow = this.#c("tr"); + const fontCharRow = this.#c("tr"); + const unicodeRow = this.#c("tr"); + for (const glyph of glyphs) { + if (typeof glyph === "object" && glyph !== null) { + charCodeRow.append(this.#c("td", glyph.originalCharCode)); + fontCharRow.append(this.#c("td", glyph.fontChar)); + unicodeRow.append(this.#c("td", glyph.unicode)); + } else { + // null or number + const advanceEl = this.#c("td", glyph); + advanceEl.classList.add("advance"); + charCodeRow.append(advanceEl); + fontCharRow.append(this.#c("td")); + unicodeRow.append(this.#c("td")); + } + } + decArgs = this.#c("td"); + const table = this.#c("table"); + table.classList.add("showText"); + decArgs.append(table); + table.append(charCodeRow, fontCharRow, unicodeRow); + } else if (fn === "restore" && this.indentLevel > 0) { + this.indentLevel--; + } + line.append(this.#c("td", " ".repeat(this.indentLevel * 2) + fn)); + if (fn === "save") { + this.indentLevel++; + } + + if (decArgs instanceof HTMLElement) { + line.append(decArgs); + } else { + line.append(this.#c("td", JSON.stringify(this.#simplifyArgs(decArgs)))); + } + } + if (operatorsToDisplay < operatorList.fnArray.length) { + const lastCell = this.#c("td", "..."); + lastCell.colspan = 4; + chunk.append(lastCell); + } + this.operatorListIdx = operatorList.fnArray.length; + this.table.append(chunk); + } + + getNextBreakPoint() { + this.breakPoints.sort(function (a, b) { + return a - b; + }); + for (const breakPoint of this.breakPoints) { + if (breakPoint > this.currentIdx) { + return breakPoint; + } + } + return null; + } + + breakIt(idx, callback) { + StepperManager.selectStepper(this.pageIndex, true); + this.currentIdx = idx; + + const listener = evt => { + switch (evt.keyCode) { + case 83: // step + document.removeEventListener("keydown", listener); + this.nextBreakPoint = this.currentIdx + 1; + this.goTo(-1); + callback(); + break; + case 67: // continue + document.removeEventListener("keydown", listener); + this.nextBreakPoint = this.getNextBreakPoint(); + this.goTo(-1); + callback(); + break; + } + }; + document.addEventListener("keydown", listener); + this.goTo(idx); + } + + goTo(idx) { + const allRows = this.panel.getElementsByClassName("line"); + for (const row of allRows) { + if ((row.dataset.idx | 0) === idx) { + row.style.backgroundColor = "rgb(251,250,207)"; + row.scrollIntoView(); + } else { + row.style.backgroundColor = null; + } + } + } +} + +const Stats = (function Stats() { + let stats = []; + function clear(node) { + node.textContent = ""; // Remove any `node` contents from the DOM. + } + function getStatIndex(pageNumber) { + for (const [i, stat] of stats.entries()) { + if (stat.pageNumber === pageNumber) { + return i; + } + } + return false; + } + return { + // Properties/functions needed by PDFBug. + id: "Stats", + name: "Stats", + panel: null, + manager: null, + init() {}, + enabled: false, + active: false, + // Stats specific functions. + add(pageNumber, stat) { + if (!stat) { + return; + } + const statsIndex = getStatIndex(pageNumber); + if (statsIndex !== false) { + stats[statsIndex].div.remove(); + stats.splice(statsIndex, 1); + } + const wrapper = document.createElement("div"); + wrapper.className = "stats"; + const title = document.createElement("div"); + title.className = "title"; + title.textContent = "Page: " + pageNumber; + const statsDiv = document.createElement("div"); + statsDiv.textContent = stat.toString(); + wrapper.append(title, statsDiv); + stats.push({ pageNumber, div: wrapper }); + stats.sort(function (a, b) { + return a.pageNumber - b.pageNumber; + }); + clear(this.panel); + for (const entry of stats) { + this.panel.append(entry.div); + } + }, + cleanup() { + stats = []; + clear(this.panel); + }, + }; +})(); + +// Manages all the debugging tools. +class PDFBug { + static #buttons = []; + + static #activePanel = null; + + static tools = [FontInspector, StepperManager, Stats]; + + static enable(ids) { + const all = ids.length === 1 && ids[0] === "all"; + const tools = this.tools; + for (const tool of tools) { + if (all || ids.includes(tool.id)) { + tool.enabled = true; + } + } + if (!all) { + // Sort the tools by the order they are enabled. + tools.sort(function (a, b) { + let indexA = ids.indexOf(a.id); + indexA = indexA < 0 ? tools.length : indexA; + let indexB = ids.indexOf(b.id); + indexB = indexB < 0 ? tools.length : indexB; + return indexA - indexB; + }); + } + } + + static init(container, ids) { + this.loadCSS(); + this.enable(ids); + /* + * Basic Layout: + * PDFBug + * Controls + * Panels + * Panel + * Panel + * ... + */ + const ui = document.createElement("div"); + ui.id = "PDFBug"; + + const controls = document.createElement("div"); + controls.setAttribute("class", "controls"); + ui.append(controls); + + const panels = document.createElement("div"); + panels.setAttribute("class", "panels"); + ui.append(panels); + + container.append(ui); + container.style.right = "var(--panel-width)"; + + // Initialize all the debugging tools. + for (const tool of this.tools) { + const panel = document.createElement("div"); + const panelButton = document.createElement("button"); + panelButton.textContent = tool.name; + panelButton.addEventListener("click", event => { + event.preventDefault(); + this.selectPanel(tool); + }); + controls.append(panelButton); + panels.append(panel); + tool.panel = panel; + tool.manager = this; + if (tool.enabled) { + tool.init(); + } else { + panel.textContent = + `${tool.name} is disabled. To enable add "${tool.id}" to ` + + "the pdfBug parameter and refresh (separate multiple by commas)."; + } + this.#buttons.push(panelButton); + } + this.selectPanel(0); + } + + static loadCSS() { + const { url } = import.meta; + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = url.replace(/\.mjs$/, ".css"); + + document.head.append(link); + } + + static cleanup() { + for (const tool of this.tools) { + if (tool.enabled) { + tool.cleanup(); + } + } + } + + static selectPanel(index) { + if (typeof index !== "number") { + index = this.tools.indexOf(index); + } + if (index === this.#activePanel) { + return; + } + this.#activePanel = index; + for (const [j, tool] of this.tools.entries()) { + const isActive = j === index; + this.#buttons[j].classList.toggle("active", isActive); + tool.active = isActive; + tool.panel.hidden = !isActive; + } + } +} + +globalThis.FontInspector = FontInspector; +globalThis.StepperManager = StepperManager; +globalThis.Stats = Stats; + +export { PDFBug }; diff --git a/static/pdf.js/embeds.js b/static/pdf.js/embeds.js index d6e52d068..c773feebd 100644 --- a/static/pdf.js/embeds.js +++ b/static/pdf.js/embeds.js @@ -3,25 +3,27 @@ Ox.load({ loadCSS: false } }, function() { - var currentPage = PDFView.page; - window.addEventListener('pagechange', function (evt) { - var page = evt.pageNumber; - if (page && page != currentPage) { - currentPage = page; - Ox.$parent.postMessage('page', { - page: Math.round(page) - }); - } - }); + var currentPage = PDFViewerApplication.page; + PDFViewerApplication.initializedPromise.then(function() { + PDFViewerApplication.pdfViewer.eventBus.on("pagechanging", function(event) { + var page = event.pageNumber; + if (page && page != currentPage) { + currentPage = page; + Ox.$parent.postMessage('page', { + page: page + }); + } + }) + }) Ox.$parent.bindMessage({ page: function(data) { - if (data.page != PDFView.page) { - PDFView.page = data.page; + if (data.page != PDFViewerApplication.page) { + PDFViewerApplication.page = data.page; } }, pdf: function(data) { - if (PDFView.url != data.pdf) { - PDFView.open(data.pdf); + if (PDFViewerApplication.url != data.pdf) { + PDFViewerApplication.open(data.pdf); } } }); diff --git a/static/pdf.js/images/altText_add.svg b/static/pdf.js/images/altText_add.svg new file mode 100644 index 000000000..3451b536c --- /dev/null +++ b/static/pdf.js/images/altText_add.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/altText_done.svg b/static/pdf.js/images/altText_done.svg new file mode 100644 index 000000000..f54924ebf --- /dev/null +++ b/static/pdf.js/images/altText_done.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/annotation-paperclip.svg b/static/pdf.js/images/annotation-paperclip.svg new file mode 100644 index 000000000..2bed2250a --- /dev/null +++ b/static/pdf.js/images/annotation-paperclip.svg @@ -0,0 +1,6 @@ + + + + diff --git a/static/pdf.js/images/annotation-pushpin.svg b/static/pdf.js/images/annotation-pushpin.svg new file mode 100644 index 000000000..6e0896cf4 --- /dev/null +++ b/static/pdf.js/images/annotation-pushpin.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/static/pdf.js/images/cursor-editorFreeHighlight.svg b/static/pdf.js/images/cursor-editorFreeHighlight.svg new file mode 100644 index 000000000..513f6bdfd --- /dev/null +++ b/static/pdf.js/images/cursor-editorFreeHighlight.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/static/pdf.js/images/cursor-editorFreeText.svg b/static/pdf.js/images/cursor-editorFreeText.svg new file mode 100644 index 000000000..de2838ef1 --- /dev/null +++ b/static/pdf.js/images/cursor-editorFreeText.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/cursor-editorInk.svg b/static/pdf.js/images/cursor-editorInk.svg new file mode 100644 index 000000000..1dadb5c01 --- /dev/null +++ b/static/pdf.js/images/cursor-editorInk.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/pdf.js/images/cursor-editorTextHighlight.svg b/static/pdf.js/images/cursor-editorTextHighlight.svg new file mode 100644 index 000000000..800340cb5 --- /dev/null +++ b/static/pdf.js/images/cursor-editorTextHighlight.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/pdf.js/images/editor-toolbar-delete.svg b/static/pdf.js/images/editor-toolbar-delete.svg new file mode 100644 index 000000000..f84520d85 --- /dev/null +++ b/static/pdf.js/images/editor-toolbar-delete.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/static/pdf.js/images/findbarButton-next-rtl.png b/static/pdf.js/images/findbarButton-next-rtl.png deleted file mode 100644 index bef02743f..000000000 Binary files a/static/pdf.js/images/findbarButton-next-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-next-rtl@2x.png b/static/pdf.js/images/findbarButton-next-rtl@2x.png deleted file mode 100644 index 1da6dc949..000000000 Binary files a/static/pdf.js/images/findbarButton-next-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-next.png b/static/pdf.js/images/findbarButton-next.png deleted file mode 100644 index de1d0fc90..000000000 Binary files a/static/pdf.js/images/findbarButton-next.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-next.svg b/static/pdf.js/images/findbarButton-next.svg new file mode 100644 index 000000000..8cb39bec6 --- /dev/null +++ b/static/pdf.js/images/findbarButton-next.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/findbarButton-next@2x.png b/static/pdf.js/images/findbarButton-next@2x.png deleted file mode 100644 index 0250307c0..000000000 Binary files a/static/pdf.js/images/findbarButton-next@2x.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-previous-rtl.png b/static/pdf.js/images/findbarButton-previous-rtl.png deleted file mode 100644 index de1d0fc90..000000000 Binary files a/static/pdf.js/images/findbarButton-previous-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-previous-rtl@2x.png b/static/pdf.js/images/findbarButton-previous-rtl@2x.png deleted file mode 100644 index 0250307c0..000000000 Binary files a/static/pdf.js/images/findbarButton-previous-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-previous.png b/static/pdf.js/images/findbarButton-previous.png deleted file mode 100644 index bef02743f..000000000 Binary files a/static/pdf.js/images/findbarButton-previous.png and /dev/null differ diff --git a/static/pdf.js/images/findbarButton-previous.svg b/static/pdf.js/images/findbarButton-previous.svg new file mode 100644 index 000000000..b610879da --- /dev/null +++ b/static/pdf.js/images/findbarButton-previous.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/findbarButton-previous@2x.png b/static/pdf.js/images/findbarButton-previous@2x.png deleted file mode 100644 index 1da6dc949..000000000 Binary files a/static/pdf.js/images/findbarButton-previous@2x.png and /dev/null differ diff --git a/static/pdf.js/images/grab.cur b/static/pdf.js/images/grab.cur deleted file mode 100644 index db7ad5aed..000000000 Binary files a/static/pdf.js/images/grab.cur and /dev/null differ diff --git a/static/pdf.js/images/grabbing.cur b/static/pdf.js/images/grabbing.cur deleted file mode 100644 index e0dfd04e4..000000000 Binary files a/static/pdf.js/images/grabbing.cur and /dev/null differ diff --git a/static/pdf.js/images/gv-toolbarButton-download.svg b/static/pdf.js/images/gv-toolbarButton-download.svg new file mode 100644 index 000000000..d56cf3ce7 --- /dev/null +++ b/static/pdf.js/images/gv-toolbarButton-download.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/loading-small.png b/static/pdf.js/images/loading-small.png deleted file mode 100644 index 8831a8058..000000000 Binary files a/static/pdf.js/images/loading-small.png and /dev/null differ diff --git a/static/pdf.js/images/loading-small@2x.png b/static/pdf.js/images/loading-small@2x.png deleted file mode 100644 index b25b4452a..000000000 Binary files a/static/pdf.js/images/loading-small@2x.png and /dev/null differ diff --git a/static/pdf.js/images/loading.svg b/static/pdf.js/images/loading.svg new file mode 100644 index 000000000..0a15ff688 --- /dev/null +++ b/static/pdf.js/images/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/pdf.js/images/secondaryToolbarButton-documentProperties.png b/static/pdf.js/images/secondaryToolbarButton-documentProperties.png deleted file mode 100644 index 40925e25a..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-documentProperties.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-documentProperties.svg b/static/pdf.js/images/secondaryToolbarButton-documentProperties.svg new file mode 100644 index 000000000..dd3917b91 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-documentProperties.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-documentProperties@2x.png b/static/pdf.js/images/secondaryToolbarButton-documentProperties@2x.png deleted file mode 100644 index adb240eaa..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-documentProperties@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-firstPage.png b/static/pdf.js/images/secondaryToolbarButton-firstPage.png deleted file mode 100644 index e68846aa5..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-firstPage.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-firstPage.svg b/static/pdf.js/images/secondaryToolbarButton-firstPage.svg new file mode 100644 index 000000000..f5c917f12 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-firstPage.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-firstPage@2x.png b/static/pdf.js/images/secondaryToolbarButton-firstPage@2x.png deleted file mode 100644 index 3ad8af517..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-firstPage@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-handTool.png b/static/pdf.js/images/secondaryToolbarButton-handTool.png deleted file mode 100644 index cb85a841b..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-handTool.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-handTool.svg b/static/pdf.js/images/secondaryToolbarButton-handTool.svg new file mode 100644 index 000000000..b7073b598 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-handTool.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-handTool@2x.png b/static/pdf.js/images/secondaryToolbarButton-handTool@2x.png deleted file mode 100644 index 5c13f77ff..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-handTool@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-lastPage.png b/static/pdf.js/images/secondaryToolbarButton-lastPage.png deleted file mode 100644 index be763e0c4..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-lastPage.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-lastPage.svg b/static/pdf.js/images/secondaryToolbarButton-lastPage.svg new file mode 100644 index 000000000..c04f65079 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-lastPage.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-lastPage@2x.png b/static/pdf.js/images/secondaryToolbarButton-lastPage@2x.png deleted file mode 100644 index 8570984f2..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-lastPage@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCcw.png b/static/pdf.js/images/secondaryToolbarButton-rotateCcw.png deleted file mode 100644 index 675d6da2c..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-rotateCcw.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCcw.svg b/static/pdf.js/images/secondaryToolbarButton-rotateCcw.svg new file mode 100644 index 000000000..da73a1b16 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-rotateCcw.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCcw@2x.png b/static/pdf.js/images/secondaryToolbarButton-rotateCcw@2x.png deleted file mode 100644 index b9e743122..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-rotateCcw@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCw.png b/static/pdf.js/images/secondaryToolbarButton-rotateCw.png deleted file mode 100644 index e1c759888..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-rotateCw.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCw.svg b/static/pdf.js/images/secondaryToolbarButton-rotateCw.svg new file mode 100644 index 000000000..c41ce7365 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-rotateCw.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-rotateCw@2x.png b/static/pdf.js/images/secondaryToolbarButton-rotateCw@2x.png deleted file mode 100644 index cb257b41c..000000000 Binary files a/static/pdf.js/images/secondaryToolbarButton-rotateCw@2x.png and /dev/null differ diff --git a/static/pdf.js/images/secondaryToolbarButton-scrollHorizontal.svg b/static/pdf.js/images/secondaryToolbarButton-scrollHorizontal.svg new file mode 100644 index 000000000..fb440b946 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-scrollHorizontal.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-scrollPage.svg b/static/pdf.js/images/secondaryToolbarButton-scrollPage.svg new file mode 100644 index 000000000..64a9f5007 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-scrollPage.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-scrollVertical.svg b/static/pdf.js/images/secondaryToolbarButton-scrollVertical.svg new file mode 100644 index 000000000..dc7e80520 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-scrollVertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-scrollWrapped.svg b/static/pdf.js/images/secondaryToolbarButton-scrollWrapped.svg new file mode 100644 index 000000000..75fe26bcf --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-scrollWrapped.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-selectTool.svg b/static/pdf.js/images/secondaryToolbarButton-selectTool.svg new file mode 100644 index 000000000..94d51410d --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-selectTool.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-spreadEven.svg b/static/pdf.js/images/secondaryToolbarButton-spreadEven.svg new file mode 100644 index 000000000..ce201e33c --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-spreadEven.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-spreadNone.svg b/static/pdf.js/images/secondaryToolbarButton-spreadNone.svg new file mode 100644 index 000000000..e8d487fa3 --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-spreadNone.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/secondaryToolbarButton-spreadOdd.svg b/static/pdf.js/images/secondaryToolbarButton-spreadOdd.svg new file mode 100644 index 000000000..9211a427b --- /dev/null +++ b/static/pdf.js/images/secondaryToolbarButton-spreadOdd.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/shadow.png b/static/pdf.js/images/shadow.png deleted file mode 100644 index 31d3bdb14..000000000 Binary files a/static/pdf.js/images/shadow.png and /dev/null differ diff --git a/static/pdf.js/images/texture.png b/static/pdf.js/images/texture.png deleted file mode 100644 index eb5ccb5ec..000000000 Binary files a/static/pdf.js/images/texture.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-bookmark.png b/static/pdf.js/images/toolbarButton-bookmark.png deleted file mode 100644 index a187be6c9..000000000 Binary files a/static/pdf.js/images/toolbarButton-bookmark.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-bookmark.svg b/static/pdf.js/images/toolbarButton-bookmark.svg new file mode 100644 index 000000000..c4c37c905 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-bookmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-bookmark@2x.png b/static/pdf.js/images/toolbarButton-bookmark@2x.png deleted file mode 100644 index 4efbaa675..000000000 Binary files a/static/pdf.js/images/toolbarButton-bookmark@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-currentOutlineItem.svg b/static/pdf.js/images/toolbarButton-currentOutlineItem.svg new file mode 100644 index 000000000..01e67623b --- /dev/null +++ b/static/pdf.js/images/toolbarButton-currentOutlineItem.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-download.png b/static/pdf.js/images/toolbarButton-download.png deleted file mode 100644 index eaab35f09..000000000 Binary files a/static/pdf.js/images/toolbarButton-download.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-download.svg b/static/pdf.js/images/toolbarButton-download.svg new file mode 100644 index 000000000..e2e850adf --- /dev/null +++ b/static/pdf.js/images/toolbarButton-download.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/pdf.js/images/toolbarButton-download@2x.png b/static/pdf.js/images/toolbarButton-download@2x.png deleted file mode 100644 index 896face45..000000000 Binary files a/static/pdf.js/images/toolbarButton-download@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-editorFreeText.svg b/static/pdf.js/images/toolbarButton-editorFreeText.svg new file mode 100644 index 000000000..13a67bd9b --- /dev/null +++ b/static/pdf.js/images/toolbarButton-editorFreeText.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/pdf.js/images/toolbarButton-editorHighlight.svg b/static/pdf.js/images/toolbarButton-editorHighlight.svg new file mode 100644 index 000000000..b3cd7fda9 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-editorHighlight.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/static/pdf.js/images/toolbarButton-editorInk.svg b/static/pdf.js/images/toolbarButton-editorInk.svg new file mode 100644 index 000000000..b579eec7e --- /dev/null +++ b/static/pdf.js/images/toolbarButton-editorInk.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/pdf.js/images/toolbarButton-editorStamp.svg b/static/pdf.js/images/toolbarButton-editorStamp.svg new file mode 100644 index 000000000..a1fef4922 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-editorStamp.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/static/pdf.js/images/toolbarButton-fullscreen.png b/static/pdf.js/images/toolbarButton-fullscreen.png deleted file mode 100644 index fa7309550..000000000 Binary files a/static/pdf.js/images/toolbarButton-fullscreen.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-menuArrow.svg b/static/pdf.js/images/toolbarButton-menuArrow.svg new file mode 100644 index 000000000..82ffeaabb --- /dev/null +++ b/static/pdf.js/images/toolbarButton-menuArrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-menuArrows.png b/static/pdf.js/images/toolbarButton-menuArrows.png deleted file mode 100644 index 306eb43b8..000000000 Binary files a/static/pdf.js/images/toolbarButton-menuArrows.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-menuArrows@2x.png b/static/pdf.js/images/toolbarButton-menuArrows@2x.png deleted file mode 100644 index f7570bc0d..000000000 Binary files a/static/pdf.js/images/toolbarButton-menuArrows@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-openFile.png b/static/pdf.js/images/toolbarButton-openFile.png deleted file mode 100644 index b5cf1bd06..000000000 Binary files a/static/pdf.js/images/toolbarButton-openFile.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-openFile.svg b/static/pdf.js/images/toolbarButton-openFile.svg new file mode 100644 index 000000000..e773781d6 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-openFile.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-openFile@2x.png b/static/pdf.js/images/toolbarButton-openFile@2x.png deleted file mode 100644 index 91ab76593..000000000 Binary files a/static/pdf.js/images/toolbarButton-openFile@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageDown-rtl.png b/static/pdf.js/images/toolbarButton-pageDown-rtl.png deleted file mode 100644 index 1957f79ab..000000000 Binary files a/static/pdf.js/images/toolbarButton-pageDown-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageDown-rtl@2x.png b/static/pdf.js/images/toolbarButton-pageDown-rtl@2x.png deleted file mode 100644 index 16ebcb8ef..000000000 Binary files a/static/pdf.js/images/toolbarButton-pageDown-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageDown.png b/static/pdf.js/images/toolbarButton-pageDown.png deleted file mode 100644 index 8219ecf83..000000000 Binary files a/static/pdf.js/images/toolbarButton-pageDown.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageDown.svg b/static/pdf.js/images/toolbarButton-pageDown.svg new file mode 100644 index 000000000..1fc12e733 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-pageDown.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-pageDown@2x.png b/static/pdf.js/images/toolbarButton-pageDown@2x.png deleted file mode 100644 index 758c01d83..000000000 Binary files a/static/pdf.js/images/toolbarButton-pageDown@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageUp-rtl.png b/static/pdf.js/images/toolbarButton-pageUp-rtl.png deleted file mode 100644 index 98e7ce481..000000000 Binary files a/static/pdf.js/images/toolbarButton-pageUp-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageUp-rtl@2x.png b/static/pdf.js/images/toolbarButton-pageUp-rtl@2x.png deleted file mode 100644 index a01b02380..000000000 Binary files a/static/pdf.js/images/toolbarButton-pageUp-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageUp.png b/static/pdf.js/images/toolbarButton-pageUp.png deleted file mode 100644 index fb9daa337..000000000 Binary files a/static/pdf.js/images/toolbarButton-pageUp.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-pageUp.svg b/static/pdf.js/images/toolbarButton-pageUp.svg new file mode 100644 index 000000000..0936b9a57 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-pageUp.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-pageUp@2x.png b/static/pdf.js/images/toolbarButton-pageUp@2x.png deleted file mode 100644 index a5cfd755b..000000000 Binary files a/static/pdf.js/images/toolbarButton-pageUp@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-presentationMode.png b/static/pdf.js/images/toolbarButton-presentationMode.png deleted file mode 100644 index 3ac21244d..000000000 Binary files a/static/pdf.js/images/toolbarButton-presentationMode.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-presentationMode.svg b/static/pdf.js/images/toolbarButton-presentationMode.svg new file mode 100644 index 000000000..901d5672b --- /dev/null +++ b/static/pdf.js/images/toolbarButton-presentationMode.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-presentationMode@2x.png b/static/pdf.js/images/toolbarButton-presentationMode@2x.png deleted file mode 100644 index cada9e791..000000000 Binary files a/static/pdf.js/images/toolbarButton-presentationMode@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-print.png b/static/pdf.js/images/toolbarButton-print.png deleted file mode 100644 index 51275e54b..000000000 Binary files a/static/pdf.js/images/toolbarButton-print.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-print.svg b/static/pdf.js/images/toolbarButton-print.svg new file mode 100644 index 000000000..97a390474 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-print.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-print@2x.png b/static/pdf.js/images/toolbarButton-print@2x.png deleted file mode 100644 index 53d18daf7..000000000 Binary files a/static/pdf.js/images/toolbarButton-print@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-search.png b/static/pdf.js/images/toolbarButton-search.png deleted file mode 100644 index f9b75579b..000000000 Binary files a/static/pdf.js/images/toolbarButton-search.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-search.svg b/static/pdf.js/images/toolbarButton-search.svg new file mode 100644 index 000000000..0cc7ae21a --- /dev/null +++ b/static/pdf.js/images/toolbarButton-search.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-search@2x.png b/static/pdf.js/images/toolbarButton-search@2x.png deleted file mode 100644 index 456b13324..000000000 Binary files a/static/pdf.js/images/toolbarButton-search@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl.png b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl.png deleted file mode 100644 index 843709527..000000000 Binary files a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png deleted file mode 100644 index 9d9bfa4f6..000000000 Binary files a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.png b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.png deleted file mode 100644 index 1f90f83da..000000000 Binary files a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.svg b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.svg new file mode 100644 index 000000000..cace86372 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle@2x.png b/static/pdf.js/images/toolbarButton-secondaryToolbarToggle@2x.png deleted file mode 100644 index b066fe5cb..000000000 Binary files a/static/pdf.js/images/toolbarButton-secondaryToolbarToggle@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-sidebarToggle-rtl.png b/static/pdf.js/images/toolbarButton-sidebarToggle-rtl.png deleted file mode 100644 index 6f85ec061..000000000 Binary files a/static/pdf.js/images/toolbarButton-sidebarToggle-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-sidebarToggle-rtl@2x.png b/static/pdf.js/images/toolbarButton-sidebarToggle-rtl@2x.png deleted file mode 100644 index 291e00679..000000000 Binary files a/static/pdf.js/images/toolbarButton-sidebarToggle-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-sidebarToggle.png b/static/pdf.js/images/toolbarButton-sidebarToggle.png deleted file mode 100644 index 025dc9040..000000000 Binary files a/static/pdf.js/images/toolbarButton-sidebarToggle.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-sidebarToggle.svg b/static/pdf.js/images/toolbarButton-sidebarToggle.svg new file mode 100644 index 000000000..1d8d0e4b2 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-sidebarToggle.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-sidebarToggle@2x.png b/static/pdf.js/images/toolbarButton-sidebarToggle@2x.png deleted file mode 100644 index 7f834df94..000000000 Binary files a/static/pdf.js/images/toolbarButton-sidebarToggle@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewAttachments.png b/static/pdf.js/images/toolbarButton-viewAttachments.png deleted file mode 100644 index fcd0b268a..000000000 Binary files a/static/pdf.js/images/toolbarButton-viewAttachments.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewAttachments.svg b/static/pdf.js/images/toolbarButton-viewAttachments.svg new file mode 100644 index 000000000..ab73f6e6e --- /dev/null +++ b/static/pdf.js/images/toolbarButton-viewAttachments.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-viewAttachments@2x.png b/static/pdf.js/images/toolbarButton-viewAttachments@2x.png deleted file mode 100644 index b979e523e..000000000 Binary files a/static/pdf.js/images/toolbarButton-viewAttachments@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewLayers.svg b/static/pdf.js/images/toolbarButton-viewLayers.svg new file mode 100644 index 000000000..1d7266826 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-viewLayers.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-viewOutline-rtl.png b/static/pdf.js/images/toolbarButton-viewOutline-rtl.png deleted file mode 100644 index aaa943021..000000000 Binary files a/static/pdf.js/images/toolbarButton-viewOutline-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewOutline-rtl@2x.png b/static/pdf.js/images/toolbarButton-viewOutline-rtl@2x.png deleted file mode 100644 index 3410f70df..000000000 Binary files a/static/pdf.js/images/toolbarButton-viewOutline-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewOutline.png b/static/pdf.js/images/toolbarButton-viewOutline.png deleted file mode 100644 index 976365a50..000000000 Binary files a/static/pdf.js/images/toolbarButton-viewOutline.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewOutline.svg b/static/pdf.js/images/toolbarButton-viewOutline.svg new file mode 100644 index 000000000..7ed1bd97f --- /dev/null +++ b/static/pdf.js/images/toolbarButton-viewOutline.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-viewOutline@2x.png b/static/pdf.js/images/toolbarButton-viewOutline@2x.png deleted file mode 100644 index b6a197fdf..000000000 Binary files a/static/pdf.js/images/toolbarButton-viewOutline@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewThumbnail.png b/static/pdf.js/images/toolbarButton-viewThumbnail.png deleted file mode 100644 index 584ba5588..000000000 Binary files a/static/pdf.js/images/toolbarButton-viewThumbnail.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-viewThumbnail.svg b/static/pdf.js/images/toolbarButton-viewThumbnail.svg new file mode 100644 index 000000000..040d12326 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-viewThumbnail.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-viewThumbnail@2x.png b/static/pdf.js/images/toolbarButton-viewThumbnail@2x.png deleted file mode 100644 index fb7db9383..000000000 Binary files a/static/pdf.js/images/toolbarButton-viewThumbnail@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-zoomIn.png b/static/pdf.js/images/toolbarButton-zoomIn.png deleted file mode 100644 index 513d081bc..000000000 Binary files a/static/pdf.js/images/toolbarButton-zoomIn.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-zoomIn.svg b/static/pdf.js/images/toolbarButton-zoomIn.svg new file mode 100644 index 000000000..30ec51a2f --- /dev/null +++ b/static/pdf.js/images/toolbarButton-zoomIn.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-zoomIn@2x.png b/static/pdf.js/images/toolbarButton-zoomIn@2x.png deleted file mode 100644 index d5d49d5ff..000000000 Binary files a/static/pdf.js/images/toolbarButton-zoomIn@2x.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-zoomOut.png b/static/pdf.js/images/toolbarButton-zoomOut.png deleted file mode 100644 index 156c26b94..000000000 Binary files a/static/pdf.js/images/toolbarButton-zoomOut.png and /dev/null differ diff --git a/static/pdf.js/images/toolbarButton-zoomOut.svg b/static/pdf.js/images/toolbarButton-zoomOut.svg new file mode 100644 index 000000000..f273b5995 --- /dev/null +++ b/static/pdf.js/images/toolbarButton-zoomOut.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/pdf.js/images/toolbarButton-zoomOut@2x.png b/static/pdf.js/images/toolbarButton-zoomOut@2x.png deleted file mode 100644 index 959e1919d..000000000 Binary files a/static/pdf.js/images/toolbarButton-zoomOut@2x.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-collapsed-rtl.png b/static/pdf.js/images/treeitem-collapsed-rtl.png deleted file mode 100644 index 1c8b9f701..000000000 Binary files a/static/pdf.js/images/treeitem-collapsed-rtl.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-collapsed-rtl@2x.png b/static/pdf.js/images/treeitem-collapsed-rtl@2x.png deleted file mode 100644 index 84279368d..000000000 Binary files a/static/pdf.js/images/treeitem-collapsed-rtl@2x.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-collapsed.png b/static/pdf.js/images/treeitem-collapsed.png deleted file mode 100644 index 06d4d3769..000000000 Binary files a/static/pdf.js/images/treeitem-collapsed.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-collapsed.svg b/static/pdf.js/images/treeitem-collapsed.svg new file mode 100644 index 000000000..831cddfc8 --- /dev/null +++ b/static/pdf.js/images/treeitem-collapsed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/pdf.js/images/treeitem-collapsed@2x.png b/static/pdf.js/images/treeitem-collapsed@2x.png deleted file mode 100644 index eec1e58c1..000000000 Binary files a/static/pdf.js/images/treeitem-collapsed@2x.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-expanded.png b/static/pdf.js/images/treeitem-expanded.png deleted file mode 100644 index c8d557351..000000000 Binary files a/static/pdf.js/images/treeitem-expanded.png and /dev/null differ diff --git a/static/pdf.js/images/treeitem-expanded.svg b/static/pdf.js/images/treeitem-expanded.svg new file mode 100644 index 000000000..2d45f0c8d --- /dev/null +++ b/static/pdf.js/images/treeitem-expanded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/pdf.js/images/treeitem-expanded@2x.png b/static/pdf.js/images/treeitem-expanded@2x.png deleted file mode 100644 index 3b3b6103b..000000000 Binary files a/static/pdf.js/images/treeitem-expanded@2x.png and /dev/null differ diff --git a/static/pdf.js/index.html b/static/pdf.js/index.html index 5b1038315..aef63700e 100644 --- a/static/pdf.js/index.html +++ b/static/pdf.js/index.html @@ -20,50 +20,55 @@ Adobe CMap resources are covered by their own copyright but the same license: See https://github.com/adobe-type-tools/cmap-resources --> - + - - - - - - - - - - + PDF.js viewer - - - + + + + + + - - - - - + - +
-
- - - +
+
+ + + + +
+
+ +
+
+
+ + +
@@ -73,80 +78,195 @@ See https://github.com/adobe-type-tools/cmap-resources
+
+
- + +
+ + + + +
+
+ + + + +
+ +
+ + +
+
+ + + + + + + + +
@@ -155,81 +275,86 @@ See https://github.com/adobe-type-tools/cmap-resources
-
- -
-
-
- - + + +
- + + + +
+ +
+ + - - +
- - - Current View - - -
- -
-
-
-
- -
- -
- - - +
+
+ +
+
+ + +
@@ -241,187 +366,147 @@ See https://github.com/adobe-type-tools/cmap-resources
- - - - - - -
- -
-