[
  {
    "path": ".dockerignore",
    "content": "HELP.md\n.gradle\nbuild/\n!gradle/wrapper/gradle-wrapper.jar\n!**/src/main/**\n!**/src/test/**\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\nout/\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\n\n### VS Code ###\n.vscode/\n\n\n.vertx\nout\n\n#/src/main/resources/application-prod.yml\n/src/main/resources/application-default.yml\n\n/src/main/resources/web\n/storage*\n/logs\n/bin\n/file-uploads\nnode_modules\n/reader-assets"
  },
  {
    "path": ".gitattributes",
    "content": "*.java linguist-language=Kotlin"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report--问题反馈-.md",
    "content": "---\nname: Bug report (问题反馈)\nabout: 描述你在使用中遇到的问题（issue语言：1. 中文；2. 英文）\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**为避免无效问题和冗余问题，提问前请确认**\n1. 你确定Google不能解决你的问题\n2. 你确定已有的issue不能解决你的问题\n3. 你确定issue的title按照格式如下：[web/simple-web/server]：description\n\n**Describe the bug 描述你遇到的问题**\nA clear and concise description of what the bug is.  简洁有效的说明。\n\n**To Reproduce 如何重现问题**\nSteps to reproduce the behavior:  把你遇到的问题的发生步骤替换掉下面的内容：\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior  期待修复的效果**\nA clear and concise description of what you expected to happen. 简单描述。\n\n**Screenshots 如有必要，可以截图说明**\nIf applicable, add screenshots to help explain your problem.\n\n**版本说明**\n - OS: [e.g. win]  说明操作系统\n - Deploy Method 说明软件部署方式\n - Program Version 说明软件版本\n - Browser [e.g. chrome, safari]  说明终端、浏览器型号\n\n**Additional context 其他说明**\nAdd any other context about the problem here. 添加你认为有必要的内容，否则不写。\n"
  },
  {
    "path": ".github/workflows/Dockerfile",
    "content": "FROM openjdk:8-jdk-alpine\n# Install base packages\nRUN \\\n    # apk update; \\\n    # apk upgrade; \\\n    # Add CA certs tini tzdata\n    apk add --no-cache ca-certificates tini tzdata; \\\n    update-ca-certificates; \\\n    # Clean APK cache\n    rm -rf /var/cache/apk/*;\n\n# 时区\nENV TZ=Asia/Shanghai\n\nEXPOSE 8080\nENTRYPOINT [\"/sbin/tini\", \"--\"]\nADD ./reader.jar /app/bin/reader.jar\nCMD [\"java\", \"-jar\", \"/app/bin/reader.jar\" ]\n"
  },
  {
    "path": ".github/workflows/Openj9-Dockerfile",
    "content": "FROM ibm-semeru-runtimes:open-8u332-b09-jre\n# Install base packages\nRUN \\\n    apt-get update; \\\n    apt-get install -y ca-certificates tini tzdata; \\\n    update-ca-certificates; \\\n    # Clean apt cache\n    rm -rf /var/lib/apt/lists/*\n\n# 时区\nENV TZ=Asia/Shanghai\n\nEXPOSE 8080\nENTRYPOINT [\"/usr/bin/tini\", \"--\"]\nADD ./reader.jar /app/bin/reader.jar\nCMD [\"java\", \"-jar\", \"/app/bin/reader.jar\" ]\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build Docker Image\non:\n  push:\n    branches:\n      - master\njobs:\n  build:\n    if: github.repository == 'hectorqin/reader'\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@master\n    - name: Publish to Registry\n      uses: elgohr/Publish-Docker-Github-Action@master\n      with:\n        name: hectorqin/reader-basic\n        username: ${{ secrets.DOCKER_USERNAME }}\n        password: ${{ secrets.DOCKER_PASSWORD }}\n        snapshot: true\n        tags: \"test\"\n"
  },
  {
    "path": ".github/workflows/pull-request.yml",
    "content": "name: Pull Request Check\n\non:\n  pull_request:\n    types: [synchronize, reopened, labeled]\n    branches:\n      - master\n\nconcurrency:\n  group: ${{ github.head_ref }}\n  cancel-in-progress: true\n\ndefaults:\n  run:\n    shell: bash\n\njobs:\n  docker:\n    if: github.repository == 'hectorqin/reader'\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v2\n        with:\n          ref: ${{ github.event.pull_request.base.sha }}\n          clean: false\n      -\n        name: Setup node\n        uses: actions/setup-node@v2\n        with:\n          node-version: '14'\n      -\n        name: Build web\n        run: cd web && npm install && npm run build\n      -\n        name: Setup Java\n        uses: actions/setup-java@v2\n        with:\n          distribution: 'temurin'\n          java-version: '8'\n          cache: 'gradle'\n      -\n        name: Build Java\n        run:\n          mv ./web/dist ./src/main/resources/web && rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info && mv ./build/libs/*.jar ./reader.jar"
  },
  {
    "path": ".github/workflows/release-github.yml",
    "content": "name: Publish Github Releases\n\non:\n  # push:\n  #   tags:\n  #     - 'v**'\n  #   branches:\n  #     - master\n  workflow_dispatch:\n\njobs:\n  buildRelease:\n    if: github.repository == 'hectorqin/reader'\n    name: \"Build And Release\"\n    runs-on: macos-11\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v2\n      -\n        name: Setup node\n        uses: actions/setup-node@v2\n        with:\n          node-version: '14'\n      -\n        name: Build web\n        run: cd web && npm install && npm run build && mv ./dist ../src/main/resources/web\n      -\n        name: Setup Java\n        uses: actions/setup-java@v2\n        with:\n          distribution: 'temurin'\n          java-version: '11'\n          cache: 'gradle'\n      -\n        name: Build MacOS package\n        run:\n          JAVAFX_PLATFORM=mac ./gradlew packageReaderMac\n      -\n        name: Build Linux package\n        run:\n          JAVAFX_PLATFORM=linux ./gradlew packageReaderLinux\n      -\n        name: Build Windows package\n        run:\n          JAVAFX_PLATFORM=win ./gradlew packageReaderWin\n      -\n        name: Build server jar\n        run:\n          rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info\n      -\n        name: Show files.\n        run: |\n          echo Showing current directory:\n          ls\n          echo Showing ./target directory:\n          ls ./build\n          echo Showing ./target directory:\n          ls ./build/libs\n      -\n        name: Pre Release\n        if: ${{contains(github.ref, 'master')}}\n        uses: \"marvinpinto/action-automatic-releases@latest\"\n        with:\n          repo_token: \"${{ secrets.GITHUB_TOKEN }}\"\n          automatic_release_tag: \"latest\"\n          prerelease: true\n          title: \"Development Build\"\n          files: |\n            ./build/*.pkg\n            ./build/*.zip\n            ./build/libs/*.jar\n      -\n        name: Tagged Release\n        if: ${{contains(github.ref, 'v')}}\n        uses: \"marvinpinto/action-automatic-releases@latest\"\n        with:\n          repo_token: \"${{ secrets.GITHUB_TOKEN }}\"\n          prerelease: false\n          files: |\n            ./build/*.pkg\n            ./build/*.zip\n            ./build/libs/*.jar"
  },
  {
    "path": ".github/workflows/release-openj9.yml",
    "content": "name: Publish Docker Multi-Platform Images Using Openj9\n\non:\n  # push:\n  #   tags:\n  #     - 'v**'\n  workflow_dispatch:\n\njobs:\n  docker:\n    if: github.repository == 'hectorqin/reader'\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v2\n      -\n        name: Setup node\n        uses: actions/setup-node@v2\n        with:\n          node-version: '14'\n      -\n        name: Build web\n        run: cd web && npm install && npm run build\n      -\n        name: Setup Java\n        uses: actions/setup-java@v2\n        with:\n          distribution: 'adopt-openj9'\n          java-version: '8'\n          cache: 'gradle'\n      -\n        name: Build Java\n        run:\n          mv ./web/dist ./src/main/resources/web && rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info && mv ./build/libs/*.jar ./reader.jar\n      -\n        name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v3\n        with:\n          # list of Docker images to use as base name for tags\n          images: |\n            hectorqin/reader-basic\n          # generate Docker tags based on the following events/attributes\n          flavor: |\n            latest=false\n            prefix=openj9-,onlatest=true\n            suffix=\n          tags: |\n            type=semver,pattern={{version}}\n            type=raw,value=latest,enable=${{ !contains(github.ref, 'beta') }}\n      -\n        name: Set up QEMU\n        uses: docker/setup-qemu-action@v1\n      -\n        name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v1\n      -\n        name: Login to DockerHub\n        uses: docker/login-action@v1\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n      -\n        name: Build and push\n        uses: docker/build-push-action@v2\n        with:\n          context: .\n          file: ./.github/workflows/Openj9-Dockerfile\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: |\n            linux/amd64\n            linux/arm64/v8\n            linux/ppc64le\n            linux/s390x"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Publish Docker Multi-Platform Images\n\non:\n  # push:\n  #   tags:\n  #     - 'v**'\n  workflow_dispatch:\n\njobs:\n  docker:\n    if: github.repository == 'hectorqin/reader'\n    runs-on: ubuntu-latest\n    steps:\n      -\n        name: Checkout\n        uses: actions/checkout@v2\n      -\n        name: Setup node\n        uses: actions/setup-node@v2\n        with:\n          node-version: '14'\n      -\n        name: Build web\n        run: cd web && npm install && npm run build\n      -\n        name: Setup Java\n        uses: actions/setup-java@v2\n        with:\n          distribution: 'temurin'\n          java-version: '8'\n          cache: 'gradle'\n      -\n        name: Build Java\n        run:\n          mv ./web/dist ./src/main/resources/web && rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info && mv ./build/libs/*.jar ./reader.jar\n      -\n        name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v3\n        with:\n          # list of Docker images to use as base name for tags\n          images: |\n            hectorqin/reader-basic\n          # generate Docker tags based on the following events/attributes\n          tags: |\n            type=semver,pattern={{version}}\n            type=raw,value=latest,enable=${{ !contains(github.ref, 'beta') }}\n      -\n        name: Set up QEMU\n        uses: docker/setup-qemu-action@v1\n      -\n        name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v1\n      -\n        name: Login to DockerHub\n        uses: docker/login-action@v1\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n      -\n        name: Build and push\n        uses: docker/build-push-action@v2\n        with:\n          context: .\n          file: ./.github/workflows/Dockerfile\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: |\n            linux/amd64\n            linux/arm64\n            linux/arm/v6\n            linux/arm/v7\n            linux/386\n            linux/ppc64le\n            linux/s390x"
  },
  {
    "path": ".gitignore",
    "content": "HELP.md\n.gradle\nbuild/\n!gradle/wrapper/gradle-wrapper.jar\n!**/src/main/**\n!**/src/test/**\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\nout/\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\n\n### VS Code ###\n.vscode/\n\n.vertx\nout\n\n#/src/main/resources/application-prod.yml\n/src/main/resources/application-default.yml\n\n/src/main/resources/web\n/storage*\n/logs\n/bin\n/file-uploads\n/reader-assets\n/.lh/\n\n/simple-web/\n\n/server/logs\n/server/storage\n/server/target"
  },
  {
    "path": "Dockerfile",
    "content": "FROM hectorqin/reader\n\n# 时区\nENV TZ=Asia/Shanghai\n\nEXPOSE 8080\nENTRYPOINT [\"/sbin/tini\", \"--\"]\nCMD [\"java\", \"-jar\", \"/app/bin/reader.jar\" ]\n"
  },
  {
    "path": "Dockerfile.source",
    "content": "FROM node:lts-alpine3.14 AS build-web\nADD . /app\nWORKDIR /app/web\n# Build web\nRUN yarn && yarn build\n\n# Build jar\nFROM gradle:6.1.1-jdk8 AS build-env\nADD --chown=gradle:gradle . /app\nWORKDIR /app\nCOPY --from=build-web /app/web/dist /app/src/main/resources/web\nRUN \\\n    rm src/main/java/com/htmake/reader/ReaderUIApplication.kt; \\\n    gradle -b cli.gradle assemble --info; \\\n    mv ./build/libs/*.jar ./build/libs/reader.jar\n\nFROM amazoncorretto:8u332-alpine3.14-jre\n# Install base packages\nRUN \\\n    # apk update; \\\n    # apk upgrade; \\\n    # Add CA certs tini tzdata\n    apk add --no-cache ca-certificates tini tzdata; \\\n    update-ca-certificates; \\\n    # Clean APK cache\n    rm -rf /var/cache/apk/*;\n\n# 时区\nENV TZ=Asia/Shanghai\n\n#RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \\\n#  && echo Asia/Shanghai > /etc/timdezone \\\n#  && dpkg-reconfigure -f noninteractive tzdata\n\nEXPOSE 8080\nENTRYPOINT [\"/sbin/tini\", \"--\"]\n# COPY --from=hengyunabc/arthas:latest /opt/arthas /opt/arthas\nCOPY --from=build-env /app/build/libs/reader.jar /app/bin/reader.jar\nCMD [\"java\", \"-jar\", \"/app/bin/reader.jar\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    Reader Copyright (C) 2022 hectorqin\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "# reader\n\n阅读3服务器版，不需要手机。\n\n加入TG群(限时开放) 👉 [https://t.me/+pQ8HDlANPZ84ZWNl](https://t.me/+pQ8HDlANPZ84ZWNl)\n\n关注公众号，查看教程和书源👇\n\n![](imgs/mpcode.png)\n\n[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com \"Powered by DartNode - Free VPS for Open Source\")\n\n> 注意❗️\n>\n> Reader 完整源码仅开放到 v2.5.4，新版本当前仅开放部分开源源码，见 https://github.com/hectorqin/reader-legado.\n\n<details><summary>免责声明（Disclaimer）</summary>\n阅读是一款提供网络文学搜索的工具，为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。\n\n当您搜索一本书的时，阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。各第三方网站返回的内容与阅读无关，阅读对其概不负责，亦不承担任何法律责任。任何通过使用阅读而链接到的第三方网页均系他人制作或提供，您可能从第三方网页上获得其他服务，阅读对其合法性概不负责，亦不承担任何法律责任。第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读，不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。您应该对使用搜索引擎的结果自行承担风险。\n\n阅读不做任何形式的保证：不保证第三方搜索引擎的搜索结果满足您的要求，不保证搜索服务不中断，不保证搜索结果的安全性、正确性、及时性、合法性。因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读，阅读不承担任何法律责任。阅读尊重并保护所有使用阅读用户的个人隐私权，您注册的用户名、电子邮件地址等个人资料，非经您亲自许可或根据相关法律、法规的强制性规定，阅读不会主动地泄露给第三方。\n\n阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费，通过专业搜索展示不同网站中网络文学的最新章节。阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时，也使优秀网络文学得以迅速、更广泛的传播，从而达到了在一定程度促进网络文学充分繁荣发展之目的。阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商，并建议阅读正版图书。任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权，应该及时向阅读提出书面权力通知，并提供身份证明、权属证明及详细侵权情况证明。阅读在收到上述法律文件后，将会依法尽快断开相关链接内容。\n</details>\n\n<details><summary>功能说明</summary>\n  书源管理 <br/>\n- 书架管理 <br/>\n- 书架布局 <br/>\n- 搜索 <br/>\n- 书海 <br/>\n- 看书 <br/>\n- 移动端适配 <br/>\n- 换源 <br/>\n- 翻页方式 <br/>\n- 手势支持 <br/>\n- 自定义主题 <br/>\n- 自定义样式 <br/>\n- WebDAV同步 <br/>\n- 文字替换过滤 <br/>\n- 听书<仅部分浏览器支持，手机端会因为锁屏而失效> <br/>\n- 用户配置备份恢复 <br/>\n- 支持漫画 <br/>\n- 支持音频 <br/>\n- 书源失效检测 <br/>\n- 导入本地TXT、EPUB、UMD、PDF格式的书籍 <br/>\n- 书籍分组 <br/>\n- RSS订阅 <br/>\n- 定时更新书架 <br/>\n- 并发搜书 <br/>\n- 本地书仓 <br/>\n- 支持kindle阅读 <br/>\n</details>\n\n## 下载与安装\n\n详见[文档](https://github.com/hectorqin/reader/blob/master/doc.md)\n\n## 问题\n\n- 部分使用了 `Javascript` 的书源可能会报错，如调用原生java等高级Javascript功能\n- `webview` 功能需要另外部署接口，不支持 `sourceRegex` 匹配资源响应\n- 不支持书源登录功能\n\n## 感谢\n\n- 项目初期参考了 [lightink-小说API](https://github.com/lightink-qingmo/lightink-server)\n- [阅读](https://github.com/gedoor/MyBookshelf)\n- [阅读3.0](https://github.com/gedoor/legado)\n- 项目初期参考了 [阅读3.0Web端](https://github.com/celetor/web-yuedu3)\n\n## 其它\n\n- [帮助文档](https://github.com/hectorqin/reader/blob/master/doc.md)\n- [界面预览](https://github.com/hectorqin/reader/blob/master/preview.md)\n"
  },
  {
    "path": "UPDATELOG.md",
    "content": "# Update Log\n\n## v3.2.6\n\n### Features\n\n- 新增清除最近阅读功能\n- 新增编辑文章内容功能\n- 优化多个弹窗显示\n- 优化书源导入逻辑\n- 优化订阅同步逻辑\n\n## v3.2.5\n\n### Features\n\n- 新增simple-web书架排序\n- 优化文件读写锁\n- 开启连读优化后缓存后三段TTS\n\n### Bug Fixes\n\n- 修改simple-web书架搜索bug\n\n## v3.2.4\n\n### Features\n\n- 修改服务器版本脚本\n- 去掉gc，优化文件读写锁\n- 测试 iOS 朗读\n- 修改接口请求默认超时时间为30秒\n\n## v3.2.3\n\n### Features\n\n- 新增直接添加书签\n- 新增simple-web页面设置\n- 添加定时gc逻辑\n- 优化simple-web页面\n- 新增服务器端脚本\n\n### Bug Fixes\n\n- 修改后端链接状态bug\n\n## v3.2.2\n\n### Features\n\n- 优化自动阅读\n- 优化书海,书源管理样式\n- 优化simple-web兼容性\n- 新增PC端设置提示\n\n### Bug Fixes\n\n- 修复windows环境漫画路径问题\n- 修复往前翻页bug\n\n## v3.2.1\n\n### Features\n\n- 新增kindle7天试用申请\n- 优化epub阅读位置记忆跳转\n\n### Bug Fixes\n\n- 尝试修复PC端启动jvm参数bug\n\n## v3.2.0\n\n### Features\n\n- 新增视频源支持\n- epub支持朗读和自动阅读，优化阅读界面\n- 新增simple-web搜索及RSS页面，优化simple-web页面样式\n- 修改epub导入兼容性\n- 新增书源管理搜索功能\n- 修改桌面端jvm启动参数\n- 新增书源订阅管理\n- 优化探索样式\n- 优化书源调试页面\n\n### Bug Fixes\n\n- 修复epub书名包含+号时注入js失败问题\n- 修复simple-web分页组件\n- 修复remote-webview不能访问https问题\n- 修改封面上传后弹窗无法关闭的bug\n- 修改上下滚动模式目录跳转bug\n\n## v3.1.1\n\n### Features\n\n- 新增在线TTS朗读（Edge大声朗读）\n\n### Bug Fixes\n\n- 修复朗读bug\n\n## v3.1.0\n\n### Features\n\n- 新增下载数据备份功能,新增自动备份功能。通过 --reader.app.autoBackupUserData=true 启用，每天23:50开始会自动备份用户数据到webdav目录\n- 延长kindle试用期至 2023-06-30，过期以后需要购买授权来使用kindle页面\n- 新增书籍设置pdf图片宽度选项\n- 新增朗读时跳过全标点段落，跳过段末标点符号\n- 新增更新错误分组排除不追更书籍\n\n### Bug Fixes\n\n- 修复用户管理界面排序bug\n\n## v3.0.5\n\n### Features\n\n- 去掉书籍上限启动参数\n- 延长kindle试用期\n\n## v3.0.4\n\n### Features\n\n- 新增封面代理设置\n- 优化文件并发读写加锁逻辑\n- 优化多源搜索\n- 延长kindle试用期\n\n## v3.0.3\n\n### Features\n\n- 新增章节请求超时设置\n- 新增simple—web搜索书架功能\n- 桌面端新增jvm配置\n- 尝试修复音频音量bug,尝试优化pwa\n- 页面优化\n- 延长kindle试用期\n\n### Bug Fixes\n\n- 修复simple-web端bug\n\n## v3.0.2\n\n### Features\n\n- 支持pdf格式\n- 页面优化\n- 延长kindle试用期15天\n\n### Bug Fixes\n\n- 修复用户默认书源bug\n- simple-web兼容kindle\n\n## v3.0.1\n\n### Features\n\n- 新增书架布局设置，新增分列布局\n- 修改书籍分组的字段类型，支持更多分组\n- 新增用户更多设置项\n- 优化simple-web分页逻辑\n- 优化simple-web兼容性\n- 优化音频音量设置\n\n### Bug Fixes\n\n- 修复simple-web渲染器bug\n- 修复删除书籍未刷新书架bug\n\n## v3.0.0\n\n### Features\n\n- 新增epub iframe模式自定义字体支持\n- 新增simple-web端，支持kindle使用（限时免费）\n- 新增授权管理，多用户版用户上限降低至 15（已超出的无法再注册，但可以继续使用）\n- 新增用户管理、书籍管理、书签管理等分页排序过滤功能\n- 新增书源请求头设置\n- 新增书籍批量缓存操作\n- 新增contextPath设置项\n- 新增书架搜索作者及分类\n- 优化书籍信息页面\n- 优化阅读界面功能按钮\n\n### Bug Fixes\n\n- 修复音频播放bug\n- 修复调试书源跳转链接bug\n\n## v2.7.4\n\n### Features\n\n- 新增一键导入本地书籍功能\n- 添加新增替换规则入口,优化替换逻辑\n- 新增更新错误内置分组\n- 优化日志跟踪\n- 优化音频时长获取逻辑\n\n### Bug Fixes\n\n- 修复Windows环境webdav路径判断bug\n- 尝试修复音频时长获取bug\n- 修复书架刷新并发bug\n- 修复书籍封面接口bug\n\n## v2.7.3\n\n### Features\n\n- 新增书仓搜索及解析书籍功能\n- 修改语音库选择样式\n- 优化书源分组\n- 优化协程逻辑,拆分解析库和控制层库\n- 新增原文阅读模式,优化缓存判断逻辑\n\n### Bug Fixes\n\n- 尝试修复自动切换主题bug\n\n## v2.7.2\n\n### Features\n\n- 新增Epub解析模式， 支持简繁切换、左右翻页\n- 新增Epub iframe 模式左右翻页功能\n- 修改日志配置，仅保留7天\n- 新增书仓文件管理筛选功能\n- 修改简繁切换库，样式优化，感谢 [@terry3041](https://github.com/hectorqin/reader/pull/227)\n\n### Bug Fixes\n\n- 修复桌面端bug\n\n## v2.7.1\n\n### Bug Fixes\n\n- 修复书仓下载bug\n- 修复书籍追更选项设置bug\n\n## v2.7.0\n\n### Features\n\n- 新增远程webview镜像，支持使用 `hectorqin/remote-webview` 镜像作为远程 `webview`，使用 --reader.app.remoteWebviewApi=\"http://0.0.0.0:8050\" 启用。\n- 新增书源`cookie`,`cache`功能支持\n- 新增上下左右边距设置\n\n## v2.6.4\n\n### Features\n\n- 新增远程 `webview` 支持，目前仅支持 `scrapinghub/splash` 镜像作为远程 `webview`，使用 --reader.app.remoteWebviewApi=\"http://0.0.0.0:8050\" 启用。\n- 优化听书逻辑\n\n### Bug Fixes\n\n- 修复清理用户bug\n\n## v2.6.3\n\n### Features\n\n- 新增清理不活跃用户功能，使用 --reader.app.autoClearInactiveUser=31 (不活跃天数) 启用\n- 优化书架更新逻辑\n- 新增书架更新间隔设置选项，使用 --reader.app.shelfUpdateInteval=10 (更新间隔分钟，必须是10的倍数) 启用\n\n### Bug Fixes\n\n- 修复加入书架bug\n- 修改CI\n- 修复配置方案失效bug\n\n---\n## v2.6.2\n\n### Features\n\n- 添加用户并发修改锁\n\n### Bug Fixes\n\n- 修改CI\n\n---\n## v2.6.1\n\n### Bug Fixes\n\n- 修改CI\n\n---\n## v2.6.0\n\n### Bug Fixes\n\n- 修复本地书籍路径问题\n\n---\n## v2.5.8\n\n### Features\n\n- 测试CI\n\n---\n\n## v2.5.7\n\n### Features\n\n- 临时书籍使用临时缓存\n- 新增支持mongodb存放数据\n\n---\n\n## v2.5.6\n\n### Bug Fixes\n\n- 修复书架路径bug\n\n---\n\n## v2.5.5\n\n### Features\n\n- 统一文件管理\n- 优化书籍内容使用缓存图片\n\n### Bug Fixes\n\n- 修复书架绝对路径bug\n\n---\n\n## v2.5.4\n\n### Features\n\n- 新增分组排序功能\n- 新增朗读定时功能\n- 新增音频音量调整功能\n\n### Bug Fixes\n\n- 修复书架更新并发bug\n- 修复window环境问题\n- 修复书源调试bug\n- 修复音频bug\n\n---\n\n## v2.5.3\n\n### Features\n\n- 新增 webdav 书仓功能,新增修改目录规则功能,优化本地书籍换源功能\n\n---\n\n## v2.5.2\n\n### Bug Fixes\n\n- 修复jdk8编译依赖\n\n---\n\n## v2.5.1\n\n### Features\n\n- 新增自定义字体功能,优化阅读设置功能,新增书签同步功能\n\n---\n\n## v2.5.0\n\n### Features\n\n- 新增全文搜索功能,本地书籍生成封面优化,epub设置增强\n- 新增书签功能\n\n---\n\n## v2.4.1\n\n### Features\n\n- 完善注册登录,新增删除用户书源和恢复默认书源功能,测试CI\n- 新增本地书仓功能,新增自定义书籍封面功能,新增用户上限和用户书籍上限\n- 更新阅读内核\n- 新增清空书源功能,新增自动缓存下一章\n- 优化本地导入逻辑\n- 完成替换规则改版逻辑,新增配置方案设置功能,修复bug,优化页面\n- 新增上下滚动翻页模式,优化页面\n- 新增简繁转换功能,新增替换规则导入功能,新增设置默认书源功能,新增像素滚动自动翻页,兼容ie\n- 新增书源调试功能,优化书海功能,优化缓存功能\n- 新增支持CBZ书籍,新增支持卷名,优化调试功能\n- 新增书籍管理功能,新增缓存及导出功能,优化书海功能\n\n### Bug Fixes\n\n- 页面优化\n- bug修复\n\n---\n\n## v2.0.3\n\n### Features\n\n- 更新阅读解析库\n- 优化多源搜索和书源搜索功能\n- 新增服务器缓存章节内容功能,优化阅读宽度设置\n- 迁移缓存到indexdb\n- 新增简洁模式\n\n### Bug Fixes\n\n- 修复书源分组搜索选择bug\n- 页面优化\n- bug修复\n\n---\n\n## v1.9.4\n\n### Features\n\n- 新增缓存管理，优化缓存逻辑\n- 新增页面模式设置\n- 新增自定义主题模式设置\n- 新增远程书源导入功能\n- 新增读取epub封面，优化导入逻辑，优化书源错误标记\n- 新增书源导出功能\n- 新增按分组搜索书源功能\n- 新增刷新章节内容功能\n- 新增翻页动画时长设置\n\n### Bug Fixes\n\n- 修复iPad兼容问题\n- 修复精确搜索bug,优化json序列化\n- 页面优化\n- bug修复\n\n---\n\n## V1.8.0\n\n### Features\n\n- 新增点击翻页和选择文字过滤关闭选项\n- 支持设置代理（待测试）\n- 修复旧版本自动迁移bug\n- 修复搜索bug\n- 完善失败源标记和恢复逻辑\n- 优化ios pwa样式\n- 优化书籍标签显示\n\n### Bug Fixes\n\n- 页面优化\n- bug修复\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "import org.openjfx.gradle.*\nimport org.jetbrains.kotlin.gradle.tasks.KotlinCompile\nimport java.lang.reflect.*\nimport io.github.fvarrui.javapackager.model.Platform\nimport io.github.fvarrui.javapackager.model.WindowsConfig\nimport de.undercouch.gradle.tasks.download.Download\n\nbuildscript {\n    val kotlin_version: String by extra{\"1.5.21\"}\n    // extra[\"kotlin_version\"] = \"1.5.21\"\n    repositories {\n\t    mavenLocal()\n        mavenCentral()\n    }\n    dependencies {\n        classpath(\"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\")\n        classpath(\"io.github.fvarrui:javapackager:1.6.5\")\n    }\n}\nplugins {\n    id(\"org.springframework.boot\") version \"2.1.6.RELEASE\"\n    id(\"java\")\n    id(\"application\")\n    id(\"org.openjfx.javafxplugin\") version \"0.0.9\"\n    id(\"org.jetbrains.kotlin.plugin.spring\") version \"1.3.61\"\n}\n\nconfigure<JavaFXOptions> {\n    version = \"11.0.2\"\n    modules = listOf(\"javafx.web\")\n\n    // Set JAVAFX_PLATFORM to \"linux\", \"win\", or \"mac\"\n    val javafxPlatformOverride = System.getenv(\"JAVAFX_PLATFORM\")\n    if (javafxPlatformOverride != null) {\n        val javafxPlatform: JavaFXPlatform = JavaFXPlatform.values()\n            .firstOrNull { it.classifier == javafxPlatformOverride }\n            ?: throw IllegalArgumentException(\"JAVAFX_PLATFORM $javafxPlatformOverride not in list:\" +\n                    \" ${JavaFXPlatform.values().map { it.classifier }}\")\n\n        logger.info(\"Overriding JavaFX platform to {}\", javafxPlatform)\n\n        // Override the private platform field\n        val platformField: Field = JavaFXOptions::class.java.getDeclaredField(\"platform\")\n        platformField.isAccessible = true\n        platformField.set(this, javafxPlatform)\n\n        // Invoke the private updateJavaFXDependencies() method\n        val updateDeps: Method = JavaFXOptions::class.java.getDeclaredMethod(\"updateJavaFXDependencies\")\n        updateDeps.isAccessible = true\n        updateDeps.invoke(this)\n    }\n}\n\napply(plugin = \"io.spring.dependency-management\")\napply(plugin = \"kotlin\")\napply(plugin = \"io.github.fvarrui.javapackager.plugin\")\n\ngroup = \"com.htmake\"\nversion = \"2.5.4\"\n\njava {\n    sourceCompatibility = JavaVersion.VERSION_1_8\n    targetCompatibility = JavaVersion.VERSION_1_8\n}\n\nrepositories {\n    mavenCentral()\n    maven(\"https://jitpack.io\")\n    maven(\"https://gitlab.com/api/v4/projects/26729549/packages/maven\")\n    google()\n    jcenter()\n}\n\nval compileOnly by configurations.getting {\n    extendsFrom(configurations[\"annotationProcessor\"])\n}\n\ndependencies {\n    val kotlin_version: String by extra{\"1.5.21\"}\n    // val kotlin_version: String by extra\n    implementation(\"org.springframework.boot:spring-boot-starter\")\n    testImplementation(\"org.springframework.boot:spring-boot-starter-test\")\n    implementation(\"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version\")\n    // vertx\n    implementation(\"io.vertx:vertx-core:3.8.1\")\n    implementation(\"io.vertx:vertx-lang-kotlin:3.8.1\")\n    implementation(\"io.vertx:vertx-lang-kotlin-coroutines:3.8.1\")\n    implementation(\"io.vertx:vertx-web:3.8.1\")\n    implementation(\"io.vertx:vertx-web-client:3.8.1\")\n\n    // json\n    implementation(\"com.google.code.gson:gson:2.8.5\")\n    implementation(\"com.fasterxml.jackson.module:jackson-module-kotlin:2.13.+\")\n\n    // log\n    implementation(\"io.github.microutils:kotlin-logging:1.6.24\")\n    implementation(\"uk.org.lidalia:sysout-over-slf4j:1.0.2\")\n\n    implementation(\"com.google.guava:guava:28.0-jre\")\n\n    // 网络\n    implementation(\"com.squareup.okhttp3:okhttp:4.9.1\")\n    implementation(\"com.squareup.okhttp3:logging-interceptor:4.1.0\")\n    // Retrofit\n    implementation(\"com.squareup.retrofit2:retrofit:2.6.1\")\n    implementation(\"com.julienviet:retrofit-vertx:1.1.3\")\n\n    //JS rhino\n    // implementation(\"com.github.gedoor:rhino-android:1.6\")\n    implementation(fileTree(\"src/lib\").include(\"rhino-*.jar\"))\n\n    // 规则相关\n    implementation(\"org.jsoup:jsoup:1.14.1\")\n    implementation(\"cn.wanghaomiao:JsoupXpath:2.5.0\")\n    implementation(\"com.jayway.jsonpath:json-path:2.6.0\")\n\n    // xml\n    // 弃用 xmlpull-1.1.4.0，因为它需要 Java9\n    // implementation(\"org.xmlpull:xmlpull:1.1.4.0\")\n    implementation(fileTree(\"src/lib\").include(\"xmlpull-*.jar\"))\n    // implementation(\"com.github.stefanhaustein:kxml2:2.5.0\")\n\n    //加解密类库\n    implementation(\"cn.hutool:hutool-crypto:5.8.0.M1\")\n\n    // 转换繁体\n    // implementation(\"com.github.liuyueyi.quick-chinese-transfer:quick-transfer-core:0.2.1\")\n}\n\n// val compileKotlin: KotlinCompile by tasks\n// val compileTestKotlin: KotlinCompile by tasks\n\n// compileKotlin.kotlinOptions {\n//     jvmTarget = \"1.8\"\n// }\n// compileTestKotlin.kotlinOptions {\n//     jvmTarget = \"1.8\"\n// }\n\ntasks.withType<KotlinCompile> {\n    kotlinOptions.jvmTarget = \"1.8\"\n}\n\napplication {\n    // Define the main class for the application\n    mainClassName = \"com.htmake.reader.ReaderUIApplicationKt\"\n}\n\ntasks.create<io.github.fvarrui.javapackager.gradle.PackageTask>(\"buildReader\"){\n    dependsOn(\"build\")\n\t// mandatory\n\tmainClass = \"com.htmake.reader.ReaderUIApplicationKt\"\n\t// optional\n    setBundleJre(false)\n    vmArgs = arrayListOf<String>(\"-Dreader.app.showUI=true\", \"-Dspring.profiles.active=prod\", \"-Dreader.app.packaged=true\", \"-Dreader.app.debug=true\")\n}\n\ntasks.create<io.github.fvarrui.javapackager.gradle.PackageTask>(\"packageReaderMac\") {\n    dependsOn(\"build\")\n\t// mandatory\n\tmainClass = \"com.htmake.reader.ReaderUIApplicationKt\"\n\t// optional\n    setBundleJre(false)\n\t// bundleJre = false\n    // setCreateZipball(true)\n    platform = Platform.mac\n    vmArgs = arrayListOf<String>(\"-Dreader.app.showUI=true\", \"-Dspring.profiles.active=prod\", \"-Dreader.app.packaged=true\", \"-Dreader.app.debug=false\", \"-Dlogging.path=\\$HOME/.reader/logs\")\n}\n\ntasks.create<io.github.fvarrui.javapackager.gradle.PackageTask>(\"packageReaderWin\") {\n    dependsOn(\"build\")\n\t// mandatory\n\tmainClass = \"com.htmake.reader.ReaderUIApplicationKt\"\n\t// optional\n    setBundleJre(false)\n\t// bundleJre = true\n    // jrePath = File(buildDir, \"win64-jre\")\n    setCreateZipball(true)\n    platform = Platform.windows\n    vmArgs = arrayListOf<String>(\"-Dreader.app.showUI=true\", \"-Dspring.profiles.active=prod\", \"-Dreader.app.debug=false\")\n    withGroovyBuilder {\n        \"winConfig\" {\n            \"setWrapJar\"(false)\n        }\n    }\n    // winConfig {\n    //     wrapJar = false\n    // }\n}\n\ntasks.create<io.github.fvarrui.javapackager.gradle.PackageTask>(\"packageReaderLinux\") {\n    dependsOn(\"build\")\n\t// mandatory\n\tmainClass = \"com.htmake.reader.ReaderUIApplicationKt\"\n\t// optional\n    setBundleJre(false)\n\t// bundleJre = false\n    setCreateZipball(true)\n    platform = Platform.linux\n    vmArgs = arrayListOf<String>(\"-Dreader.app.showUI=true\", \"-Dspring.profiles.active=prod\", \"-Dreader.app.debug=false\")\n    withGroovyBuilder {\n        \"linuxConfig\" {\n            \"setWrapJar\"(false)\n        }\n    }\n}\n\ntasks {\n    val downloadWinJre by registering(Download::class) {\n        src(\"https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.8%2B10/OpenJDK11U-jre_x64_windows_hotspot_11.0.8_10.zip\")\n        dest(File(buildDir, \"win64-jre.zip\"))\n        onlyIfModified(true)\n    }\n}\n\ntasks.register<Copy>(\"unpackWinJre\") {\n    dependsOn(\"downloadWinJre\")\n    from(zipTree(\"$buildDir/win64-jre.zip\")) {\n        include(\"jdk*/**\")\n        eachFile {\n            relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray())\n        }\n        includeEmptyDirs = false\n    }\n    into(File(buildDir, \"win64-jre\"))\n}\n\n// javafx {\n//     version = \"11.0.2\"\n//     modules = [ 'javafx.web' ]\n// }"
  },
  {
    "path": "build.sh",
    "content": "#!/bin/bash\n\noldJAVAHome=$JAVA_HOME\n\ntask=$1\n\nversion=\"\"\n\ncheckJava()\n{\n    if [ -d /Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home ]; then\n        export JAVA_HOME=/Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home\n    fi\n\n    javaVersion=$(java -version 2>&1 | sed -n ';s/.* version \"\\(.*\\)\\.\\(.*\\)\\..*\".*/\\1\\2/p;')\n\n    if [[ \"$javaVersion\" -lt \"110\" ]]; then\n        echo \"Java version must not lower than 11.0\"\n        exit 1\n    fi\n}\n\ngetVersion()\n{\n    version=$(grep -Eo \"^version = .*\" $1 | grep -Eo \"['\\\"].*['\\\"]\" | tr -d \"'\\\"\")\n}\n\ngetVersion ./build.gradle.kts\n\ncase $task in\n    build)\n        checkJava\n        # 调试打包\n        ./gradlew buildReader\n    ;;\n    run)\n        checkJava\n        # 运行 javafx UI\n        port=$2\n        if [[ -z \"$port\" ]]; then\n            port=8080\n        fi\n        ./gradlew assemble --info\n        if test $? -eq 0; then\n            shift\n            shift\n            java -jar build/libs/reader-$version.jar --reader.app.showUI=true --reader.server.port=$port $@\n        fi\n    ;;\n    win)\n        checkJava\n        # 打包 windows 安装包\n        JAVAFX_PLATFORM=win ./gradlew packageReaderWin\n    ;;\n    linux)\n        checkJava\n        # 打包 linux 安装包\n        JAVAFX_PLATFORM=linux ./gradlew packageReaderLinux\n    ;;\n    mac)\n        checkJava\n        # 打包 mac 安装包\n        JAVAFX_PLATFORM=mac ./gradlew packageReaderMac\n    ;;\n    serve)\n        # 服务端一键运行\n        port=$2\n        if [[ -z \"$port\" ]]; then\n            port=8080\n        fi\n        mv src/main/java/com/htmake/reader/ReaderUIApplication.kt src/main/java/com/htmake/reader/ReaderUIApplication.kt.back\n        getVersion ./cli.gradle\n        ./gradlew -b cli.gradle assemble --info\n        if test $? -eq 0; then\n            shift\n            shift\n            mv src/main/java/com/htmake/reader/ReaderUIApplication.kt.back src/main/java/com/htmake/reader/ReaderUIApplication.kt\n            java -jar build/libs/reader-$version.jar --reader.server.port=$port $@\n        else\n            mv src/main/java/com/htmake/reader/ReaderUIApplication.kt.back src/main/java/com/htmake/reader/ReaderUIApplication.kt\n        fi\n    ;;\n    cli)\n        # 服务端打包命令\n        shift\n        export JAVA_HOME=$oldJAVAHome\n        mv src/main/java/com/htmake/reader/ReaderUIApplication.kt src/main/java/com/htmake/reader/ReaderUIApplication.kt.back\n        getVersion ./cli.gradle\n        ./gradlew -b cli.gradle $@\n        mv src/main/java/com/htmake/reader/ReaderUIApplication.kt.back src/main/java/com/htmake/reader/ReaderUIApplication.kt\n    ;;\n    yarn)\n        # yarn 快捷命令，默认 install\n        shift\n        cd web\n        yarn $@\n    ;;\n    web)\n        # 开发web页面\n        cd web\n        yarn serve\n    ;;\n    sync)\n        # 编译同步web资源\n        cd web\n        yarn sync\n    ;;\n    *)\n        echo \"\nUSAGE: ./build.sh build|run|win|linux|mac|serve|cli|yarn|web|sync\n\nbuild   调试打包\nrun     桌面端编译运行，需要先执行 sync 命令编译同步web资源\nwin     打包 windows 安装包\nlinux   打包 linux 安装包\nmac     打包 mac 安装包\nserve   服务端编译运行\ncli     服务端打包命令\nyarn    web页面 yarn 快捷命令，默认 install\nweb     开发web页面\nsync    编译同步web资源\n\"\n    ;;\nesac\n\nexport JAVA_HOME=$oldJAVAHome\n"
  },
  {
    "path": "cli.gradle",
    "content": "buildscript {\n    ext.kotlin_version = '1.5.21'\n    repositories {\n        mavenCentral()\n    }\n    dependencies {\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n    }\n}\nplugins {\n    id 'org.springframework.boot' version '2.1.6.RELEASE'\n    id 'java'\n    id \"org.jetbrains.kotlin.plugin.spring\" version \"1.3.61\"\n}\n\napply plugin: 'io.spring.dependency-management'\napply plugin: 'kotlin'\n\ngroup = 'com.htmake'\nversion = '2.5.4'\nsourceCompatibility = '1.8'\n\nrepositories {\n    mavenCentral()\n    maven { url \"https://jitpack.io\" }\n    maven { url \"https://gitlab.com/api/v4/projects/26729549/packages/maven\" }\n    google()\n    jcenter()\n}\n\nconfigurations {\n    compileOnly {\n        extendsFrom annotationProcessor\n    }\n}\n\ndependencies {\n    implementation 'org.springframework.boot:spring-boot-starter'\n    testImplementation 'org.springframework.boot:spring-boot-starter-test'\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version\"\n    // vertx\n    implementation \"io.vertx:vertx-core:3.8.1\"\n    implementation \"io.vertx:vertx-lang-kotlin:3.8.1\"\n    implementation \"io.vertx:vertx-lang-kotlin-coroutines:3.8.1\"\n    implementation 'io.vertx:vertx-web:3.8.1'\n    implementation 'io.vertx:vertx-web-client:3.8.1'\n\n    // json\n    implementation \"com.google.code.gson:gson:2.8.5\"\n    implementation \"com.fasterxml.jackson.module:jackson-module-kotlin:2.13.+\"\n\n    // log\n    implementation \"io.github.microutils:kotlin-logging:1.6.24\"\n    implementation \"uk.org.lidalia:sysout-over-slf4j:1.0.2\"\n\n    implementation \"com.google.guava:guava:28.0-jre\"\n\n    // 网络\n    implementation \"com.squareup.okhttp3:okhttp:4.9.1\"\n    implementation \"com.squareup.okhttp3:logging-interceptor:4.1.0\"\n    // Retrofit\n    implementation \"com.squareup.retrofit2:retrofit:2.6.1\"\n    implementation \"com.julienviet:retrofit-vertx:1.1.3\"\n\n    //JS rhino\n    // implementation \"com.github.gedoor:rhino-android:1.6\"\n    implementation(fileTree(dir: 'src/lib', include: ['rhino-*.jar']))\n\n    // 规则相关\n    implementation \"org.jsoup:jsoup:1.14.1\"\n    implementation \"cn.wanghaomiao:JsoupXpath:2.5.0\"\n    implementation \"com.jayway.jsonpath:json-path:2.6.0\"\n\n    // xml\n    // 弃用 xmlpull-1.1.4.0，因为它需要 Java9\n    // implementation \"org.xmlpull:xmlpull:1.1.4.0\"\n    implementation(fileTree(dir: 'src/lib', include: ['xmlpull-*.jar']))\n    // implementation \"com.github.stefanhaustein:kxml2:2.4.2\"\n\n    //加解密类库\n    implementation \"cn.hutool:hutool-crypto:5.8.0.M1\"\n\n    // 转换繁体\n    // implementation \"com.github.liuyueyi.quick-chinese-transfer:quick-transfer-core:0.2.1\"\n}\ncompileKotlin {\n    kotlinOptions {\n        jvmTarget = \"1.8\"\n    }\n}\ncompileTestKotlin {\n    kotlinOptions {\n        jvmTarget = \"1.8\"\n    }\n}\n"
  },
  {
    "path": "doc.md",
    "content": "# 文档\n\n- [文档](#文档)\n  - [免责声明（Disclaimer）](#免责声明disclaimer)\n  - [数据存储](#数据存储)\n    - [本地书仓](#本地书仓)\n  - [阅读页面地址](#阅读页面地址)\n    - [全功能web端](#全功能web端)\n    - [适配kindle的 `simple-web`](#适配kindle的-simple-web)\n  - [自定义阅读主题](#自定义阅读主题)\n  - [自定义样式](#自定义样式)\n  - [接口服务配置](#接口服务配置)\n  - [WebDAV同步配置](#webdav同步配置)\n  - [客户端](#客户端)\n    - [Windows / MacOS / Linux](#windows--macos--linux)\n    - [手机端](#手机端)\n    - [服务器版](#服务器版)\n    - [Docker版](#docker版)\n    - [Docker-Compose版(推荐)](#docker-compose版推荐)\n    - [通过脚本一键部署](#通过脚本一键部署)\n    - [Arch Linux 安装](#Arch-Linux-安装)\n      - [配置文件](#配置文件)\n  - [Nginx反向代理(如果有域名可以考虑80端口复用)](#nginx反向代理如果有域名可以考虑80端口复用)\n  - [开发编译](#开发编译)\n    - [编译脚本](#编译脚本)\n    - [编译前端](#编译前端)\n    - [编译接口](#编译接口)\n  - [接口文档](#接口文档)\n    - [新增接口](#新增接口)\n      - [加入书架](#加入书架)\n      - [获取书籍书源](#获取书籍书源)\n      - [搜索书籍更多书源](#搜索书籍更多书源)\n      - [书籍换源](#书籍换源)\n\n## 免责声明（Disclaimer）\n\n阅读是一款提供网络文学搜索的工具，为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。\n\n当您搜索一本书的时，阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。各第三方网站返回的内容与阅读无关，阅读对其概不负责，亦不承担任何法律责任。任何通过使用阅读而链接到的第三方网页均系他人制作或提供，您可能从第三方网页上获得其他服务，阅读对其合法性概不负责，亦不承担任何法律责任。第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读，不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。您应该对使用搜索引擎的结果自行承担风险。\n\n阅读不做任何形式的保证：不保证第三方搜索引擎的搜索结果满足您的要求，不保证搜索服务不中断，不保证搜索结果的安全性、正确性、及时性、合法性。因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读，阅读不承担任何法律责任。阅读尊重并保护所有使用阅读用户的个人隐私权，您注册的用户名、电子邮件地址等个人资料，非经您亲自许可或根据相关法律、法规的强制性规定，阅读不会主动地泄露给第三方。\n\n阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费，通过专业搜索展示不同网站中网络文学的最新章节。阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时，也使优秀网络文学得以迅速、更广泛的传播，从而达到了在一定程度促进网络文学充分繁荣发展之目的。阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商，并建议阅读正版图书。任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权，应该及时向阅读提出书面权力通知，并提供身份证明、权属证明及详细侵权情况证明。阅读在收到上述法律文件后，将会依法尽快断开相关链接内容。\n\n## 数据存储\n\n接口服务使用文件存储书源及目录等信息，存储位置为 storage 目录(可通过运行时添加 `-Dreader.app.storagePath=/path/to/storage` 修改)。\n\n> MacOS客户端的存储目录是 `~/.reader/storage`，Window和Linux客户端为 `运行目录/storage`\n\n数据存储目录结构如下：\n\n```bash\nstorage\n├── assets                                        # 静态资源\n│   ├── hector                                    # 用户 hector 的资源目录\n│   |   |── covers                                # 本地 epub 书籍的封面图片目录\n│   │   ├── background                            # 自定义阅读背景图片保存目录\n│   │   │   └── 6.jpg\n│   └── reader.css                                # 自定义CSS样式文件\n├── cache                                         # 缓存目录\n│   ├── 6190ac40068e74c2c82624e91a5f8a0c.jpg      # 书籍封面缓存\n│   ├── bookInfoCache                             # 书籍搜索缓存 ACache 目录\n│   └── ea11967236129bdae6133c3c9ff8c2dd.jpg\n├── data                                          # 数据目录\n│   ├── default                                   # 系统默认用户的数据目录 (reader.app.secure为false时)\n│   │   ├── bookSource.json                       # 书源列表\n│   │   ├── bookshelf.json                        # 书架书籍列表\n│   │   ├── 斗罗大陆_唐家三少                        # 书籍缓存目录\n│   │   │   ├── 5d01bc88d6b19ebbe974acaac1675811         # A书源章节缓存目录\n│   │   │   ├── 5d01bc88d6b19ebbe974acaac1675811.json    # A书源目录列表\n│   │   │   ├── 7e5ca1cc2a1ea2e09fdec4ee2e150f02         # B书源章节缓存目录\n│   │   │   ├── 7e5ca1cc2a1ea2e09fdec4ee2e150f02.json    # B书源目录列表\n│   │   │   └── bookSource.json                          # 书籍书源列表\n│   ├── hector                                    # 用户 hector 的数据目录 (reader.app.secure为true时的用户目录)\n│   │   ├── bookSource.json                       # 书源列表\n│   │   ├── bookshelf.json                        # 书架书籍列表\n│   │   ├── webdav                                # webdav 存储目录 可能会存在 legado 子目录\n│   │   │   ├── backup2021-09-15.zip              # 阅读3备份文件\n│   │   │   └── bookProgress                      # 阅读3书籍进度备份目录\n│   │   │       └── 斗罗大陆_唐家三少.json           # 阅读3书籍进度\n│   │   └── 斗罗大陆_唐家三少                        # 书籍缓存目录\n│   │       |── 2d44d0ec2397b6c1d4010b97d914031e       # A书源章节缓存目录\n│   │       └── 2d44d0ec2397b6c1d4010b97d914031e.json  # A书源目录列表\n│   └── users.json                                # 用户列表\n├── localStore                                    # 本地书仓，所有用户共享(用户需要开启书仓权限，才能访问)\n│   |── 斗破苍穹.txt                               # 本地书仓书籍\n│   └── 斗罗大陆.txt                               # 本地书仓书籍\n└── windowConfig.json                             # 窗口配置文件\n```\n\n### 本地书仓\n\n在 `storage/localStore` 中可以集中存放管理本地书籍，开启访问权限的用户可以在 `页面-浏览书仓` 中选择批量导入到自己的书架进行阅读。\n\n## 阅读页面地址\n\n### 全功能web端\n\n`http://ip:端口/`\n\n### 适配kindle的 `simple-web`\n\n`http://ip:端口/simple-web`\n\n> 注意，加入TG群了解详情\n\n## 自定义阅读主题\n\n书架页面仅支持白天模式和黑夜模式。\n\n阅读页面支持设置多款主题，还可以自定义主题。自定义阅读主题包括:\n\n- 自定义页面背景颜色\n- 自定义浮窗背景颜色\n- 自定义阅读背景颜色\n- 自定义阅读背景图片\n\n## 自定义样式\n\n页面还会加载应用目录下的 `reader-assets/reader.css` 这个CSS样式文件，在这个文件中可以自定义页面样式。\n\n> 自定义样式可能需要配合 `!important` 来设定属性\n\n## 接口服务配置\n\n```yml\nreader:\n  app:\n    workDir: \"\"            # 工作目录\n    secure: false          # 是否需要登录鉴权，开启后将支持多用户模式\n    inviteCode: \"\"         # 注册邀请码，为空时则开放注册，否则注册时需要输入邀请码\n    secureKey: \"\"          # 管理密码，开启鉴权时，前端管理用户空间的管理密码\n    cacheChapterContent: false # 是否缓存章节内容\n    debugLog: false        # 是否打开调试日志\n    autoClearInactiveUser: 0 # 是否自动清理不活跃用户，为0不清理，大于0为清理超过 autoClearInactiveUser 天未登录的用户\n    mongoUri: \"\"           # mongodb uri 用于备份数据\n    mongoDbName: \"reader\"  # mongodb 数据库名称\n    shelfUpdateInteval: 10 # 书架自动更新间隔时间，单位分钟，必须是10的倍数\n    userLimit: 15          # 用户上限，最大 15\n    remoteWebviewApi: \"\"   # remote-webview 地址\n    defaultUserEnableWebdav: true  # 新用户是否默认启用webdav\n    defaultUserEnableLocalStore: true # 新用户是否默认启用localStore\n    defaultUserEnableBookSource: true # 新用户是否默认可编辑书源，如果为false，则只能使用默认书源，不能新增/修改/删除\n    defaultUserEnableRssSource: true # 新用户是否默认可编辑RSS源\n    defaultUserBookSourceLimit: 100  # 新用户默认书源上限\n    defaultUserBookLimit: 200 # 新用户默认书籍上限\n    autoBackupUserData: false # 是否自动备份用户数据\n    minUserPasswordLength: 8  # 用户密码最小长度\n    remoteBookSourceUpdateInterval: 720   # 远程书源定时更新间隔时间，单位分钟，必须是10的倍数\n\n  server:\n    port: 8080             # 监听端口\n    contextPath: \"\"        # 二级目录，为空则不使用二级目录\n    webUrl: http://localhost:${reader.server.port}    # web链接\n\n```\n\n## WebDAV同步配置\n\n1. 首先需要在阅读App里面配置 `WebDAV备份`\n\n    服务器地址： `http://IP:端口/reader3/webdav/`\n\n    如果开启了 `reader.app.secure` 选项，那么使用网页注册的用户名和密码登录，否则使用用户名 `default` 和 密码 `123456` 登录\n\n2. 然后在阅读App里面点击备份\n\n3. 在网页里面查看WebDAV文件，确认是否备份成功\n\n4. 备份成功之后\n    - 服务器会自动同步书籍阅读进度(暂不支持章节内阅读位置，也不会自动同步书架信息变更)\n\n    - 可以直接选择阅读App的备份文件进行恢复，这样会直接覆盖书源和书架信息\n\n    - 可以备份当前书源和书架信息到WebDAV，但是必须要先备份成功\n\n    - 需要通过恢复备份文件来同步书籍和书源信息\n\n5. PS: 本地书源的书籍同步后无法打开，除非换源\n\n## 客户端\n\n### Windows / MacOS / Linux\n\n从 [releases](https://github.com/hectorqin/reader/releases) 下载对应平台安装包安装即可，需要安装java8及以上环境\n\nMacOS 版 `storage` 默认是 `用户目录/.reader/storage`，其它版本 `storage` 默认是 `程序目录/storage`\n\n#### 配置文件\n\n`storage/windowConfig.json`\n\n包含图形界面和接口服务的相关配置，JSON格式，修改后，程序重启才会生效\n\n> 请仔细检查配置内容，不支持注释，此处注释只是为了方便理解\n\n```json\n{\n    \"serverPort\": 8080,            // web服务端口，默认为 8080\n    \"showUI\": true,                // 是否显示UI界面，默认为显示\n    \"debug\": false,                // 是否调试模式，默认为否\n    \"positionX\": 0.0,              // 窗口位置 横坐标\n    \"positionY\": 0.0,              // 窗口位置 纵坐标\n    \"width\": 1280.0,               // 窗口大小 宽度\n    \"height\": 800.0,               // 窗口大小 高度\n    \"rememberSize\": true,          // 改变窗口大小时，是否记住窗口大小，默认记住\n    \"rememberPosition\": false,     // 移动窗口时，是否记住窗口位置，默认不记住\n    \"setWindowPosition\": false,    // 启动时是否设置窗口位置，默认不设置，窗口默认居中\n    \"setWindowSize\": true,         // 启动时是否设置窗口大小，默认按照配置文件进行设置\n    \"serverConfig\": {                // 接口服务配置，此处配置会被 `serverPort|showUI|debug` 等覆盖\n        \"reader.app.secure\": false,  // 是否需要登录鉴权，开启后将支持多用户模式\n        \"reader.app.inviteCode\": \"\",  // 注册邀请码，为空时则开放注册，否则注册时需要输入邀请码。仅多用户模式下有效\n        \"reader.app.secureKey\": \"\",  // 管理密码，开启鉴权时，前端管理用户空间的管理密码。仅多用户模式下有效\n    }\n}\n```\n\n### 手机端\n\n使用docker版本或者服务器版本，访问web页面\n\n可以添加为桌面应用\n\n### 服务器版\n\n从 [releases](https://github.com/hectorqin/reader/releases) 下载 `reader-server-$version.zip` 解压后运行即可，需要安装java8及以上环境\n\n```bash\n# 安装jdk10以上环境...\n\n# 解压文件\nunzip reader-server-$version.zip\n\n# 运行\ncd reader-server-$version\n\n./bin/startup.sh\n# windows 上直接点击 bin/startup.cmd 文件\n\n# startup 脚本支持以下选项，这些选项如果使用命令行参数修改，则会覆盖配置文件的设置\n# -m single|multi 选择单用户/多用户模式，默认 以配置文件 conf/application.properties 为准\n# -s reader-xx 选择 jar 文件名(不含.jar后缀)，默认使用target目录里最新的jar\n# -i inviteCode 设置多用户模式下的邀请码，默认 以配置文件 conf/application.properties 为准\n# -k secureKey 设置多用户模式下的管理密码，默认 以配置文件 conf/application.properties 为准\n\n# 注意！！！startup 脚本在单用户模式下 默认占用 256m 内存，在多用户模式下 默认占用 1g 内存，如果内存不够，请自行修改脚本\n\n# web端 http://localhost:8080/\n# 接口地址 http://localhost:8080/reader3/\n```\n\n### Docker版\n\n```bash\n# 自行编译\n# docker build -t reader:latest .\n\n# 使用环境变量覆盖服务配置，环境变量采用大写字母，不允许使用.-符号，采用下划线“_”取代点“.”  减号“-”直接删除\n\n# docker run -d --restart=always --name=reader -e \"SPRING_PROFILES_ACTIVE=prod\" -v $(pwd)/logs:/logs -v $(pwd)/storage:/storage -p 8080:8080 reader:latest\n\n# 跨平台镜像\n\n# 新建构建器\n# docker buildx create --use --name mybuilder\n# 启动构建器\n# docker buildx inspect mybuilder --bootstrap\n# 查看构建器及其所支持的cpu架构\n# docker buildx ls\n# 构建跨平台镜像\n# docker buildx build -t reader:latest --platform=linux/arm,linux/arm64,linux/amd64 . --push\n\n# 使用预编译的镜像\n\n# 自用版(建议修改映射端口)\ndocker run -d --restart=always --name=reader -e \"SPRING_PROFILES_ACTIVE=prod\" -v $(pwd)/logs:/logs -v $(pwd)/storage:/storage -p 8080:8080 hectorqin/reader\n\n# 多用户版(建议修改映射端口)\ndocker run -d --restart=always --name=reader -v $(pwd)/logs:/logs -v $(pwd)/storage:/storage -p 8080:8080 hectorqin/reader java -jar /app/bin/reader.jar --spring.profiles.active=prod --reader.app.secure=true --reader.app.secureKey=管理密码 --reader.app.inviteCode=注册邀请码\n\n# 多用户版 使用环境变量(建议修改映射端口)\ndocker run -d --restart=always --name=reader -e \"SPRING_PROFILES_ACTIVE=prod\" -e \"READER_APP_SECURE=true\" -e \"READER_APP_SECUREKEY=管理密码\" -e \"READER_APP_INVITECODE=注册邀请码\" -v $(pwd)/logs:/logs -v $(pwd)/storage:/storage -p 8080:8080 hectorqin/reader\n\n# 更新docker镜像\n# docker pull hectorqin/reader\n\n#:后面的端口修改为映射端口\n# web端 http://localhost:8080/\n# 接口地址 http://localhost:8080/reader3/\n\n# 通过watchtower手动更新\ndocker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --run-once reader\n\n# 使用 remote-webview 功能\n# 1.创建 remote-webview 容器\ndocker run -d --network host --restart=always hectorqin/remote-webview\n# 2.重建 reader 容器\nreader使用宿主机网络：--network host\nreader添加环境变量：-e \"READER_APP_REMOTEWEBVIEWAPI=http://localhost:8050\"\n获取reader添加参数：--reader.app.remoteWebviewApi=http://localhost:8050\"\n```\n\n### Docker-Compose版(推荐)\n\n```shell\n#腾讯云，阿里云，华为云，甲骨文等服务器提供商需在控制台面板手动关闭防火墙并放行端口\n#安装docker 及 docker-compose\n#Debian/Ubuntu\napt install docker-compose -y\n#CentOS\ncurl -fsSL https://get.docker.com | bash -s docker #国外服务器\ncurl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun #国内服务器\n\n# 下载项目里的 docker-compose.yaml\nwget https://mirror.ghproxy.com/https://raw.githubusercontent.com/hectorqin/reader/master/docker-compose.yaml\n# 根据 docker-compose.yaml 里面的注释编辑所需配置\nvim docker-compose.yaml\n# 保存\nesc\n:wq\n# 启动 docker-compose\ndocker-compose up -d\n\n# 停止 docker-compose\ndocker-compose stop\n\n# 查看实时日志\ndocker logs -f reader\n\n# 自行导入远程书源(打开链接后复制网址导入即可)\nhttps://legado.aoaostar.com/\n\n# 手动更新\ndocker-compose pull && docker-compose up -d\n```\n\n### 通过脚本一键部署\n\n```shell\n# 此脚本对甲骨文非Ubuntu系统,CentOS9可能不兼容。建议网上手动搜索\n#curl\nbash <(curl -L -s https://mirror.ghproxy.com/https://raw.githubusercontent.com/hectorqin/reader/master/reader.sh)\n\n#wget\nbash <(wget -qO- --no-check-certificate https://mirror.ghproxy.com/https://raw.githubusercontent.com/hectorqin/reader/master/reader.sh)\n\n```\n\n### Arch Linux 安装\n\n> 注意，此软件源并非官方提供，后果自负\n\n从 [AUR 仓库](https://aur.archlinux.org/packages/reader-pro-bin)安装或[自建软件源](https://github.com/taotieren/aur-repo)\n\n\n```bash\nyay -Syu reader-pro\n# 开启开机自启\nsudo systemctl enable reader-pro-single\nsudo systemctl enable reader-pro-multi\n# 运行\nsudo systemctl start reader-pro-single\nsudo systemctl start reader-pro-multi\n# 状态\nsudo systemctl status reader-pro-single\nsudo systemctl status reader-pro-multi\n# 停止\nsudo systemctl stop reader-pro-single\nsudo systemctl stop reader-pro-multi\n# 停止开机自启\nsudo systemctl disable reader-pro-single\nsudo systemctl disable reader-pro-multi\n```\n\n> Arch Linux 的存储目录是 `/var/lib/reader-pro/`\n\n> Arch Linux 的配置文件是 `/usr/share/java/reader-pro/conf/application.properties`\n\n## Nginx反向代理(如果有域名可以考虑80端口复用)\n\n```shell\n# 宝塔等各种面板不适用下列教程\n# Debian/Ubuntu\napt install nginx -y\n# CentOS\nyum install nginx -y\nvim /etc/nginx/conf.d/reader.conf\n将下面代码复制进reader.conf后，修改域名输入\nesc\n:wq\n保持即可\n```\n\n```nginx\nserver {\n    listen 80;\n    server_name 域名;\n    #开启ssl解除注释\n    # SSL证书获取\n    # https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E\n    #listen 443 ssl;\n    #ssl_certificate 证书.cer;\n    #ssl_certificate_key 证书.key;\n    #ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;\n    #ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;\n    #ssl_prefer_server_ciphers on;\n    #ssl_session_cache shared:SSL:10m;\n    #ssl_session_timeout 10m;\n    #if ($server_port !~ 443){\n    #    rewrite ^(/.*)$ https://$host$1 permanent;\n    #}\n    #error_page 497  https://$host$request_uri;\n\n    gzip on; #开启gzip压缩\n    gzip_min_length 1k; #设置对数据启用压缩的最少字节数\n    gzip_buffers 4 16k;\n    gzip_http_version 1.0;\n    gzip_comp_level 6; #设置数据的压缩等级,等级为1-9，压缩比从小到大\n    gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml; #设置需要压缩的数据格式\n    gzip_vary on;\n\n    client_max_body_size   50m; #允许上传50MB文件,上传本地书籍需要修改此项大小.如nginx主配置文件已添加,删除此行并修改主配置即可\n\n    location / {\n        proxy_pass  http://127.0.0.1:4396; #端口自行修改为映射端口\n        proxy_http_version 1.1;\n        proxy_cache_bypass $http_upgrade;\n        proxy_set_header Upgrade           $http_upgrade;\n        proxy_set_header Connection        \"upgrade\";\n        proxy_set_header Host              $host;\n        proxy_set_header X-Real-IP         $remote_addr;\n        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        proxy_set_header X-Forwarded-Host  $host;\n        proxy_set_header X-Forwarded-Port  $server_port;\n    }\n}\n```\n\n## 开发编译\n\n### 编译脚本\n\n```bash\n$ ./build.sh\n\nUSAGE: ./build.sh build|run|win|linux|mac|serve|cli|yarn|web|sync\n\nbuild   调试打包\nrun     桌面端编译运行，需要先执行 sync 命令编译同步web资源\nwin     打包 windows 安装包\nlinux   打包 linux 安装包\nmac     打包 mac 安装包\nserve   服务端编译运行\ncli     服务端打包命令\nyarn    web页面 yarn 快捷命令，默认 install\nweb     开发web页面\nsync    编译同步web资源\n```\n\n### 编译前端\n\n```bash\ncd web\n# 启动开发服务 访问 http://localhost:8081/\n# yarn serve\n\n# 编译，并拷贝到 src/main/resources/web 目录\nyarn sync\n```\n\n### 编译接口\n\n```bash\n./gradlew assemble --info\n\njava -jar build/libs/reader-${version}.jar\n\n# 指定 storage 路径  默认为相对路径 storage\n# java -Dreader.app.storagePath=cacheStorage  -jar build/libs/reader-${version}.jar\n\n# web端 http://localhost:8080/\n# 接口地址 http://localhost:8080/reader3/\n```\n\n## 接口文档\n\n与 [阅读3Web接口](https://github.com/gedoor/legado/blob/master/api.md) 基本一致，只是多了接口前缀 `/reader3/`\n\n### 新增接口\n\n#### 加入书架\n\n- URL `http://localhost:8080/reader3/saveBook`\n- Method `POST`\n- Body `json 格式`\n\n```JSON\n{\n    \"infoHtml\": \"\",\n    \"tocHtml\": \"\",\n    \"bookUrl\": \"https://www.damixs.com/book/dmfz.html\",\n    \"origin\": \"https://www.damixs.com\",\n    \"originName\": \"🎉大米小说\",\n    \"type\": 0,\n    \"name\": \"道门法则\",\n    \"author\": \"八宝饭\",\n    \"kind\": \"02-14\",\n    \"intro\": \"在道门掌控的天下，应该怎么修炼？符箓、丹药、道士、灵妖、斋醮科仪......想要修仙，很好，请从扫厕所开始做起！符诏到来的时候，你需要站在什么位置？Q群：1701556（需验证订阅截图）、954782460“盟主群”\",\n    \"wordCount\": \"\",\n    \"latestChapterTitle\": \"番外四（贺消脱止-M荣升盟主）\",\n    \"tocUrl\": \"\",\n    \"time\": 1628756214810,\n    \"originOrder\": 16\n}\n```\n\n- Response Body\n\n[Book字段参考](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)\n\n```JSON\n{\n    \"isSuccess\": true,\n    \"errorMsg\": \"\",\n    \"data\": Book\n}\n```\n\n#### 获取书籍书源\n\n- URL `http://localhost:8080/reader3/getBookSource?url=xxx`\n- Method `GET`\n\n获取指定URL对应的书源信息, 和 `阅读3Web接口` 的 `getSource` 接口相同\n\n- Response Body\n\n[SearchBook字段参考](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/SearchBook.kt)\n\n```JSON\n{\n    \"isSuccess\": true,\n    \"errorMsg\": \"\",\n    \"data\": [SearchBook]\n}\n```\n\n#### 搜索书籍更多书源\n\n- URL `http://localhost:8080/reader3/searchBookSource?name=xxx&lastIndex=0`\n- Method `GET`\n\n搜索指定name对应的书源列表信息\n\nlastIndex 是上次搜索结果中返回的字段，默认为 0，可以传入 `getBookSource` 接口返回的SearchBook列表长度\n\n- Response Body\n\n[SearchBook字段参考](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/SearchBook.kt)\n\n```JSON\n{\n    \"isSuccess\": true,\n    \"errorMsg\": \"\",\n    \"data\": [SearchBook]\n}\n```\n\n#### 书籍换源\n\n- URL `http://localhost:8080/reader3/setBookSource`\n- Method `POST`\n- Body `json 格式`\n\n```JSON\n{\n    \"newUrl\": \"新源书籍链接\",\n    \"name\": \"书籍名称\",\n    \"bookSourceUrl\": \"书源链接\"\n}\n```\n\n- Response Body\n\n[Book字段参考](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)\n\n```JSON\n{\n    \"isSuccess\": true,\n    \"errorMsg\": \"\",\n    \"data\": Book\n}\n```\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "version: '3.1'\nservices:\n# reader 在线阅读\n# 公开服务器(服务器位于日本)：[https://reader.nxnow.top](https://reader.nxnow.top) 测试账号/密码分别为guest/guest123，也可自行创建账号添加书源，不定期删除长期未登录账号(2周)\n# 书源集合 : [https://legado.aoaostar.com/](https://legado.aoaostar.com/) 点击打开连接，添加远程书源即可\n# 公众号汇总 : [https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect)\n# 手动更新方式 : docker-compose pull && docker-compose up -d\n  reader:\n    image: hectorqin/reader\n    #image: hectorqin/reader:openj9-latest #docker镜像，arm64架构或小内存机器优先使用此镜像.启用需删除上一行\n    container_name: reader #容器名 可自行修改\n    restart: always\n    ports:\n      - 4396:8080 #4396端口映射可自行修改\n    networks:\n      - share_net\n    volumes:\n      - /home/reader/logs:/logs #log映射目录 /home/reader/logs 映射目录可自行修改\n      - /home/reader/storage:/storage #数据映射目录 /home/reader/storage 映射目录可自行修改\n    environment:\n      - SPRING_PROFILES_ACTIVE=prod\n      - READER_APP_USERLIMIT=50 #用户上限,默认50\n      - READER_APP_USERBOOKLIMIT=200 #用户书籍上限,默认200\n      - READER_APP_CACHECHAPTERCONTENT=true #开启缓存章节内容 V2.0\n      # 如果启用远程webview，需要取消注释下面的 remote-webview 服务\n      # - READER_APP_REMOTEWEBVIEWAPI=http://remote-webview:8050 #开启远程webview\n      # 下面都是多用户模式配置\n      - READER_APP_SECURE=true #开启登录鉴权，开启后将支持多用户模式\n      - READER_APP_SECUREKEY=adminpwd  #管理员密码  建议修改\n      - READER_APP_INVITECODE=registercode #注册邀请码 建议修改,如不需要可注释或删除\n  # remote-webview:\n  #   image: hectorqin/remote-webview\n  #   container_name: remote-webview #容器名 可自行修改\n  #   restart: always\n  #   ports:\n  #     - 8050:8050\n  #   networks:\n  #     - share_net\n# 自动更新docker镜像\n  watchtower:\n    image: containrrr/watchtower\n    container_name: watchtower\n    restart: always\n    # 环境变量,设置为上海时区\n    environment:\n        - TZ=Asia/Shanghai\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n    command: reader watchtower --cleanup --schedule \"0 0 4 * * *\"\n    networks:\n      - share_net\n    # 仅更新reader与watchtower容器,如需其他自行添加 '容器名' ,如:reader watchtower nginx\n    # --cleanup 更新后清理旧版本镜像\n    # --schedule 自动检测更新 crontab定时(限定6位crontab) 此处代表凌晨4点整\nnetworks:\n  share_net:\n    driver: bridge"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.1'\nservices:\n# reader 在线阅读\n# 公开服务器(服务器位于日本)：[https://reader.nxnow.top](https://reader.nxnow.top) 测试账号/密码分别为guest/guest123，也可自行创建账号添加书源，不定期删除长期未登录账号(2周)\n# 书源集合 : [https://legado.aoaostar.com/](https://legado.aoaostar.com/) 点击打开连接，添加远程书源即可\n# 公众号汇总 : [https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect)\n# 手动更新方式 : docker-compose pull && docker-compose up -d\n  reader:\n    #image: hectorqin/reader # 普通镜像\n    image: hectorqin/reader:openj9-latest # Openj9镜像，arm64架构或小内存机器优先使用\n    container_name: reader #容器名 可自行修改\n    restart: always\n    ports:\n      - 4396:8080 #4396端口映射可自行修改,8080请勿修改\n    volumes:\n      - /home/reader/logs:/logs #log映射目录 /home/reader/logs 映射目录可自行修改\n      - /home/reader/storage:/storage #数据映射目录 /home/reader/storage 映射目录可自行修改\n    environment:\n      - SPRING_PROFILES_ACTIVE=prod\n      #- READER_APP_USERLIMIT=50 #用户上限,默认且最大值为50\n      - READER_APP_USERBOOKLIMIT=200 #用户书籍上限,默认200\n      - READER_APP_CACHECHAPTERCONTENT=true #开启缓存章节内容\n      - READER_APP_REMOTEWEBVIEWAPI=http://readerwebview:8050 #启用webview(若下方readerwebview容器不开启需注释此行\n      # ↓多用户模式配置↓\n      - READER_APP_SECURE=true #开启登录鉴权，开启后将支持多用户模式\n      - READER_APP_SECUREKEY=adminpwd  #管理员密码  建议修改\n      - READER_APP_INVITECODE=registercode #注册邀请码 建议修改,如不需要可注释或删除\n# 如需支持webview书源，打开(占用较大，不需要可加 # 注释)\n  readerwebview:\n    image: hectorqin/remote-webview\n    container_name: readerwebview\n    restart: always\n    environment:\n      - TZ=Asia/Shanghai\n# 自动更新docker镜像\n  watchtower:\n    image: containrrr/watchtower\n    container_name: watchtower\n    restart: always\n    environment:\n        - TZ=Asia/Shanghai\n    volumes:\n      - /var/run/docker.sock:/var/run/docker.sock\n    command: reader readerwebview watchtower --cleanup --schedule \"0 0 4 * * *\"\n    # 仅更新reader与watchtower容器,如需其他自行添加 '容器名' ,如:reader watchtower nginx\n    # --cleanup 更新后清理旧版本镜像\n    # --schedule 自动检测更新 crontab定时(限定6位crontab) 此处代表凌晨4点整\nvolumes:\n  reader:\n  readerwebview:"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.1.1-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx2048m"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windows variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "nixpacks.toml",
    "content": "[phases.build]\ncmds = ['railway run']\n\n[start]\nrunImage = 'hectorqin/reader'\nonlyIncludeFiles = ['Dockerfile']\ncmd = '/sbin/tini --'"
  },
  {
    "path": "preview.md",
    "content": "# 预览\n\n![](imgs/1.jpg)\n![](imgs/2.jpg)\n![](imgs/3.jpg)\n![](imgs/4.jpg)\n![](imgs/5.jpg)\n![](imgs/6.jpg)\n![](imgs/7.jpg)\n![](imgs/8.jpg)\n![](imgs/9.jpg)\n![](imgs/10.jpg)"
  },
  {
    "path": "reader.sh",
    "content": "#!/bin/bash\n\nred='\\033[0;31m'\ngreen=\"\\033[32m\"\nyellow='\\033[0;33m'\nplain='\\033[0m'\n\nfile_dir=\"\"\nremotePort=\"\"\nisMultiUser=\"\"\nadminPassword=\"\"\nregisterCode=\"\"\nstrTrue=\"true\"\ndockerImages=\"\"\n\n# CheckRoot\nif [[ $EUID -ne 0 ]]; then\n    echo \"请使用root用户登录!\" 1>&2\n    exit 1\nfi\n\n# CheckSystem\nif [[ -f /etc/redhat-release ]]; then\n    release=\"centos\"\nelif cat /etc/issue | grep -q -E -i \"debian\"; then\n    release=\"debian\"\nelif cat /etc/issue | grep -q -E -i \"ubuntu\"; then\n    release=\"ubuntu\"\nelif cat /etc/issue | grep -q -E -i \"centos|red hat|redhat\"; then\n    release=\"centos\"\nelif cat /proc/version | grep -q -E -i \"debian\"; then\n    release=\"debian\"\nelif cat /proc/version | grep -q -E -i \"ubuntu\"; then\n    release=\"ubuntu\"\nelif cat /proc/version | grep -q -E -i \"centos|red hat|redhat\"; then\n    release=\"centos\"\nfi\nbit=$(uname -m)\n    if test \"$bit\" != \"x86_64\"; then\n        bit=\"arm64\"\n    else bit=\"amd64\"\nfi\n\nos_version=\"\"\n\n# os version\nif [[ -f /etc/os-release ]]; then\n    os_version=$(awk -F'[= .\"]' '/VERSION_ID/{print $3}' /etc/os-release)\nfi\nif [[ -z \"$os_version\" && -f /etc/lsb-release ]]; then\n    os_version=$(awk -F'[= .\"]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release)\nfi\n\nif [[ x\"${release}\" == x\"centos\" ]]; then\n    if [[ ${os_version} -le 6 ]]; then\n        echo -e \"${red}请使用 CentOS 7 或更高版本的系统！${plain}\\n\" && exit 1\n    fi\nelif [[ x\"${release}\" == x\"ubuntu\" ]]; then\n    if [[ ${os_version} -lt 16 ]]; then\n        echo -e \"${red}请使用 Ubuntu 16 或更高版本的系统！${plain}\\n\" && exit 1\n    fi\nelif [[ x\"${release}\" == x\"debian\" ]]; then\n    if [[ ${os_version} -lt 9 ]]; then\n        echo -e \"${red}请使用 Debian 9 或更高版本的系统！${plain}\\n\" && exit 1\n    fi\nfi\n\ninstall_dockercompose() {\n    if [[ x\"${release}\" == x\"centos\" ]]; then\n        yum install wget curl -y\n        echo -e \"${green} 正在移除CentOS遗留无效Docker文件 ${plain}\"\n        yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine -y\n        echo -e \"${green} 正在安装Docker ${plain}\"\n        yum install yum-utils device-mapper-persistent-data lvm2 -y\n        yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo\n        yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y\n        echo -e \"${green} 正在启动Docker ${plain}\"\n        systemctl start docker\n        systemctl restart docker\n        systemctl enable docker\n        echo -e \"${green} 正在安装docker-compose ${plain}\"\n        curl -L \"https://mirror.ghproxy.com/https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose\n    else\n        echo -e \"${green} 正在安装docker-compose ${plain}\"\n        apt update && apt install wget curl docker-compose -y\n    fi\n}\n\ninstall_reader() {\n    mkdir -p ${orgin_file_dir}/storage/data/default\n    cd ${orgin_file_dir}\n    rm docker-compose*\n    wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/hectorqin/reader/master/docker-compose.yml\n    echo -e \"${green} 正在配置默认书源 ${plain}\"\n    wget https://jihulab.com/aoaostar/legado/-/raw/release/cache/6c35d84798ddbf4aad3fe3f0fd6cec53dd788be8.json -O storage/data/default/bookSource.json\n    # 判断是否合法json\n    local first_character=$(head -c 1 \"storage/data/default/bookSource.json\")\n    if [[ x\"$first_character\" == x\"[\" ]]; then\n    #\n        echo \"\"\n    else\n        echo -e \"${red} 书源错误，已为您删除，请自行导入书源 ${plain}\"\n        echo \"[]\" > storage/data/default/bookSource.json\n    fi\n    echo -e \"${green} 正在配置docker变量 ${plain}\"\n    sed -i \"s/\\/home\\/reader/${file_dir}/\" docker-compose.yml\n    sed -i \"s/4396/${remotePort}/\" docker-compose.yml\n    sed -i \"s/openj9-latest/${dockerImages}/\" docker-compose.yml\n    # 多用户\n    sed -i \"s/READER_APP_SECURE\\=true/READER_APP_SECURE\\=${isMultiUser}/\" docker-compose.yml\n    sed -i \"s/adminpwd/${adminPassword}/\" docker-compose.yml\n    sed -i \"s/registercode/${registerCode}/\" docker-compose.yml\n    echo -e \"${green} 准备启动 ${plain}\"\n    # 远程webview\n    docker-compose up -d\n}\n\ngetRemotePort () {\n    echo \"请输入部署端口,例如 4396\"\n    read -p \"不填默认为4396: \" remotePort\n    if [[ -z \"$remotePort\" ]];then\n    \tremotePort=\"4396\"\n    fi\n    if [ \"$remotePort\" -gt 0 ] 2>/dev/null;then\n\t\tif [[ $remotePort -lt 0 || $remotePort -gt 65535 ]];then\n\t\t echo -e \"${red} 端口号不正确,请输入0-65535${plain}\"\n\t\t getRemotePort\n\t\t exit 0\n\t\tfi\n\telse\n        echo -e \"${red} 端口号不正确,请输入0-65535${plain}\"\n\t    getRemotePort\n\t    exit 0\n    fi\n}\n\n\ngetfileDir () {\n    echo -e \"${green} 请输入数据存放目录,例如 /home/reader : ${plain}\"\n    read -p \"不填默认为/home/reader : \" file_dir\n    if [[ -z \"$file_dir\" ]];then\n        file_dir=\"/home/reader\"\n    fi\n    orgin_file_dir=$file_dir\n    file_dir=${file_dir//\\//\\\\\\/}\n}\n\ngetMultiUser () {\n    echo -e \"${green} 是否需要开启多用户 : ${plain}\"\n    read -p \"填0不开启,不填开启 : \" isMultiUser\n    if [[ -z \"$isMultiUser\" ]];then\n        isMultiUser=\"true\"\n    else\n        isMultiUser=\"false\"\n    fi\n}\n\ngetPwdOrCode () {\n    echo -e \"${green} 请输入管理密码,用于加载用户空间 : ${plain}\"\n    read -p \"建议修改此参数,默认为adminpwd : \" adminPassword\n    if [[ -z \"$adminPassword\" ]];then\n        adminPassword=\"adminpwd\"\n    fi\n    echo -e \"${green} 请输入邀请码,用于注册使用 : ${plain}\"\n    read -p \"不填默认为空 : \" registerCode\n    if [[ -z \"$registerCode\" ]];then\n        registerCode=\"\"\n    fi\n}\n\ngetDockerImages () {\n    echo -e \"${green} 请输入需要的镜像 arm或者小内存(1G)机器建议openj9,其余建议基础镜像 : ${plain}\"\n    read -p \"不输入为基础镜像,输入其他值为openj9 : \" dockerImages\n    if [[ -z \"$dockerImages\" ]];then\n        dockerImages=\"latest\"\n    else\n        dockerImages=\"openj9-latest\"\n    fi\n}\n\nServer_IP=''\nPublic_IP=''\ngetIpaddr () {\n    Server_IP=$(hostname -I | awk -F \" \" '{printf $1}')\n    Public_IP=$(curl http://pv.sohu.com/cityjson 2>> /dev/null | awk -F '\"' '{print $4}')\n}\n\necho -e \"${green}准备部署reader${plain}\"\necho -e \"${green}甲骨文官方系统可能并不适用此脚本,本脚本仅测试CentOS7,8,Ubuntu20+,Debian10+${plain}\"\ninstall_dockercompose\ngetfileDir\ngetRemotePort\ngetMultiUser\nif [ $isMultiUser == \"true\" ]; then\n    getPwdOrCode\nfi\ngetDockerImages\ninstall_reader\ngetIpaddr\n\necho -e \"${green}初步部署完成,已配置默认书源,国内服务器等有控制台面板的服务器厂商请手动在控制台打开reader所需的端口${remotePort}${plain}\"\nif [ $Server_IP == $Public_IP ];then\n    echo -e \"${green}网址:${plain} http://${Server_IP}:${remotePort}\"\nelse\n    echo -e \"${green}内网网址:${plain} http://${Server_IP}:${remotePort}\"\n    echo -e \"${green}公网网址:${plain} http://${Public_IP}:${remotePort}\"\nfi\n\necho -e \"${green}如需修改其他配置请前往 cd${orgin_file_dir} 根据注释修改 vim docker-compose.yml文件后${plain}\"\necho -e \"${green}先自行学习vim用法,否则建议使用sftp或WindTerm等ssh自带sftp的软件直接打开编辑${plain}\"\necho -e \"${green}修改后前往 cd${orgin_file_dir} 后通过命令docker-compose up -d 重启即可${plain}\"\n"
  },
  {
    "path": "server/bin/shutdown.cmd",
    "content": "@echo off\nrem Copyright 1999-2018 Alibaba Group Holding Ltd.\nrem Licensed under the Apache License, Version 2.0 (the \"License\");\nrem you may not use this file except in compliance with the License.\nrem You may obtain a copy of the License at\nrem\nrem      http://www.apache.org/licenses/LICENSE-2.0\nrem\nrem Unless required by applicable law or agreed to in writing, software\nrem distributed under the License is distributed on an \"AS IS\" BASIS,\nrem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nrem See the License for the specific language governing permissions and\nrem limitations under the License.\n\nif not exist \"%JAVA_HOME%\\bin\\java.exe\" (\n    rem find java_home from reg\n    for /f \"tokens=2*\" %%i in ('reg query \"HKLM\\SOFTWARE\\JavaSoft\\Java Runtime Environment\" /s ^| findstr \"JavaHome\"') do (\n        set \"JAVA_HOME=%%j\"\n    )\n)\nif exist \"%JAVA_HOME%\\bin\\java.exe\" (\n    set \"JAVA=%JAVA_HOME%\\bin\\java.exe\"\n) else (\n    echo Please set the JAVA_HOME variable in your environment, We need jdk8 or later!\n    pause\n    EXIT /B 1\n)\n\nsetlocal\n\nset \"PATH=%JAVA_HOME%\\bin;%PATH%\"\n\necho killing reader server\n\nfor /f \"tokens=1\" %%i in ('jps -m ^| find \"reader.server\"') do ( taskkill /F /PID %%i )\n\necho Done!\n"
  },
  {
    "path": "server/bin/shutdown.sh",
    "content": "#!/bin/bash\n\n# Copyright 1999-2018 Alibaba Group Holding Ltd.\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\ncd `dirname $0`/../target\ntarget_dir=`pwd`\n\npid=`ps ax | grep -i 'reader.server' | grep ${target_dir} | grep java | grep -v grep | awk '{print $1}'`\nif [ -z \"$pid\" ] ; then\n        echo \"No reader running.\"\n        exit -1;\nfi\n\necho \"The reader(${pid}) is running...\"\n\nkill ${pid}\n\necho \"Send shutdown request to reader(${pid}) OK\"\n"
  },
  {
    "path": "server/bin/startup.cmd",
    "content": "@echo off\nREM Copyright 1999-2018 Alibaba Group Holding Ltd.\nREM Licensed under the Apache License, Version 2.0 (the \"License\");\nREM you may not use this file except in compliance with the License.\nREM You may obtain a copy of the License at\nREM\nREM      http://www.apache.org/licenses/LICENSE-2.0\nREM\nREM Unless required by applicable law or agreed to in writing, software\nREM distributed under the License is distributed on an \"AS IS\" BASIS,\nREM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nREM See the License for the specific language governing permissions and\nREM limitations under the License.\n\nif not exist \"%JAVA_HOME%\\bin\\java.exe\" (\n    rem find java_home from reg\n    for /f \"tokens=2*\" %%i in ('reg query \"HKLM\\SOFTWARE\\JavaSoft\\Java Runtime Environment\" /s ^| findstr \"JavaHome\"') do (\n        set \"JAVA_HOME=%%j\"\n    )\n)\nif exist \"%JAVA_HOME%\\bin\\java.exe\" (\n    set \"JAVA=%JAVA_HOME%\\bin\\java.exe\"\n) else (\n    rem check java command\n    for /f \"usebackq delims=\" %%i in (`where java`) do (\n        set JAVA=%%i\n    )\n\n    if not \"%JAVA%\" == \"\" if exist \"%JAVA%\"  (\n        rem java path is exist\n    ) else (\n        echo Please set the JAVA_HOME variable in your environment, We need jdk8 or later!\n        pause\n        EXIT /B 1\n    )\n)\n\nsetlocal enabledelayedexpansion\n\nset BASE_DIR=%~dp0\nrem added double quotation marks to avoid the issue caused by the folder names containing spaces.\nrem removed the last 5 chars(which means \\bin\\) to get the base DIR.\nset BASE_DIR=\"%BASE_DIR:~0,-5%\"\n\nset CUSTOM_SEARCH_LOCATIONS=file:%BASE_DIR%/conf/\n\nset SERVER=reader\n\nfor /f \"delims=\" %%i in ('dir /b /o:-n %BASE_DIR%\\target\\reader*.jar') do set NEWEST_JAR=%%i\n\nif not \"%NEWEST_JAR%\"==\"\" (\n  set SERVER=%NEWEST_JAR:.jar=%\n)\n\nset MODE=\"\"\nset INVITE_CODE=\"\"\nset SECURE_KEY=\"\"\nset MODE_INDEX=-1\nset INVITE_CODE_INDEX=-1\nset SERVER_INDEX=-1\nset SECURE_KEY_INDEX=-1\nset EMBEDDED_STORAGE=\"\"\n\n\nset i=0\nfor %%a in (%*) do (\n    if \"%%a\" == \"-m\" ( set /a MODE_INDEX=!i!+1 )\n    if \"%%a\" == \"-i\" ( set /a INVITE_CODE_INDEX=!i!+1 )\n    if \"%%a\" == \"-s\" ( set /a SERVER_INDEX=!i!+1 )\n    if \"%%a\" == \"-k\" ( set /a SECURE_KEY_INDEX=!i!+1 )\n    set /a i+=1\n)\n\nset i=0\nfor %%a in (%*) do (\n    if %MODE_INDEX% == !i! ( set MODE=\"%%a\" )\n    if %INVITE_CODE_INDEX% == !i! ( set INVITE_CODE=\"%%a\" )\n    if %SERVER_INDEX% == !i! (set SERVER=\"%%a\")\n    if %SECURE_KEY_INDEX% == !i! (set SECURE_KEY=\"%%a\")\n    set /a i+=1\n)\n\nrem if reader startup mode is single\nif %MODE% == \"\" (\n    echo The running mode of the Reader is determined by the configuration file conf/application.properties. Please note that there is currently no memory limit set for the JVM.\n)\n\nif %MODE% == \"single\" (\n    echo The running mode of the Reader is determined by the configuration file conf/application.properties. Please note that the current memory limit is set to 256m.\n    set \"READER_JVM_OPTS=-Xms256m -Xmx256m -Xmn128m\"\n)\n\nrem if reader startup mode is multi-user\nif not %MODE% == \"\" if not %MODE% == \"single\" (\n    set READER_TIPS=\"\"\n    set \"READER_OPTS=-Dreader.app.secure=true\"\n    if not \"%INVITE_CODE%\" == \"\" {\n        set \"READER_OPTS=%READER_OPTS%  -Dreader.app.inviteCode=%INVITE_CODE%\"\n        set \"READER_TIPS=%READER_TIPS% inviteCode: %INVITE_CODE%\"\n    }\n    if not \"%SECURE_KEY%\" == \"\" {\n        set \"READER_OPTS=%READER_OPTS%  -Dreader.app.secureKey=%SECURE_KEY%\"\n        set \"READER_TIPS=%READER_TIPS% secureKey: %SECURE_KEY%\"\n    }\n    if \"%READER_TIPS%\" == \"\" {\n        set \"READER_TIPS=The invitation code and administrator password are determined by the configuration file conf\\application.properties.\"\n    }\n    set \"READER_TIPS=%READER_TIPS%. Please note that the current memory limit is set to 1g.\"\n\n    echo The Reader will running in multi-user mode. %READER_TIPS%\n\n    set \"READER_JVM_OPTS=-server -Xms1g -Xmx1g -Xmn512m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=160m -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=%BASE_DIR%\\logs\\java_heapdump.hprof -XX:-UseLargePages\"\n)\n\nrem set reader options\n@REM set \"READER_OPTS=%READER_OPTS% -Dloader.path=%BASE_DIR%/plugins,%BASE_DIR%/plugins/health,%BASE_DIR%/plugins/cmdb,%BASE_DIR%/plugins/selector\"\nset \"READER_OPTS=%READER_OPTS% -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Dspring.profiles.active=prod -Dreader.app.workDir=%BASE_DIR%\"\nset \"READER_OPTS=%READER_OPTS% -jar %BASE_DIR%\\target\\%SERVER%.jar\"\n\nrem set reader spring config location\nset \"READER_CONFIG_OPTS=--spring.config.additional-location=%CUSTOM_SEARCH_LOCATIONS%\"\n\nrem set reader log4j file location\n@REM set \"READER_LOG4J_OPTS=--logging.config=%BASE_DIR%/conf/reader-logback.xml\"\n\n\nset COMMAND=\"%JAVA%\" %READER_JVM_OPTS% %READER_OPTS% %READER_CONFIG_OPTS% %READER_LOG4J_OPTS% reader.server %*\n\necho Run command:\necho %COMMAND%\necho\n\necho Reader is starting, you can check the %BASE_DIR%\\logs\n\nrem start reader command\n%COMMAND%\n"
  },
  {
    "path": "server/bin/startup.sh",
    "content": "#!/bin/bash\n\n# Copyright 1999-2018 Alibaba Group Holding Ltd.\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\ncygwin=false\ndarwin=false\nos400=false\ncase \"`uname`\" in\nCYGWIN*) cygwin=true;;\nDarwin*) darwin=true;;\nOS400*) os400=true;;\nesac\nerror_exit ()\n{\n    echo \"ERROR: $1 !!\"\n    exit 1\n}\n[ ! -e \"$JAVA_HOME/bin/java\" ] && JAVA_HOME=$HOME/jdk/java\n[ ! -e \"$JAVA_HOME/bin/java\" ] && JAVA_HOME=/usr/java\n[ ! -e \"$JAVA_HOME/bin/java\" ] && JAVA_HOME=/opt/taobao/java\n[ ! -e \"$JAVA_HOME/bin/java\" ] && unset JAVA_HOME\n\nif [ -z \"$JAVA_HOME\" ]; then\n  if $darwin; then\n\n    if [ -x '/usr/libexec/java_home' ] ; then\n      export JAVA_HOME=`/usr/libexec/java_home`\n\n    elif [ -d \"/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home\" ]; then\n      export JAVA_HOME=\"/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home\"\n    fi\n  else\n    JAVA_PATH=`dirname $(readlink -f $(which javac))`\n    if [ \"x$JAVA_PATH\" != \"x\" ]; then\n      export JAVA_HOME=`dirname $JAVA_PATH 2>/dev/null`\n    fi\n  fi\n  if [ -z \"$JAVA_HOME\" ]; then\n        error_exit \"请设置 JAVA_HOME 环境变量，需要jdk8及以上的java环境！\"\n  fi\nfi\n\nexport BASE_DIR=`cd $(dirname $0)/..; pwd`\n\nSERVER=\"reader\"\nNEWEST_JAR=$(ls $BASE_DIR/target | grep -Eo 'reader.*\\.jar' | sort -nr | head -1)\nif [ -n \"$NEWEST_JAR\" ]; then\n  SERVER=${NEWEST_JAR/.jar/}\nfi\n\nMODE=\"\"\nINVITE_CODE=\"\"\nSECURE_KEY=\"\"\nwhile getopts \":m:s:i:k:\" opt\ndo\n    case $opt in\n        m)\n            MODE=$OPTARG;;\n        s)\n            SERVER=$OPTARG;;\n        i)\n            INVITE_CODE=$OPTARG;;\n        k)\n            SECURE_KEY=$OPTARG;;\n        ?)\n        echo \"未知的参数: $opt\"\n        exit 1;;\n    esac\ndone\n\nexport JAVA_HOME\nexport JAVA=\"$JAVA_HOME/bin/java\"\nexport CUSTOM_SEARCH_LOCATIONS=file:${BASE_DIR}/conf/\n\n#===========================================================================================\n# JVM Configuration\n#===========================================================================================\nif [[ \"${MODE}\" == \"\" ]]; then\n    echo \"Reader 的运行模式以配置文件 conf/application.properties 为准。注意，当前未限制jvm内存\"\nelif [[ \"${MODE}\" == \"single\" ]]; then\n    JAVA_OPT=\"${JAVA_OPT} -Xms256m -Xmx256m -Xmn128m\"\n    JAVA_OPT=\"${JAVA_OPT} -Dreader.app.secure=false\"\n    echo \"Reader 将以单用户模式运行。注意，当前内存限制为256m\"\nelse\n    JAVA_OPT=\"${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn512m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=160m\"\n    JAVA_OPT=\"${JAVA_OPT} -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${BASE_DIR}/logs/java_heapdump.hprof\"\n    JAVA_OPT=\"${JAVA_OPT} -XX:-UseLargePages\"\n    JAVA_OPT=\"${JAVA_OPT} -Dreader.app.secure=true\"\n\n    TIPS=\"\"\n    if [[ \"${INVITE_CODE}\" != \"\" ]]; then\n      JAVA_OPT=\"${JAVA_OPT} -Dreader.app.inviteCode=${INVITE_CODE}\"\n      TIPS=\"${TIPS} 邀请码：${INVITE_CODE}\"\n    fi\n\n    if [[ \"${SECURE_KEY}\" != \"\" ]]; then\n      JAVA_OPT=\"${JAVA_OPT} -Dreader.app.secureKey=${SECURE_KEY}\"\n      TIPS=\"${TIPS} 管理员密码：${SECURE_KEY}\"\n    fi\n\n    if [[ \"${TIPS}\" == \"\" ]]; then\n      TIPS=\"邀请码和管理员密码以配置文件 conf/application.properties 为准\"\n    fi\n    TIPS=\"${TIPS}。注意，当前内存限制为1g\"\n    echo \"Reader 将以多用户模式运行。${TIPS}\"\nfi\n\nJAVA_MAJOR_VERSION=$($JAVA -version 2>&1 | sed -E -n 's/.* version \"([0-9]*).*$/\\1/p')\nif [[ \"$JAVA_MAJOR_VERSION\" -ge \"9\" ]] ; then\n  JAVA_OPT=\"${JAVA_OPT} -Xlog:gc*:file=${BASE_DIR}/logs/reader_gc.log:time,tags:filecount=10,filesize=100m\"\nelse\n  JAVA_OPT_EXT_FIX=\"-Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${JAVA_HOME}/lib/ext\"\n  JAVA_OPT=\"${JAVA_OPT} -Xloggc:${BASE_DIR}/logs/reader_gc.log -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M\"\nfi\n\n# JAVA_OPT=\"${JAVA_OPT} -Dloader.path=${BASE_DIR}/plugins,${BASE_DIR}/plugins/health,${BASE_DIR}/plugins/cmdb,${BASE_DIR}/plugins/selector\"\nJAVA_OPT=\"${JAVA_OPT} -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Dreader.app.workDir=${BASE_DIR}\"\nJAVA_OPT=\"${JAVA_OPT} -jar ${BASE_DIR}/target/${SERVER}.jar\"\nJAVA_OPT=\"${JAVA_OPT} ${JAVA_OPT_EXT}\"\nJAVA_OPT=\"${JAVA_OPT} --spring.config.additional-location=${CUSTOM_SEARCH_LOCATIONS}\"\n# JAVA_OPT=\"${JAVA_OPT} --logging.config=${BASE_DIR}/conf/nacos-logback.xml\"\nJAVA_OPT=\"${JAVA_OPT} --server.max-http-header-size=524288\"\n\nif [ ! -d \"${BASE_DIR}/logs\" ]; then\n  mkdir ${BASE_DIR}/logs\nfi\n\necho \"启动命令：\"\necho \"$JAVA $JAVA_OPT_EXT_FIX ${JAVA_OPT}\"\necho\n\n# check the start.out log output file\nif [ ! -f \"${BASE_DIR}/logs/start.out\" ]; then\n  touch \"${BASE_DIR}/logs/start.out\"\nelse\n  mv ${BASE_DIR}/logs/start.out ${BASE_DIR}/logs/start-$(date +'%Y-%m-%d_%H_%M').out\nfi\n# start\necho \"$JAVA $JAVA_OPT_EXT_FIX ${JAVA_OPT}\" > ${BASE_DIR}/logs/start.out 2>&1 &\n\nif [[ \"$JAVA_OPT_EXT_FIX\" == \"\" ]]; then\n  nohup \"$JAVA\" ${JAVA_OPT} reader.server >> ${BASE_DIR}/logs/start.out 2>&1 &\nelse\n  nohup \"$JAVA\" \"$JAVA_OPT_EXT_FIX\" ${JAVA_OPT} reader.server >> ${BASE_DIR}/logs/start.out 2>&1 &\nfi\n\necho \"Reader 正在启动中，你可以在 ${BASE_DIR}/logs/start.out 查看日志\"\n"
  },
  {
    "path": "server/conf/application.properties",
    "content": "# 是否多用户模式，如果启动 startup 脚本时使用了 -m 1 选择多用户模式，-m single 运行单用户模式，否则根据此处的参数选择模式\nreader.app.secure=true\n\n# 邀请码，如果启动 startup 脚本时使用了参数 -i 邀请码，则会覆盖此处\nreader.app.inviteCode=\n\n# 管理密码，如果启动 startup 脚本时使用了参数 -k 管理密码，则会覆盖此处\nreader.app.secureKey=\n\n# 书源代理可通过 header 设置\n\n# 是否缓存章节内容\nreader.app.cacheChapterContent=true\n\n# 用户上限，免费版用户上限最大15\nreader.app.userLimit=15\n\n# 是否开启书源调试日志\nreader.app.debugLog=false\n\n# 自动清理不活跃用户，单位天，0为不清理，大于0的数字为清理多少天未登录用户\nreader.app.autoClearInactiveUser=0\n\n# mongodb数据备份，mongodb链接地址\nreader.app.mongoUri=\n# mongodb数据备份，mongodb数据库名\nreader.app.mongoDbName=reader\n\n# 书架自动更新间隔，单位分钟\nreader.app.shelfUpdateInteval=10\n\n# 远程webview接口地址，可通过部署 hectorqin/remote-webview 来设置，http://IP:8050\nreader.app.remoteWebviewApi=\n\n# 新用户默认是否启用 webdav\nreader.app.defaultUserEnableWebdav=true\n# 新用户是否默认启用 本地书仓\nreader.app.defaultUserEnableLocalStore=true\n# 新用户是否默认启用 书源编辑\nreader.app.defaultUserEnableBookSource=true\n# 新用户是否默认启用 RSS源编辑\nreader.app.defaultUserEnableRssSource=true\n# 新用户是否默认书源数量上限\nreader.app.defaultUserBookSourceLimit=100\n# 新用户是否默认书籍数量上限\nreader.app.defaultUserBookLimit=200\n\n# 是否自动备份用户数据\nreader.app.autoBackupUserData=false\n\n# reader服务监听端口\nreader.server.port=8080\n# reader接口目录\nreader.server.contextPath="
  },
  {
    "path": "settings.gradle",
    "content": "pluginManagement {\n    repositories {\n        gradlePluginPortal()\n    }\n}\nrootProject.name = 'reader'\n"
  },
  {
    "path": "src/main/java/com/htmake/reader/ReaderApplication.kt",
    "content": "package com.htmake.reader\n\nimport com.fasterxml.jackson.databind.DeserializationFeature\nimport com.fasterxml.jackson.module.kotlin.registerKotlinModule\nimport io.vertx.core.Future\nimport io.vertx.core.Vertx\nimport io.vertx.core.http.*\nimport io.vertx.core.json.Json\nimport io.vertx.ext.web.client.WebClient\nimport io.vertx.ext.web.client.WebClientOptions\nimport mu.KotlinLogging\nimport com.htmake.reader.api.YueduApi\n\nimport com.htmake.reader.verticle.RestVerticle\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.boot.SpringApplication\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.scheduling.annotation.EnableScheduling;\nimport org.springframework.context.annotation.Bean\nimport javax.annotation.PostConstruct\n\nprivate val logger = KotlinLogging.logger {}\n\n@SpringBootApplication\n@EnableScheduling\nclass ReaderApplication {\n\n    @Autowired\n    private lateinit var yueduApi: YueduApi\n\n    companion object {\n        val vertx by lazy { Vertx.vertx() }\n        fun vertx() = vertx\n    }\n\n    @PostConstruct\n    fun deployVerticle() {\n        Json.mapper.apply {\n            registerKotlinModule()\n        }\n\n        Json.prettyMapper.apply {\n            registerKotlinModule()\n        }\n\n        Json.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);\n\n        vertx().deployVerticle(yueduApi)\n    }\n\n    @Bean\n    fun webClient(): WebClient {\n        val webClientOptions = WebClientOptions()\n        webClientOptions.isTryUseCompression = true\n        webClientOptions.logActivity = true\n        webClientOptions.isFollowRedirects = true\n        webClientOptions.isTrustAll = true\n\n        val httpClient = vertx().createHttpClient(HttpClientOptions().setTrustAll(true))\n\n//        val webClient = WebClient.wrap(HttpClient(delegateHttpClient), webClientOptions)\n        val webClient = WebClient.wrap(httpClient, webClientOptions)\n\n        return webClient\n    }\n\n}\n\nfun main(args: Array<String>) {\n    SpringApplication.run(ReaderApplication::class.java, *args)\n}\n\n\n\n\n"
  },
  {
    "path": "src/main/java/com/htmake/reader/ReaderUIApplication.kt",
    "content": "package com.htmake.reader\n\nimport com.fasterxml.jackson.databind.DeserializationFeature\nimport com.fasterxml.jackson.module.kotlin.registerKotlinModule\nimport io.vertx.core.Future\nimport io.vertx.core.Vertx\nimport io.vertx.core.http.*\nimport io.vertx.core.http.impl.HttpUtils\nimport io.vertx.core.json.Json\nimport io.vertx.ext.web.client.WebClient\nimport io.vertx.ext.web.client.WebClientOptions\nimport io.vertx.core.json.JsonObject\nimport mu.KotlinLogging\nimport com.htmake.reader.api.YueduApi\nimport com.htmake.reader.entity.Size\n\nimport com.htmake.reader.verticle.RestVerticle\nimport com.htmake.reader.utils.SpringContextUtils\nimport com.htmake.reader.SpringEvent\n\nimport com.htmake.reader.utils.getStorage\nimport com.htmake.reader.utils.saveStorage\nimport com.htmake.reader.utils.asJsonObject\n\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.boot.SpringApplication\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent\nimport org.springframework.boot.context.event.ApplicationReadyEvent\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.ConfigurableApplicationContext\nimport org.springframework.context.ApplicationListener\nimport org.springframework.context.ApplicationEvent\nimport uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J\nimport javax.annotation.PostConstruct\n\nimport javafx.application.Application\nimport javafx.application.Platform\nimport javafx.scene.Scene\nimport javafx.scene.web.WebView\nimport javafx.scene.web.WebErrorEvent\nimport javafx.stage.Stage\nimport javafx.stage.WindowEvent\nimport javafx.stage.StageStyle\nimport javafx.event.EventHandler\nimport com.sun.javafx.application.LauncherImpl\nimport com.sun.javafx.scene.text.FontHelper\nimport javafx.scene.text.Font\n\nimport javafx.scene.control.ProgressBar\nimport javafx.scene.control.Dialog\nimport javafx.scene.control.ButtonType\nimport javafx.scene.image.ImageView\nimport javafx.scene.layout.VBox\nimport javafx.scene.paint.Color;\nimport javafx.scene.image.Image;\nimport javafx.beans.value.ChangeListener\nimport javafx.beans.value.ObservableValue\nimport javafx.concurrent.Worker\n\nimport org.springframework.core.env.Environment\nimport org.springframework.core.env.ConfigurableEnvironment\nimport org.springframework.core.env.MapPropertySource\n\nimport java.util.concurrent.CompletableFuture\n\nprivate val logger = KotlinLogging.logger {}\nprivate var launchArgs = arrayOf<String>()\n\nclass ReaderUIApplication: Application() {\n\n    private lateinit var primaryStage: Stage;\n    private lateinit var splashStage: Stage;\n\n    lateinit var webUrl: String\n    lateinit var env: ConfigurableEnvironment\n    var windowConfigMap = mutableMapOf<String, Any>()\n\n    var isSpringBootLaunched = false\n    var springBootError = \"\"\n    var showUI = false\n\n    var defaultIcons = arrayOf<Image>();\n\n    fun boot() {\n        launch(*launchArgs)\n    }\n\n    override fun init() {\n        Thread() {\n            var app = SpringApplication(ReaderApplication::class.java)\n            var envListener = object: ApplicationListener<ApplicationEnvironmentPreparedEvent> {\n                override fun onApplicationEvent(event: ApplicationEnvironmentPreparedEvent) {\n                    env = event.getEnvironment()\n                    // 加载 windowConfig\n                    var windowConfigSource = loadPropertySourceFromWindowConfig()\n                    env.getPropertySources().addFirst(windowConfigSource)\n                    // 获取应用相关配置\n                    showUI = env.getProperty(\"reader.app.showUI\", Boolean::class.java) ?: false\n                    logger.info(\"showUI: {}\", showUI)\n                    var debug = env.getProperty(\"reader.app.debug\", Boolean::class.java)\n                    logger.info(\"debug: {}\", debug)\n                    var serverPort = env.getProperty(\"reader.server.port\", Int::class.java)\n                    logger.info(\"serverPort: {}\", serverPort)\n                    var port = 8080\n                    if (serverPort != null && serverPort > 0) {\n                        port = serverPort;\n                    }\n                    webUrl = env.getProperty(\"reader.server.webUrl\") ?: (\"http://localhost:\" + port)\n                    var sep = if(webUrl.contains(\"?\")) {\n                        \"&\"\n                    } else {\n                        \"?\"\n                    }\n                    if (debug != null && debug) {\n                        webUrl = webUrl + sep + \"debug=1&nopwa=1\"\n                    } else {\n                        webUrl = webUrl + sep + \"nopwa=1\"\n                    }\n                    logger.info(\"webUrl: {}\", webUrl)\n                    // System.setProperty(\"reader.system.fonts\", Font.getFontNames().joinToString(separator = \",\"))\n                    if (showUI && ::primaryStage.isInitialized){\n                        Platform.runLater(object : Runnable {\n                            override fun run() {\n                                showSplashScreen()\n                            }\n                        })\n                    }\n                }\n            }\n            app.addListeners(envListener)\n            var springListener = object: ApplicationListener<SpringEvent> {\n                override fun onApplicationEvent(event: SpringEvent) {\n                    val eventType = event.getEvent()\n                    if (eventType == \"READY\") {\n                        isSpringBootLaunched = true\n                        if (showUI && ::primaryStage.isInitialized && ::webUrl.isInitialized){\n                            Platform.runLater(object : Runnable {\n                                override fun run() {\n                                    splashStage.hide()\n                                    splashStage.setScene(null)\n                                    showWebScreen(primaryStage, webUrl)\n                                }\n                            })\n                        }\n                    } else if (eventType == \"START_ERROR\") {\n                        springBootError = event.getMessage()\n                        if (showUI){\n                            Platform.runLater(object : Runnable {\n                                override fun run() {\n                                    if (::splashStage.isInitialized) {\n                                        splashStage.hide()\n                                        splashStage.setScene(null)\n                                    }\n                                    showAlert(springBootError);\n                                    stop();\n                                }\n                            })\n                        } else {\n                            logger.error(springBootError);\n                            stop();\n                        }\n                    }\n                }\n            }\n            app.addListeners(springListener)\n            app.run(*launchArgs)\n        }.start()\n    }\n\n    override fun start(stage: Stage) {\n        try {\n            logger.info(\"javafx start: {}\", stage)\n            primaryStage = stage\n            if (showUI) {\n                defaultIcons = arrayOf<Image>(\n                    Image(ReaderUIApplication::class.java.getResource(\"/icons/16x16.png\").toExternalForm()),\n                    Image(ReaderUIApplication::class.java.getResource(\"/icons/24x24.png\").toExternalForm()),\n                    Image(ReaderUIApplication::class.java.getResource(\"/icons/32x32.png\").toExternalForm()),\n                    Image(ReaderUIApplication::class.java.getResource(\"/icons/48x48.png\").toExternalForm()),\n                    Image(ReaderUIApplication::class.java.getResource(\"/icons/64x64.png\").toExternalForm()),\n                    Image(ReaderUIApplication::class.java.getResource(\"/icons/128x128.png\").toExternalForm())\n                )\n                if (isSpringBootLaunched) {\n                    showWebScreen(stage, webUrl)\n                } else {\n                    if (springBootError.isNotEmpty()) {\n                        showAlert(springBootError)\n                        stop()\n                    } else {\n                        showSplashScreen()\n                    }\n                }\n            }\n        } catch(e: Exception) {\n            e.printStackTrace()\n        }\n    }\n\n    fun showSplashScreen() {\n        splashStage = Stage()\n        var imageView = ImageView(ReaderUIApplication::class.java.getResource(\"/images/loading.gif\").toExternalForm());\n        // var splashProgressBar = ProgressBar();\n        // splashProgressBar.setPrefWidth(imageView.getImage().getWidth());\n        // splashProgressBar.setPrefHeight(10.0);\n\n        var vbox = VBox();\n        vbox.getChildren().addAll(imageView);\n        // vbox.setStyle(\"-fx-background-color: transparent;\" +\n        //               \"-fx-padding: 0;\" +\n        //               \"-fx-border-style: solid inside;\" +\n        //               \"-fx-border-width: 1;\" +\n        //               \"-fx-border-insets: 0;\" +\n        //               \"-fx-border-radius: 0;\" +\n        //               \"-fx-border-color: #999;\");\n\n        var splashScene = Scene(vbox, Color.TRANSPARENT);\n        splashStage.setScene(splashScene);\n        splashStage.getIcons().addAll(defaultIcons);\n        splashStage.initStyle(StageStyle.TRANSPARENT);\n        logger.info(\"showSplashScreen: {}\", splashStage)\n        splashStage.show()\n    }\n\n    fun showAlert(message: String, wait: Boolean = true) {\n        var alert = Dialog<Any>();\n        alert.getDialogPane().setContentText(message);\n        alert.getDialogPane().getButtonTypes().add(ButtonType.OK);\n        if (wait) {\n            alert.showAndWait();\n        } else {\n            alert.show();\n        }\n    }\n\n    fun showConfirm(message: String): Boolean {\n        var confirm = Dialog<Any>();\n        confirm.getDialogPane().setContentText(message);\n        confirm.getDialogPane().getButtonTypes().addAll(ButtonType.YES, ButtonType.NO);\n        val result = confirm.showAndWait().filter(ButtonType.YES::equals).isPresent();\n\n        return result\n    }\n\n    fun loadPropertySourceFromWindowConfig(): MapPropertySource {\n        loadWindowConfig()\n        var windowConfigPort = 0\n        var windowConfigSource = mutableMapOf<String, Any>()\n        try {\n            // 支持配置 接口服务\n            val serverConfig = windowConfigMap.getOrDefault(\"serverConfig\", null) as? MutableMap<String, Any>\n            if (serverConfig != null) {\n                windowConfigSource = serverConfig\n            }\n\n            val serverPort = windowConfigMap.getOrDefault(\"serverPort\", null)\n            if (serverPort != null) {\n                windowConfigPort = serverPort as Int\n                if (windowConfigPort > 0) {\n                    windowConfigSource.put(\"reader.server.port\", windowConfigPort)\n                }\n            }\n            val showUI = windowConfigMap.getOrDefault(\"showUI\", true) as Boolean? ?: true\n            windowConfigSource.put(\"reader.app.showUI\", showUI)\n            val debug = windowConfigMap.getOrDefault(\"debug\", null)\n            if (debug != null) {\n                windowConfigSource.put(\"reader.app.debug\", debug as Boolean)\n            }\n        } catch(e: Exception) {\n            e.printStackTrace()\n        }\n\n        logger.info(\"windowConfigSource: {}\", windowConfigSource)\n        return MapPropertySource(\"windowConfig\", windowConfigSource)\n    }\n\n    fun loadWindowConfig() {\n        val windowConfigObject = asJsonObject(getStorage(\"windowConfig\"))\n        if (windowConfigObject != null) {\n            windowConfigMap = windowConfigObject.map\n        }\n        logger.info(\"windowConfigMap: {}\", windowConfigMap)\n    }\n\n    fun getWindowConfigDoubleProperty(name: String, defaultVal: Double): Double {\n        var value = windowConfigMap.getOrDefault(name, defaultVal)\n        return when(value) {\n            is Int -> value.toDouble()\n            is Double -> value\n            else -> defaultVal\n        }\n    }\n\n    fun applyWindowConfig(stage: Stage): Size {\n        var width = 1280.0;\n        var height = 800.0;\n        try {\n            loadWindowConfig()\n            val setWindowPosition = windowConfigMap.getOrDefault(\"setWindowPosition\", false) as Boolean? ?: false\n            if (setWindowPosition) {\n                var positionX = getWindowConfigDoubleProperty(\"positionX\", 0.0)\n                var positionY = getWindowConfigDoubleProperty(\"positionY\", 0.0)\n                stage.setX(positionX)\n                stage.setY(positionY)\n            }\n            val rememberSize = windowConfigMap.getOrDefault(\"rememberSize\", true) as Boolean? ?: true\n            val rememberPosition = windowConfigMap.getOrDefault(\"rememberPosition\", false) as Boolean? ?: false\n            if (rememberSize) {\n                stage.widthProperty().addListener{_, _, w ->\n                    windowConfigMap.put(\"width\", w)\n                }\n                // stage.heightProperty().addListener{_, _, h ->\n                //     windowConfigMap.put(\"height\", h)\n                // }\n                stage.sceneProperty().addListener{_, _, s ->\n                    s.heightProperty().addListener{_, _, h ->\n                        windowConfigMap.put(\"height\", h)\n                    }\n                }\n            }\n            if (rememberPosition) {\n                stage.xProperty().addListener{_, _, x ->\n                    windowConfigMap.put(\"positionX\", x)\n                }\n                stage.yProperty().addListener{_, _, y ->\n                    windowConfigMap.put(\"positionY\", y)\n                }\n            }\n            val setWindowSize = windowConfigMap.getOrDefault(\"setWindowSize\", true) as Boolean? ?: true\n            if (setWindowSize) {\n                width = getWindowConfigDoubleProperty(\"width\", width)\n                height = getWindowConfigDoubleProperty(\"height\", height)\n            }\n        } catch(e: Exception) {\n            showAlert(\"窗口配置加载失败，请检查窗口配置文件(windowConfig.json)\", false)\n            e.printStackTrace()\n        }\n        return Size(width, height)\n    }\n\n    fun showWebScreen(stage: Stage, url: String) {\n        // 配置主窗口\n        var windowSize = applyWindowConfig(stage);\n        System.setProperty(\"sun.net.http.allowRestrictedHeaders\", \"true\")\n        // logger.info(\"Font.getFontNames: {}\", Font.getFontNames())\n        // logger.info(\"showWebScreen: {}\", url)\n        var webView = WebView();\n        var webEngine = webView.getEngine();\n        webEngine.setOnError{ event ->\n            logger.info(\"error: {}\", event)\n        };\n        webEngine.setOnAlert{ event ->\n            showAlert(event.data.toString())\n        };\n        webEngine.setConfirmHandler{ message ->\n            showConfirm(message)\n        };\n        var reloadCount = 0;\n        webEngine.getLoadWorker().stateProperty().addListener{_, oldState, newState ->\n            logger.info(\"State from {} to {} , exception: {}\", oldState, newState, webEngine.getLoadWorker().getException());\n            if (newState == Worker.State.FAILED) {\n                if (reloadCount < 5) {\n                    reloadCount += 1\n                    logger.info(\"reload {}\", url)\n                    webEngine.load(url);\n                }\n            }\n        }\n        webEngine.titleProperty().addListener{_, _, t ->\n            if (t != null && t.isNotEmpty()) {\n                stage.setTitle(t)\n            }\n        }\n        webEngine.load(url);\n        val scene = Scene(webView, windowSize.width, windowSize.height)\n        stage.setScene(scene)\n        stage.setTitle(\"阅读\")\n        stage.getIcons().addAll(defaultIcons);\n        stage.initStyle(StageStyle.UNIFIED);\n        stage.show()\n    }\n\n    override fun stop() {\n        saveStorage(\"windowConfig\", value = windowConfigMap, pretty = true)\n        super.stop()\n        var context = SpringContextUtils.getApplicationContext()\n        logger.info(\"application stop: {}\", context)\n        System.exit(SpringApplication.exit(context))\n    }\n}\n\nfun main(args: Array<String>) {\n    logger.info(\"args: {}\", args)\n    launchArgs = args\n    val app = ReaderUIApplication()\n    app.boot()\n}\n\n\n\n\n"
  },
  {
    "path": "src/main/java/com/htmake/reader/SpringEvent.java",
    "content": "package com.htmake.reader;\n\nimport org.springframework.context.ApplicationEvent;\n\npublic class SpringEvent extends ApplicationEvent {\n    private String event;\n    private String message;\n    public SpringEvent(Object source, String event, String message) {\n        super(source);\n        this.event = event;\n        this.message = message;\n    }\n    public String getEvent() {\n        return event;\n    }\n    public void setEvent(String event) {\n        this.event = event;\n    }\n    public String getMessage() {\n        return message;\n    }\n    public void setMessage(String message) {\n        this.message = message;\n    }\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/api/ReturnData.kt",
    "content": "package com.htmake.reader.api\n\n\nclass ReturnData {\n\n    var isSuccess: Boolean = false\n        private set\n\n    var errorMsg: String = \"未知错误,请联系开发者!\"\n        private set\n\n    var data: Any? = null\n        private set\n\n    fun setErrorMsg(errorMsg: String): ReturnData {\n        this.isSuccess = false\n        this.errorMsg = errorMsg\n        return this\n    }\n\n    fun setData(data: Any): ReturnData {\n        this.isSuccess = true\n        this.errorMsg = \"\"\n        this.data = data\n        return this\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/htmake/reader/api/YueduApi.kt",
    "content": "package com.htmake.reader.api\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.SearchBook\nimport io.legado.app.data.entities.BookGroup\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.RssSource\nimport io.legado.app.data.entities.RssArticle\nimport io.legado.app.model.webBook.WebBook\nimport io.vertx.ext.web.Router\nimport io.vertx.ext.web.RoutingContext\nimport io.vertx.ext.web.handler.StaticHandler;\nimport mu.KotlinLogging\nimport com.htmake.reader.config.AppConfig\nimport com.htmake.reader.config.BookConfig\nimport io.legado.app.constant.DeepinkBookSource\nimport com.htmake.reader.api.controller.BookController\nimport com.htmake.reader.api.controller.BookSourceController\nimport com.htmake.reader.api.controller.RssSourceController\nimport com.htmake.reader.api.controller.UserController\nimport com.htmake.reader.api.controller.WebdavController\nimport com.htmake.reader.api.controller.ReplaceRuleController\nimport com.htmake.reader.api.controller.BookmarkController\nimport com.htmake.reader.utils.error\nimport com.htmake.reader.utils.success\nimport com.htmake.reader.utils.getStorage\nimport com.htmake.reader.utils.saveStorage\nimport com.htmake.reader.utils.asJsonArray\nimport com.htmake.reader.utils.asJsonObject\nimport com.htmake.reader.utils.toDataClass\nimport com.htmake.reader.utils.toMap\nimport com.htmake.reader.utils.fillData\nimport com.htmake.reader.utils.getWorkDir\nimport com.htmake.reader.utils.getRandomString\nimport com.htmake.reader.utils.genEncryptedPassword\nimport com.htmake.reader.entity.User\nimport com.htmake.reader.utils.SpringContextUtils\nimport com.htmake.reader.utils.deleteRecursively\nimport com.htmake.reader.utils.unzip\nimport com.htmake.reader.utils.zip\nimport com.htmake.reader.utils.jsonEncode\nimport com.htmake.reader.utils.getRelativePath\nimport com.htmake.reader.verticle.RestVerticle\nimport com.htmake.reader.SpringEvent\nimport org.springframework.stereotype.Component\nimport io.vertx.core.json.JsonObject\nimport io.vertx.core.json.JsonArray\nimport io.vertx.core.http.HttpMethod\nimport com.htmake.reader.api.ReturnData\nimport io.legado.app.utils.MD5Utils\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.net.URL;\nimport java.util.UUID;\nimport io.vertx.ext.web.client.WebClient\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.core.env.Environment\nimport java.io.File\nimport java.lang.Runtime\nimport kotlin.collections.mutableMapOf\nimport kotlin.system.measureTimeMillis\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.text.SimpleDateFormat;\nimport io.legado.app.utils.EncoderUtils\nimport io.legado.app.model.rss.Rss\nimport org.springframework.scheduling.annotation.Scheduled\nimport io.legado.app.model.localBook.LocalBook\nimport java.nio.file.Paths\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.CoroutineScope\n\nprivate val logger = KotlinLogging.logger {}\n\n@Component\nclass YueduApi : RestVerticle() {\n    @Autowired\n    private lateinit var appConfig: AppConfig\n\n    @Autowired\n    private lateinit var env: Environment\n\n    override suspend fun initRouter(router: Router) {\n        setupPort()\n\n        // 旧版数据迁移\n        migration()\n\n        // web界面\n        router.route(\"/*\").handler(StaticHandler.create(\"web\").setDefaultContentEncoding(\"UTF-8\"));\n\n        // assets\n        var assetsDir = getWorkDir(\"storage\", \"assets\");\n        var assetsDirFile = File(assetsDir);\n        if (!assetsDirFile.exists()) {\n            assetsDirFile.mkdirs();\n        }\n        var assetsCss = getWorkDir(\"storage\", \"assets\", \"reader.css\");\n        var assetsCssFile = File(assetsCss);\n        if (!assetsCssFile.exists()) {\n            assetsCssFile.writeText(\"/* 在此处可以编写CSS样式来自定义页面 */\");\n        }\n        router.route(\"/assets/*\").handler(StaticHandler.create().setAllowRootFileSystemAccess(true).setWebRoot(assetsDir).setDefaultContentEncoding(\"UTF-8\"));\n\n        // epub资源\n        var dataDir = getWorkDir(\"storage\", \"data\");\n        router.route(\"/epub/*\").handler {\n            var path = it.request().path().replace(\"/epub/\", \"/\", true)\n            path = URLDecoder.decode(path, \"UTF-8\")\n            if (path.endsWith(\"html\", true)) {\n                var filePath = File(dataDir + path)\n                if (filePath.exists()) {\n                    // 处理 js 注入脚本\n                    BookConfig.injectJavascriptToEpubChapter(filePath.toString())\n                }\n            }\n            it.next()\n        }\n        router.route(\"/epub/*\").handler(StaticHandler.create().setAllowRootFileSystemAccess(true).setWebRoot(dataDir).setDefaultContentEncoding(\"UTF-8\"));\n\n        // 获取系统信息\n        router.get(\"/reader3/getSystemInfo\").coroutineHandler { getSystemInfo(it) }\n\n\n        ////////// 接口部分\n        val bookController = BookController(coroutineContext)\n        val bookSourceController = BookSourceController(coroutineContext)\n        val rssSourceController = RssSourceController(coroutineContext)\n        val userController = UserController(coroutineContext)\n        val webdavController = WebdavController(coroutineContext, router) { ctx, error ->\n            onHandlerError(ctx, error)\n        }\n        val replaceRuleController = ReplaceRuleController(coroutineContext)\n        val bookmarkController = BookmarkController(coroutineContext)\n\n        /** 书源模块 */\n        router.post(\"/reader3/saveBookSource\").coroutineHandler { bookSourceController.saveBookSource(it) }\n        router.post(\"/reader3/saveBookSources\").coroutineHandler { bookSourceController.saveBookSources(it) }\n\n        router.get(\"/reader3/getBookSource\").coroutineHandler { bookSourceController.getBookSource(it) }\n        router.post(\"/reader3/getBookSource\").coroutineHandler { bookSourceController.getBookSource(it) }\n        router.get(\"/reader3/getBookSources\").coroutineHandler { bookSourceController.getBookSources(it) }\n        router.post(\"/reader3/getBookSources\").coroutineHandler { bookSourceController.getBookSources(it) }\n\n        router.post(\"/reader3/deleteAllBookSources\").coroutineHandler { bookSourceController.deleteAllBookSources(it) }\n        router.post(\"/reader3/deleteBookSource\").coroutineHandler { bookSourceController.deleteBookSource(it) }\n        router.post(\"/reader3/deleteBookSources\").coroutineHandler { bookSourceController.deleteBookSources(it) }\n\n        // 上传书源文件\n        router.post(\"/reader3/readSourceFile\").coroutineHandler { bookSourceController.readSourceFile(it) }\n\n        // 读取远程书源文件\n        router.post(\"/reader3/readRemoteSourceFile\").coroutineHandlerWithoutRes { bookSourceController.readRemoteSourceFile(it) }\n\n        // 设置默认书源\n        router.post(\"/reader3/setAsDefaultBookSources\").coroutineHandler { bookSourceController.setAsDefaultBookSources(it) }\n        router.post(\"/reader3/deleteUserBookSource\").coroutineHandler { bookSourceController.deleteUserBookSource(it) }\n        router.post(\"/reader3/deleteBookSourcesFile\").coroutineHandler { bookSourceController.deleteBookSourcesFile(it) }\n\n        /** 书籍模块 */\n        // 书架\n        router.get(\"/reader3/getBookshelf\").coroutineHandler { bookController.getBookshelf(it) }\n        router.get(\"/reader3/getShelfBook\").coroutineHandler { bookController.getShelfBook(it) }\n        router.post(\"/reader3/saveBook\").coroutineHandler { bookController.saveBook(it) }\n        router.post(\"/reader3/deleteBook\").coroutineHandler { bookController.deleteBook(it) }\n        router.post(\"/reader3/deleteBooks\").coroutineHandler { bookController.deleteBooks(it) }\n\n        // 失效书源\n        router.post(\"/reader3/getInvalidBookSources\").coroutineHandler { bookController.getInvalidBookSources(it) }\n\n        // 探索\n        router.post(\"/reader3/exploreBook\").coroutineHandler { bookController.exploreBook(it) }\n        router.get(\"/reader3/exploreBook\").coroutineHandler { bookController.exploreBook(it) }\n\n        // 搜索\n        router.get(\"/reader3/searchBook\").coroutineHandler { bookController.searchBook(it) }\n        router.post(\"/reader3/searchBook\").coroutineHandler { bookController.searchBook(it) }\n        router.get(\"/reader3/searchBookMulti\").coroutineHandler { bookController.searchBookMulti(it) }\n        router.post(\"/reader3/searchBookMulti\").coroutineHandler { bookController.searchBookMulti(it) }\n        router.get(\"/reader3/searchBookMultiSSE\").coroutineHandlerWithoutRes { bookController.searchBookMultiSSE(it) }\n\n        // 书籍详情\n        router.get(\"/reader3/getBookInfo\").coroutineHandler { bookController.getBookInfo(it) }\n        router.post(\"/reader3/getBookInfo\").coroutineHandler { bookController.getBookInfo(it) }\n\n        // 章节列表\n        router.get(\"/reader3/getChapterList\").coroutineHandler { bookController.getChapterList(it) }\n        router.post(\"/reader3/getChapterList\").coroutineHandler { bookController.getChapterList(it) }\n\n        // 内容\n        router.get(\"/reader3/getBookContent\").coroutineHandler { bookController.getBookContent(it) }\n        router.post(\"/reader3/getBookContent\").coroutineHandler { bookController.getBookContent(it) }\n\n        // 保存阅读进度\n        router.post(\"/reader3/saveBookProgress\").coroutineHandler { bookController.saveBookProgress(it) }\n\n        // 封面\n        router.get(\"/reader3/cover\").coroutineHandlerWithoutRes { bookController.getBookCover(it) }\n\n        // 搜索其它来源\n        router.get(\"/reader3/searchBookSource\").coroutineHandler { bookController.searchBookSource(it) }\n        router.post(\"/reader3/searchBookSource\").coroutineHandler { bookController.searchBookSource(it) }\n        router.get(\"/reader3/getAvailableBookSource\").coroutineHandler { bookController.getAvailableBookSource(it) }\n        router.post(\"/reader3/getAvailableBookSource\").coroutineHandler { bookController.getAvailableBookSource(it) }\n        router.get(\"/reader3/searchBookSourceSSE\").coroutineHandlerWithoutRes { bookController.searchBookSourceSSE(it) }\n\n        // 换源\n        router.get(\"/reader3/setBookSource\").coroutineHandler { bookController.setBookSource(it) }\n        router.post(\"/reader3/setBookSource\").coroutineHandler { bookController.setBookSource(it) }\n\n        // 修改分组\n        router.post(\"/reader3/saveBookGroupId\").coroutineHandler { bookController.saveBookGroupId(it) }\n        router.post(\"/reader3/addBookGroupMulti\").coroutineHandler { bookController.addBookGroupMulti(it) }\n        router.post(\"/reader3/removeBookGroupMulti\").coroutineHandler { bookController.removeBookGroupMulti(it) }\n\n        // 导入本地文件\n        router.post(\"/reader3/importBookPreview\").coroutineHandler { bookController.importBookPreview(it) }\n        router.post(\"/reader3/refreshLocalBook\").coroutineHandler { bookController.refreshLocalBook(it) }\n\n        // 获取txt章节规则\n        router.get(\"/reader3/getTxtTocRules\").coroutineHandler { bookController.getTxtTocRules(it) }\n        router.post(\"/reader3/getChapterListByRule\").coroutineHandler { bookController.getChapterListByRule(it) }\n\n        // 书籍分组\n        router.get(\"/reader3/getBookGroups\").coroutineHandler { bookController.getBookGroups(it) }\n        router.post(\"/reader3/saveBookGroup\").coroutineHandler { bookController.saveBookGroup(it) }\n        router.post(\"/reader3/deleteBookGroup\").coroutineHandler { bookController.deleteBookGroup(it) }\n        router.post(\"/reader3/saveBookGroupOrder\").coroutineHandler { bookController.saveBookGroupOrder(it) }\n\n        // 书仓功能\n        // 获取书仓文件列表\n        router.get(\"/reader3/getLocalStoreFileList\").coroutineHandler { bookController.getLocalStoreFileList(it) }\n        // 下载书仓文件\n        router.get(\"/reader3/getLocalStoreFile\").coroutineHandlerWithoutRes { bookController.getLocalStoreFile(it) }\n        // 删除书仓文件\n        router.post(\"/reader3/deleteLocalStoreFile\").coroutineHandler { bookController.deleteLocalStoreFile(it) }\n        router.post(\"/reader3/deleteLocalStoreFileList\").coroutineHandler { bookController.deleteLocalStoreFileList(it) }\n        // 从本地书仓/webdav导入\n        router.post(\"/reader3/importFromLocalPathPreview\").coroutineHandler { bookController.importFromLocalPathPreview(it) }\n        // 上传文件到书仓\n        router.post(\"/reader3/uploadFileToLocalStore\").coroutineHandler { bookController.uploadFileToLocalStore(it) }\n\n        // 调试书源\n        router.get(\"/reader3/bookSourceDebugSSE\").coroutineHandlerWithoutRes { bookController.bookSourceDebugSSE(it) }\n\n        // 缓存书籍章节\n        router.get(\"/reader3/cacheBookSSE\").coroutineHandlerWithoutRes { bookController.cacheBookSSE(it) }\n        // 获取书籍缓存信息\n        router.get(\"/reader3/getShelfBookWithCacheInfo\").coroutineHandler { bookController.getShelfBookWithCacheInfo(it) }\n        // 删除书籍章节缓存\n        router.post(\"/reader3/deleteBookCache\").coroutineHandler { bookController.deleteBookCache(it) }\n\n        // 导出书籍\n        router.post(\"/reader3/exportBook\").coroutineHandlerWithoutRes { bookController.exportBook(it) }\n        router.get(\"/reader3/exportBook\").coroutineHandlerWithoutRes { bookController.exportBook(it) }\n\n        // 全文搜索\n        router.get(\"/reader3/searchBookContent\").coroutineHandler { bookController.searchBookContent(it) }\n        router.post(\"/reader3/searchBookContent\").coroutineHandler { bookController.searchBookContent(it) }\n\n        /** 用户模块 */\n        // 上传文件\n        router.post(\"/reader3/uploadFile\").coroutineHandler { userController.uploadFile(it) }\n\n        // 删除文件\n        router.post(\"/reader3/deleteFile\").coroutineHandler { userController.deleteFile(it) }\n\n        // 登录\n        router.post(\"/reader3/login\").coroutineHandler { userController.login(it) }\n        // 注销登录\n        router.post(\"/reader3/logout\").coroutineHandler { userController.logout(it) }\n\n        // 获取用户信息\n        router.get(\"/reader3/getUserInfo\").coroutineHandler { userController.getUserInfo(it) }\n\n        // 用户备份本地配置\n        router.post(\"/reader3/saveUserConfig\").coroutineHandler { userController.saveUserConfig(it) }\n\n        // 用户恢复本地配置\n        router.get(\"/reader3/getUserConfig\").coroutineHandler { userController.getUserConfig(it) }\n\n        // 获取用户列表\n        router.get(\"/reader3/getUserList\").coroutineHandler { userController.getUserList(it) }\n\n        // 删除用户\n        router.post(\"/reader3/deleteUsers\").coroutineHandler { userController.deleteUsers(it) }\n\n        // 添加用户\n        router.post(\"/reader3/addUser\").coroutineHandler { userController.addUser(it) }\n\n        // 重置用户密码\n        router.post(\"/reader3/resetPassword\").coroutineHandler { userController.resetPassword(it) }\n\n        // 更新用户\n        router.post(\"/reader3/updateUser\").coroutineHandler { userController.updateUser(it) }\n\n\n        /** webdav模块 */\n        // 获取webdav备份列表\n        router.get(\"/reader3/getWebdavFileList\").coroutineHandler { webdavController.getWebdavFileList(it) }\n\n        // 下载webdav文件\n        router.get(\"/reader3/getWebdavFile\").coroutineHandlerWithoutRes { webdavController.getWebdavFile(it) }\n\n        // 上传webdav文件\n        router.post(\"/reader3/uploadFileToWebdav\").coroutineHandler { webdavController.uploadFileToWebdav(it) }\n\n        // 删除webdav文件\n        router.get(\"/reader3/deleteWebdavFile\").coroutineHandler { webdavController.deleteWebdavFile(it) }\n        router.post(\"/reader3/deleteWebdavFile\").coroutineHandler { webdavController.deleteWebdavFile(it) }\n        router.post(\"/reader3/deleteWebdavFileList\").coroutineHandler { webdavController.deleteWebdavFileList(it) }\n\n        // 从webdav备份恢复\n        router.post(\"/reader3/restoreFromWebdav\").coroutineHandler { webdavController.restoreFromWebdav(it) }\n\n        // 备份到webdav\n        router.post(\"/reader3/backupToWebdav\").coroutineHandler { webdavController.backupToWebdav(it) }\n\n\n        /** rss模块 */\n        // rss\n        router.get(\"/reader3/getRssSources\").coroutineHandler { rssSourceController.getRssSources(it) }\n        router.post(\"/reader3/saveRssSource\").coroutineHandler { rssSourceController.saveRssSource(it) }\n        router.post(\"/reader3/saveRssSources\").coroutineHandler { rssSourceController.saveRssSources(it) }\n        router.post(\"/reader3/deleteRssSource\").coroutineHandler { rssSourceController.deleteRssSource(it) }\n        // rss 列表\n        router.get(\"/reader3/getRssArticles\").coroutineHandler { rssSourceController.getRssArticles(it) }\n        router.post(\"/reader3/getRssArticles\").coroutineHandler { rssSourceController.getRssArticles(it) }\n        // rss 内容\n        router.get(\"/reader3/getRssContent\").coroutineHandler { rssSourceController.getRssContent(it) }\n        router.post(\"/reader3/getRssContent\").coroutineHandler { rssSourceController.getRssContent(it) }\n\n        /** 替换规则模块 */\n        router.get(\"/reader3/getReplaceRules\").coroutineHandler { replaceRuleController.getReplaceRules(it) }\n        router.post(\"/reader3/saveReplaceRule\").coroutineHandler { replaceRuleController.saveReplaceRule(it) }\n        router.post(\"/reader3/saveReplaceRules\").coroutineHandler { replaceRuleController.saveReplaceRules(it) }\n        router.post(\"/reader3/deleteReplaceRule\").coroutineHandler { replaceRuleController.deleteReplaceRule(it) }\n        router.post(\"/reader3/deleteReplaceRules\").coroutineHandler { replaceRuleController.deleteReplaceRules(it) }\n\n        /** 书签模块 */\n        router.get(\"/reader3/getBookmarks\").coroutineHandler { bookmarkController.getBookmarks(it) }\n        router.post(\"/reader3/saveBookmark\").coroutineHandler { bookmarkController.saveBookmark(it) }\n        router.post(\"/reader3/saveBookmarks\").coroutineHandler { bookmarkController.saveBookmarks(it) }\n        router.post(\"/reader3/deleteBookmark\").coroutineHandler { bookmarkController.deleteBookmark(it) }\n        router.post(\"/reader3/deleteBookmarks\").coroutineHandler { bookmarkController.deleteBookmarks(it) }\n    }\n\n    suspend fun setupPort() {\n        logger.info(\"port: {}\", port)\n        var serverPort = env.getProperty(\"reader.server.port\", Int::class.java)\n        logger.info(\"serverPort: {}\", serverPort)\n        if (serverPort != null && serverPort > 0) {\n            port = serverPort;\n        }\n    }\n\n    suspend fun migration() {\n        try {\n            var storageDir = File(getWorkDir(\"storage\"))\n            var dataDir = File(getWorkDir(\"storage\", \"data\", \"default\"))\n            if (!storageDir.exists()) {\n                // 直接使用新版本，则创建 default 目录，防止重启之后被迁移\n                dataDir.mkdirs()\n            } else if (!dataDir.exists()) {\n                // 旧版本不管了\n                dataDir.mkdirs()\n                // 可能存在旧版本，尝试迁移\n                // var backupDir = File(getWorkDir(\"storage-backup\"))\n                // storageDir.renameTo(backupDir)\n                // dataDir.parentFile.mkdirs()\n                // backupDir.copyRecursively(dataDir)\n            }\n        } catch(e: Exception) {\n            e.printStackTrace()\n        }\n    }\n\n    override fun started() {\n        SpringContextUtils.getApplicationContext().publishEvent(SpringEvent(this as java.lang.Object, \"READY\", \"\"));\n    }\n\n    override fun onStartError() {\n        SpringContextUtils.getApplicationContext().publishEvent(SpringEvent(this as java.lang.Object, \"START_ERROR\", \"应用启动失败，请检查\" + port + \"端口是否被占用\"));\n    }\n\n    override fun onHandlerError(ctx: RoutingContext, error: Exception) {\n        val returnData = ReturnData()\n        logger.error(\"onHandlerError: \", error)\n        if (!ctx.response().headWritten()) {\n            ctx.success(returnData.setErrorMsg(error.toString()))\n        } else {\n            ctx.response().end(error.toString())\n        }\n    }\n\n    private suspend fun getSystemInfo(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        var systemFont = System.getProperty(\"reader.system.fonts\")\n        var freeMemory = \"\" + (Runtime.getRuntime().freeMemory() / 1024 / 1024) + \"M\"\n        var totalMemory = \"\" + (Runtime.getRuntime().totalMemory() / 1024 / 1024) + \"M\"\n        var maxMemory = \"\" + (Runtime.getRuntime().maxMemory() / 1024 / 1024) + \"M\"\n        return returnData.setData(mapOf(\n            \"fonts\" to systemFont,\n            \"freeMemory\" to freeMemory,\n            \"totalMemory\" to totalMemory,\n            \"maxMemory\" to maxMemory\n        ))\n    }\n\n    /**\n     * 定时任务\n     */\n\n    /**\n     * 每十分钟检查一次书架书籍更新\n     */\n    @Scheduled(cron = \"0 0/10 * * * ?\")\n    fun shelfUpdateJob()\n    {\n        launch(Dispatchers.IO) {\n            try {\n                val bookController = BookController(coroutineContext)\n\n                logger.info(\"开始检查书架书籍更新\")\n                // 刷新系统默认书架\n                bookController.getBookShelfBooks(true, \"default\")\n\n                // 刷新用户书架\n                if (appConfig.secure) {\n                    var userMap = mutableMapOf<String, Map<String, Any>>()\n                    var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n                    if (userMapJson != null) {\n                        userMap = userMapJson.map as MutableMap<String, Map<String, Any>>\n                    }\n                    userMap.forEach{\n                        try {\n                            var ns = it.value.getOrDefault(\"username\", \"\") as String? ?: \"\"\n                            if (ns.isNotEmpty()) {\n                                bookController.getBookShelfBooks(true, ns)\n                            }\n                        } catch (e: Exception) {\n                            e.printStackTrace()\n                        }\n                    }\n                }\n                logger.info(\"书架书籍更新检查结束\")\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n        }\n    }\n\n    /**\n     * 每天清理不活跃用户\n     */\n    @Scheduled(cron = \"0 59 23 * * ?\")\n    fun clearUser()\n    {\n        if (appConfig.autoClearInactiveUser <= 0 || !appConfig.secure) {\n            return\n        }\n        launch(Dispatchers.IO) {\n            try {\n                logger.info(\"开始清理 {} 天未登录用户\", appConfig.autoClearInactiveUser)\n\n                var userMap = mutableMapOf<String, Map<String, Any>>()\n                var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n                if (userMapJson != null) {\n                    userMap = userMapJson.map as MutableMap<String, Map<String, Any>>\n                }\n                val expireTime = System.currentTimeMillis() - appConfig.autoClearInactiveUser * 86400L * 1000L\n                userMap.keys.forEach{\n                    try {\n                        var user = userMap.get(it)\n                        if (user != null) {\n                            var username = user.getOrDefault(\"username\", \"\") as String? ?: \"\"\n                            var last_login_at = user.getOrDefault(\"last_login_at\", 0) as Long? ?: 0L\n                            if (username.isNotEmpty() && last_login_at < expireTime) {\n                                logger.info(\"delete user: {}\", user)\n                                // 删除用户信息\n                                userMap.remove(username)\n                                // 移除用户目录\n                                var userHome = File(getWorkDir(\"storage\", \"data\", username))\n                                logger.info(\"delete userHome: {}\", userHome)\n                                if (userHome.exists()) {\n                                    userHome.deleteRecursively()\n                                }\n                            }\n                        }\n                    } catch (e: Exception) {\n                        e.printStackTrace()\n                    }\n                }\n                logger.info(\"不活跃用户自动清理结束\")\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/api/controller/BaseController.kt",
    "content": "package com.htmake.reader.api.controller\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.SearchBook\nimport io.legado.app.data.entities.BookGroup\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.RssSource\nimport io.legado.app.data.entities.RssArticle\nimport io.legado.app.model.webBook.WebBook\nimport io.vertx.ext.web.Route\nimport io.vertx.ext.web.Router\nimport io.vertx.ext.web.RoutingContext\nimport io.vertx.ext.web.handler.StaticHandler;\nimport mu.KotlinLogging\nimport com.htmake.reader.config.AppConfig\nimport com.htmake.reader.config.BookConfig\nimport io.legado.app.constant.DeepinkBookSource\nimport com.htmake.reader.utils.error\nimport com.htmake.reader.utils.success\nimport com.htmake.reader.utils.getStorage\nimport com.htmake.reader.utils.saveStorage\nimport com.htmake.reader.utils.asJsonArray\nimport com.htmake.reader.utils.asJsonObject\nimport com.htmake.reader.utils.toDataClass\nimport com.htmake.reader.utils.toMap\nimport com.htmake.reader.utils.fillData\nimport com.htmake.reader.utils.getWorkDir\nimport com.htmake.reader.utils.getRandomString\nimport com.htmake.reader.utils.genEncryptedPassword\nimport com.htmake.reader.entity.User\nimport com.htmake.reader.utils.SpringContextUtils\nimport com.htmake.reader.utils.deleteRecursively\nimport com.htmake.reader.utils.unzip\nimport com.htmake.reader.utils.zip\nimport com.htmake.reader.utils.jsonEncode\nimport com.htmake.reader.utils.getRelativePath\nimport com.htmake.reader.utils.getFileExtetion\nimport com.htmake.reader.verticle.RestVerticle\nimport com.htmake.reader.SpringEvent\nimport org.springframework.stereotype.Component\nimport io.vertx.core.json.JsonObject\nimport io.vertx.core.json.JsonArray\nimport io.vertx.core.http.HttpMethod\nimport com.htmake.reader.api.ReturnData\nimport io.legado.app.utils.MD5Utils\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.net.URL;\nimport java.util.UUID;\nimport io.vertx.ext.web.client.WebClient\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.core.env.Environment\nimport java.io.File\nimport java.lang.Runtime\nimport kotlin.collections.mutableMapOf\nimport kotlin.system.measureTimeMillis\nimport kotlin.coroutines.CoroutineContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.text.SimpleDateFormat;\nimport io.legado.app.utils.EncoderUtils\nimport io.legado.app.model.rss.Rss\nimport org.springframework.scheduling.annotation.Scheduled\nimport io.legado.app.model.localBook.LocalBook\nimport java.nio.file.Paths\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.delay\nimport io.legado.app.help.coroutine.Coroutine\n\nprivate val logger = KotlinLogging.logger {}\n\nopen class BaseController(override val coroutineContext: CoroutineContext): CoroutineScope {\n    var loginExpireDays = 7\n\n    val appConfig: AppConfig\n    val env: Environment\n\n    init {\n        appConfig = SpringContextUtils.getBean(\"appConfig\", AppConfig::class.java)\n        env = SpringContextUtils.getBean(Environment::class.java)\n    }\n\n    suspend fun saveUserSession(context: RoutingContext, userMap: MutableMap<String, Map<String, Any>>, user: User, regenerateToken: Boolean = true): Map<String, Any> {\n        user.last_login_at = System.currentTimeMillis()\n        if (regenerateToken) {\n            user.token = genEncryptedPassword(user.username, System.currentTimeMillis().toString())\n            var tokenMap: MutableMap<String, Long>? = null\n            var expire = System.currentTimeMillis() + loginExpireDays * 86400 * 1000\n            if (user.token_map != null) {\n                tokenMap = user.token_map as? MutableMap<String, Long>\n            }\n            if (tokenMap == null) {\n                tokenMap = mutableMapOf(user.token to expire)\n            } else {\n                tokenMap.put(user.token, expire)\n            }\n            // 删除已过期token\n            tokenMap.values.removeAll { it < user.last_login_at }\n            user.token_map = tokenMap\n        }\n        userMap.put(user.username, user.toMap())\n        saveStorage(\"data\", \"users\", value = userMap)\n\n        val loginData = formatUser(user)\n\n        context.session().put(\"username\", user.username)\n        context.put(\"username\", user.username)\n\n        return loginData\n    }\n\n    suspend fun checkAuth(context: RoutingContext): Boolean {\n        if (!appConfig.secure) {\n            return true\n        }\n        var username = context.session().get(\"username\") as String? ?: \"\"\n        var userInfo = getUserInfoClass(username)\n        if (userInfo != null) {\n            context.put(\"username\", userInfo.username)\n            context.put(\"userInfo\", userInfo)\n            return true\n        }\n        // 自动登录\n        var accessToken = context.queryParam(\"accessToken\").firstOrNull() ?: \"\"\n        if (accessToken.isNotEmpty()) {\n            var userMap = mutableMapOf<String, Map<String, Any>>()\n            var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n            if (userMapJson != null) {\n                userMap = userMapJson.map as? MutableMap<String, Map<String, Any>> ?: mutableMapOf<String, Map<String, Any>>()\n            }\n            var tmp = accessToken.split(\":\", limit=2)\n            if (tmp.size >= 2) {\n                var _username = tmp[0]\n                var token = tmp[1]\n                var existedUser: User? = userMap.getOrDefault(_username, null)?.toDataClass()\n                if (existedUser != null && token.isNotEmpty()) {\n                    var isLogin = false\n                    if (existedUser.token.isNotEmpty() && existedUser.token.equals(token)) {\n                        isLogin = true\n                    }\n                    // 查找历史有效会话\n                    if (!isLogin && existedUser.token_map != null) {\n                        var tokenMap = existedUser.token_map as? MutableMap<String, Long>\n                        if (tokenMap != null &&\n                            tokenMap.containsKey(token)) {\n                            if (tokenMap.getOrDefault(token, 0L) > System.currentTimeMillis()) {\n                                isLogin = true\n                                // 延长有效期\n                                tokenMap.put(token, System.currentTimeMillis() + loginExpireDays * 86400 * 1000)\n                            } else {\n                                // 删除过期token\n                                tokenMap.remove(token)\n                            }\n                            existedUser.token_map = tokenMap\n                        }\n                    }\n                    if (isLogin) {\n                        // 保存用户session\n                        saveUserSession(context, userMap, existedUser, false)\n                        context.put(\"username\", existedUser.username)\n                        context.put(\"userInfo\", existedUser)\n                    }\n                    return isLogin\n                }\n            }\n        }\n\n        return false\n    }\n\n    fun checkManagerAuth(context: RoutingContext): Boolean {\n        if (!appConfig.secure) {\n            return true\n        }\n        if (appConfig.secureKey.isEmpty()) {\n            return true\n        }\n        var secureKey = context.queryParam(\"secureKey\").firstOrNull() ?: \"\"\n        if (secureKey.equals(appConfig.secureKey)) {\n            // 判断是否需要修改 userNameSpace\n            var userNS = context.queryParam(\"userNS\").firstOrNull()\n            if (userNS != null && userNS.isNotEmpty()) {\n                context.put(\"userNameSpace\", userNS)\n            }\n            return true\n        }\n        return false\n    }\n\n    fun getUserNameSpace(context: RoutingContext): String {\n        if (!appConfig.secure) {\n            return \"default\"\n        }\n        // 管理权限，可以修改 userNameSpace 来获取任意用户信息\n        checkManagerAuth(context)\n        var userNS = context.get(\"userNameSpace\") as String?\n        if (userNS != null && userNS.isNotEmpty()) {\n            return userNS\n        }\n        var username = context.get(\"username\") as String?\n        if (username != null) {\n            return username;\n        }\n        return \"default\"\n    }\n\n    fun getUserStorage(context: Any, vararg path: String): String? {\n        var userNameSpace = \"\"\n        when(context) {\n            is RoutingContext -> userNameSpace = getUserNameSpace(context)\n            is String -> userNameSpace = context\n        }\n        if (userNameSpace.isEmpty()) {\n            return getStorage(\"data\", *path)\n        }\n        return getStorage(\"data\", userNameSpace, *path)\n    }\n\n    fun saveUserStorage(context: Any, path: String, value: Any) {\n        var userNameSpace = \"\"\n        when(context) {\n            is RoutingContext -> userNameSpace = getUserNameSpace(context)\n            is String -> userNameSpace = context\n        }\n        if (userNameSpace.isEmpty()) {\n            return saveStorage(\"data\", path, value = value)\n        }\n        return saveStorage(\"data\", userNameSpace, path, value = value)\n    }\n\n    fun getUserInfoClass(username: String): User? {\n        var user: User? = getUserInfoMap(username)?.toDataClass()\n        return user\n    }\n\n    fun getUserInfoMap(username: String): Map<String, Any>? {\n        if (username.isEmpty()) {\n            return null\n        }\n        var userMap = mutableMapOf<String, Map<String, Any>>()\n        var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n        if (userMapJson != null) {\n            userMap = userMapJson.map as MutableMap<String, Map<String, Any>>\n        }\n        return userMap.getOrDefault(username, null)\n    }\n\n    fun formatUser(userInfo: Any): MutableMap<String, Any> {\n        var user: User? = null\n        if (userInfo !is User) {\n            var userMap = userInfo as? Map<String, Any>\n            if (userMap != null) {\n                user = userMap.toDataClass()\n            }\n        } else {\n            user = userInfo\n        }\n        if (user == null) {\n            return mutableMapOf()\n        }\n        return mutableMapOf(\n            \"username\" to user.username,\n            \"lastLoginAt\" to user.last_login_at,\n            \"accessToken\" to user.username + \":\" + user.token,\n            \"enableWebdav\" to user.enable_webdav,\n            \"enableLocalStore\" to user.enable_local_store,\n            \"createdAt\" to user.created_at\n        )\n    }\n\n    fun getUserWebdavHome(context: Any): String {\n        var prefix = getWorkDir(\"storage\", \"data\")\n        var userNameSpace = \"\"\n        when(context) {\n            is RoutingContext -> userNameSpace = getUserNameSpace(context)\n            is String -> userNameSpace = context\n        }\n        if (userNameSpace.isNotEmpty()) {\n            prefix = prefix + File.separator + userNameSpace\n        }\n        prefix = prefix + File.separator + \"webdav\"\n        var file = File(prefix)\n        if (!file.exists()) {\n            file.mkdirs()\n        }\n        return prefix\n    }\n\n    fun getFileExt(url: String, defaultExt: String=\"\"): String {\n        return getFileExtetion(url, defaultExt)\n    }\n\n    suspend fun limitConcurrent(concurrentCount: Int, startIndex: Int, endIndex: Int, handler: suspend CoroutineScope.(Int) -> Any) {\n        limitConcurrent(concurrentCount, startIndex, endIndex, handler) {_, _ ->\n            true\n        }\n    }\n\n    suspend fun limitConcurrent(concurrentCount: Int, startIndex: Int, endIndex: Int, handler: suspend CoroutineScope.(Int) -> Any, needContinue: (ArrayList<Any>, Int) -> Boolean) {\n        var lastIndex = startIndex\n        var loopCount = 0\n        var resultCount = 0\n        var loopStart = System.currentTimeMillis()\n        var costTime = 0L\n        var deferredList = arrayListOf<Deferred<Any>>()\n        while(true) {\n            var croutineCount = deferredList.size;\n            if (croutineCount < concurrentCount) {\n                for(i in lastIndex until endIndex) {\n                    croutineCount += 1;\n                    deferredList.add(async {\n                        handler(i)\n                    })\n\n                    lastIndex = i\n                    if (croutineCount >= concurrentCount) {\n                        break;\n                    }\n                }\n            }\n            var resultList = arrayListOf<Any>()\n\n            // 等待任何一个完成\n            while (resultList.size <= 0) {\n                delay(10)\n                var stillDeferredList = arrayListOf<Deferred<Any>>()\n                for (i in 0 until deferredList.size) {\n                    try {\n                        var deferred = deferredList.get(i)\n                        if (deferred.isCompleted) {\n                            resultCount++\n                            resultList.add(deferred.getCompleted())\n                        } else if (!deferred.isCancelled) {\n                            stillDeferredList.add(deferred)\n                        } else {\n                            resultCount++\n                        }\n                    } catch(e: Exception) {\n\n                    }\n                }\n                deferredList.clear()\n                deferredList.addAll(stillDeferredList)\n            }\n\n            if (resultCount / concurrentCount > loopCount) {\n                loopCount = resultCount / concurrentCount\n                costTime = System.currentTimeMillis() - loopStart\n                logger.info(\"Loop: {} concurrentCount: {} lastIndex: {} endIndex: {} costTime: {} ms deferredList size: {}\", loopCount, croutineCount, lastIndex, endIndex, costTime, deferredList.size)\n            }\n\n            if (lastIndex >= endIndex - 1) {\n                // 搞完了，等待所有结束\n                for (i in 0 until deferredList.size) {\n                    try {\n                        resultList.add(deferredList.get(i).await())\n                    } catch(e: Exception) {\n\n                    }\n                }\n                deferredList.clear()\n                needContinue(resultList, loopCount)\n                break;\n            }\n            if (resultList.size > 0) {\n                if (!needContinue(resultList, loopCount)) {\n                    break;\n                }\n            }\n            lastIndex = lastIndex + 1\n        }\n\n        // for (i in 0 until concurrentCount) {\n        //     runBlocking(concurrentCount, startIndex + i , endIndex, handler, needContinue)\n        // }\n    }\n\n    suspend fun runBlocking(concurrentCount: Int, startIndex: Int, endIndex: Int, handler: suspend CoroutineScope.(Int) -> Any, needContinue: (ArrayList<Any>, Int) -> Boolean) {\n        var lastIndex = startIndex\n\n        Coroutine.async(this, coroutineContext) {\n            handler(lastIndex)\n        }.timeout(30000L)\n        .onSuccess(Dispatchers.IO) {\n            if (lastIndex < endIndex - concurrentCount && needContinue(arrayListOf(it), 0)) {\n                lastIndex += concurrentCount\n                runBlocking(concurrentCount, lastIndex, endIndex, handler, needContinue)\n            }\n        }\n        .onError(Dispatchers.IO) {\n            if (lastIndex < endIndex - concurrentCount) {\n                lastIndex += concurrentCount\n                runBlocking(concurrentCount, lastIndex, endIndex, handler, needContinue)\n            } else {\n                needContinue(arrayListOf(), 0)\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/api/controller/BookController.kt",
    "content": "package com.htmake.reader.api.controller\n\nimport io.legado.app.constant.AppPattern\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.SearchBook\nimport io.legado.app.data.entities.BookGroup\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.RssSource\nimport io.legado.app.data.entities.RssArticle\nimport io.legado.app.data.entities.SearchResult\nimport io.legado.app.exception.TocEmptyException\nimport io.legado.app.model.webBook.WebBook\nimport io.legado.app.help.DefaultData\nimport io.vertx.ext.web.Route\nimport io.vertx.ext.web.Router\nimport io.vertx.ext.web.RoutingContext\nimport io.vertx.ext.web.handler.StaticHandler;\nimport mu.KotlinLogging\nimport com.htmake.reader.config.AppConfig\nimport com.htmake.reader.config.BookConfig\nimport io.legado.app.constant.DeepinkBookSource\nimport com.htmake.reader.utils.error\nimport com.htmake.reader.utils.success\nimport com.htmake.reader.utils.getStorage\nimport com.htmake.reader.utils.saveStorage\nimport com.htmake.reader.utils.asJsonArray\nimport com.htmake.reader.utils.asJsonObject\nimport com.htmake.reader.utils.toDataClass\nimport com.htmake.reader.utils.toMap\nimport com.htmake.reader.utils.fillData\nimport com.htmake.reader.utils.getWorkDir\nimport com.htmake.reader.utils.getRandomString\nimport com.htmake.reader.utils.genEncryptedPassword\nimport com.htmake.reader.entity.User\nimport com.htmake.reader.utils.SpringContextUtils\nimport com.htmake.reader.utils.deleteRecursively\nimport com.htmake.reader.utils.unzip\nimport com.htmake.reader.utils.zip\nimport com.htmake.reader.utils.jsonEncode\nimport com.htmake.reader.utils.getRelativePath\nimport com.htmake.reader.verticle.RestVerticle\nimport com.htmake.reader.SpringEvent\nimport org.springframework.stereotype.Component\nimport io.vertx.core.json.JsonObject\nimport io.vertx.core.json.JsonArray\nimport io.vertx.core.http.HttpMethod\nimport com.htmake.reader.api.ReturnData\nimport io.legado.app.utils.MD5Utils\nimport io.legado.app.utils.FileUtils\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.net.URL;\nimport java.nio.charset.Charset\nimport java.util.UUID;\nimport io.vertx.ext.web.client.WebClient\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.core.env.Environment\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.lang.Runtime\nimport kotlin.collections.mutableMapOf\nimport kotlin.system.measureTimeMillis\nimport kotlin.coroutines.CoroutineContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.text.SimpleDateFormat;\nimport io.legado.app.utils.EncoderUtils\nimport io.legado.app.utils.ACache\nimport io.legado.app.utils.HtmlFormatter\nimport io.legado.app.utils.NetworkUtils\nimport io.legado.app.model.rss.Rss\nimport io.legado.app.model.Debugger\nimport io.legado.app.help.BookHelp\nimport org.springframework.scheduling.annotation.Scheduled\nimport io.legado.app.model.localBook.LocalBook\nimport io.legado.app.model.analyzeRule.AnalyzeUrl\nimport java.nio.file.Paths\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.CoroutineScope\nimport me.ag2s.epublib.domain.*\nimport me.ag2s.epublib.epub.EpubWriter\nimport me.ag2s.epublib.util.ResourceUtil\n// import io.legado.app.help.coroutine.Coroutine\n\nprivate val logger = KotlinLogging.logger {}\n\nclass BookController(coroutineContext: CoroutineContext): BaseController(coroutineContext) {\n\n    var bookInfoCache = ACache.get(\"bookInfoCache\", 1000 * 1000 * 2L, 10000) // 缓存 2M 的书籍信息\n    val concurrentLoopCount = 8\n\n    private var webClient: WebClient\n\n    init {\n        webClient = SpringContextUtils.getBean(\"webClient\", WebClient::class.java)\n    }\n\n    private fun getInvalidBookSourceCache(userNameSpace: String): ACache {\n        val cacheDir = File(getWorkDir(\"storage\", \"cache\", \"invalidBookSourceCache\", userNameSpace))\n        // 缓存 5M 的失效书源信息\n        var invalidBookSourceCache = ACache.get(cacheDir, 1000 * 1000 * 5L, 1000000)\n        return invalidBookSourceCache\n    }\n\n    private fun isInvalidBookSource(bookSource: BookSource, userNameSpace: String): Boolean {\n        return getInvalidBookSourceCache(userNameSpace).getAsString(bookSource.bookSourceUrl) != null\n    }\n\n    private fun addInvalidBookSource(sourceUrl: String, invalidInfo: Map<String, Any>, userNameSpace: String) {\n        // 保存600秒时间\n        getInvalidBookSourceCache(userNameSpace).put(sourceUrl, jsonEncode(invalidInfo), 600)\n    }\n\n    suspend fun getInvalidBookSources(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        val invalidBookSourceCache = getInvalidBookSourceCache(userNameSpace)\n        val cacheDir = File(getWorkDir(\"storage\", \"cache\", \"invalidBookSourceCache\", userNameSpace))\n        val files = cacheDir.listFiles()\n        val invalidBookSourceList = arrayListOf<Map<String, Any>>()\n        if (files != null) {\n            for (f in files) {\n                invalidBookSourceCache.getByHashCode(f.name)?.let { info ->\n                    invalidBookSourceList.add(info.toMap())\n                }\n            }\n        }\n\n        return returnData.setData(invalidBookSourceList)\n    }\n\n    suspend fun getBookInfo(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        var bookUrl: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"url\") ?: context.bodyAsJson.getJsonObject(\"searchBook\").getString(\"bookUrl\") ?: \"\"\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"url\").firstOrNull() ?: \"\"\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入书籍链接\")\n        }\n        logger.info(\"getBookInfo with bookUrl: {}\", bookUrl)\n        var bookInfo: Book? = null\n        if (checkAuth(context)) {\n            bookInfo = getShelfBookByURL(bookUrl, getUserNameSpace(context))\n        }\n        if (bookInfo == null) {\n            // 看看有没有缓存数据\n            var bookSource: String? = null\n            var cacheInfo: Book? = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass()\n            if (cacheInfo != null) {\n                // 使用缓存的书籍信息包含的书源\n                bookSource = getBookSourceString(context, cacheInfo.origin)\n            } else {\n                bookSource = getBookSourceString(context)\n            }\n            if (bookSource.isNullOrEmpty()) {\n                return returnData.setErrorMsg(\"未配置书源\")\n            }\n            bookInfo = mergeBookCacheInfo(WebBook(bookSource, appConfig.debugLog).getBookInfo(bookUrl))\n        }\n\n        // 缓存书籍信息\n        saveBookInfoCache(arrayListOf<Book>(bookInfo))\n        return returnData.setData(bookInfo)\n    }\n\n    suspend fun getBookCover(context: RoutingContext) {\n        var coverUrl = context.queryParam(\"path\").firstOrNull() ?: \"\"\n        if (coverUrl.isNullOrEmpty()) {\n            context.response().setStatusCode(404).end()\n            return\n        }\n        var ext = getFileExt(coverUrl, \"png\")\n        val md5Encode = MD5Utils.md5Encode(coverUrl).toString()\n        var cachePath = getWorkDir(\"storage\", \"cache\", md5Encode + \".\" + ext)\n        var cacheFile = File(cachePath)\n        if (cacheFile.exists()) {\n            logger.info(\"send cache: {}\", cacheFile)\n            context.response().putHeader(\"Cache-Control\", \"86400\").sendFile(cacheFile.toString())\n            return;\n        }\n\n        if (!cacheFile.parentFile.exists()) {\n            cacheFile.parentFile.mkdirs()\n        }\n\n        launch(Dispatchers.IO) {\n            webClient.getAbs(coverUrl).timeout(3000).send {\n                var bodyBytes = it.result()?.bodyAsBuffer()?.getBytes()\n                if (bodyBytes != null) {\n                    var res = context.response().putHeader(\"Cache-Control\", \"86400\")\n                    cacheFile.writeBytes(bodyBytes)\n                    res.sendFile(cacheFile.toString())\n                } else {\n                    context.response().setStatusCode(404).end()\n                }\n            }\n        }\n    }\n\n    suspend fun importBookPreview(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (context.fileUploads() == null || context.fileUploads().isEmpty()) {\n            return returnData.setErrorMsg(\"请上传书籍文件\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var fileList = arrayListOf<Map<String, Any>>()\n        context.fileUploads().forEach {\n            var file = File(it.uploadedFileName())\n            logger.info(\"uploadFile: {} {} {}\", it.uploadedFileName(), it.fileName(), file)\n            if (file.exists()) {\n                var fileName = it.fileName()\n                val ext = getFileExt(fileName)\n                if (ext != \"txt\" && ext != \"epub\" && ext != \"umd\" && ext != \"cbz\") {\n                    file.deleteRecursively()\n                    return returnData.setErrorMsg(\"不支持导入\" + ext + \"格式的书籍文件\")\n                }\n                // 文件名格式化\n                fileName = FileUtils.getNameExcludeExtension(fileName)\n                fileName = fileName.replace(AppPattern.fileNameRegex, \"\")\n                fileName = fileName.substring(0, Math.min(50, fileName.length)) + \".\" + ext\n\n                val localFilePath = Paths.get(\"storage\", \"assets\", userNameSpace, \"book\", fileName).toString()\n                val localFileUrl = \"/assets/\" + userNameSpace + \"/book/\" + fileName\n                var filePath = localFilePath\n                if (fileName.endsWith(\".epub\", true)) {\n                    filePath = filePath + File.separator + \"index.epub\"\n                }\n                if (fileName.endsWith(\".cbz\", true)) {\n                    filePath = filePath + File.separator + \"index.cbz\"\n                }\n                var newFile = File(getWorkDir(filePath))\n                if (!newFile.parentFile.exists()) {\n                    newFile.parentFile.mkdirs()\n                }\n                if (newFile.exists()) {\n                    newFile.delete()\n                }\n                logger.info(\"moveTo: {}\", newFile)\n                if (file.copyRecursively(newFile)) {\n                    val book = Book.initLocalBook(localFileUrl, localFilePath, getWorkDir())\n                    book.setUserNameSpace(userNameSpace)\n                    try {\n                        val chapters = LocalBook.getChapterList(book)\n                        fileList.add(mapOf(\"book\" to book, \"chapters\" to chapters))\n                    } catch(e: TocEmptyException) {\n                        fileList.add(mapOf(\"book\" to book, \"chapters\" to arrayListOf<Int>()))\n                    }\n                }\n                file.deleteRecursively()\n            }\n        }\n        return returnData.setData(fileList)\n    }\n\n    suspend fun getTxtTocRules(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        return returnData.setData(DefaultData.txtTocRules)\n    }\n\n    suspend fun getChapterListByRule(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var book = context.bodyAsJson.mapTo(Book::class.java)\n        if (book.origin.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"未找到书源信息\")\n        }\n        if (!book.isLocalTxt() && !book.isLocalEpub()) {\n            return returnData.setErrorMsg(\"非本地txt/epub书籍\")\n        }\n        book.setRootDir(getWorkDir())\n        book.setUserNameSpace(getUserNameSpace(context))\n        val chapters = LocalBook.getChapterList(book)\n        return returnData.setData(mapOf(\"book\" to book, \"chapters\" to chapters))\n    }\n\n    suspend fun refreshLocalBook(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var bookUrl: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"bookUrl\")\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"bookUrl\").firstOrNull() ?: \"\"\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入书籍链接\")\n        }\n        // 根据书籍url获取书本信息\n        var userNameSpace = getUserNameSpace(context)\n        var bookInfo = getShelfBookByURL(bookUrl, userNameSpace)\n        if (bookInfo == null) {\n            return returnData.setErrorMsg(\"书籍信息错误\")\n        }\n        bookInfo.updateFromLocal(true)\n\n        editShelfBook(bookInfo, userNameSpace) { existBook ->\n            existBook.coverUrl = bookInfo.coverUrl\n            logger.info(\"refreshLocalBook: {}\", existBook)\n            existBook\n        }\n\n        return returnData.setData(bookInfo)\n    }\n\n    suspend fun getChapterList(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var bookUrl: String\n        var refresh: Int = 0\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"url\") ?: context.bodyAsJson.getJsonObject(\"book\").getString(\"bookUrl\") ?: \"\"\n            refresh = context.bodyAsJson.getInteger(\"refresh\", 0)\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"url\").firstOrNull() ?: \"\"\n            refresh = context.queryParam(\"refresh\").firstOrNull()?.toInt() ?: 0\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入书籍链接\")\n        }\n        // 根据书籍url获取书本信息\n        var userNameSpace = getUserNameSpace(context)\n        var bookInfo = getShelfBookByURL(bookUrl, userNameSpace)\n        var bookSource: String? = null\n        if (bookInfo == null) {\n            // 看看有没有缓存数据\n            var cacheInfo: Book? = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass()\n            if (cacheInfo != null) {\n                // 使用缓存的书籍信息包含的书源\n                bookSource = getBookSourceString(context, cacheInfo.origin)\n            } else {\n                // 看看有没有传入书源\n                bookSource = getBookSourceString(context)\n            }\n            if (bookSource.isNullOrEmpty()) {\n                return returnData.setErrorMsg(\"未配置书源\")\n            }\n            bookInfo = mergeBookCacheInfo(WebBook(bookSource, appConfig.debugLog).getBookInfo(bookUrl))\n            // 缓存书籍信息\n            saveBookInfoCache(arrayListOf<Book>(bookInfo))\n        } else {\n            bookSource = getBookSourceString(context, bookInfo.origin)\n        }\n        if (!bookInfo.isLocalBook() && bookSource.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"未配置书源\")\n        }\n        bookInfo.setRootDir(getWorkDir())\n        bookInfo.setUserNameSpace(userNameSpace)\n        if (bookInfo.isLocalBook()) {\n            val localFile = bookInfo.getLocalFile()\n            if (!localFile.exists()) {\n                logger.info(\"localFile: {} not exists\", localFile)\n                return returnData.setErrorMsg(\"本地书籍源文件不存在\")\n            }\n        }\n        // 缓存章节列表\n        logger.info(\"bookInfo: {}\", bookInfo)\n        var chapterList = getLocalChapterList(bookInfo, bookSource ?: \"\", refresh > 0, getUserNameSpace(context))\n\n        return returnData.setData(chapterList)\n    }\n\n    suspend fun saveBookProgress(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var bookUrl: String\n        var chapterIndex: Int\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"url\") ?: context.bodyAsJson.getJsonObject(\"searchBook\").getString(\"bookUrl\") ?: \"\"\n            chapterIndex = context.bodyAsJson.getInteger(\"index\", -1)\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"url\").firstOrNull() ?: \"\"\n            chapterIndex = context.queryParam(\"index\").firstOrNull()?.toInt() ?: -1\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入书籍链接\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        // 看看有没有加入书架\n        var bookInfo = getShelfBookByURL(bookUrl, userNameSpace)\n        if (bookInfo == null || bookInfo.origin.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"书籍未加入书架\")\n        }\n        var bookSource = getBookSourceStringBySourceURL(bookInfo.origin, userNameSpace)\n\n        if (!bookInfo.isLocalBook() && bookSource.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"未配置书源\")\n        }\n        var chapterList = getLocalChapterList(bookInfo, bookSource ?: \"\", false, userNameSpace)\n        if (chapterIndex >= chapterList.size) {\n            return returnData.setErrorMsg(\"章节不存在\")\n        }\n        var chapterInfo = chapterList.get(chapterIndex)\n        // 书架书籍保存阅读进度\n        saveShelfBookProgress(bookInfo, chapterInfo, userNameSpace)\n        // 保存到 webdav\n        saveBookProgressToWebdav(bookInfo, chapterInfo, userNameSpace)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun getBookContent(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var chapterUrl: String\n        var bookUrl: String\n        var chapterIndex: Int\n        var cache: Int\n        var refresh: Int\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            chapterUrl = context.bodyAsJson.getString(\"chapterUrl\") ?: context.bodyAsJson.getJsonObject(\"bookChapter\")?.getString(\"url\") ?: \"\"\n            bookUrl = context.bodyAsJson.getString(\"url\") ?: context.bodyAsJson.getJsonObject(\"searchBook\")?.getString(\"bookUrl\") ?: \"\"\n            chapterIndex = context.bodyAsJson.getInteger(\"index\", -1)\n            cache = context.bodyAsJson.getInteger(\"cache\", 0)\n            refresh = context.bodyAsJson.getInteger(\"refresh\", 0)\n        } else {\n            // get 请求\n            chapterUrl = context.queryParam(\"chapterUrl\").firstOrNull() ?: \"\"\n            bookUrl = context.queryParam(\"url\").firstOrNull() ?: \"\"\n            chapterIndex = context.queryParam(\"index\").firstOrNull()?.toInt() ?: -1\n            cache = context.queryParam(\"cache\").firstOrNull()?.toInt() ?: 0\n            refresh = context.queryParam(\"refresh\").firstOrNull()?.toInt() ?: 0\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入书籍链接\")\n        }\n        var bookSource = getBookSourceString(context)\n        var userNameSpace = getUserNameSpace(context)\n        var isInBookShelf = false\n        var bookInfo: Book? = null\n        var chapterInfo: BookChapter? = null\n        var nextChapterUrl: String? = null\n        if (!bookUrl.isNullOrEmpty()) {\n            // 看看有没有加入书架\n            bookInfo = getShelfBookByURL(bookUrl, userNameSpace)\n            if (bookInfo != null && !bookInfo.origin.isNullOrEmpty()) {\n                isInBookShelf = true\n                bookSource = getBookSourceStringBySourceURL(bookInfo.origin, userNameSpace)\n            }\n            // 看看有没有缓存数据\n            var cacheInfo: Book? = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass()\n            if (cacheInfo != null) {\n                // 使用缓存的书籍信息包含的书源\n                bookSource = getBookSourceString(context, cacheInfo.origin)\n            }\n            if (chapterUrl.isNullOrEmpty() && chapterIndex >= 0) {\n                // 根据 url 和 index 获取章节内容\n                if (bookUrl.isNullOrEmpty()) {\n                    return returnData.setErrorMsg(\"请输入书籍链接\")\n                }\n                if (bookInfo != null && !bookInfo.isLocalBook() && bookSource.isNullOrEmpty()) {\n                    return returnData.setErrorMsg(\"未配置书源\")\n                }\n                bookInfo = bookInfo ?: mergeBookCacheInfo(WebBook(bookSource ?: \"\", appConfig.debugLog).getBookInfo(bookUrl))\n                var chapterList = getLocalChapterList(bookInfo, bookSource ?: \"\", false, userNameSpace)\n                if (chapterIndex < chapterList.size) {\n                    chapterInfo = chapterList.get(chapterIndex)\n                    // 书架书籍保存阅读进度\n                    if (isInBookShelf && cache != 1) {\n                        saveShelfBookProgress(bookInfo, chapterInfo, userNameSpace)\n                        // 保存到 webdav\n                        saveBookProgressToWebdav(bookInfo, chapterInfo, userNameSpace)\n                    }\n                    chapterUrl = chapterInfo.url\n                    if (chapterIndex + 1 < chapterList.size) {\n                        var nextChapterInfo = chapterList.get(chapterIndex + 1)\n                        nextChapterUrl = nextChapterInfo.url\n                    }\n                }\n            }\n        }\n        if (bookInfo == null) {\n            return returnData.setErrorMsg(\"获取书籍信息失败\")\n        }\n        if (!bookInfo.isLocalBook() && bookSource.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"未配置书源\")\n        }\n        if (chapterInfo == null || chapterUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"获取章节链接失败\")\n        }\n\n        var content = \"\"\n        bookInfo.setRootDir(getWorkDir())\n        bookInfo.setUserNameSpace(userNameSpace)\n        if (bookInfo.isLocalBook()) {\n            val localFile = bookInfo.getLocalFile()\n            if (!localFile.exists()) {\n                return returnData.setErrorMsg(\"本地源书籍文件不存在\")\n            }\n            if (chapterInfo == null) {\n                var chapterList = getLocalChapterList(bookInfo, bookSource ?: \"\", false, userNameSpace)\n                for(i in 0 until chapterList.size) {\n                    if (chapterUrl == chapterList.get(i).url) {\n                        chapterInfo = chapterList.get(i)\n                        break\n                    }\n                }\n                if (chapterInfo == null) {\n                    return returnData.setErrorMsg(\"获取章节信息失败\")\n                }\n            }\n            if (bookInfo.isEpub()) {\n                if (!extractEpub(bookInfo)) {\n                    return returnData.setErrorMsg(\"Epub书籍解压失败\")\n                }\n\n                val epubRootDir = bookInfo.getEpubRootDir()\n                var chapterFilePath = getWorkDir(bookInfo.bookUrl, \"index\", epubRootDir, chapterInfo.url)\n                logger.info(\"chapterFilePath: {} {}\", chapterFilePath, epubRootDir)\n                if (!File(chapterFilePath).exists()) {\n                    return returnData.setErrorMsg(\"章节文件不存在\")\n                }\n                // 处理 js 注入脚本\n                // BookConfig.injectJavascriptToEpubChapter(chapterFilePath);\n\n                // 直接返回 html访问地址\n                if (epubRootDir.isEmpty()) {\n                    content = bookInfo.bookUrl.replace(\"storage/data/\", \"/epub/\") + \"/index/\" + chapterInfo.url\n                } else {\n                    content = bookInfo.bookUrl.replace(\"storage/data/\", \"/epub/\") + \"/index/\" + epubRootDir + \"/\" + chapterInfo.url\n                }\n                return returnData.setData(content)\n            } else if (bookInfo.isCbz()) {\n                if (!extractCbz(bookInfo)) {\n                    return returnData.setErrorMsg(\"CBZ书籍解压失败\")\n                }\n                var chapterFilePath = getWorkDir(bookInfo.bookUrl, \"index\", chapterInfo.url)\n                logger.info(\"chapterFilePath: {}\", chapterFilePath)\n                val chapterFile = File(chapterFilePath)\n                if (!chapterFile.exists()) {\n                    return returnData.setErrorMsg(\"章节文件不存在\")\n                }\n                val ext = getFileExt(chapterFile.name).lowercase()\n                val imageExt = listOf(\"jpg\", \"jpeg\", \"gif\", \"png\", \"bmp\", \"webp\", \"svg\")\n                val fileUrl = \"__API_ROOT__\" + bookInfo.bookUrl.replace(\"storage/data/\", \"/epub/\") + \"/index/\" + chapterInfo.url\n                if (!imageExt.contains(ext)) {\n                    return returnData.setData(fileUrl)\n                }\n                content = \"<img src='\" + fileUrl + \"' />\"\n                return returnData.setData(content)\n            }\n            var bookContent = LocalBook.getContent(bookInfo, chapterInfo)\n            if (bookContent == null) {\n                return returnData.setErrorMsg(\"获取章节内容失败\")\n            }\n            content = bookContent\n        } else {\n            // 查找章节缓存\n            var chapterCacheFile: File? = null\n            if (refresh <= 0 && appConfig.cacheChapterContent) {\n                val localCacheDir = getChapterCacheDir(bookInfo, userNameSpace)\n                chapterCacheFile = File(localCacheDir.absolutePath + File.separator + chapterIndex + \".txt\")\n                if (chapterCacheFile.exists()) {\n                    content = chapterCacheFile.readText()\n                    logger.info(\"使用缓存的章节内容: {}\", chapterCacheFile.toString())\n                    return returnData.setData(content)\n                }\n            }\n            try {\n                content = WebBook(bookSource ?: \"\", appConfig.debugLog).getBookContent(bookInfo, chapterInfo, nextChapterUrl)\n                if (appConfig.cacheChapterContent && chapterCacheFile != null) {\n                    chapterCacheFile.writeText(content)\n                    // 保存图片\n                    BookHelp.saveImages(\n                        this,\n                        BookSource.fromJson(bookSource ?: \"\").getOrNull() ?: BookSource(),\n                        bookInfo,\n                        chapterInfo,\n                        content\n                    )\n                }\n            } catch(e: Exception) {\n                if (!bookSource.isNullOrEmpty()) {\n                    var bookSourceObject = asJsonObject(bookSource)?.mapTo(BookSource::class.java)\n                    if (bookSourceObject != null) {\n                        // 标记为失败源\n                        val info = mutableMapOf<String, Any>(\"sourceUrl\" to bookSourceObject.bookSourceUrl, \"time\" to System.currentTimeMillis(), \"error\" to e.toString())\n                        addInvalidBookSource(bookSourceObject.bookSourceUrl, info, userNameSpace)\n                    }\n                }\n                throw e\n            }\n        }\n\n        return returnData.setData(content)\n    }\n\n    suspend fun exploreBook(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        // 如果登录了，就使用用户的书源\n        checkAuth(context)\n        var bookSource = getBookSourceString(context)\n        if (bookSource.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"未配置书源\")\n        }\n        var page: Int\n        var ruleFindUrl: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            ruleFindUrl = context.bodyAsJson.getString(\"ruleFindUrl\")\n            page = context.bodyAsJson.getInteger(\"page\", 1)\n        } else {\n            // get 请求\n            ruleFindUrl = context.queryParam(\"ruleFindUrl\").firstOrNull() ?: \"\"\n            page = context.queryParam(\"page\").firstOrNull()?.toInt() ?: 1\n        }\n\n        var result = WebBook(bookSource, false).exploreBook(ruleFindUrl, page)\n        return returnData.setData(result)\n    }\n\n    suspend fun searchBook(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        // 如果登录了，就使用用户的书源\n        checkAuth(context)\n        var bookSource = getBookSourceString(context)\n        if (bookSource.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"未配置书源\")\n        }\n        val key: String\n        var page: Int\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            key = context.bodyAsJson.getString(\"key\")\n            page = context.bodyAsJson.getInteger(\"page\", 1)\n        } else {\n            // get 请求\n            key = context.queryParam(\"key\").firstOrNull() ?: \"\"\n            page = context.queryParam(\"page\").firstOrNull()?.toInt() ?: 1\n        }\n        if (key.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入搜索关键字\")\n        }\n        logger.info { \"searchBook\" }\n        var result = WebBook(bookSource, appConfig.debugLog).searchBook(key, page)\n        return returnData.setData(result)\n    }\n\n    suspend fun searchBookMulti(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var key: String\n        var lastIndex: Int\n        var searchSize: Int\n        var bookSourceGroup: String\n        var concurrentCount: Int\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            key = context.bodyAsJson.getString(\"key\", \"\")\n            bookSourceGroup = context.bodyAsJson.getString(\"bookSourceGroup\", \"\")\n            lastIndex = context.bodyAsJson.getInteger(\"lastIndex\", -1)\n            searchSize = context.bodyAsJson.getInteger(\"searchSize\", 20)\n            concurrentCount = context.bodyAsJson.getInteger(\"concurrentCount\", 36)\n        } else {\n            // get 请求\n            key = context.queryParam(\"key\").firstOrNull() ?: \"\"\n            bookSourceGroup = context.queryParam(\"bookSourceGroup\").firstOrNull() ?: \"\"\n            lastIndex = context.queryParam(\"lastIndex\").firstOrNull()?.toInt() ?: -1\n            searchSize = context.queryParam(\"searchSize\").firstOrNull()?.toInt() ?: 20\n            concurrentCount = context.queryParam(\"concurrentCount\").firstOrNull()?.toInt() ?: 36\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var userBookSourceList = loadBookSourceStringList(userNameSpace, bookSourceGroup)\n        if (userBookSourceList.size <= 0) {\n            return returnData.setErrorMsg(\"未配置书源\")\n        }\n        if (key.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入搜索关键字\")\n        }\n        if (lastIndex >= userBookSourceList.size - 1) {\n            return returnData.setErrorMsg(\"没有更多了\")\n        }\n\n        searchSize = if(searchSize > 0) searchSize else 20\n        concurrentCount = if(concurrentCount > 0) concurrentCount else 36\n        logger.info(\"searchBookMulti from lastIndex: {} searchSize: {}\", lastIndex, searchSize)\n        var isEnd = false\n        context.request().connection().closeHandler{\n            logger.info(\"客户端已断开链接，停止 searchBookMulti\")\n            isEnd = true\n        }\n        var resultList = arrayListOf<SearchBook>()\n        var resultMap = mutableMapOf<String, Int>()\n        val book = Book()\n        book.name = key\n        limitConcurrent(concurrentCount, lastIndex + 1, userBookSourceList.size, {it->\n            lastIndex = it\n            var bookSource = userBookSourceList.get(it)\n            searchBookWithSource(bookSource, book, false, userNameSpace = userNameSpace)\n        }) {list, loopCount ->\n            // logger.info(\"list: {}\", list)\n            list.forEach {\n                val bookList = it as? Collection<SearchBook>\n                bookList?.forEach { book ->\n                    // 按照 书名 + 作者名 过滤\n                    val bookKey = book.name + '_' + book.author\n                    if (!resultMap.containsKey(bookKey)) {\n                        resultList.add(book)\n                        resultMap.put(bookKey, 1)\n                    }\n                }\n            }\n            logger.info(\"Loog: {} resultList.size: {}\", loopCount, resultList.size)\n            if (isEnd || loopCount >= concurrentLoopCount) {\n                // 超过最大轮次，终止执行\n                false\n            } else {\n                resultList.size < searchSize\n            }\n        }\n        return returnData.setData(mapOf(\"lastIndex\" to lastIndex, \"list\" to resultList))\n    }\n\n    suspend fun searchBookMultiSSE(context: RoutingContext) {\n        val returnData = ReturnData()\n        // 返回 event-stream\n        val response = context.response().putHeader(\"Content-Type\", \"text/event-stream\")\n            .putHeader(\"Cache-Control\", \"no-cache\")\n            .setChunked(true);\n\n        if (!checkAuth(context)) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\"), false) + \"\\n\\n\")\n            return\n        }\n        var key: String\n        var lastIndex: Int\n        var searchSize: Int\n        var bookSourceGroup: String\n        var concurrentCount: Int\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            key = context.bodyAsJson.getString(\"key\", \"\")\n            bookSourceGroup = context.bodyAsJson.getString(\"bookSourceGroup\", \"\")\n            lastIndex = context.bodyAsJson.getInteger(\"lastIndex\", -1)\n            searchSize = context.bodyAsJson.getInteger(\"searchSize\", 50)\n            concurrentCount = context.bodyAsJson.getInteger(\"concurrentCount\", 24)\n        } else {\n            // get 请求\n            key = context.queryParam(\"key\").firstOrNull() ?: \"\"\n            bookSourceGroup = context.queryParam(\"bookSourceGroup\").firstOrNull() ?: \"\"\n            lastIndex = context.queryParam(\"lastIndex\").firstOrNull()?.toInt() ?: -1\n            searchSize = context.queryParam(\"searchSize\").firstOrNull()?.toInt() ?: 50\n            concurrentCount = context.queryParam(\"concurrentCount\").firstOrNull()?.toInt() ?: 24\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var userBookSourceList = loadBookSourceStringList(userNameSpace, bookSourceGroup)\n        if (userBookSourceList.size <= 0) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"未配置书源\"), false) + \"\\n\\n\")\n            return\n        }\n        if (key.isNullOrEmpty()) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"请输入搜索关键字\"), false) + \"\\n\\n\")\n            return\n        }\n        if (lastIndex >= userBookSourceList.size - 1) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"没有更多了\"), false) + \"\\n\\n\")\n            return\n        }\n\n        searchSize = if(searchSize > 0) searchSize else 50\n        concurrentCount = if(concurrentCount > 0) concurrentCount else 24\n        logger.info(\"searchBookMulti from lastIndex: {} concurrentCount: {} searchSize: {}\", lastIndex, concurrentCount, searchSize)\n\n        var isEnd = false\n        context.request().connection().closeHandler{\n            logger.info(\"客户端已断开链接，停止 searchBookMultiSSE\")\n            isEnd = true\n        }\n        var resultList = arrayListOf<SearchBook>()\n        var resultMap = mutableMapOf<String, Int>()\n        val book = Book()\n        book.name = key\n        limitConcurrent(concurrentCount, lastIndex + 1, userBookSourceList.size, {it->\n            lastIndex = it\n            var bookSource = userBookSourceList.get(it)\n            searchBookWithSource(bookSource, book, false, userNameSpace = userNameSpace)\n        }) {list, loopCount ->\n            // logger.info(\"list: {}\", list)\n            val loopResult = arrayListOf<SearchBook>()\n            list.forEach {\n                val bookList = it as? Collection<SearchBook>\n                bookList?.forEach { book ->\n                    // 按照 书名 + 作者名 过滤\n                    val bookKey = book.name + '_' + book.author\n                    if (!resultMap.containsKey(bookKey)) {\n                        resultList.add(book)\n                        loopResult.add(book)\n                        resultMap.put(bookKey, 1)\n                    }\n                }\n            }\n            // 返回本轮数据\n            response.write(\"data: \" + jsonEncode(mapOf(\"lastIndex\" to lastIndex, \"data\" to loopResult), false) + \"\\n\\n\")\n            logger.info(\"Loog: {} resultList.size: {}\", loopCount, resultList.size)\n\n            if (isEnd || loopCount >= concurrentLoopCount) {\n                // 超过最大轮次，终止执行\n                false\n            } else {\n                resultList.size < searchSize\n            }\n        }\n        response.write(\"event: end\\n\")\n        response.end(\"data: \" + jsonEncode(mapOf(\"lastIndex\" to lastIndex), false) + \"\\n\\n\")\n    }\n\n    suspend fun searchBookSource(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var bookUrl: String\n        var lastIndex: Int\n        var searchSize: Int\n        var bookSourceGroup: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"url\")\n            lastIndex = context.bodyAsJson.getInteger(\"lastIndex\", -1)\n            searchSize = context.bodyAsJson.getInteger(\"searchSize\", 5)\n            bookSourceGroup = context.bodyAsJson.getString(\"bookSourceGroup\", \"\")\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"url\").firstOrNull() ?: \"\"\n            lastIndex = context.queryParam(\"lastIndex\").firstOrNull()?.toInt() ?: -1\n            searchSize = context.queryParam(\"searchSize\").firstOrNull()?.toInt() ?: 5\n            bookSourceGroup = context.queryParam(\"bookSourceGroup\").firstOrNull() ?: \"\"\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var userBookSourceList = loadBookSourceStringList(userNameSpace, bookSourceGroup)\n        if (userBookSourceList.size <= 0) {\n            return returnData.setErrorMsg(\"未配置书源\")\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入书籍链接\")\n        }\n        if (lastIndex >= userBookSourceList.size - 1) {\n            return returnData.setErrorMsg(\"没有更多了\")\n        }\n        var book = getShelfBookByURL(bookUrl, userNameSpace)\n        if (book == null) {\n            book = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass()\n        }\n        if (book == null) {\n            return returnData.setErrorMsg(\"书籍信息错误\")\n        }\n        // 校正 lastIndex\n        var bookSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, book.name + \"_\" + book.author, \"bookSource\"))\n        if (bookSourceList != null && bookSourceList.size() > 0) {\n            try {\n                val lastBookSourceUrl = bookSourceList.getJsonObject(bookSourceList.size() - 1).getString(\"origin\")\n                lastIndex = Math.max(lastIndex, getBookSourceBySourceURL(lastBookSourceUrl, userNameSpace, userBookSourceList).second)\n            } catch(e: Exception) {\n                e.printStackTrace()\n            }\n        }\n\n        logger.info(\"searchBookSource from lastIndex: {}\", lastIndex)\n        var isEnd = false\n        context.request().connection().closeHandler{\n            logger.info(\"客户端已断开链接，停止 searchBookSource\")\n            isEnd = true\n        }\n        searchSize = if(searchSize > 0) searchSize else 5\n        var resultList = arrayListOf<SearchBook>()\n        var concurrentCount = Math.max(searchSize * 2, 24)\n        limitConcurrent(concurrentCount, lastIndex + 1, userBookSourceList.size, {it->\n            lastIndex = it\n            var bookSource = userBookSourceList.get(it)\n            searchBookWithSource(bookSource, book, userNameSpace = userNameSpace)\n        }) {list, loopCount ->\n            // logger.info(\"list: {}\", list)\n            list.forEach {\n                val bookList = it as? Collection<SearchBook>\n                bookList?.let {\n                    resultList.addAll(it)\n                }\n            }\n            if (isEnd || loopCount >= concurrentLoopCount) {\n                // 超过最大轮次，终止执行\n                false\n            } else {\n                resultList.size < searchSize\n            }\n        }\n        saveBookSources(book, resultList, userNameSpace)\n        return returnData.setData(mapOf(\"lastIndex\" to lastIndex, \"list\" to resultList))\n    }\n\n    suspend fun searchBookSourceSSE(context: RoutingContext) {\n        val returnData = ReturnData()\n        // 返回 event-stream\n        val response = context.response().putHeader(\"Content-Type\", \"text/event-stream\")\n            .putHeader(\"Cache-Control\", \"no-cache\")\n            .setChunked(true);\n\n        if (!checkAuth(context)) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\"), false) + \"\\n\\n\")\n            return\n        }\n        var bookUrl: String\n        var lastIndex: Int\n        var searchSize: Int\n        var bookSourceGroup: String\n        var refresh: Int = 0\n\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"url\")\n            lastIndex = context.bodyAsJson.getInteger(\"lastIndex\", -1)\n            searchSize = context.bodyAsJson.getInteger(\"searchSize\", 30)\n            bookSourceGroup = context.bodyAsJson.getString(\"bookSourceGroup\", \"\")\n            refresh = context.bodyAsJson.getInteger(\"refresh\", 0)\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"url\").firstOrNull() ?: \"\"\n            lastIndex = context.queryParam(\"lastIndex\").firstOrNull()?.toInt() ?: -1\n            searchSize = context.queryParam(\"searchSize\").firstOrNull()?.toInt() ?: 30\n            bookSourceGroup = context.queryParam(\"bookSourceGroup\").firstOrNull() ?: \"\"\n            refresh = context.queryParam(\"refresh\").firstOrNull()?.toInt() ?: 0\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var userBookSourceList = loadBookSourceStringList(userNameSpace, bookSourceGroup)\n        if (userBookSourceList.size <= 0) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"未配置书源\"), false) + \"\\n\\n\")\n            return\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"请输入书籍链接\"), false) + \"\\n\\n\")\n            return\n        }\n\n        var book = getShelfBookByURL(bookUrl, userNameSpace)\n        if (book == null) {\n            book = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass()\n        }\n        if (book == null) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"书籍信息错误\"), false) + \"\\n\\n\")\n            return\n        }\n        // 校正 lastIndex\n        var bookSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, book.name + \"_\" + book.author, \"bookSource\"))\n        // refresh > 0 校验书源最后一个书源作为lastIndex的可选项\n        if (refresh <= 0 && bookSourceList != null && bookSourceList.size() > 0) {\n            try {\n                val lastBookSourceUrl = bookSourceList.getJsonObject(bookSourceList.size() - 1).getString(\"origin\")\n                lastIndex = Math.max(lastIndex, getBookSourceBySourceURL(lastBookSourceUrl, userNameSpace, userBookSourceList).second)\n            } catch(e: Exception) {\n                e.printStackTrace()\n            }\n        }\n\n        if (lastIndex >= userBookSourceList.size - 1) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setData(mapOf(\"lastIndex\" to lastIndex)).setErrorMsg(\"没有更多了\"), false) + \"\\n\\n\")\n            return\n        }\n\n        searchSize = if(searchSize > 0) searchSize else 30\n        var resultList = arrayListOf<SearchBook>()\n        var concurrentCount = Math.max(searchSize * 2, 24)\n        logger.info(\"searchBookMulti from lastIndex: {} concurrentCount: {} searchSize: {}\", lastIndex, concurrentCount, searchSize)\n        var isEnd = false\n        context.request().connection().closeHandler{\n            logger.info(\"客户端已断开链接，停止 searchBookSourceSSE\")\n            isEnd = true\n        }\n\n        limitConcurrent(concurrentCount, lastIndex + 1, userBookSourceList.size, {it->\n            lastIndex = it\n            var bookSource = userBookSourceList.get(it)\n            searchBookWithSource(bookSource, book, userNameSpace = userNameSpace)\n        }) {list, loopCount ->\n            // logger.info(\"list: {}\", list)\n            val loopResult = arrayListOf<SearchBook>()\n            list.forEach {\n                val bookList = it as? Collection<SearchBook>\n                bookList?.let {\n                    resultList.addAll(it)\n                    loopResult.addAll(it)\n                }\n            }\n            // 返回本轮数据\n            response.write(\"data: \" + jsonEncode(mapOf(\"lastIndex\" to lastIndex, \"data\" to loopResult), false) + \"\\n\\n\")\n            logger.info(\"Loog: {} resultList.size: {}\", loopCount, resultList.size)\n\n            if (isEnd || loopCount >= concurrentLoopCount) {\n                // 超过最大轮次，终止执行\n                false\n            } else {\n                resultList.size < searchSize\n            }\n        }\n        saveBookSources(book, resultList, userNameSpace)\n        response.write(\"event: end\\n\")\n        response.end(\"data: \" + jsonEncode(mapOf(\"lastIndex\" to lastIndex), false) + \"\\n\\n\")\n    }\n\n    suspend fun searchBookWithSource(bookSourceString: String, book: Book, accurate: Boolean = true, userNameSpace: String = \"default\"): ArrayList<SearchBook> {\n        var resultList = arrayListOf<SearchBook>()\n        var bookSource = asJsonObject(bookSourceString)?.mapTo(BookSource::class.java)\n        if (bookSource == null) {\n            return resultList;\n        }\n        if (isInvalidBookSource(bookSource, userNameSpace)) {\n            return resultList;\n        }\n        withContext(Dispatchers.IO) {\n            // val costTime = measureTimeMillis {\n            try {\n                val start = System.currentTimeMillis()\n                var result = WebBook(bookSourceString, false).searchBook(book.name, 1)\n                val end = System.currentTimeMillis()\n                if (result.size > 0) {\n                    for (j in 0 until result.size) {\n                        var _book = result.get(j)\n                        if (accurate && _book.name.equals(book.name) && _book.author.equals(book.author)) {\n                            _book.time = end - start\n                            resultList.add(_book)\n                        } else if (!accurate && (_book.name.indexOf(book.name, ignoreCase=true) >= 0 || _book.author.indexOf(book.name, ignoreCase=true) >= 0)) {\n                            _book.time = end - start\n                            resultList.add(_book)\n                        }\n                    }\n                }\n            } catch(e: Exception) {\n                // 标记为失败源\n                val info = mutableMapOf<String, Any>(\"sourceUrl\" to bookSource.bookSourceUrl, \"time\" to System.currentTimeMillis(), \"error\" to e.toString())\n                addInvalidBookSource(bookSource.bookSourceUrl, info, userNameSpace)\n\n                e.printStackTrace()\n            }\n            // }\n            // logger.info(\"searchBookWithSource in Thread: {} Cost: {}\", Thread.currentThread().name, costTime)\n        }\n        return resultList;\n    }\n\n    suspend fun getAvailableBookSource(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var bookUrl: String\n        var refresh: Int = 0\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"url\")\n            refresh = context.bodyAsJson.getInteger(\"refresh\", 0)\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"url\").firstOrNull() ?: \"\"\n            refresh = context.queryParam(\"refresh\").firstOrNull()?.toInt() ?: 0\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入书籍链接\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var book = getShelfBookByURL(bookUrl, userNameSpace)\n        if (book == null) {\n            book = bookInfoCache.getAsString(bookUrl)?.toMap()?.toDataClass()\n        }\n        if (book == null) {\n            return returnData.setErrorMsg(\"书籍信息错误\")\n        }\n        var bookSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, book.name + \"_\" + book.author, \"bookSource\"))\n        if (bookSourceList != null && bookSourceList.size() > 0) {\n            if (refresh <= 0) {\n                return returnData.setData(bookSourceList.getList())\n            }\n\n            // 刷新源\n            var resultList = arrayListOf<SearchBook>()\n            val concurrentCount = 16\n            val userBookSourceStringList = loadBookSourceStringList(userNameSpace)\n            limitConcurrent(concurrentCount, 0, bookSourceList.size(), {it ->\n                var searchBook = bookSourceList.getJsonObject(it).mapTo(SearchBook::class.java)\n                if (searchBook.origin.equals(\"loc_book\")) {\n                    arrayListOf(searchBook)\n                } else {\n                    var bookSource = getBookSourceStringBySourceURL(searchBook.origin, userNameSpace, userBookSourceStringList)\n                    if (bookSource != null) {\n                        searchBookWithSource(bookSource, book, userNameSpace = userNameSpace)\n                    } else {\n                        arrayListOf<SearchBook>()\n                    }\n                }\n            }) {list, _->\n                // logger.info(\"list: {}\", list)\n                list.forEach {\n                    val bookList = it as? Collection<SearchBook>\n                    bookList?.let {\n                        resultList.addAll(it)\n                    }\n                }\n                true\n            }\n            // logger.info(\"refreshed bookSourceList: {}\", resultList)\n            saveBookSources(book, resultList, userNameSpace, true)\n            return returnData.setData(resultList)\n        }\n        return returnData.setData(arrayListOf<Int>())\n    }\n\n    suspend fun getBookshelf(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var refresh: Int = 0\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            refresh = context.bodyAsJson.getInteger(\"refresh\", 0)\n        } else {\n            // get 请求\n            refresh = context.queryParam(\"refresh\").firstOrNull()?.toInt() ?: 0\n        }\n        var bookList = getBookShelfBooks(refresh > 0, getUserNameSpace(context))\n        return returnData.setData(bookList)\n    }\n\n    suspend fun getShelfBook(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var url: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            url = context.bodyAsJson.getString(\"url\")\n        } else {\n            // get 请求\n            url = context.queryParam(\"url\").firstOrNull() ?: \"\"\n        }\n        if (url.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"书源链接不能为空\")\n        }\n\n        var book = getShelfBookByURL(url, getUserNameSpace(context))\n        if (book == null) {\n            return returnData.setErrorMsg(\"书籍不存在\")\n        }\n        return returnData.setData(book)\n    }\n\n    suspend fun saveBook(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var book = context.bodyAsJson.mapTo(Book::class.java)\n        if (book.origin.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"未找到书源信息\")\n        }\n        if (book.bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"书籍链接不能为空\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookshelf\"))\n        if (bookshelf == null) {\n            bookshelf = JsonArray()\n        }\n\n        // 遍历判断书本是否存在\n        var existIndex: Int = -1\n        for (i in 0 until bookshelf.size()) {\n            var _book = bookshelf.getJsonObject(i).mapTo(Book::class.java)\n            if (_book.name.equals(book.name) && _book.author.equals(book.author)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex < 0) {\n            // 判断书籍是否超过限制\n            if (bookshelf.size() >= appConfig.userBookLimit) {\n                return returnData.setErrorMsg(\"超过用户书籍数上限\")\n            }\n        }\n        // 导入本地书籍\n        if (book.isLocalBook()) {\n            if (book.bookUrl.startsWith(\"/assets/\")) {\n                // 临时文件，移动到书籍目录\n                // storage/assets/default/book/《极道天魔》（校对版全本）作者：滚开/《极道天魔》（校对版全本）作者：滚开.txt\n                val tempFile = File(getWorkDir(\"storage\" + book.bookUrl))\n                if (!tempFile.exists()) {\n                    return returnData.setErrorMsg(\"上传书籍不存在\")\n                }\n                val relativeLocalFilePath = Paths.get(\"storage\", \"data\", userNameSpace, book.name + \"_\" + book.author, tempFile.name).toString()\n                val localFilePath = getWorkDir(relativeLocalFilePath)\n                logger.info(\"localFilePath: {}\", localFilePath)\n                var localFile = File(localFilePath)\n                localFile.deleteRecursively()\n                if (!localFile.parentFile.exists()) {\n                    localFile.parentFile.mkdirs()\n                }\n                if (!tempFile.copyRecursively(localFile)) {\n                    return returnData.setErrorMsg(\"导入本地书籍失败\")\n                }\n                tempFile.deleteRecursively()\n                // 修改书籍信息\n                book.bookUrl = relativeLocalFilePath\n                book.originName = relativeLocalFilePath\n\n                if (book.isEpub()) {\n                    // 解压文件 index.epub\n                    if (!extractEpub(book)) {\n                        return returnData.setErrorMsg(\"导入本地Epub书籍失败\")\n                    }\n                } else if (book.isCbz()) {\n                    // 解压文件 index.cbz\n                    if (!extractCbz(book)) {\n                        return returnData.setErrorMsg(\"导入本地CBZ书籍失败\")\n                    }\n                }\n            } else if (book.bookUrl.indexOf(\"storage/localStore\") >= 0) {\n                // 本地书仓，不用移动书籍\n                val tempFile = File(getWorkDir(book.bookUrl))\n                if (!tempFile.exists()) {\n                    return returnData.setErrorMsg(\"本地书仓书籍不存在\")\n                }\n                val relativeLocalFilePath = Paths.get(\"storage\", \"data\", userNameSpace, book.name + \"_\" + book.author, tempFile.name).toString()\n                book.bookUrl = relativeLocalFilePath\n\n                if (book.isEpub()) {\n                    // 解压文件 index.epub\n                    if (!extractEpub(book)) {\n                        return returnData.setErrorMsg(\"导入本地Epub书籍失败\")\n                    }\n                } else if (book.isCbz()) {\n                    // 解压文件 index.cbz\n                    if (!extractCbz(book)) {\n                        return returnData.setErrorMsg(\"导入本地CBZ书籍失败\")\n                    }\n                }\n            } else if (book.bookUrl.indexOf(\"webdav/\") >= 0) {\n                // webdav书仓，不用移动书籍\n                val tempFile = File(getWorkDir(book.bookUrl))\n                if (!tempFile.exists()) {\n                    return returnData.setErrorMsg(\"webdav书仓书籍不存在\")\n                }\n                val relativeLocalFilePath = Paths.get(\"storage\", \"data\", userNameSpace, book.name + \"_\" + book.author, tempFile.name).toString()\n                book.bookUrl = relativeLocalFilePath\n\n                if (book.isEpub()) {\n                    // 解压文件 index.epub\n                    if (!extractEpub(book)) {\n                        return returnData.setErrorMsg(\"导入本地Epub书籍失败\")\n                    }\n                } else if (book.isCbz()) {\n                    // 解压文件 index.cbz\n                    if (!extractCbz(book)) {\n                        return returnData.setErrorMsg(\"导入本地CBZ书籍失败\")\n                    }\n                }\n            }\n        } else if (book.tocUrl.isNullOrEmpty()) {\n            // 补全书籍信息\n            var bookSource = getBookSourceStringBySourceURL(book.origin, userNameSpace)\n            if (bookSource == null) {\n                return returnData.setErrorMsg(\"书源信息错误\")\n            }\n            var newBook = WebBook(bookSource, appConfig.debugLog).getBookInfo(book.bookUrl)\n            book.fillData(newBook, listOf(\"name\", \"author\", \"coverUrl\", \"tocUrl\", \"intro\", \"latestChapterTitle\", \"wordCount\"))\n        }\n        book = mergeBookCacheInfo(book)\n\n        if (existIndex >= 0) {\n            var bookList = bookshelf.getList()\n            var existBook = bookshelf.getJsonObject(existIndex).mapTo(Book::class.java)\n            book.durChapterIndex = existBook.durChapterIndex\n            book.durChapterTitle = existBook.durChapterTitle\n            book.durChapterTime = existBook.durChapterTime\n\n            bookList.set(existIndex, JsonObject.mapFrom(book))\n            bookshelf = JsonArray(bookList)\n        } else {\n            bookshelf.add(JsonObject.mapFrom(book))\n        }\n        // 保存书源信息\n        val sourceList = listOf(book.toSearchBook())\n        saveBookSources(book, sourceList, userNameSpace)\n\n        // logger.info(\"bookshelf: {}\", bookshelf)\n        saveUserStorage(userNameSpace, \"bookshelf\", bookshelf)\n        return returnData.setData(book)\n    }\n\n    suspend fun setBookSource(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var bookUrl: String\n        var newBookUrl: String\n        var bookSourceUrl: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"bookUrl\")\n            newBookUrl = context.bodyAsJson.getString(\"newUrl\")\n            bookSourceUrl = context.bodyAsJson.getString(\"bookSourceUrl\")\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"bookUrl\").firstOrNull() ?: \"\"\n            newBookUrl = context.queryParam(\"newUrl\").firstOrNull() ?: \"\"\n            bookSourceUrl = context.queryParam(\"bookSourceUrl\").firstOrNull() ?: \"\"\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"书籍链接不能为空\")\n        }\n        if (newBookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"新源书籍链接不能为空\")\n        }\n        if (bookSourceUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"书源链接不能为空\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var book = getShelfBookByURL(bookUrl, userNameSpace)\n        if (book == null) {\n            return returnData.setErrorMsg(\"书籍信息错误\")\n        }\n        // 查找是否存在该书源\n        var bookSourceString = getBookSourceStringBySourceURL(bookSourceUrl, userNameSpace)\n\n        var searchBook: Book? = null\n        if (bookSourceString.isNullOrEmpty()) {\n            // 判断是不是本地书籍\n            val localBookSourceList = asJsonArray(getUserStorage(userNameSpace, book.name + \"_\" + book.author, \"bookSource\"))\n\n            // 遍历判断书本是否存在\n            if (localBookSourceList != null) {\n                for (i in 0 until localBookSourceList.size()) {\n                    var _searchBook = localBookSourceList.getJsonObject(i).mapTo(SearchBook::class.java)\n                    if (_searchBook.bookUrl.equals(newBookUrl)) {\n                        searchBook = _searchBook.toBook()\n                        break;\n                    }\n                }\n            }\n            if (searchBook == null) {\n                return returnData.setErrorMsg(\"书源信息错误\")\n            }\n        }\n\n        var newBookInfo = if (searchBook != null) {\n            searchBook\n        } else {\n            if (bookSourceString.isNullOrEmpty()) {\n                return returnData.setErrorMsg(\"书源信息错误\")\n            }\n            WebBook(bookSourceString, appConfig.debugLog).getBookInfo(newBookUrl)\n        }\n\n        editShelfBook(book, userNameSpace) { existBook ->\n            existBook.origin = newBookInfo.origin\n            existBook.originName = newBookInfo.originName\n            existBook.bookUrl = newBookInfo.bookUrl\n            existBook.tocUrl = newBookInfo.tocUrl\n            if (existBook.coverUrl.isNullOrEmpty() && !newBookInfo.coverUrl.isNullOrEmpty()) {\n                existBook.coverUrl = newBookInfo.coverUrl\n            }\n\n            logger.info(\"setBookSource: {}\", existBook)\n\n            newBookInfo = existBook\n\n            existBook\n        }\n\n        // 更新目录\n        getLocalChapterList(newBookInfo, bookSourceString ?: \"\", true, userNameSpace)\n        return returnData.setData(newBookInfo)\n    }\n\n    suspend fun saveBookGroupId(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var bookUrl: String\n        var groupId: Int\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"bookUrl\")\n            groupId = context.bodyAsJson.getInteger(\"groupId\", 0)\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"bookUrl\").firstOrNull() ?: \"\"\n            groupId = context.queryParam(\"groupId\").firstOrNull()?.toInt() ?: 0\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"书籍链接不能为空\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var book = getShelfBookByURL(bookUrl, userNameSpace)\n        if (book == null) {\n            return returnData.setErrorMsg(\"书籍信息错误\")\n        }\n\n        if (groupId <= 0) {\n            return returnData.setErrorMsg(\"分组信息错误\")\n        }\n\n        editShelfBook(book, userNameSpace) { existBook ->\n            existBook.group = groupId\n            logger.info(\"saveBookGroupId: {}\", existBook)\n            existBook\n        }\n\n        book.group = groupId\n        return returnData.setData(book)\n    }\n\n    suspend fun addBookGroupMulti(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val groupId: Int = context.bodyAsJson.getInteger(\"groupId\", 0)\n        if (groupId <= 0) {\n            return returnData.setErrorMsg(\"分组信息错误\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        val bookJsonArray = context.bodyAsJson.getJsonArray(\"bookList\", JsonArray())\n        for (k in 0 until bookJsonArray.size()) {\n            var book = bookJsonArray.getJsonObject(k).mapTo(Book::class.java)\n            editShelfBook(book, userNameSpace) { existBook ->\n                existBook.group = existBook.group or groupId\n                logger.info(\"saveBookGroupId: {}\", existBook)\n                existBook\n            }\n        }\n\n        return returnData.setData(\"\")\n    }\n\n    suspend fun removeBookGroupMulti(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val groupId: Int = context.bodyAsJson.getInteger(\"groupId\", 0)\n        if (groupId <= 0) {\n            return returnData.setErrorMsg(\"分组信息错误\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        val bookJsonArray = context.bodyAsJson.getJsonArray(\"bookList\", JsonArray())\n        for (k in 0 until bookJsonArray.size()) {\n            var book = bookJsonArray.getJsonObject(k).mapTo(Book::class.java)\n            editShelfBook(book, userNameSpace) { existBook ->\n                existBook.group = existBook.group xor groupId\n                logger.info(\"saveBookGroupId: {}\", existBook)\n                existBook\n            }\n        }\n\n        return returnData.setData(\"\")\n    }\n\n    suspend fun deleteBook(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var book = context.bodyAsJson.mapTo(Book::class.java)\n        var userNameSpace = getUserNameSpace(context)\n        var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookshelf\"))\n        if (bookshelf == null) {\n            bookshelf = JsonArray()\n        }\n        // 遍历判断书本是否存在\n        var existIndex: Int = -1\n        for (i in 0 until bookshelf.size()) {\n            var _book = bookshelf.getJsonObject(i).mapTo(Book::class.java)\n            if (_book.bookUrl.equals(book.bookUrl)) {\n                existIndex = i\n                book = _book\n                break;\n            }\n            if (_book.name.equals(book.name) && _book.author.equals(book.author)) {\n                existIndex = i\n                book = _book\n                break;\n            }\n        }\n        if (existIndex < 0) {\n            return returnData.setErrorMsg(\"书架书籍不存在\")\n        }\n        bookshelf.remove(existIndex)\n        // logger.info(\"bookshelf: {}\", bookshelf)\n        saveUserStorage(userNameSpace, \"bookshelf\", bookshelf)\n\n        // 删除书籍目录\n        val localBookPath = File(getWorkDir(\"storage\", \"data\", userNameSpace, book.name + \"_\" + book.author))\n        localBookPath.deleteRecursively()\n\n        return returnData.setData(\"删除书籍成功\")\n    }\n\n    suspend fun deleteBooks(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookJsonArray = context.bodyAsJsonArray\n\n        var userNameSpace = getUserNameSpace(context)\n        var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookshelf\"))\n        if (bookshelf == null) {\n            bookshelf = JsonArray()\n        }\n        for (k in 0 until bookJsonArray.size()) {\n            var book = bookJsonArray.getJsonObject(k).mapTo(Book::class.java)\n            // 遍历判断书本是否存在\n            var existIndex: Int = -1\n            for (i in 0 until bookshelf.size()) {\n                var _book = bookshelf.getJsonObject(i).mapTo(Book::class.java)\n                if (_book.bookUrl.equals(book.bookUrl)) {\n                    existIndex = i\n                    book = _book\n                    break;\n                }\n                if (_book.name.equals(book.name) && _book.author.equals(book.author)) {\n                    existIndex = i\n                    book = _book\n                    break;\n                }\n            }\n            if (existIndex >= 0) {\n                bookshelf.remove(existIndex)\n            }\n            // 删除书籍目录\n            val localBookPath = File(getWorkDir(\"storage\", \"data\", userNameSpace, book.name + \"_\" + book.author))\n            localBookPath.deleteRecursively()\n        }\n\n        saveUserStorage(userNameSpace, \"bookshelf\", bookshelf)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun getBookGroups(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        checkAuth(context)\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var bookGroupList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookGroup\"))\n        if (bookGroupList == null) {\n            bookGroupList = asJsonArray(\"\"\"\n            [{\"groupId\":-1,\"groupName\":\"全部\",\"order\":-10,\"show\":true},{\"groupId\":-2,\"groupName\":\"本地\",\"order\":-9,\"show\":true},{\"groupId\":-3,\"groupName\":\"音频\",\"order\":-8,\"show\":true},{\"groupId\":-4,\"groupName\":\"未分组\",\"order\":-7,\"show\":true}]\n            \"\"\")\n            if (bookGroupList == null) {\n                return returnData.setData(arrayListOf<Int>())\n            }\n            saveUserStorage(userNameSpace, \"bookGroup\", bookGroupList)\n        }\n        return returnData.setData(bookGroupList.getList())\n    }\n\n    suspend fun saveBookGroup(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookGroup = context.bodyAsJson.mapTo(BookGroup::class.java)\n        if (bookGroup.groupName.isEmpty()) {\n            return returnData.setErrorMsg(\"分组名称不能为空\")\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        var bookGroupList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookGroup\"))\n        if (bookGroupList == null) {\n            bookGroupList = JsonArray()\n        }\n        // 遍历判断书本是否存在\n        var existIndex: Int = -1\n        for (i in 0 until bookGroupList.size()) {\n            var _bookGroup = bookGroupList.getJsonObject(i).mapTo(BookGroup::class.java)\n            if (_bookGroup.groupId.equals(bookGroup.groupId)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex >= 0) {\n            var groupList = bookGroupList.getList()\n            groupList.set(existIndex, JsonObject.mapFrom(bookGroup))\n            bookGroupList = JsonArray(groupList)\n        } else {\n            // 新增分组\n            if (bookGroup.groupId >= 0) {\n                var maxOrder = 0;\n                val idsSum = bookGroupList.sumBy{\n                    val id = asJsonObject(it)?.getInteger(\"groupId\", 0) ?: 0\n                    val order = asJsonObject(it)?.getInteger(\"order\", 0) ?: 0\n                    maxOrder = if (order > maxOrder) order else maxOrder\n                    if (id > 0) id else 0\n                }\n                var id = 1\n                while (id and idsSum != 0) {\n                    id = id.shl(1)\n                }\n                bookGroup.groupId = id\n                bookGroup.order = maxOrder + 1\n            }\n            bookGroupList.add(JsonObject.mapFrom(bookGroup))\n        }\n\n        // logger.info(\"bookGroupList: {}\", bookGroupList)\n        saveUserStorage(userNameSpace, \"bookGroup\", bookGroupList)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun saveBookGroupOrder(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookGroupOrder = context.bodyAsJson.getJsonArray(\"order\", null)\n        if (bookGroupOrder == null) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        var bookGroupList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookGroup\"))\n        if (bookGroupList == null) {\n            bookGroupList = JsonArray()\n        }\n        var orderMap: MutableMap<Int, Int> = mutableMapOf()\n        for (i in 0 until bookGroupOrder.size()) {\n            orderMap.put(bookGroupOrder.getJsonObject(i).getInteger(\"groupId\"), bookGroupOrder.getJsonObject(i).getInteger(\"order\"))\n        }\n        // 遍历判断书本是否存在\n        var groupList = bookGroupList.getList()\n        for (i in 0 until bookGroupList.size()) {\n            var bookGroup = bookGroupList.getJsonObject(i).mapTo(BookGroup::class.java)\n            if (orderMap.containsKey(bookGroup.groupId)) {\n                bookGroup.order = orderMap.get(bookGroup.groupId) as? Int ?: bookGroup.order\n                groupList.set(i, JsonObject.mapFrom(bookGroup))\n            }\n        }\n        bookGroupList = JsonArray(groupList)\n\n        // logger.info(\"bookGroupList: {}\", bookGroupList)\n        saveUserStorage(userNameSpace, \"bookGroup\", bookGroupList)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun deleteBookGroup(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookgroup = context.bodyAsJson.mapTo(BookGroup::class.java)\n        var userNameSpace = getUserNameSpace(context)\n        var bookGroupList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookGroup\"))\n        if (bookGroupList == null) {\n            bookGroupList = JsonArray()\n        }\n        // 遍历判断分组是否存在\n        var existIndex: Int = -1\n        for (i in 0 until bookGroupList.size()) {\n            var _bookGroup = bookGroupList.getJsonObject(i).mapTo(BookGroup::class.java)\n            if (_bookGroup.groupId.equals(bookgroup.groupId)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex >= 0) {\n            bookGroupList.remove(existIndex)\n        }\n        // logger.info(\"bookGroup: {}\", bookGroup)\n        saveUserStorage(userNameSpace, \"bookGroup\", bookGroupList)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun saveBookInfoCache(bookList: List<Book>): List<Book> {\n        if (bookList.size > 0) {\n            for (i in 0 until bookList.size) {\n                var book = bookList.get(i)\n                bookInfoCache.put(book.bookUrl, jsonEncode(JsonObject.mapFrom(book).map))\n            }\n            saveStorage(\"cache\", \"bookInfoCache\", value = bookInfoCache)\n        }\n        return bookList\n    }\n\n    suspend fun mergeBookCacheInfo(book: Book): Book {\n        var cacheInfo: Book? = bookInfoCache.getAsString(book.bookUrl)?.toMap()?.toDataClass()\n\n        if (cacheInfo != null) {\n            return book.fillData(cacheInfo, listOf(\"name\", \"author\", \"coverUrl\", \"tocUrl\", \"intro\", \"latestChapterTitle\", \"wordCount\"))\n        }\n        return book\n    }\n\n    suspend fun getBookShelfBooks(refresh: Boolean = false, userNameSpace: String): List<Book> {\n        var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookshelf\"))\n        if (bookshelf == null) {\n            return arrayListOf<Book>()\n        }\n        if (bookshelf.size() == 0) {\n            return arrayListOf<Book>()\n        }\n        var bookList = arrayListOf<Book>()\n        val concurrentCount = 16\n        val userBookSourceStringList = loadBookSourceStringList(userNameSpace)\n        val mutex = Mutex()\n        limitConcurrent(concurrentCount, 0, bookshelf.size()) {\n            var book = bookshelf.getJsonObject(it).mapTo(Book::class.java)\n            if (!book.isLocalBook() && book.canUpdate && refresh) {\n                try {\n                    var bookSource = getBookSourceStringBySourceURL(book.origin, userNameSpace, userBookSourceStringList)\n                    if (bookSource != null) {\n                        withContext(Dispatchers.IO) {\n                            var bookChapterList = getLocalChapterList(book, bookSource, refresh, userNameSpace, false, mutex)\n                            if (bookChapterList.size > 0) {\n                                var bookChapter = bookChapterList.last()\n                                book.latestChapterTitle = bookChapter.title\n                            }\n                            if (bookChapterList.size - book.totalChapterNum > 0) {\n                                book.lastCheckTime = System.currentTimeMillis()\n                                book.lastCheckCount = bookChapterList.size - book.totalChapterNum\n                            }\n                            book.totalChapterNum = bookChapterList.size\n                        }\n                    }\n                } catch(e: Exception) {\n                    e.printStackTrace()\n                }\n            }\n            bookList.add(book)\n        }\n        return bookList\n    }\n\n\n    suspend fun getLocalChapterList(book: Book, bookSource: String, refresh: Boolean = false, userNameSpace: String, debugLog: Boolean = true, mutex: Mutex? = null): List<BookChapter> {\n        val md5Encode = MD5Utils.md5Encode(book.bookUrl).toString()\n        var chapterList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, book.name + \"_\" + book.author, md5Encode))\n\n        if (chapterList == null || refresh) {\n            var newChapterList: List<BookChapter>\n            book.setRootDir(getWorkDir())\n            book.setUserNameSpace(userNameSpace)\n            if (book.isLocalBook()) {\n                // 重新解压epub文件\n                if (book.isEpub() && !extractEpub(book, refresh)) {\n                    throw Exception(\"Epub书籍解压失败\")\n                }\n                // 重新解压cbz文件\n                if (book.isCbz() && !extractCbz(book, refresh)) {\n                    throw Exception(\"CBZ书籍解压失败\")\n                }\n                newChapterList = LocalBook.getChapterList(book)\n            } else {\n                try {\n                    newChapterList = WebBook(bookSource, debugLog).getChapterList(book)\n                } catch(e: Exception) {\n                    if (!bookSource.isNullOrEmpty()) {\n                        var bookSourceObject = asJsonObject(bookSource)?.mapTo(BookSource::class.java)\n                        if (bookSourceObject != null) {\n                            // 标记为失败源\n                            val info = mutableMapOf<String, Any>(\"sourceUrl\" to bookSourceObject.bookSourceUrl, \"time\" to System.currentTimeMillis(), \"error\" to e.toString())\n                            addInvalidBookSource(bookSourceObject.bookSourceUrl, info, userNameSpace)\n                        }\n                    }\n                    throw e\n                }\n            }\n            saveUserStorage(userNameSpace, getRelativePath(book.name + \"_\" + book.author, md5Encode), newChapterList)\n            saveShelfBookLatestChapter(book, newChapterList, userNameSpace, mutex)\n            return newChapterList\n        }\n        var localChapterList = arrayListOf<BookChapter>()\n        for (i in 0 until chapterList.size()) {\n            var _chapter = chapterList.getJsonObject(i).mapTo(BookChapter::class.java)\n            localChapterList.add(_chapter)\n        }\n        return localChapterList\n    }\n\n    suspend fun getBookSourceString(context: RoutingContext, sourceUrl: String = \"\"): String? {\n        var bookSourceString: String? = null\n        if (context.request().method() == HttpMethod.POST) {\n            var bookSource = context.bodyAsJson.getJsonObject(\"bookSource\")\n            if (bookSource != null) {\n                bookSourceString = bookSource.toString()\n            }\n        }\n        var userNameSpace = getUserNameSpace(context)\n        if (bookSourceString.isNullOrEmpty()) {\n            var bookSourceUrl: String\n            if (context.request().method() == HttpMethod.POST) {\n                bookSourceUrl = context.bodyAsJson.getString(\"bookSourceUrl\", \"\")\n            } else {\n                bookSourceUrl = context.queryParam(\"bookSourceUrl\").firstOrNull() ?: \"\"\n            }\n            bookSourceString = getBookSourceStringBySourceURL(bookSourceUrl, userNameSpace)\n        }\n        if (bookSourceString.isNullOrEmpty() && !sourceUrl.isNullOrEmpty()) {\n            bookSourceString = getBookSourceStringBySourceURL(sourceUrl, userNameSpace)\n        }\n        return bookSourceString\n    }\n\n    fun loadBookSourceStringList(userNameSpace: String, bookSourceGroup: String = \"\"): List<String> {\n        var bookSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookSource\"))\n        var userBookSourceList = arrayListOf<String>()\n        if (bookSourceList != null) {\n            for (i in 0 until bookSourceList.size()) {\n                var isAdd = true\n                if (!bookSourceGroup.isEmpty()) {\n                    val bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java)\n                    if (!bookSource.bookSourceGroup.equals(bookSourceGroup)) {\n                        isAdd = false\n                    }\n                }\n                if (isAdd) {\n                    userBookSourceList.add(bookSourceList.getJsonObject(i).toString())\n                }\n            }\n        }\n        return userBookSourceList\n    }\n\n    fun getBookSourceStringBySourceURL(sourceUrl: String, userNameSpace: String, bookSourceList: List<String>? = null): String? {\n        var bookSourcePair = getBookSourceBySourceURL(sourceUrl, userNameSpace, bookSourceList)\n        return bookSourcePair.first\n    }\n\n    fun getBookSourceBySourceURL(sourceUrl: String, userNameSpace: String, bookSourceList: List<String>? = null): Pair<String?, Int> {\n        var bookSourceString: String? = null\n        var index: Int = -1\n        if (sourceUrl.isNullOrEmpty()) {\n            return Pair(bookSourceString, index)\n        }\n        // 优先查找用户的书源\n        var userBookSourceList = bookSourceList ?: loadBookSourceStringList(userNameSpace)\n        for (i in 0 until userBookSourceList.size) {\n            val sourceMap = userBookSourceList.get(i).toMap()\n            if (sourceUrl.equals(sourceMap.get(\"bookSourceUrl\") as String)) {\n                bookSourceString = userBookSourceList.get(i)\n                index = i\n                break;\n            }\n        }\n        return Pair(bookSourceString, index)\n    }\n\n    fun getShelfBookByURL(url: String, userNameSpace: String): Book? {\n        if (url.isEmpty()) {\n            return null\n        }\n        var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookshelf\"))\n        if (bookshelf == null) {\n            return null\n        }\n        for (i in 0 until bookshelf.size()) {\n            var _book = bookshelf.getJsonObject(i).mapTo(Book::class.java)\n            if (_book.bookUrl.equals(url)) {\n                _book.setRootDir(getWorkDir())\n                _book.setUserNameSpace(userNameSpace)\n                return _book\n            }\n        }\n        return null\n    }\n\n    fun saveShelfBookProgress(book: Book, bookChapter: BookChapter, userNameSpace: String) {\n        editShelfBook(book, userNameSpace) { existBook ->\n            existBook.durChapterIndex = bookChapter.index\n            existBook.durChapterTitle = bookChapter.title\n            existBook.durChapterTime = System.currentTimeMillis()\n\n            // logger.info(\"saveShelfBookProgress: {}\", existBook)\n\n            existBook\n        }\n    }\n\n    suspend fun saveShelfBookLatestChapter(book: Book, bookChapterList: List<BookChapter>, userNameSpace: String, mutex: Mutex? = null) {\n        try {\n            mutex?.lock()\n            editShelfBook(book, userNameSpace) { existBook ->\n                if (bookChapterList.size > 0) {\n                    var bookChapter = bookChapterList.last()\n                    existBook.latestChapterTitle = bookChapter.title\n                }\n                if (bookChapterList.size - existBook.totalChapterNum > 0) {\n                    existBook.lastCheckCount = bookChapterList.size - existBook.totalChapterNum\n                    existBook.lastCheckTime = System.currentTimeMillis()\n                }\n                existBook.totalChapterNum = bookChapterList.size\n                // TODO 最新章节更新时间\n                // existBook.latestChapterTime = System.currentTimeMillis()\n                // logger.info(\"saveShelfBookLatestChapter: {}\", existBook)\n                existBook\n            }\n        } finally {\n            mutex?.unlock()\n        }\n    }\n\n    fun editShelfBook(book: Book, userNameSpace: String, handler: (Book)->Book) {\n        var bookshelf: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookshelf\"))\n        if (bookshelf == null) {\n            bookshelf = JsonArray()\n        }\n        // 遍历判断书本是否存在\n        var existIndex: Int = -1\n        for (i in 0 until bookshelf.size()) {\n            var _book = bookshelf.getJsonObject(i).mapTo(Book::class.java)\n            // 根据书籍链接查找\n            if (book.bookUrl.isNotEmpty() && _book.bookUrl.equals(book.bookUrl)) {\n                existIndex = i\n                break;\n            }\n            // 根据作者和书名查找\n            if (book.name.isNotEmpty() && _book.name.equals(book.name) && book.author.isNotEmpty() && _book.author.equals(book.author)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex >= 0) {\n            var bookList = bookshelf.getList()\n            var existBook = bookshelf.getJsonObject(existIndex).mapTo(Book::class.java)\n            existBook = handler(existBook)\n\n            // logger.info(\"editShelfBook: {}\", existBook)\n\n            bookList.set(existIndex, JsonObject.mapFrom(existBook))\n            bookshelf = JsonArray(bookList)\n            saveUserStorage(userNameSpace, \"bookshelf\", bookshelf)\n        }\n    }\n\n    fun saveBookSources(book: Book, sourceList: List<SearchBook>, userNameSpace: String, replace: Boolean = false) {\n        if (book.name.isEmpty()) {\n            return;\n        }\n        var bookSourceList = JsonArray()\n        if (!replace) {\n            val localBookSourceList = asJsonArray(getUserStorage(userNameSpace, book.name + \"_\" + book.author, \"bookSource\"))\n            if (localBookSourceList != null) {\n                bookSourceList = localBookSourceList\n            }\n        }\n\n        for (k in 0 until sourceList.size) {\n            var searchBook = sourceList.get(k)\n            // 遍历判断书本是否存在\n            var existIndex: Int = -1\n            for (i in 0 until bookSourceList.size()) {\n                var _searchBook = bookSourceList.getJsonObject(i).mapTo(SearchBook::class.java)\n                if (_searchBook.bookUrl.equals(searchBook.bookUrl)) {\n                    existIndex = i\n                    break;\n                }\n            }\n            if (existIndex >= 0) {\n                var _sourceList = bookSourceList.getList()\n                _sourceList.set(existIndex, JsonObject.mapFrom(searchBook))\n                bookSourceList = JsonArray(_sourceList)\n            } else {\n                bookSourceList.add(JsonObject.mapFrom(searchBook))\n            }\n        }\n\n        // logger.info(\"bookSourceList: {}\", bookSourceList)\n        saveUserStorage(userNameSpace, getRelativePath(book.name + \"_\" + book.author, \"bookSource\"), bookSourceList)\n    }\n\n    fun extractEpub(book: Book, force: Boolean = false): Boolean {\n        val epubExtractDir = File(getWorkDir(book.bookUrl + File.separator + \"index\"))\n        if (force || !epubExtractDir.exists()) {\n            epubExtractDir.deleteRecursively()\n            var localEpubFile = File(getWorkDir(book.originName + File.separator + \"index.epub\"))\n            if (book.originName.indexOf(\"localStore\") > 0) {\n                // 本地书仓的源文件\n                localEpubFile = File(getWorkDir(book.originName))\n            }\n            if (book.originName.indexOf(\"webdav\") > 0) {\n                // webdav 书仓的源文件\n                localEpubFile = File(getWorkDir(book.originName))\n            }\n            if (!localEpubFile.unzip(epubExtractDir.toString())) {\n                return false\n            }\n        }\n        return true\n    }\n\n    fun extractCbz(book: Book, force: Boolean = false): Boolean {\n        val extractDir = File(getWorkDir(book.bookUrl + File.separator + \"index\"))\n        if (force || !extractDir.exists()) {\n            extractDir.deleteRecursively()\n            var localFile = File(getWorkDir(book.originName + File.separator + \"index.cbz\"))\n            if (book.originName.indexOf(\"localStore\") > 0) {\n                // 本地书仓的源文件\n                localFile = File(getWorkDir(book.originName))\n            }\n            if (book.originName.indexOf(\"webdav\") > 0) {\n                // webdav 书仓的源文件\n                localFile = File(getWorkDir(book.originName))\n            }\n            if (!localFile.unzip(extractDir.toString())) {\n                return false\n            }\n        }\n        return true\n    }\n\n    suspend fun syncBookProgressFromWebdav(progressFilePath: Any, userNameSpace: String) {\n        var progressFile: File? = null\n        when (progressFilePath) {\n            is File -> progressFile = progressFilePath\n            is String -> progressFile = File(progressFilePath)\n        }\n        if (progressFile == null) {\n            return\n        }\n        var book = asJsonObject(progressFile.readText())?.mapTo(Book::class.java)\n        if (book != null) {\n            editShelfBook(book, userNameSpace) { existBook ->\n                existBook.durChapterIndex = book.durChapterIndex\n                existBook.durChapterPos = book.durChapterPos\n                existBook.durChapterTime = book.durChapterTime\n                existBook.durChapterTitle = book.durChapterTitle\n\n                logger.info(\"syncShelfBookProgress: {}\", existBook)\n                existBook\n            }\n        }\n    }\n\n    suspend fun saveBookProgressToWebdav(book: Book, bookChapter: BookChapter, userNameSpace: String) {\n        val userHome = getUserWebdavHome(userNameSpace)\n        var bookProgressDir = File(userHome + File.separator + \"bookProgress\")\n        if (!bookProgressDir.exists()) {\n            bookProgressDir = File(userHome + File.separator + \"legado\" + File.separator + \"bookProgress\")\n            if (!bookProgressDir.exists()) {\n                return\n            }\n        }\n        var progressFile = File(bookProgressDir.toString() + File.separator + book.name + \"_\" + book.author + \".json\")\n        progressFile.writeText(jsonEncode(mapOf(\n            \"name\" to book.name,\n            \"author\" to book.author,\n            \"durChapterIndex\" to bookChapter.index,\n            \"durChapterPos\" to 0,\n            \"durChapterTime\" to System.currentTimeMillis(),\n            \"durChapterTitle\" to bookChapter.title\n        ), true))\n    }\n\n    suspend fun syncFromWebdav(zipFilePath: String, userNameSpace: String): Boolean {\n        var descDir = getWorkDir(\"storage\", \"data\", userNameSpace, \"tmp\")\n        var descDirFile = File(descDir)\n        try {\n            val userHome = getUserWebdavHome(userNameSpace)\n            var zipFile = File(zipFilePath)\n            if (!zipFile.exists()) {\n                return false\n            }\n            descDirFile.deleteRecursively()\n            if (zipFile.unzip(descDir)) {\n                // 同步 书源\n                val bookSourceFile = File(descDir + File.separator + \"bookSource.json\")\n                if (bookSourceFile.exists()) {\n                    val userBookSourceFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"bookSource.json\"))\n                    userBookSourceFile.deleteRecursively()\n                    bookSourceFile.renameTo(userBookSourceFile)\n                }\n                // 同步 书架\n                val bookshelfFile = File(descDir + File.separator + \"bookshelf.json\")\n                if (bookshelfFile.exists()) {\n                    val userBookSourceFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"bookshelf.json\"))\n                    userBookSourceFile.deleteRecursively()\n                    bookshelfFile.renameTo(userBookSourceFile)\n                }\n                // 同步 书籍分组\n                val bookGroupFile = File(descDir + File.separator + \"bookGroup.json\")\n                if (bookGroupFile.exists()) {\n                    val userBookGroupFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"bookGroup.json\"))\n                    userBookGroupFile.deleteRecursively()\n                    bookGroupFile.renameTo(userBookGroupFile)\n                }\n                // 同步 RSS订阅\n                val rssSourcesFile = File(descDir + File.separator + \"rssSources.json\")\n                if (rssSourcesFile.exists()) {\n                    val userRssSourcesFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"rssSources.json\"))\n                    userRssSourcesFile.deleteRecursively()\n                    rssSourcesFile.renameTo(userRssSourcesFile)\n                }\n                // 同步 替换规则\n                val replaceRuleFile = File(descDir + File.separator + \"replaceRule.json\")\n                if (replaceRuleFile.exists()) {\n                    val userReplaceRuleFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"replaceRule.json\"))\n                    userReplaceRuleFile.deleteRecursively()\n                    replaceRuleFile.renameTo(userReplaceRuleFile)\n                }\n                // 同步 书签\n                val bookmarkFile = File(descDir + File.separator + \"bookmark.json\")\n                if (bookmarkFile.exists()) {\n                    val userBookmarkFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"bookmark.json\"))\n                    userBookmarkFile.deleteRecursively()\n                    bookmarkFile.renameTo(userBookmarkFile)\n                }\n                // 同步阅读进度\n                var bookProgressDir = File(userHome + File.separator + \"bookProgress\")\n                if (!bookProgressDir.exists()) {\n                    bookProgressDir = File(userHome + File.separator +  \"legado\" + File.separator +  \"bookProgress\")\n                }\n                if (bookProgressDir.exists() && bookProgressDir.isDirectory()) {\n                    bookProgressDir.listFiles().forEach{\n                        syncBookProgressFromWebdav(it, userNameSpace)\n                    }\n                }\n                return true\n            }\n        } catch(e: Exception) {\n            e.printStackTrace()\n        } finally {\n            descDirFile.deleteRecursively()\n        }\n        return true;\n    }\n\n    suspend fun saveToWebdav(latestZipFilePath: String, userNameSpace: String): Boolean {\n        var descDir = getWorkDir(\"storage\", \"data\", userNameSpace, \"tmp\")\n        var descDirFile = File(descDir)\n        descDirFile.deleteRecursively()\n        try {\n            val userHome = getUserWebdavHome(userNameSpace)\n            var legadoHome = userHome\n            if (latestZipFilePath.indexOf(\"legado\") > 0) {\n                legadoHome = userHome + File.separator + \"legado\"\n            }\n            var zipFile = File(latestZipFilePath)\n            if (zipFile.unzip(descDir)) {\n                // 同步 书源\n                val userBookSourceFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"bookSource.json\"))\n                if (userBookSourceFile.exists()) {\n                    val bookSourceFile = File(descDir + File.separator + \"bookSource.json\")\n                    bookSourceFile.deleteRecursively()\n                    userBookSourceFile.copyRecursively(bookSourceFile)\n                }\n                // 同步 书架\n                val userBookshelfFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"bookshelf.json\"))\n                if (userBookshelfFile.exists()) {\n                    val bookshelfFile = File(descDir + File.separator + \"bookshelf.json\")\n                    bookshelfFile.deleteRecursively()\n                    userBookshelfFile.copyRecursively(bookshelfFile)\n                }\n                // 同步 书籍分组\n                val userBookGroupFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"bookGroup.json\"))\n                if (userBookGroupFile.exists()) {\n                    val bookGroupFile = File(descDir + File.separator + \"bookGroup.json\")\n                    bookGroupFile.deleteRecursively()\n                    userBookGroupFile.renameTo(bookGroupFile)\n                }\n                // 同步 RSS订阅\n                val userRssSourcesFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"rssSources.json\"))\n                if (userRssSourcesFile.exists()) {\n                    val rssSourcesFile = File(descDir + File.separator + \"rssSources.json\")\n                    rssSourcesFile.deleteRecursively()\n                    userRssSourcesFile.renameTo(rssSourcesFile)\n                }\n                // 同步 替换规则\n                val userReplaceRuleFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"replaceRule.json\"))\n                if (userReplaceRuleFile.exists()) {\n                    val replaceRuleFile = File(descDir + File.separator + \"replaceRule.json\")\n                    replaceRuleFile.deleteRecursively()\n                    userReplaceRuleFile.renameTo(replaceRuleFile)\n                }\n                // 同步 书签\n                val userBookmarkFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"bookmark.json\"))\n                if (userBookmarkFile.exists()) {\n                    val bookmarkFile = File(descDir + File.separator + \"bookmark.json\")\n                    bookmarkFile.deleteRecursively()\n                    userBookmarkFile.renameTo(bookmarkFile)\n                }\n                // 压缩\n                val today = SimpleDateFormat(\"yyyy-MM-dd\").format(System.currentTimeMillis())\n                return descDirFile.zip(legadoHome + File.separator + \"backup\" + today + \".zip\")\n            }\n        } catch(e: Exception) {\n            e.printStackTrace()\n        }  finally {\n            descDirFile.deleteRecursively()\n        }\n        return false;\n    }\n\n    suspend fun getLastBackFileFromWebdav(userNameSpace: String): String? {\n        val userHome = getUserWebdavHome(userNameSpace)\n        var legadoHome = File(userHome + File.separator + \"legado\")\n        if (!legadoHome.exists()) {\n            legadoHome = File(userHome)\n        }\n        if (!legadoHome.exists()) {\n            return null\n        }\n        var latestZipFile: String? = null\n        val zipFileReg = Regex(\"^backup[0-9-]+.zip$\", RegexOption.IGNORE_CASE)    //忽略大小写\n        legadoHome.listFiles().also{\n            it.sortByDescending {\n                it.lastModified()\n            }\n        }.forEach {\n            if (zipFileReg.matches(it.name)) {\n                latestZipFile = it.toString()\n                return@forEach\n            }\n        }\n        return latestZipFile\n    }\n\n    // 从本地导入文件预览\n    suspend fun importFromLocalPathPreview(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var paths = context.bodyAsJson.getJsonArray(\"path\")\n        if (paths == null) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n        var webdav = context.bodyAsJson.getBoolean(\"webdav\", false)\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n            }\n            if (webdav && !userInfo.enable_webdav) {\n                return returnData.setErrorMsg(\"未开启 Webdav 功能\")\n            } else if (!userInfo.enable_local_store) {\n                return returnData.setErrorMsg(\"未开启本地书仓功能\")\n            }\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var home = if (webdav) {\n            getUserWebdavHome(context)\n        } else {\n            getWorkDir(\"storage\", \"localStore\")\n        }\n        var fileList = arrayListOf<Map<String, Any>>()\n        paths.forEach {\n            var path = it as String? ?: \"\"\n            if (path.isNotEmpty()) {\n                path = home + path\n                var file = File(path)\n                logger.info(\"localFile: {} {}\", path, file)\n                if (file.exists()) {\n                    val fileName = file.name\n                    val ext = getFileExt(fileName)\n                    if (ext != \"txt\" && ext != \"epub\" && ext != \"umd\" && ext != \"cbz\") {\n                        return returnData.setErrorMsg(\"不支持导入\" + ext + \"格式的书籍文件\")\n                    }\n                    val book = Book.initLocalBook(path, path, getWorkDir())\n                    book.setUserNameSpace(userNameSpace)\n                    try {\n                        val chapters = LocalBook.getChapterList(book)\n                        fileList.add(mapOf(\"book\" to book, \"chapters\" to chapters))\n                    } catch(e: TocEmptyException) {\n                        fileList.add(mapOf(\"book\" to book, \"chapters\" to arrayListOf<Int>()))\n                    }\n                }\n            }\n        }\n        return returnData.setData(fileList)\n    }\n\n    suspend fun uploadFileToLocalStore(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (context.fileUploads() == null || context.fileUploads().isEmpty()) {\n            return returnData.setErrorMsg(\"请上传文件\")\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n            }\n            if (!userInfo.enable_local_store) {\n                return returnData.setErrorMsg(\"未开启本地书仓功能\")\n            }\n        }\n        if (!checkManagerAuth(context)) {\n            return returnData.setData(\"NEED_SECURE_KEY\").setErrorMsg(\"请输入管理密码\")\n        }\n        var path = context.request().getParam(\"path\")\n        if (path.isNullOrEmpty()) {\n            path = \"\"\n        }\n        var fileList = arrayListOf<Map<String, Any>>()\n        var home = getWorkDir(\"storage\", \"localStore\")\n\n        // logger.info(\"type: {}\", type)\n        context.fileUploads().forEach {\n            var file = File(it.uploadedFileName())\n            logger.info(\"uploadFile: {} {} {}\", it.uploadedFileName(), it.fileName(), file)\n            if (file.exists()) {\n                var fileName = it.fileName()\n                var newFile = File(getWorkDir(\"storage\", \"localStore\", path, fileName))\n                if (!newFile.parentFile.exists()) {\n                    newFile.parentFile.mkdirs()\n                }\n                if (newFile.exists()) {\n                    newFile.delete()\n                }\n                logger.info(\"moveTo: {}\", newFile)\n                if (file.copyRecursively(newFile)) {\n                    fileList.add(mapOf(\n                        \"name\" to newFile.name,\n                        \"size\" to newFile.length(),\n                        \"path\" to newFile.toString().replace(home, \"\"),\n                        \"lastModified\" to newFile.lastModified(),\n                        \"isDirectory\" to newFile.isDirectory()\n                    ))\n                }\n                file.deleteRecursively()\n            }\n        }\n        return returnData.setData(fileList)\n    }\n\n    suspend fun getLocalStoreFileList(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n            }\n            if (!userInfo.enable_local_store) {\n                return returnData.setErrorMsg(\"未开启本地书仓功能\")\n            }\n        }\n        var path: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            path = context.bodyAsJson.getString(\"path\") ?: \"\"\n        } else {\n            // get 请求\n            path = context.queryParam(\"path\").firstOrNull() ?: \"\"\n        }\n        if (path.isEmpty()) {\n            path = \"/\"\n        }\n        var home = getWorkDir(\"storage\", \"localStore\")\n        var file = File(home + path)\n        logger.info(\"file: {} {}\", path, file)\n        if (!file.exists()) {\n            return returnData.setErrorMsg(\"路径不存在\")\n        }\n        if (!file.isDirectory()) {\n            return returnData.setErrorMsg(\"路径不是目录\")\n        }\n        var fileList = arrayListOf<Map<String, Any>>()\n        file.listFiles().forEach{\n            if (!it.name.startsWith(\".\")) {\n                fileList.add(mapOf(\n                    \"name\" to it.name,\n                    \"size\" to it.length(),\n                    \"path\" to it.toString().replace(home, \"\"),\n                    \"lastModified\" to it.lastModified(),\n                    \"isDirectory\" to it.isDirectory()\n                ))\n            }\n        }\n        return returnData.setData(fileList)\n    }\n\n    suspend fun getLocalStoreFile(context: RoutingContext) {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            context.success(returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\"))\n            return\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                context.success(returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\"))\n                return\n            }\n            if (!userInfo.enable_local_store) {\n                context.success(returnData.setErrorMsg(\"未开启本地书仓功能\"))\n                return\n            }\n        }\n        var path: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            path = context.bodyAsJson.getString(\"path\") ?: \"\"\n        } else {\n            // get 请求\n            path = context.queryParam(\"path\").firstOrNull() ?: \"\"\n        }\n        if (path.isEmpty()) {\n            context.success(returnData.setErrorMsg(\"参数错误\"))\n            return\n        }\n        var home = getWorkDir(\"storage\", \"localStore\")\n        var file = File(home + path)\n        logger.info(\"file: {} {}\", path, file)\n        if (!file.exists()) {\n            context.success(returnData.setErrorMsg(\"路径不存在\"))\n            return\n        }\n        context.response().putHeader(\"Cache-Control\", \"86400\")\n                        .putHeader(\"Content-Disposition\", \"attachment; filename=\" + URLEncoder.encode(file.name, \"UTF-8\"))\n                        .sendFile(file.toString())\n    }\n\n    suspend fun deleteLocalStoreFile(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n            }\n            if (!userInfo.enable_local_store) {\n                return returnData.setErrorMsg(\"未开启本地书仓功能\")\n            }\n        }\n        if (!checkManagerAuth(context)) {\n            return returnData.setData(\"NEED_SECURE_KEY\").setErrorMsg(\"请输入管理密码\")\n        }\n        var path: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            path = context.bodyAsJson.getString(\"path\") ?: \"\"\n        } else {\n            // get 请求\n            path = context.queryParam(\"path\").firstOrNull() ?: \"\"\n        }\n        if (path.isEmpty()) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n        var home = getWorkDir(\"storage\", \"localStore\")\n        var file = File(home + path)\n        logger.info(\"file: {} {}\", path, file)\n        if (!file.exists()) {\n            return returnData.setErrorMsg(\"路径不存在\")\n        }\n        file.deleteRecursively()\n        return returnData.setData(\"\")\n    }\n\n    suspend fun deleteLocalStoreFileList(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n            }\n            if (!userInfo.enable_local_store) {\n                return returnData.setErrorMsg(\"未开启本地书仓功能\")\n            }\n        }\n        if (!checkManagerAuth(context)) {\n            return returnData.setData(\"NEED_SECURE_KEY\").setErrorMsg(\"请输入管理密码\")\n        }\n        var path = context.bodyAsJson.getJsonArray(\"path\")\n        if (path == null) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n        var home = getWorkDir(\"storage\", \"localStore\")\n        path.forEach {\n            var filePath = it as String? ?: \"\"\n            if (filePath.isNotEmpty()) {\n                var file = File(home + filePath)\n                file.deleteRecursively()\n            }\n        }\n        return returnData.setData(\"\")\n    }\n\n    suspend fun bookSourceDebugSSE(context: RoutingContext) {\n        val returnData = ReturnData()\n        // 返回 event-stream\n        val response = context.response().putHeader(\"Content-Type\", \"text/event-stream\")\n            .putHeader(\"Cache-Control\", \"no-cache\")\n            .setChunked(true);\n\n        if (!checkAuth(context)) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\"), false) + \"\\n\\n\")\n            return\n        }\n        var bookSourceUrl = context.queryParam(\"bookSourceUrl\").firstOrNull() ?: \"\"\n        var keyword = context.queryParam(\"keyword\").firstOrNull() ?: \"\"\n\n        if (bookSourceUrl.isNullOrEmpty()) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"未配置书源\"), false) + \"\\n\\n\")\n            return\n        }\n        if (keyword.isNullOrEmpty()) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"请输入搜索关键词\"), false) + \"\\n\\n\")\n            return\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        var bookSourceString = getBookSourceBySourceURL(bookSourceUrl, userNameSpace).first\n        if (bookSourceString.isNullOrEmpty()) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"未配置书源\"), false) + \"\\n\\n\")\n            return\n        }\n\n        logger.info(\"bookSourceDebugSSE bookSource: {} keyword: {}\", bookSourceString, keyword)\n\n        val debugger = Debugger { msg ->\n            response.write(\"data: \" + jsonEncode(mapOf(\"msg\" to msg), false) + \"\\n\\n\")\n        }\n\n        val webBook = WebBook(bookSourceString)\n\n        debugger.startDebug(webBook, keyword)\n\n        response.write(\"event: end\\n\")\n        response.end(\"data: \" + jsonEncode(mapOf(\"end\" to true), false) + \"\\n\\n\")\n    }\n\n    suspend fun cacheBookSSE(context: RoutingContext) {\n        val returnData = ReturnData()\n        // 返回 event-stream\n        val response = context.response().putHeader(\"Content-Type\", \"text/event-stream\")\n            .putHeader(\"Cache-Control\", \"no-cache\")\n            .setChunked(true);\n\n        if (!checkAuth(context)) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\"), false) + \"\\n\\n\")\n            return\n        }\n        var bookUrl: String\n        var refresh: Int\n        var concurrentCount: Int\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"url\") ?: context.bodyAsJson.getString(\"bookUrl\") ?: \"\"\n            refresh = context.bodyAsJson.getInteger(\"refresh\", 0)\n            concurrentCount = context.bodyAsJson.getInteger(\"concurrentCount\", 24)\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"url\").firstOrNull() ?: \"\"\n            refresh = context.queryParam(\"refresh\").firstOrNull()?.toInt() ?: 0\n            concurrentCount = context.queryParam(\"concurrentCount\").firstOrNull()?.toInt() ?: 24\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"请输入书籍链接\"), false) + \"\\n\\n\")\n            return\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        val bookInfo = getShelfBookByURL(bookUrl, userNameSpace)\n        if (bookInfo == null) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"请先加入书架\"), false) + \"\\n\\n\")\n            return\n        }\n        if (bookInfo.isLocalBook()) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"本地书籍无需缓存\"), false) + \"\\n\\n\")\n            return\n        }\n        var bookSource = getBookSourceString(context, bookInfo.origin)\n        if (bookSource.isNullOrEmpty()) {\n            response.write(\"event: error\\n\")\n            response.end(\"data: \" + jsonEncode(returnData.setErrorMsg(\"未配置书源\"), false) + \"\\n\\n\")\n            return\n        }\n\n        var chapterList = getLocalChapterList(bookInfo, bookSource, false, userNameSpace)\n        var cachedChapterContentSet = mutableSetOf<Int>()\n        if (refresh <= 0) {\n            cachedChapterContentSet = getCachedChapterContentSet(bookInfo, userNameSpace)\n        }\n        val localCacheDir = getChapterCacheDir(bookInfo, userNameSpace)\n        var isEnd = false\n        var successCount = 0;\n        var failedCount = 0;\n\n        context.request().connection().closeHandler{\n            logger.info(\"客户端已断开链接，停止 cacheBookSSE\")\n            isEnd = true\n        }\n\n        concurrentCount = if(concurrentCount > 0) concurrentCount else 24\n        logger.info(\"cacheBookSSE concurrentCount: {} refresh: {}\", concurrentCount, refresh)\n        limitConcurrent(concurrentCount, 0, chapterList.size, {it->\n            if (!cachedChapterContentSet.contains(it)) {\n                val chapterIndex = it\n                var chapterInfo = chapterList.get(it)\n                try {\n                    var nextChapterUrl: String? = null\n                    if (chapterIndex + 1 < chapterList.size) {\n                        var nextChapterInfo = chapterList.get(chapterIndex + 1)\n                        nextChapterUrl = nextChapterInfo.url\n                    }\n                    var content = WebBook(bookSource, appConfig.debugLog).getBookContent(bookInfo, chapterInfo, nextChapterUrl)\n                    var chapterCacheFile = File(localCacheDir.absolutePath + File.separator + chapterIndex + \".txt\")\n                    chapterCacheFile.writeText(content)\n                    // 保存图片\n                    BookHelp.saveImages(\n                        this,\n                        BookSource.fromJson(bookSource).getOrNull() ?: BookSource(),\n                        bookInfo,\n                        chapterInfo,\n                        content\n                    )\n                    successCount++;\n                    cachedChapterContentSet.add(chapterIndex)\n                } catch(e: Exception) {\n                    isEnd = true\n                    failedCount++\n                }\n            }\n            it\n        }) {list, loopCount ->\n            if (isEnd) {\n                false\n            } else {\n                // 返回本轮数据\n                val result = mapOf(\n                    \"cachedCount\" to cachedChapterContentSet.size,\n                    \"successCount\" to successCount,\n                    \"failedCount\" to failedCount\n                )\n                response.write(\"data: \" + jsonEncode(result, false) + \"\\n\\n\")\n                logger.info(\"Loog: {} list.size: {} result: {}\", loopCount, list.size, result)\n                true\n            }\n        }\n        response.write(\"event: end\\n\")\n        response.end(\"data: \" + jsonEncode(mapOf(\n            \"cachedCount\" to cachedChapterContentSet.size,\n            \"successCount\" to successCount,\n            \"failedCount\" to failedCount\n        ), false) + \"\\n\\n\")\n    }\n\n    suspend fun deleteBookCache(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var bookUrl: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"url\") ?: context.bodyAsJson.getString(\"bookUrl\") ?: \"\"\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"url\").firstOrNull() ?: \"\"\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入书籍链接\")\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        val bookInfo = getShelfBookByURL(bookUrl, userNameSpace)\n        if (bookInfo == null) {\n            return returnData.setErrorMsg(\"请先加入书架\")\n        }\n        if (bookInfo.isLocalBook()) {\n            return returnData.setErrorMsg(\"本地书籍无需删除缓存\")\n        }\n        val localCacheDir = getChapterCacheDir(bookInfo, userNameSpace)\n        localCacheDir.deleteRecursively()\n\n        return returnData.setData(\"\")\n    }\n\n    fun getChapterCacheDir(bookInfo: Book, userNameSpace: String): File {\n        val md5Encode = MD5Utils.md5Encode(bookInfo.bookUrl).toString()\n        val localCacheDirPath = getWorkDir(\"storage\", \"data\", userNameSpace, bookInfo.name + \"_\" + bookInfo.author, md5Encode)\n        val localCacheDir = File(localCacheDirPath)\n        if (!localCacheDir.exists()) {\n            localCacheDir.mkdirs()\n        }\n        return localCacheDir\n    }\n\n    fun getCachedChapterContentSet(bookInfo: Book, userNameSpace: String): MutableSet<Int> {\n        val localCacheDir = getChapterCacheDir(bookInfo, userNameSpace)\n        val cachedChapterContentSet = mutableSetOf<Int>()\n        localCacheDir.listFiles().forEach{\n            if (!it.name.startsWith(\".\") && it.name.endsWith(\".txt\")) {\n                cachedChapterContentSet.add(it.name.replace(\".txt\", \"\").toInt())\n            }\n        }\n        return cachedChapterContentSet\n    }\n\n    suspend fun getShelfBookWithCacheInfo(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val userNameSpace = getUserNameSpace(context)\n        var bookList = getBookShelfBooks(false, userNameSpace)\n        var result = mutableListOf<Any>()\n        for (i in 0 until bookList.size) {\n            val bookInfo = bookList.get(i)\n            if (!bookInfo.isLocalBook()) {\n                val cachedSet = getCachedChapterContentSet(bookInfo, userNameSpace)\n                val bookInfoMap = bookInfo.toMap() as MutableMap<String, Any>\n                bookInfoMap.put(\"cachedChapterCount\", cachedSet.size)\n                result.add(bookInfoMap)\n            } else {\n                result.add(bookInfo)\n            }\n        }\n        return returnData.setData(result)\n    }\n\n    suspend fun exportBook(context: RoutingContext) {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            context.success(returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\"))\n            return\n        }\n        var bookUrl: String\n        var isEpub: Int\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"url\") ?: context.bodyAsJson.getString(\"bookUrl\") ?: \"\"\n            isEpub = context.bodyAsJson.getInteger(\"isEpub\", 0)\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"url\").firstOrNull() ?: \"\"\n            isEpub = context.queryParam(\"isEpub\").firstOrNull()?.toInt() ?: 0\n        }\n\n        if (bookUrl.isNullOrEmpty()) {\n            context.success(returnData.setErrorMsg(\"请输入书籍链接\"))\n            return\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        val bookInfo = getShelfBookByURL(bookUrl, userNameSpace)\n        if (bookInfo == null) {\n            context.success(returnData.setErrorMsg(\"请先加入书架\"))\n            return\n        }\n\n        if (bookInfo.isLocalBook()) {\n            val localFile = bookInfo.getLocalFile()\n            context.response().putHeader(\"Cache-Control\", \"300\")\n                            .putHeader(\"Content-Disposition\", \"attachment; filename=\" + URLEncoder.encode(localFile.name, \"UTF-8\"))\n                            .sendFile(localFile.toString())\n            return\n        }\n        var bookSource = getBookSourceString(context, bookInfo.origin)\n        if (bookSource.isNullOrEmpty()) {\n            context.success(returnData.setErrorMsg(\"未配置书源\"))\n            return\n        }\n        var exportDir = File(getWorkDir(\"storage\", \"assets\", userNameSpace, \"export\"))\n\n        val bookFile = if (isEpub > 0) {\n            exportToEpub(exportDir, bookInfo, bookSource, userNameSpace)\n        } else {\n            exportToTxt(exportDir, bookInfo, bookSource, userNameSpace)\n        }\n        context.response().putHeader(\"Cache-Control\", \"300\")\n                        .putHeader(\"Content-Disposition\", \"attachment; filename=\" + URLEncoder.encode(bookFile.name, \"UTF-8\"))\n                        .sendFile(bookFile.toString())\n    }\n\n    suspend fun exportToTxt(exportDir: File, bookInfo: Book, bookSource: String, userNameSpace: String): File {\n        val filename = \"《${bookInfo.name}》作者：${bookInfo.getRealAuthor()}.txt\"\n        val bookPath = FileUtils.getPath(exportDir, filename)\n        val bookFile = FileUtils.createFileWithReplace(bookPath)\n        // val stringBuilder = StringBuilder()\n        getAllContents(bookInfo, bookSource, userNameSpace) { text, srcList ->\n            bookFile.appendText(text, Charset.forName(appConfig.exportCharset))\n            // stringBuilder.append(text)\n            // srcList?.forEach {\n            //     val vFile = BookHelp.getImage(bookInfo, it.third)\n            //     if (vFile.exists()) {\n            //         FileUtils.createFileIfNotExist(\n            //             exportDir,\n            //             \"${book.name}_${book.author}\",\n            //             \"images\",\n            //             it.first,\n            //             \"${it.second}-${MD5Utils.md5Encode16(it.third)}.jpg\"\n            //         ).writeBytes(vFile.readBytes())\n            //     }\n            // }\n        }\n        return bookFile\n    }\n\n    private suspend fun getAllContents(\n        book: Book,\n        bookSourceString: String,\n        userNameSpace: String,\n        append: (text: String, srcList: ArrayList<Triple<String, Int, String>>?) -> Unit\n    ) {\n        // val useReplace = appConfig.exportUseReplace && book.getUseReplaceRule()\n        // val contentProcessor = ContentProcessor.get(book.name, book.origin)\n        val qy = \"${book.name}\\n作者：${\n            book.getRealAuthor()\n        }\\n简介：${\n            HtmlFormatter.format(book.getDisplayIntro())\n        }\"\n\n        append(qy, null)\n        var chapterList = getLocalChapterList(book, bookSourceString, false, userNameSpace)\n        val localCacheDir = getChapterCacheDir(book, userNameSpace)\n\n        chapterList.forEachIndexed { index, chapter ->\n            var chapterCacheFile = File(localCacheDir.absolutePath + File.separator + index + \".txt\")\n            var content = \"\"\n            if (!appConfig.exportNoChapterName) {\n                content += chapter.title + \"\\n\"\n            }\n            if (chapterCacheFile.exists()) {\n                content += chapterCacheFile.readText() + \"\\n\"\n            } else {\n                content += \"暂无缓存内容。\\n\"\n            }\n\n            append.invoke(\"\\n\\n$content\", null)\n\n            // BookHelp.getContent(book, chapter).let { content ->\n            //     val content1 = contentProcessor\n            //         .getContent(\n            //             book,\n            //             chapter,\n            //             content ?: \"null\",\n            //             includeTitle = !appConfig.exportNoChapterName,\n            //             useReplace = useReplace,\n            //             chineseConvert = false,\n            //             reSegment = false\n            //         ).joinToString(\"\\n\")\n            //     if (appConfig.exportPictureFile) {\n            //         //txt导出图片文件\n            //         val srcList = arrayListOf<Triple<String, Int, String>>()\n            //         content?.split(\"\\n\")?.forEachIndexed { index, text ->\n            //             val matcher = AppPattern.imgPattern.matcher(text)\n            //             while (matcher.find()) {\n            //                 matcher.group(1)?.let {\n            //                     val src = NetworkUtils.getAbsoluteURL(chapter.url, it)\n            //                     srcList.add(Triple(chapter.title, index, src))\n            //                 }\n            //             }\n            //         }\n            //         append.invoke(\"\\n\\n$content1\", srcList)\n            //     } else {\n            //         append.invoke(\"\\n\\n$content1\", null)\n            //     }\n            // }\n        }\n    }\n\n    private suspend fun exportToEpub(exportDir: File, book: Book, bookSource: String, userNameSpace: String): File {\n        val filename = \"《${book.name}》作者：${book.getRealAuthor()}.epub\"\n        val bookPath = FileUtils.getPath(exportDir, filename)\n        val bookFile = FileUtils.createFileWithReplace(bookPath)\n\n        val epubBook = EpubBook()\n        epubBook.version = \"2.0\"\n        //set metadata\n        setEpubMetadata(book, epubBook)\n        //set cover\n        setCover(book, epubBook, bookSource)\n        //set css\n        val contentModel = setAssets(book, epubBook)\n\n        //设置正文\n        setEpubContent(contentModel, book, epubBook, bookSource, userNameSpace)\n        EpubWriter().write(epubBook, FileOutputStream(bookFile))\n\n        return bookFile\n    }\n\n    private fun setAssets(book: Book, epubBook: EpubBook): String {\n        epubBook.resources.add(\n            Resource(\n                BookController::class.java.getResource(\"/epub/fonts.css\").readBytes(),\n                \"Styles/fonts.css\"\n            )\n        )\n        epubBook.resources.add(\n            Resource(\n                BookController::class.java.getResource(\"/epub/main.css\").readBytes(),\n                \"Styles/main.css\"\n            )\n        )\n        epubBook.resources.add(\n            Resource(\n                BookController::class.java.getResource(\"/epub/logo.png\").readBytes(),\n                \"Images/logo.png\"\n            )\n        )\n        epubBook.addSection(\n            \"封面\",\n            ResourceUtil.createPublicResource(\n                book.name,\n                book.getRealAuthor(),\n                book.getDisplayIntro(),\n                book.kind,\n                book.wordCount,\n                String(BookController::class.java.getResource(\"/epub/cover.html\").readBytes()),\n                \"Text/cover.html\"\n            )\n        )\n        epubBook.addSection(\n            \"简介\",\n            ResourceUtil.createPublicResource(\n                book.name,\n                book.getRealAuthor(),\n                book.getDisplayIntro(),\n                book.kind,\n                book.wordCount,\n                String(BookController::class.java.getResource(\"/epub/intro.html\").readBytes()),\n                \"Text/intro.html\"\n            )\n        )\n\n        return String(BookController::class.java.getResource(\"/epub/chapter.html\").readBytes())\n    }\n\n    private suspend fun setCover(book: Book, epubBook: EpubBook, bookSourceString: String) {\n        val coverUrl = book.getDisplayCover()\n        if (coverUrl == null) {\n            // TODO 默认封面\n\n        } else if (coverUrl.startsWith(\"/\")) {\n            // 本地 /assets 封面\n            val coverFile = File(getWorkDir(\"storage\", coverUrl.substring(1)))\n            val byteArray: ByteArray = coverFile.readBytes()\n            epubBook.coverImage = Resource(byteArray, \"Images/cover.jpg\")\n        } else {\n            var ext = getFileExt(coverUrl, \"jpg\")\n            val md5Encode = MD5Utils.md5Encode(coverUrl).toString()\n            var cachePath = getWorkDir(\"storage\", \"cache\", md5Encode + \".\" + ext)\n            var cacheFile = File(cachePath)\n            if (cacheFile.exists()) {\n                val byteArray: ByteArray = cacheFile.readBytes()\n                epubBook.coverImage = Resource(byteArray, \"Images/cover.jpg\")\n                return;\n            }\n            val analyzeUrl = AnalyzeUrl(coverUrl, source = BookSource.fromJson(bookSourceString).getOrNull())\n            try {\n                analyzeUrl.getByteArrayAwait().let {\n                    epubBook.coverImage = Resource(it, \"Images/cover.jpg\")\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n            } finally {\n\n            }\n            // webClient.getAbs(coverUrl).timeout(3000).send\n            // webClient.getAbs(coverUrl).timeout(3000).send {\n            //     var bodyBytes = it.result()?.bodyAsBuffer()?.getBytes()\n            //     if (bodyBytes != null) {\n            //         epubBook.coverImage = Resource(bodyBytes, \"Images/cover.jpg\")\n            //     }\n            // }\n        }\n    }\n\n    private suspend fun setEpubContent(\n        contentModel: String,\n        book: Book,\n        epubBook: EpubBook,\n        bookSourceString: String,\n        userNameSpace: String\n    ) {\n        //正文\n        var chapterList = getLocalChapterList(book, bookSourceString, false, userNameSpace)\n        val localCacheDir = getChapterCacheDir(book, userNameSpace)\n\n        chapterList.forEachIndexed { index, chapter ->\n            var chapterCacheFile = File(localCacheDir.absolutePath + File.separator + index + \".txt\")\n            var content = \"\"\n            if (!appConfig.exportNoChapterName) {\n                content += chapter.title + \"\\n\"\n            }\n            if (chapterCacheFile.exists()) {\n                content += chapterCacheFile.readText() + \"\\n\"\n            } else {\n                content += \"暂无缓存内容。\\n\"\n            }\n\n            var content1 = fixPic(epubBook, book, content, chapter)\n            // content1 = contentProcessor\n            //     .getContent(\n            //         book,\n            //         chapter,\n            //         content1,\n            //         includeTitle = false,\n            //         useReplace = useReplace,\n            //         chineseConvert = false,\n            //         reSegment = false\n            //     )\n            //     .joinToString(\"\\n\")\n            val title = chapter.title\n            epubBook.addSection(\n                title,\n                ResourceUtil.createChapterResource(\n                    title.replace(\"\\uD83D\\uDD12\", \"\"),\n                    content1,\n                    contentModel,\n                    \"Text/chapter_${index}.html\"\n                )\n            )\n        }\n    }\n\n    private fun fixPic(\n        epubBook: EpubBook,\n        book: Book,\n        content: String,\n        chapter: BookChapter\n    ): String {\n        val data = StringBuilder(\"\")\n        content.split(\"\\n\").forEach { text ->\n            var text1 = text\n            val matcher = AppPattern.imgPattern.matcher(text)\n            while (matcher.find()) {\n                matcher.group(1)?.let {\n                    val src = NetworkUtils.getAbsoluteURL(chapter.url, it)\n                    val originalHref = \"${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}\"\n                    val href = \"Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}\"\n                    val vFile = BookHelp.getImage(book, src)\n                    val fp = FileResourceProvider(vFile.parent)\n                    if (vFile.exists()) {\n                        val img = LazyResource(fp, href, originalHref)\n                        epubBook.resources.add(img)\n                    }\n                    text1 = text1.replace(src, \"../${href}\")\n                }\n            }\n            data.append(text1).append(\"\\n\")\n        }\n        return data.toString()\n    }\n\n    private fun setEpubMetadata(book: Book, epubBook: EpubBook) {\n        val metadata = Metadata()\n        metadata.titles.add(book.name)//书籍的名称\n        metadata.authors.add(Author(book.getRealAuthor()))//书籍的作者\n        metadata.language = \"zh\"//数据的语言\n        metadata.dates.add(Date())//数据的创建日期\n        metadata.publishers.add(\"Legado\")//数据的创建者\n        metadata.descriptions.add(book.getDisplayIntro())//书籍的简介\n        //metadata.subjects.add(\"\")//书籍的主题，在静读天下里面有使用这个分类书籍\n        epubBook.metadata = metadata\n    }\n\n    suspend fun searchBookContent(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var bookUrl: String\n        var keyword: String\n        var lastIndex: Int\n        var size: Int\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookUrl = context.bodyAsJson.getString(\"url\") ?: context.bodyAsJson.getString(\"bookUrl\") ?: \"\"\n            keyword = context.bodyAsJson.getString(\"keyword\") ?: \"\"\n            lastIndex = context.bodyAsJson.getInteger(\"lastIndex\", 0)\n            size = context.bodyAsJson.getInteger(\"size\", 20)\n        } else {\n            // get 请求\n            bookUrl = context.queryParam(\"url\").firstOrNull() ?: \"\"\n            keyword = context.queryParam(\"keyword\").firstOrNull() ?: \"\"\n            lastIndex = context.queryParam(\"lastIndex\").firstOrNull()?.toInt() ?: 0\n            size = context.queryParam(\"size\").firstOrNull()?.toInt() ?: 20\n        }\n        if (bookUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入书籍链接\")\n        }\n        if (keyword.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入搜索关键词\")\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        val bookInfo = getShelfBookByURL(bookUrl, userNameSpace)\n        if (bookInfo == null) {\n            return returnData.setErrorMsg(\"请先加入书架\")\n        }\n        var bookSource: String? = null\n        if (!bookInfo.isLocalBook()) {\n            bookSource = getBookSourceString(context, bookInfo.origin)\n            if (bookSource.isNullOrEmpty()) {\n                return returnData.setErrorMsg(\"未配置书源\")\n            }\n        }\n\n        var chapterList = getLocalChapterList(bookInfo, bookSource ?: \"\", false, userNameSpace)\n        if (lastIndex >= chapterList.size) {\n            return returnData.setErrorMsg(\"没有更多了\")\n        }\n\n        var isEnd = false\n        context.request().connection().closeHandler{\n            logger.info(\"客户端已断开链接，停止 searchBookContent\")\n            isEnd = true\n        }\n\n        logger.info(\"searchBookContent keyword: {} lastIndex: {}\", keyword, lastIndex)\n        var resultList = mutableListOf<SearchResult>();\n        lastIndex += 1\n        var currentIndex = lastIndex\n        for (chapterIndex in lastIndex until chapterList.size) {\n            currentIndex = chapterIndex\n            var chapter = chapterList.get(chapterIndex)\n            var chapterResult = searchChapter(bookInfo, chapter, keyword)\n            if (chapterResult.size > 0) {\n                resultList.addAll(chapterResult)\n            }\n\n            if (resultList.size >= size || isEnd) {\n                break;\n            }\n        }\n        return returnData.setData(mapOf(\"list\" to resultList, \"lastIndex\" to currentIndex))\n    }\n\n    suspend fun searchChapter(book: Book, chapter: BookChapter, query: String): List<SearchResult> {\n        val searchResultsWithinChapter: MutableList<SearchResult> = mutableListOf()\n        val chapterContent = BookHelp.getContent(book, chapter)\n        if (chapterContent != null) {\n            // withContext(Dispatchers.IO) {\n            //     chapter.title = when (AppConfig.chineseConverterType) {\n            //         1 -> ChineseUtils.t2s(chapter.title)\n            //         2 -> ChineseUtils.s2t(chapter.title)\n            //         else -> chapter.title\n            //     }\n            //     mContent = contentProcessor!!.getContent(\n            //         book, chapter, chapterContent,\n            //         chineseConvert = true,\n            //         reSegment = false,\n            //         useReplace = false\n            //     ).joinToString(\"\")\n            // }\n            val positions = searchPosition(chapterContent, query)\n            logger.info(\"positions: {}\", positions)\n            positions.forEachIndexed { index, position ->\n                val construct = getResultAndQueryIndex(chapterContent, position, query)\n                val result = SearchResult(\n                    resultCountWithinChapter = index,\n                    resultText = construct.second,\n                    chapterTitle = chapter.title,\n                    query = query,\n                    chapterIndex = chapter.index,\n                    queryIndexInResult = construct.first,\n                    queryIndexInChapter = position\n                )\n                searchResultsWithinChapter.add(result)\n            }\n        }\n        return searchResultsWithinChapter\n    }\n\n    private suspend fun searchPosition(mContent: String, pattern: String): List<Int> {\n        val position: MutableList<Int> = mutableListOf()\n        var index = mContent.indexOf(pattern)\n        if (index >= 0) {\n            //搜索到内容允许净化\n            // if (book!!.getUseReplaceRule()) {\n            //     mContent = contentProcessor!!.replaceContent(mContent)\n            //     index = mContent.indexOf(pattern)\n            // }\n            while (index >= 0) {\n                position.add(index)\n                index = mContent.indexOf(pattern, index + 1)\n            }\n        }\n        return position\n    }\n\n    private fun getResultAndQueryIndex(\n        content: String,\n        queryIndexInContent: Int,\n        query: String\n    ): Pair<Int, String> {\n        // 左右移动20个字符，构建关键词周边文字，在搜索结果里显示\n        // todo: 判断段落，只在关键词所在段落内分割\n        // todo: 利用标点符号分割完整的句\n        // todo: length和设置结合，自由调整周边文字长度\n        val length = 20\n        var po1 = queryIndexInContent - length\n        var po2 = queryIndexInContent + query.length + length\n        if (po1 < 0) {\n            po1 = 0\n        }\n        if (po2 > content.length) {\n            po2 = content.length\n        }\n        val queryIndexInResult = queryIndexInContent - po1\n        val newText = content.substring(po1, po2)\n        return queryIndexInResult to newText\n    }\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/api/controller/BookSourceController.kt",
    "content": "package com.htmake.reader.api.controller\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.SearchBook\nimport io.legado.app.data.entities.BookGroup\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.RssSource\nimport io.legado.app.data.entities.RssArticle\nimport io.legado.app.model.webBook.WebBook\nimport io.vertx.ext.web.Route\nimport io.vertx.ext.web.Router\nimport io.vertx.ext.web.RoutingContext\nimport io.vertx.ext.web.handler.StaticHandler;\nimport mu.KotlinLogging\nimport com.htmake.reader.config.AppConfig\nimport com.htmake.reader.config.BookConfig\nimport io.legado.app.constant.DeepinkBookSource\nimport com.htmake.reader.utils.error\nimport com.htmake.reader.utils.success\nimport com.htmake.reader.utils.getStorage\nimport com.htmake.reader.utils.saveStorage\nimport com.htmake.reader.utils.asJsonArray\nimport com.htmake.reader.utils.asJsonObject\nimport com.htmake.reader.utils.toDataClass\nimport com.htmake.reader.utils.toMap\nimport com.htmake.reader.utils.fillData\nimport com.htmake.reader.utils.getWorkDir\nimport com.htmake.reader.utils.getRandomString\nimport com.htmake.reader.utils.genEncryptedPassword\nimport com.htmake.reader.entity.User\nimport com.htmake.reader.utils.SpringContextUtils\nimport com.htmake.reader.utils.deleteRecursively\nimport com.htmake.reader.utils.unzip\nimport com.htmake.reader.utils.zip\nimport com.htmake.reader.utils.jsonEncode\nimport com.htmake.reader.utils.getRelativePath\nimport com.htmake.reader.verticle.RestVerticle\nimport com.htmake.reader.SpringEvent\nimport org.springframework.stereotype.Component\nimport io.vertx.core.json.JsonObject\nimport io.vertx.core.json.JsonArray\nimport io.vertx.core.http.HttpMethod\nimport com.htmake.reader.api.ReturnData\nimport io.legado.app.utils.MD5Utils\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.net.URL;\nimport java.util.UUID;\nimport io.vertx.ext.web.client.WebClient\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.core.env.Environment\nimport java.io.File\nimport java.lang.Runtime\nimport kotlin.collections.mutableMapOf\nimport kotlin.system.measureTimeMillis\nimport kotlin.coroutines.CoroutineContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.text.SimpleDateFormat;\nimport io.legado.app.utils.EncoderUtils\nimport io.legado.app.model.rss.Rss\nimport org.springframework.scheduling.annotation.Scheduled\nimport io.legado.app.model.localBook.LocalBook\nimport java.nio.file.Paths\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.CoroutineScope\n\nprivate val logger = KotlinLogging.logger {}\n\nclass BookSourceController(coroutineContext: CoroutineContext): BaseController(coroutineContext) {\n    private var webClient: WebClient\n\n    init {\n        webClient = SpringContextUtils.getBean(\"webClient\", WebClient::class.java)\n    }\n\n    suspend fun getUserBookSourceJson(userNameSpace: String): JsonArray? {\n        var bookSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookSource\"))\n        if (bookSourceList == null && !userNameSpace.equals(\"default\")) {\n            // 用户书源文件不存在，拷贝系统书源\n            var systemBookSourceList: JsonArray? = asJsonArray(getUserStorage(\"default\", \"bookSource\"))\n            if (systemBookSourceList != null) {\n                saveUserStorage(userNameSpace, \"bookSource\", systemBookSourceList.getList())\n                bookSourceList = systemBookSourceList\n            }\n        }\n        return bookSourceList\n    }\n\n    suspend fun saveBookSource(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookSource = BookSource.fromJson(context.bodyAsString).getOrNull()\n        if (bookSource == null) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n        // val bookSource = context.bodyAsJson.mapTo(BookSource::class.java)\n\n        var userNameSpace = getUserNameSpace(context)\n        var bookSourceList = getUserBookSourceJson(userNameSpace)\n        if (bookSourceList == null) {\n            bookSourceList = JsonArray()\n        }\n        // 遍历判断书本是否存在\n        var existIndex: Int = -1\n        for (i in 0 until bookSourceList.size()) {\n            var _bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java)\n            if (_bookSource.bookSourceUrl.equals(bookSource.bookSourceUrl)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex >= 0) {\n            var sourceList = bookSourceList.getList()\n            sourceList.set(existIndex, JsonObject.mapFrom(bookSource))\n            bookSourceList = JsonArray(sourceList)\n        } else {\n            bookSourceList.add(JsonObject.mapFrom(bookSource))\n        }\n\n        // logger.info(\"bookSourceList: {}\", bookSourceList)\n        saveUserStorage(userNameSpace, \"bookSource\", bookSourceList)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun saveBookSources(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookSourceJsonArray = context.bodyAsJsonArray\n        if (bookSourceJsonArray == null) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var bookSourceList = getUserBookSourceJson(userNameSpace)\n        if (bookSourceList == null) {\n            bookSourceList = JsonArray()\n        }\n        for (k in 0 until bookSourceJsonArray.size()) {\n            val bookSource = BookSource.fromJson(bookSourceJsonArray.getJsonObject(k).toString()).getOrNull()\n            if (bookSource == null) {\n                continue\n            }\n            // var bookSource = bookSourceJsonArray.getJsonObject(k).mapTo(BookSource::class.java)\n            // 遍历判断书本是否存在\n            var existIndex: Int = -1\n            for (i in 0 until bookSourceList!!.size()) {\n                var _bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java)\n                if (_bookSource.bookSourceUrl.equals(bookSource.bookSourceUrl)) {\n                    existIndex = i\n                    break;\n                }\n            }\n            if (existIndex >= 0) {\n                var sourceList = bookSourceList.getList()\n                sourceList.set(existIndex, JsonObject.mapFrom(bookSource))\n                bookSourceList = JsonArray(sourceList)\n            } else {\n                bookSourceList.add(JsonObject.mapFrom(bookSource))\n            }\n        }\n\n        // logger.info(\"bookSourceList: {}\", bookSourceList)\n        saveUserStorage(userNameSpace, \"bookSource\", bookSourceList!!)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun getBookSource(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        checkAuth(context)\n        var bookSourceUrl: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            bookSourceUrl = context.bodyAsJson.getString(\"bookSourceUrl\")\n        } else {\n            // get 请求\n            bookSourceUrl = context.queryParam(\"bookSourceUrl\").firstOrNull() ?: \"\"\n        }\n        if (bookSourceUrl.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"书源链接不能为空\")\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        var bookSourceList = getUserBookSourceJson(userNameSpace)\n        if (bookSourceList == null) {\n            bookSourceList = JsonArray()\n        }\n        // 遍历判断书本是否存在\n        var existIndex: Int = -1\n        for (i in 0 until bookSourceList.size()) {\n            var _bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java)\n            if (_bookSource.bookSourceUrl.equals(bookSourceUrl)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex < 0) {\n            return returnData.setErrorMsg(\"书源信息不存在\")\n        }\n\n        return returnData.setData(bookSourceList.getJsonObject(existIndex).map)\n    }\n\n    suspend fun getBookSources(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        checkAuth(context)\n        var simple: Int = 0\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            simple = context.bodyAsJson.getInteger(\"simple\", 0)\n        } else {\n            // get 请求\n            simple = context.queryParam(\"simple\").firstOrNull()?.toInt() ?: 0\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var bookSourceList = getUserBookSourceJson(userNameSpace)\n        if (bookSourceList != null) {\n            if (simple > 0) {\n                var list = arrayListOf<Map<String, Any?>>()\n                for (i in 0 until bookSourceList.size()) {\n                    var bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java)\n                    list.add(mapOf<String, Any?>(\n                        \"bookSourceGroup\" to bookSource.bookSourceGroup,\n                        \"bookSourceName\" to bookSource.bookSourceName,\n                        \"bookSourceUrl\" to bookSource.bookSourceUrl,\n                        \"exploreUrl\" to bookSource.exploreUrl\n                    ))\n                }\n                return returnData.setData(list)\n            }\n            return returnData.setData(bookSourceList.getList())\n        }\n        return returnData.setData(arrayListOf<Int>())\n    }\n\n    suspend fun deleteBookSource(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookSource = context.bodyAsJson.mapTo(BookSource::class.java)\n\n        var userNameSpace = getUserNameSpace(context)\n        var bookSourceList = getUserBookSourceJson(userNameSpace)\n        if (bookSourceList == null) {\n            bookSourceList = JsonArray()\n        }\n        // 遍历判断书本是否存在\n        var existIndex: Int = -1\n        for (i in 0 until bookSourceList.size()) {\n            var _bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java)\n            if (_bookSource.bookSourceUrl.equals(bookSource.bookSourceUrl)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex >= 0) {\n            bookSourceList.remove(existIndex)\n        }\n\n        // logger.info(\"bookSourceList: {}\", bookSourceList)\n        saveUserStorage(userNameSpace, \"bookSource\", bookSourceList)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun deleteBookSources(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookSourceJsonArray = context.bodyAsJsonArray\n\n        var userNameSpace = getUserNameSpace(context)\n        var bookSourceList = getUserBookSourceJson(userNameSpace)\n        if (bookSourceList == null) {\n            bookSourceList = JsonArray()\n        }\n        for (k in 0 until bookSourceJsonArray.size()) {\n            var bookSource = bookSourceJsonArray.getJsonObject(k).mapTo(BookSource::class.java)\n            // 遍历判断书本是否存在\n            var existIndex: Int = -1\n            for (i in 0 until bookSourceList.size()) {\n                var _bookSource = bookSourceList.getJsonObject(i).mapTo(BookSource::class.java)\n                if (_bookSource.bookSourceUrl.equals(bookSource.bookSourceUrl)) {\n                    existIndex = i\n                    break;\n                }\n            }\n            if (existIndex >= 0) {\n                bookSourceList.remove(existIndex)\n            }\n        }\n\n        // logger.info(\"bookSourceList: {}\", bookSourceList)\n        saveUserStorage(userNameSpace, \"bookSource\", bookSourceList)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun deleteAllBookSources(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        saveUserStorage(userNameSpace, \"bookSource\", JsonArray())\n        return returnData.setData(\"\")\n    }\n\n    suspend fun setAsDefaultBookSources(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (!checkManagerAuth(context)) {\n            return returnData.setData(\"NEED_SECURE_KEY\").setErrorMsg(\"请输入管理密码\")\n        }\n        var username = context.bodyAsJson.getString(\"username\")\n        var bookSourceList: JsonArray? = asJsonArray(getUserStorage(username, \"bookSource\"))\n        if (bookSourceList == null) {\n            return returnData.setErrorMsg(\"用户书源不存在\")\n        }\n\n        // 保存为默认书源\n        saveUserStorage(\"default\", \"bookSource\", bookSourceList.getList())\n        return returnData.setData(\"设置默认书源成功\")\n    }\n\n    suspend fun readSourceFile(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (context.fileUploads() == null || context.fileUploads().isEmpty()) {\n            return returnData.setErrorMsg(\"请上传文件\")\n        }\n        var sourceList = JsonArray()\n        context.fileUploads().forEach {\n            // logger.info(\"readSourceFile: {}\", it.uploadedFileName())\n            var file = File(it.uploadedFileName())\n            if (file.exists()) {\n                sourceList.add(file.readText())\n                file.delete()\n            }\n        }\n        return returnData.setData(sourceList.getList())\n    }\n\n    suspend fun readRemoteSourceFile(context: RoutingContext) {\n        val returnData = ReturnData()\n        var url: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            url = context.bodyAsJson.getString(\"url\") ?: \"\"\n        } else {\n            // get 请求\n            url = context.queryParam(\"url\").firstOrNull() ?: \"\"\n        }\n        if (url.isNullOrEmpty()) {\n            context.success(returnData.setErrorMsg(\"请输入远程书源链接\"))\n            return\n        }\n\n        launch(Dispatchers.IO) {\n            webClient.getAbs(url).timeout(3000).send {\n                var body = it.result()?.bodyAsString()\n                if (body != null) {\n                    context.success(returnData.setData(arrayListOf(body)))\n                } else {\n                    context.success(returnData.setErrorMsg(\"远程书源链接错误\"))\n                }\n            }\n        }\n    }\n\n    suspend fun deleteUserBookSource(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (!checkManagerAuth(context)) {\n            return returnData.setData(\"NEED_SECURE_KEY\").setErrorMsg(\"请输入管理密码\")\n        }\n        val userJsonArray = context.bodyAsJsonArray\n        for (i in 0 until userJsonArray.size()) {\n            var username = userJsonArray.getString(i)\n            var userBookSourceFile = File(getWorkDir(\"storage\", \"data\", username, \"bookSource.json\"))\n            // 删除用户书源文件，恢复默认书源\n            if (userBookSourceFile.exists()) {\n                userBookSourceFile.deleteRecursively()\n            }\n        }\n        return returnData.setData(\"删除书源成功\")\n    }\n\n    suspend fun deleteBookSourcesFile(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var userBookSourceFile = File(getWorkDir(\"storage\", \"data\", userNameSpace, \"bookSource.json\"))\n        // 删除用户书源文件，恢复默认书源\n        if (userBookSourceFile.exists()) {\n            userBookSourceFile.deleteRecursively()\n        }\n        return returnData.setData(\"\")\n    }\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/api/controller/BookmarkController.kt",
    "content": "package com.htmake.reader.api.controller\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.SearchBook\nimport io.legado.app.data.entities.BookGroup\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.Bookmark\nimport io.legado.app.model.webBook.WebBook\nimport io.vertx.ext.web.Route\nimport io.vertx.ext.web.Router\nimport io.vertx.ext.web.RoutingContext\nimport io.vertx.ext.web.handler.StaticHandler;\nimport mu.KotlinLogging\nimport com.htmake.reader.config.AppConfig\nimport com.htmake.reader.config.BookConfig\nimport io.legado.app.constant.DeepinkBookSource\nimport com.htmake.reader.utils.error\nimport com.htmake.reader.utils.success\nimport com.htmake.reader.utils.getStorage\nimport com.htmake.reader.utils.saveStorage\nimport com.htmake.reader.utils.asJsonArray\nimport com.htmake.reader.utils.asJsonObject\nimport com.htmake.reader.utils.toDataClass\nimport com.htmake.reader.utils.toMap\nimport com.htmake.reader.utils.fillData\nimport com.htmake.reader.utils.getWorkDir\nimport com.htmake.reader.utils.getRandomString\nimport com.htmake.reader.utils.genEncryptedPassword\nimport com.htmake.reader.entity.User\nimport com.htmake.reader.utils.SpringContextUtils\nimport com.htmake.reader.utils.deleteRecursively\nimport com.htmake.reader.utils.unzip\nimport com.htmake.reader.utils.zip\nimport com.htmake.reader.utils.jsonEncode\nimport com.htmake.reader.utils.getRelativePath\nimport com.htmake.reader.verticle.RestVerticle\nimport com.htmake.reader.SpringEvent\nimport org.springframework.stereotype.Component\nimport io.vertx.core.json.JsonObject\nimport io.vertx.core.json.JsonArray\nimport io.vertx.core.http.HttpMethod\nimport com.htmake.reader.api.ReturnData\nimport io.legado.app.utils.MD5Utils\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.net.URL;\nimport java.util.UUID;\nimport io.vertx.ext.web.client.WebClient\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.core.env.Environment\nimport java.io.File\nimport java.lang.Runtime\nimport kotlin.collections.mutableMapOf\nimport kotlin.system.measureTimeMillis\nimport kotlin.coroutines.CoroutineContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.text.SimpleDateFormat;\nimport io.legado.app.utils.EncoderUtils\nimport io.legado.app.model.rss.Rss\nimport org.springframework.scheduling.annotation.Scheduled\nimport io.legado.app.model.localBook.LocalBook\nimport java.nio.file.Paths\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.CoroutineScope\n\nprivate val logger = KotlinLogging.logger {}\n\nclass BookmarkController(coroutineContext: CoroutineContext): BaseController(coroutineContext) {\n    suspend fun getBookmarks(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var list: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookmark\"))\n        if (list != null) {\n            return returnData.setData(list.getList())\n        }\n        return returnData.setData(arrayListOf<Int>())\n    }\n\n    suspend fun saveBookmark(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookmark = context.bodyAsJson.mapTo(Bookmark::class.java)\n        if (bookmark.bookName.isEmpty() && bookmark.bookAuthor.isEmpty()) {\n            return returnData.setErrorMsg(\"书籍信息错误\")\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        var bookmarkList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookmark\"))\n        if (bookmarkList == null) {\n            bookmarkList = JsonArray()\n        }\n        // 遍历判断是否存在\n        var existIndex: Int = -1\n        for (i in 0 until bookmarkList.size()) {\n            var _bookmark = bookmarkList.getJsonObject(i).mapTo(Bookmark::class.java)\n            if (_bookmark.bookName.equals(bookmark.bookName) && _bookmark.bookAuthor.equals(bookmark.bookAuthor)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex >= 0) {\n            var list = bookmarkList.getList()\n            list.set(existIndex, JsonObject.mapFrom(bookmark))\n            bookmarkList = JsonArray(list)\n        } else {\n            // 新增书签\n            bookmarkList.add(JsonObject.mapFrom(bookmark))\n        }\n\n        // logger.info(\"bookmarkList: {}\", bookmarkList)\n        saveUserStorage(userNameSpace, \"bookmark\", bookmarkList)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun saveBookmarks(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookmarkJsonArray = context.bodyAsJsonArray\n        if (bookmarkJsonArray == null) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var bookmarkList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookmark\"))\n        if (bookmarkList == null) {\n            bookmarkList = JsonArray()\n        }\n        for (k in 0 until bookmarkJsonArray.size()) {\n            var bookmark = bookmarkJsonArray.getJsonObject(k).mapTo(Bookmark::class.java)\n            if (bookmark.bookName.isEmpty() && bookmark.bookAuthor.isEmpty()) {\n                continue\n            }\n            // 遍历判断是否存在\n            var existIndex: Int = -1\n            for (i in 0 until bookmarkList!!.size()) {\n                var _bookmark = bookmarkList.getJsonObject(i).mapTo(Bookmark::class.java)\n                if (_bookmark.bookName.equals(bookmark.bookName) && _bookmark.bookAuthor.equals(bookmark.bookAuthor)) {\n                    existIndex = i\n                    break;\n                }\n            }\n            if (existIndex >= 0) {\n                var list = bookmarkList.getList()\n                list.set(existIndex, JsonObject.mapFrom(bookmark))\n                bookmarkList = JsonArray(list)\n            } else {\n                // 新增书签\n                bookmarkList.add(JsonObject.mapFrom(bookmark))\n            }\n        }\n\n        // logger.info(\"bookmarkList: {}\", bookmarkList)\n        saveUserStorage(userNameSpace, \"bookmark\", bookmarkList!!)\n        return returnData.setData(\"\")\n    }\n\n\n    suspend fun deleteBookmark(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookmark = context.bodyAsJson.mapTo(Bookmark::class.java)\n        var userNameSpace = getUserNameSpace(context)\n        var bookmarkList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookmark\"))\n        if (bookmarkList == null) {\n            bookmarkList = JsonArray()\n        }\n        // 遍历判断是否存在\n        var existIndex: Int = -1\n        for (i in 0 until bookmarkList.size()) {\n            var _bookmark = bookmarkList.getJsonObject(i).mapTo(Bookmark::class.java)\n            if (_bookmark.bookName.equals(bookmark.bookName) && _bookmark.bookAuthor.equals(bookmark.bookAuthor)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex >= 0) {\n            bookmarkList.remove(existIndex)\n        }\n        // logger.info(\"bookmark: {}\", bookmark)\n        saveUserStorage(userNameSpace, \"bookmark\", bookmarkList)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun deleteBookmarks(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val bookmarkJsonArray = context.bodyAsJsonArray\n\n        var userNameSpace = getUserNameSpace(context)\n        var bookmarkList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"bookmark\"))\n        if (bookmarkList == null) {\n            bookmarkList = JsonArray()\n        }\n        for (k in 0 until bookmarkJsonArray.size()) {\n            var bookmark = bookmarkJsonArray.getJsonObject(k).mapTo(Bookmark::class.java)\n            // 遍历判断书本是否存在\n            var existIndex: Int = -1\n            for (i in 0 until bookmarkList.size()) {\n                var _bookmark = bookmarkList.getJsonObject(i).mapTo(Bookmark::class.java)\n                if (_bookmark.bookName.equals(bookmark.bookName) && _bookmark.bookAuthor.equals(bookmark.bookAuthor)) {\n                    existIndex = i\n                    break;\n                }\n            }\n            if (existIndex >= 0) {\n                bookmarkList.remove(existIndex)\n            }\n        }\n        // logger.info(\"bookmark: {}\", bookmark)\n        saveUserStorage(userNameSpace, \"bookmark\", bookmarkList)\n        return returnData.setData(\"\")\n    }\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/api/controller/ReplaceRuleController.kt",
    "content": "package com.htmake.reader.api.controller\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.SearchBook\nimport io.legado.app.data.entities.BookGroup\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.ReplaceRule\nimport io.legado.app.model.webBook.WebBook\nimport io.vertx.ext.web.Route\nimport io.vertx.ext.web.Router\nimport io.vertx.ext.web.RoutingContext\nimport io.vertx.ext.web.handler.StaticHandler;\nimport mu.KotlinLogging\nimport com.htmake.reader.config.AppConfig\nimport com.htmake.reader.config.BookConfig\nimport io.legado.app.constant.DeepinkBookSource\nimport com.htmake.reader.utils.error\nimport com.htmake.reader.utils.success\nimport com.htmake.reader.utils.getStorage\nimport com.htmake.reader.utils.saveStorage\nimport com.htmake.reader.utils.asJsonArray\nimport com.htmake.reader.utils.asJsonObject\nimport com.htmake.reader.utils.toDataClass\nimport com.htmake.reader.utils.toMap\nimport com.htmake.reader.utils.fillData\nimport com.htmake.reader.utils.getWorkDir\nimport com.htmake.reader.utils.getRandomString\nimport com.htmake.reader.utils.genEncryptedPassword\nimport com.htmake.reader.entity.User\nimport com.htmake.reader.utils.SpringContextUtils\nimport com.htmake.reader.utils.deleteRecursively\nimport com.htmake.reader.utils.unzip\nimport com.htmake.reader.utils.zip\nimport com.htmake.reader.utils.jsonEncode\nimport com.htmake.reader.utils.getRelativePath\nimport com.htmake.reader.verticle.RestVerticle\nimport com.htmake.reader.SpringEvent\nimport org.springframework.stereotype.Component\nimport io.vertx.core.json.JsonObject\nimport io.vertx.core.json.JsonArray\nimport io.vertx.core.http.HttpMethod\nimport com.htmake.reader.api.ReturnData\nimport io.legado.app.utils.MD5Utils\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.net.URL;\nimport java.util.UUID;\nimport io.vertx.ext.web.client.WebClient\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.core.env.Environment\nimport java.io.File\nimport java.lang.Runtime\nimport kotlin.collections.mutableMapOf\nimport kotlin.system.measureTimeMillis\nimport kotlin.coroutines.CoroutineContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.text.SimpleDateFormat;\nimport io.legado.app.utils.EncoderUtils\nimport io.legado.app.model.rss.Rss\nimport org.springframework.scheduling.annotation.Scheduled\nimport io.legado.app.model.localBook.LocalBook\nimport java.nio.file.Paths\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.CoroutineScope\n\nprivate val logger = KotlinLogging.logger {}\n\nclass ReplaceRuleController(coroutineContext: CoroutineContext): BaseController(coroutineContext) {\n    suspend fun getReplaceRules(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var list: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"replaceRule\"))\n        if (list != null) {\n            return returnData.setData(list.getList())\n        }\n        return returnData.setData(arrayListOf<Int>())\n    }\n\n    suspend fun saveReplaceRule(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val replaceRule = context.bodyAsJson.mapTo(ReplaceRule::class.java)\n        if (replaceRule.name.isEmpty()) {\n            return returnData.setErrorMsg(\"名称不能为空\")\n        }\n        if (replaceRule.pattern.isEmpty()) {\n            return returnData.setErrorMsg(\"替换规则不能为空\")\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        var replaceRuleList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"replaceRule\"))\n        if (replaceRuleList == null) {\n            replaceRuleList = JsonArray()\n        }\n        // 遍历判断是否存在\n        var existIndex: Int = -1\n        for (i in 0 until replaceRuleList.size()) {\n            var _replaceRule = replaceRuleList.getJsonObject(i).mapTo(ReplaceRule::class.java)\n            if (_replaceRule.name.equals(replaceRule.name)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex >= 0) {\n            var list = replaceRuleList.getList()\n            list.set(existIndex, JsonObject.mapFrom(replaceRule))\n            replaceRuleList = JsonArray(list)\n        } else {\n            // 新增替换规则\n            replaceRuleList.add(JsonObject.mapFrom(replaceRule))\n        }\n\n        // logger.info(\"replaceRuleList: {}\", replaceRuleList)\n        saveUserStorage(userNameSpace, \"replaceRule\", replaceRuleList)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun saveReplaceRules(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val replaceRuleJsonArray = context.bodyAsJsonArray\n        if (replaceRuleJsonArray == null) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var replaceRuleList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"replaceRule\"))\n        if (replaceRuleList == null) {\n            replaceRuleList = JsonArray()\n        }\n        for (k in 0 until replaceRuleJsonArray.size()) {\n            var replaceRule = replaceRuleJsonArray.getJsonObject(k).mapTo(ReplaceRule::class.java)\n            if (replaceRule.name.isEmpty()) {\n                continue\n            }\n            if (replaceRule.pattern.isEmpty()) {\n                continue\n            }\n            // 遍历判断是否存在\n            var existIndex: Int = -1\n            for (i in 0 until replaceRuleList!!.size()) {\n                var _replaceRule = replaceRuleList.getJsonObject(i).mapTo(ReplaceRule::class.java)\n                if (_replaceRule.name.equals(replaceRule.name)) {\n                    existIndex = i\n                    break;\n                }\n            }\n            if (existIndex >= 0) {\n                var list = replaceRuleList.getList()\n                list.set(existIndex, JsonObject.mapFrom(replaceRule))\n                replaceRuleList = JsonArray(list)\n            } else {\n                // 新增替换规则\n                replaceRuleList.add(JsonObject.mapFrom(replaceRule))\n            }\n        }\n\n        // logger.info(\"replaceRuleList: {}\", replaceRuleList)\n        saveUserStorage(userNameSpace, \"replaceRule\", replaceRuleList!!)\n        return returnData.setData(\"\")\n    }\n\n\n    suspend fun deleteReplaceRule(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val replaceRule = context.bodyAsJson.mapTo(ReplaceRule::class.java)\n        var userNameSpace = getUserNameSpace(context)\n        var replaceRuleList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"replaceRule\"))\n        if (replaceRuleList == null) {\n            replaceRuleList = JsonArray()\n        }\n        // 遍历判断是否存在\n        var existIndex: Int = -1\n        for (i in 0 until replaceRuleList.size()) {\n            var _replaceRule = replaceRuleList.getJsonObject(i).mapTo(ReplaceRule::class.java)\n            if (_replaceRule.name.equals(replaceRule.name)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex >= 0) {\n            replaceRuleList.remove(existIndex)\n        }\n        // logger.info(\"replaceRule: {}\", replaceRule)\n        saveUserStorage(userNameSpace, \"replaceRule\", replaceRuleList)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun deleteReplaceRules(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val replaceRuleJsonArray = context.bodyAsJsonArray\n\n        var userNameSpace = getUserNameSpace(context)\n        var replaceRuleList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"replaceRule\"))\n        if (replaceRuleList == null) {\n            replaceRuleList = JsonArray()\n        }\n        for (k in 0 until replaceRuleJsonArray.size()) {\n            var replaceRule = replaceRuleJsonArray.getJsonObject(k).mapTo(ReplaceRule::class.java)\n            // 遍历判断书本是否存在\n            var existIndex: Int = -1\n            for (i in 0 until replaceRuleList.size()) {\n                var _replaceRule = replaceRuleList.getJsonObject(i).mapTo(ReplaceRule::class.java)\n                if (_replaceRule.name.equals(replaceRule.name)) {\n                    existIndex = i\n                    break;\n                }\n            }\n            if (existIndex >= 0) {\n                replaceRuleList.remove(existIndex)\n            }\n        }\n        // logger.info(\"replaceRule: {}\", replaceRule)\n        saveUserStorage(userNameSpace, \"replaceRule\", replaceRuleList)\n        return returnData.setData(\"\")\n    }\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/api/controller/RssSourceController.kt",
    "content": "package com.htmake.reader.api.controller\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.SearchBook\nimport io.legado.app.data.entities.BookGroup\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.RssSource\nimport io.legado.app.data.entities.RssArticle\nimport io.legado.app.model.webBook.WebBook\nimport io.vertx.ext.web.Route\nimport io.vertx.ext.web.Router\nimport io.vertx.ext.web.RoutingContext\nimport io.vertx.ext.web.handler.StaticHandler;\nimport mu.KotlinLogging\nimport com.htmake.reader.config.AppConfig\nimport com.htmake.reader.config.BookConfig\nimport io.legado.app.constant.DeepinkBookSource\nimport com.htmake.reader.utils.error\nimport com.htmake.reader.utils.success\nimport com.htmake.reader.utils.getStorage\nimport com.htmake.reader.utils.saveStorage\nimport com.htmake.reader.utils.asJsonArray\nimport com.htmake.reader.utils.asJsonObject\nimport com.htmake.reader.utils.toDataClass\nimport com.htmake.reader.utils.toMap\nimport com.htmake.reader.utils.fillData\nimport com.htmake.reader.utils.getWorkDir\nimport com.htmake.reader.utils.getRandomString\nimport com.htmake.reader.utils.genEncryptedPassword\nimport com.htmake.reader.entity.User\nimport com.htmake.reader.utils.SpringContextUtils\nimport com.htmake.reader.utils.deleteRecursively\nimport com.htmake.reader.utils.unzip\nimport com.htmake.reader.utils.zip\nimport com.htmake.reader.utils.jsonEncode\nimport com.htmake.reader.utils.getRelativePath\nimport com.htmake.reader.verticle.RestVerticle\nimport com.htmake.reader.SpringEvent\nimport org.springframework.stereotype.Component\nimport io.vertx.core.json.JsonObject\nimport io.vertx.core.json.JsonArray\nimport io.vertx.core.http.HttpMethod\nimport com.htmake.reader.api.ReturnData\nimport io.legado.app.utils.MD5Utils\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.net.URL;\nimport java.util.UUID;\nimport io.vertx.ext.web.client.WebClient\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.core.env.Environment\nimport java.io.File\nimport java.lang.Runtime\nimport kotlin.collections.mutableMapOf\nimport kotlin.system.measureTimeMillis\nimport kotlin.coroutines.CoroutineContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.text.SimpleDateFormat;\nimport io.legado.app.utils.EncoderUtils\nimport io.legado.app.model.rss.Rss\nimport org.springframework.scheduling.annotation.Scheduled\nimport io.legado.app.model.localBook.LocalBook\nimport io.legado.app.model.Debug\nimport java.nio.file.Paths\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.CoroutineScope\n\nprivate val logger = KotlinLogging.logger {}\n\nclass RssSourceController(coroutineContext: CoroutineContext): BaseController(coroutineContext) {\n    suspend fun getRssSources(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var list: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"rssSources\"))\n        if (list != null) {\n            return returnData.setData(list.getList())\n        }\n        return returnData.setData(arrayListOf<Int>())\n    }\n\n    suspend fun saveRssSource(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val rssSource = context.bodyAsJson.mapTo(RssSource::class.java)\n        if (rssSource.sourceUrl.isEmpty()) {\n            return returnData.setErrorMsg(\"RSS链接不能为空\")\n        }\n        if (rssSource.sourceName.isEmpty()) {\n            return returnData.setErrorMsg(\"RSS名称不能为空\")\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        var rssSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"rssSources\"))\n        if (rssSourceList == null) {\n            rssSourceList = JsonArray()\n        }\n        // 遍历判断是否存在\n        var existIndex: Int = -1\n        for (i in 0 until rssSourceList.size()) {\n            var _rssSource = rssSourceList.getJsonObject(i).mapTo(RssSource::class.java)\n            if (_rssSource.sourceUrl.equals(rssSource.sourceUrl)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex >= 0) {\n            var list = rssSourceList.getList()\n            list.set(existIndex, JsonObject.mapFrom(rssSource))\n            rssSourceList = JsonArray(list)\n        } else {\n            // 新增rss源\n            rssSourceList.add(JsonObject.mapFrom(rssSource))\n        }\n\n        // logger.info(\"rssSourceList: {}\", rssSourceList)\n        saveUserStorage(userNameSpace, \"rssSources\", rssSourceList)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun saveRssSources(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val rssSourceJsonArray = context.bodyAsJsonArray\n        if (rssSourceJsonArray == null) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var rssSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"rssSources\"))\n        if (rssSourceList == null) {\n            rssSourceList = JsonArray()\n        }\n        for (k in 0 until rssSourceJsonArray.size()) {\n            var rssSource = rssSourceJsonArray.getJsonObject(k).mapTo(RssSource::class.java)\n            if (rssSource.sourceUrl.isEmpty()) {\n                continue\n            }\n            if (rssSource.sourceName.isEmpty()) {\n                continue\n            }\n            // 遍历判断是否存在\n            var existIndex: Int = -1\n            for (i in 0 until rssSourceList!!.size()) {\n                var _rssSource = rssSourceList.getJsonObject(i).mapTo(RssSource::class.java)\n                if (_rssSource.sourceUrl.equals(rssSource.sourceUrl)) {\n                    existIndex = i\n                    break;\n                }\n            }\n            if (existIndex >= 0) {\n                var list = rssSourceList.getList()\n                list.set(existIndex, JsonObject.mapFrom(rssSource))\n                rssSourceList = JsonArray(list)\n            } else {\n                // 新增rss源\n                rssSourceList.add(JsonObject.mapFrom(rssSource))\n            }\n        }\n\n        // logger.info(\"rssSourceList: {}\", rssSourceList)\n        saveUserStorage(userNameSpace, \"rssSources\", rssSourceList!!)\n        return returnData.setData(\"\")\n    }\n\n\n    suspend fun deleteRssSource(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val rssSource = context.bodyAsJson.mapTo(RssSource::class.java)\n        var userNameSpace = getUserNameSpace(context)\n        var rssSourceList: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"rssSources\"))\n        if (rssSourceList == null) {\n            rssSourceList = JsonArray()\n        }\n        // 遍历判断是否存在\n        var existIndex: Int = -1\n        for (i in 0 until rssSourceList.size()) {\n            var _rssSource = rssSourceList.getJsonObject(i).mapTo(RssSource::class.java)\n            if (_rssSource.sourceUrl.equals(rssSource.sourceUrl)) {\n                existIndex = i\n                break;\n            }\n        }\n        if (existIndex >= 0) {\n            rssSourceList.remove(existIndex)\n        }\n        // logger.info(\"rssSource: {}\", rssSource)\n        saveUserStorage(userNameSpace, \"rssSources\", rssSourceList)\n        return returnData.setData(\"\")\n    }\n\n    fun getRssSourceByURL(url: String, userNameSpace: String): RssSource? {\n        if (url.isEmpty()) {\n            return null\n        }\n        var list: JsonArray? = asJsonArray(getUserStorage(userNameSpace, \"rssSources\"))\n        if (list == null) {\n            return null\n        }\n        for (i in 0 until list.size()) {\n            var _rssSource = list.getJsonObject(i).mapTo(RssSource::class.java)\n            if (_rssSource.sourceUrl.equals(url)) {\n                return _rssSource\n            }\n        }\n        return null\n    }\n\n    suspend fun getRssArticles(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var sourceUrl: String\n        var sortName: String\n        var sortUrl: String\n        var page: Int\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            sourceUrl = context.bodyAsJson.getString(\"sourceUrl\")\n            sortName = context.bodyAsJson.getString(\"sortName\", \"\")\n            sortUrl = context.bodyAsJson.getString(\"sortUrl\", \"\")\n            page = context.bodyAsJson.getInteger(\"page\", 1)\n        } else {\n            // get 请求\n            sourceUrl = context.queryParam(\"sourceUrl\").firstOrNull() ?: \"\"\n            sortName = context.queryParam(\"sortName\").firstOrNull() ?: \"\"\n            sortUrl = context.queryParam(\"sortUrl\").firstOrNull() ?: \"\"\n            page = context.queryParam(\"page\").firstOrNull()?.toInt() ?: 1\n        }\n        if (sourceUrl.isEmpty()) {\n            return returnData.setErrorMsg(\"RSS源链接不能为空\")\n        }\n        if (sortUrl.isEmpty()) {\n            sortUrl = sourceUrl\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        var rssSource = getRssSourceByURL(sourceUrl, userNameSpace)\n        if (rssSource == null) {\n            return returnData.setErrorMsg(\"RSS源不存在\")\n        }\n\n        val rssArtcles = Rss.getArticles(sortName, sortUrl, rssSource, page, Debug)\n\n        return returnData.setData(rssArtcles)\n    }\n\n    suspend fun getRssContent(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var sourceUrl: String\n        var link: String\n        var origin: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            sourceUrl = context.bodyAsJson.getString(\"sourceUrl\")\n            link = context.bodyAsJson.getString(\"link\")\n            origin = context.bodyAsJson.getString(\"origin\")\n        } else {\n            // get 请求\n            sourceUrl = context.queryParam(\"sourceUrl\").firstOrNull() ?: \"\"\n            link = context.queryParam(\"link\").firstOrNull() ?: \"\"\n            origin = context.queryParam(\"origin\").firstOrNull() ?: \"\"\n        }\n        if (sourceUrl.isEmpty()) {\n            return returnData.setErrorMsg(\"RSS链接不能为空\")\n        }\n        if (link.isEmpty()) {\n            return returnData.setErrorMsg(\"RSS文章链接不能为空\")\n        }\n        if (origin.isEmpty()) {\n            return returnData.setErrorMsg(\"RSS文章来源不能为空\")\n        }\n\n        var userNameSpace = getUserNameSpace(context)\n        var rssSource = getRssSourceByURL(sourceUrl, userNameSpace)\n        if (rssSource == null) {\n            return returnData.setErrorMsg(\"RSS源不存在\")\n        }\n        val rssArticle = RssArticle(origin = origin, link = link)\n        var content = \"\"\n        if (rssSource.ruleContent != null) {\n            content = Rss.getContent(rssArticle, rssSource.ruleContent as String, rssSource, Debug)\n        }\n\n        return returnData.setData(content)\n    }\n\n\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/api/controller/UserController.kt",
    "content": "package com.htmake.reader.api.controller\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.SearchBook\nimport io.legado.app.data.entities.BookGroup\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.RssSource\nimport io.legado.app.data.entities.RssArticle\nimport io.legado.app.model.webBook.WebBook\nimport io.vertx.ext.web.Route\nimport io.vertx.ext.web.Router\nimport io.vertx.ext.web.RoutingContext\nimport io.vertx.ext.web.handler.StaticHandler;\nimport mu.KotlinLogging\nimport com.htmake.reader.config.AppConfig\nimport com.htmake.reader.config.BookConfig\nimport io.legado.app.constant.DeepinkBookSource\nimport com.htmake.reader.utils.error\nimport com.htmake.reader.utils.success\nimport com.htmake.reader.utils.getStorage\nimport com.htmake.reader.utils.saveStorage\nimport com.htmake.reader.utils.asJsonArray\nimport com.htmake.reader.utils.asJsonObject\nimport com.htmake.reader.utils.toDataClass\nimport com.htmake.reader.utils.toMap\nimport com.htmake.reader.utils.fillData\nimport com.htmake.reader.utils.getWorkDir\nimport com.htmake.reader.utils.getRandomString\nimport com.htmake.reader.utils.genEncryptedPassword\nimport com.htmake.reader.entity.User\nimport com.htmake.reader.utils.SpringContextUtils\nimport com.htmake.reader.utils.deleteRecursively\nimport com.htmake.reader.utils.unzip\nimport com.htmake.reader.utils.zip\nimport com.htmake.reader.utils.jsonEncode\nimport com.htmake.reader.utils.getRelativePath\nimport com.htmake.reader.verticle.RestVerticle\nimport com.htmake.reader.SpringEvent\nimport org.springframework.stereotype.Component\nimport io.vertx.core.json.JsonObject\nimport io.vertx.core.json.JsonArray\nimport io.vertx.core.http.HttpMethod\nimport com.htmake.reader.api.ReturnData\nimport io.legado.app.utils.MD5Utils\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.net.URL;\nimport java.util.UUID;\nimport io.vertx.ext.web.client.WebClient\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.core.env.Environment\nimport java.io.File\nimport java.lang.Runtime\nimport kotlin.collections.mutableMapOf\nimport kotlin.system.measureTimeMillis\nimport kotlin.coroutines.CoroutineContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.text.SimpleDateFormat;\nimport io.legado.app.utils.EncoderUtils\nimport io.legado.app.model.rss.Rss\nimport org.springframework.scheduling.annotation.Scheduled\nimport io.legado.app.model.localBook.LocalBook\nimport java.nio.file.Paths\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.CoroutineScope\n\nprivate val logger = KotlinLogging.logger {}\n\nclass UserController(coroutineContext: CoroutineContext): BaseController(coroutineContext) {\n    val userMaxCount = 50\n\n    private fun getUserLimit(context: RoutingContext): Int {\n        if (context.request().host().equals(\"reader.htmake.com\")) {\n            return 500;\n        }\n        return Math.min(Math.max(appConfig.userLimit, 1), userMaxCount)\n    }\n\n    suspend fun login(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        val username = context.bodyAsJson.getString(\"username\", \"\") ?: \"\"\n        val password = context.bodyAsJson.getString(\"password\", \"\") ?: \"\"\n        val isLogin = context.bodyAsJson.getBoolean(\"isLogin\", false) ?: false\n        if (username.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入用户名\")\n        }\n        if (password.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入密码\")\n        }\n        var userMap = mutableMapOf<String, Map<String, Any>>()\n        var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n        if (userMapJson != null) {\n            userMap = userMapJson.map as MutableMap<String, Map<String, Any>>\n        }\n        var existedUser = userMap.getOrDefault(username, null)\n        if (existedUser == null) {\n            if (isLogin) {\n                // 登录返回用户不存在\n                return returnData.setErrorMsg(\"用户不存在\")\n            }\n            if (username.length < 5) {\n                return returnData.setErrorMsg(\"用户名不能低于5位\")\n            }\n            if (password.length < 8) {\n                return returnData.setErrorMsg(\"密码不能低于8位\")\n            }\n            if (username.equals(\"default\")) {\n                return returnData.setErrorMsg(\"用户名不能为非法字符\")\n            }\n            val usernameReg = Regex(\"[a-z0-9]+\", RegexOption.IGNORE_CASE)    //忽略大小写\n            if (!usernameReg.matches(username)) {\n                return returnData.setErrorMsg(\"用户名只能由字母和数字组成\")\n            }\n            if (appConfig.inviteCode.isNotEmpty()) {\n                // 需要填入邀请码才能注册\n                val code = context.bodyAsJson.getString(\"code\") ?: \"\"\n                if (code.isNullOrEmpty()) {\n                    return returnData.setErrorMsg(\"请输入邀请码\")\n                }\n                if (!appConfig.inviteCode.equals(code)) {\n                    return returnData.setErrorMsg(\"邀请码错误\")\n                }\n            }\n            val userLimit = getUserLimit(context)\n            if (userMap.keys.size >= userLimit) {\n                return returnData.setErrorMsg(\"超过用户数上限\")\n            }\n\n            // 自动注册\n            var salt = getRandomString(8)\n            var passwordEncrypted = genEncryptedPassword(password, salt)\n            var newUser = User(username, passwordEncrypted, salt)\n\n            val loginData = saveUserSession(context, userMap, newUser)\n            return returnData.setData(loginData)\n        } else {\n            if (!isLogin) {\n                // 注册时返回用户名已被占用\n                return returnData.setErrorMsg(\"用户名已被占用\")\n            }\n            // 登录\n            var userInfo: User? = existedUser.toDataClass()\n            if (userInfo == null) {\n                return returnData.setErrorMsg(\"用户信息错误\")\n            }\n            var passwordEncrypted = genEncryptedPassword(password, userInfo.salt)\n            if (passwordEncrypted != userInfo.password) {\n                return returnData.setErrorMsg(\"密码错误\")\n            }\n            val loginData = saveUserSession(context, userMap, userInfo)\n            return returnData.setData(loginData)\n        }\n    }\n\n    suspend fun logout(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (!appConfig.secure) {\n            return returnData.setErrorMsg(\"不支持的操作\")\n        }\n        var username = context.session().get(\"username\") as String? ?: \"\"\n        context.session().destroy()\n\n        // 清除自动登录token\n        var accessToken = context.queryParam(\"accessToken\").firstOrNull() ?: \"\"\n        if (accessToken.isNotEmpty()) {\n            var tmp = accessToken.split(\":\", limit=2)\n            if (tmp.size >= 2) {\n                accessToken = tmp[1]\n\n                var userMap = mutableMapOf<String, MutableMap<String, Any>>()\n                var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n                if (userMapJson != null) {\n                    userMap = userMapJson.map as MutableMap<String, MutableMap<String, Any>>\n                }\n                var currentUser = userMap.getOrDefault(username, null)\n                if (currentUser == null) {\n                    return returnData.setErrorMsg(\"系统错误\")\n                }\n                var tokenMapVal = currentUser.getOrDefault(\"token_map\", null)\n                if (tokenMapVal != null) {\n                    var tokenMap: MutableMap<String, Long>? = tokenMapVal as MutableMap<String, Long>?\n                    if (tokenMap != null) {\n                        tokenMap.remove(accessToken)\n                        currentUser.put(\"token_map\", tokenMap)\n                    }\n                }\n                if (currentUser.getOrDefault(\"token\", \"\").equals(accessToken)) {\n                    currentUser.put(\"token\", \"\")\n                }\n\n                userMap.put(username, currentUser)\n                saveStorage(\"data\", \"users\", value = userMap)\n            }\n        }\n        return returnData.setErrorMsg(\"请重新登录\").setData(\"NEED_LOGIN\")\n    }\n\n    suspend fun getUserList(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (!appConfig.secure || appConfig.secureKey.isEmpty()) {\n            return returnData.setErrorMsg(\"不支持的操作\")\n        }\n        if (!checkManagerAuth(context)) {\n            return returnData.setData(\"NEED_SECURE_KEY\").setErrorMsg(\"请输入管理密码\")\n        }\n        var userMap = mutableMapOf<String, MutableMap<String, Any>>()\n        var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n        if (userMapJson != null) {\n            userMap = userMapJson.map as MutableMap<String, MutableMap<String, Any>>\n        }\n        var userList = arrayListOf<Map<String, Any>>()\n        userMap.forEach{\n            userList.add(formatUser(it.value))\n        }\n        return returnData.setData(userList)\n    }\n\n    suspend fun addUser(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (!appConfig.secure || appConfig.secureKey.isEmpty()) {\n            return returnData.setErrorMsg(\"不支持的操作\")\n        }\n        val username = context.bodyAsJson.getString(\"username\") ?: \"\"\n        val password = context.bodyAsJson.getString(\"password\") ?: \"\"\n        if (username.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入用户名\")\n        }\n        if (password.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入密码\")\n        }\n        if (username.length < 5) {\n            return returnData.setErrorMsg(\"用户名不能低于5位\")\n        }\n        if (password.length < 8) {\n            return returnData.setErrorMsg(\"密码不能低于8位\")\n        }\n        if (username.equals(\"default\")) {\n            return returnData.setErrorMsg(\"用户名不能为非法字符\")\n        }\n        if (!checkManagerAuth(context)) {\n            return returnData.setData(\"NEED_SECURE_KEY\").setErrorMsg(\"请输入管理密码\")\n        }\n        val usernameReg = Regex(\"[a-z0-9]+\", RegexOption.IGNORE_CASE)    //忽略大小写\n        if (!usernameReg.matches(username)) {\n            return returnData.setErrorMsg(\"用户名只能由字母和数字组成\")\n        }\n        var userMap = mutableMapOf<String, Map<String, Any>>()\n        var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n        if (userMapJson != null) {\n            userMap = userMapJson.map as MutableMap<String, Map<String, Any>>\n        }\n        var existedUser = userMap.getOrDefault(username, null)\n        if (existedUser != null) {\n            return returnData.setErrorMsg(\"用户已存在\")\n        }\n\n        val userLimit = getUserLimit(context)\n        if (userMap.keys.size >= userLimit) {\n            return returnData.setErrorMsg(\"超过用户数上限\")\n        }\n\n        // 自动注册\n        var salt = getRandomString(8)\n        var passwordEncrypted = genEncryptedPassword(password, salt)\n        var newUser = User(username, passwordEncrypted, salt)\n        userMap.put(newUser.username, newUser.toMap())\n        saveStorage(\"data\", \"users\", value = userMap)\n\n        var userList = arrayListOf<Map<String, Any>>()\n        userMap.forEach{\n            userList.add(formatUser(it.value))\n        }\n        return returnData.setData(userList)\n    }\n\n    suspend fun resetPassword(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (!appConfig.secure || appConfig.secureKey.isEmpty()) {\n            return returnData.setErrorMsg(\"不支持的操作\")\n        }\n        val username = context.bodyAsJson.getString(\"username\") ?: \"\"\n        val password = context.bodyAsJson.getString(\"password\") ?: \"\"\n        if (username.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入用户名\")\n        }\n        if (password.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入密码\")\n        }\n        if (password.length < 8) {\n            return returnData.setErrorMsg(\"密码不能低于8位\")\n        }\n        if (username.equals(\"default\")) {\n            return returnData.setErrorMsg(\"用户不存在\")\n        }\n        if (!checkManagerAuth(context)) {\n            return returnData.setData(\"NEED_SECURE_KEY\").setErrorMsg(\"请输入管理密码\")\n        }\n        var userMap = mutableMapOf<String, MutableMap<String, Any>>()\n        var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n        if (userMapJson != null) {\n            userMap = userMapJson.map as MutableMap<String, MutableMap<String, Any>>\n        }\n\n        var existedUser = userMap.getOrDefault(username, null)\n        if (existedUser == null) {\n            return returnData.setErrorMsg(\"用户不存在\")\n        }\n\n        var salt = getRandomString(8)\n        var passwordEncrypted = genEncryptedPassword(password, salt)\n        existedUser.put(\"salt\", salt)\n        existedUser.put(\"password\", passwordEncrypted)\n        userMap.put(username, existedUser)\n        saveStorage(\"data\", \"users\", value = userMap as MutableMap<String, Map<String, Any>>)\n\n        return returnData.setData(\"\")\n    }\n\n    suspend fun deleteUsers(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (!appConfig.secure || appConfig.secureKey.isEmpty()) {\n            return returnData.setErrorMsg(\"不支持的操作\")\n        }\n        if (!checkManagerAuth(context)) {\n            return returnData.setData(\"NEED_SECURE_KEY\").setErrorMsg(\"请输入管理密码\")\n        }\n        var userMap = mutableMapOf<String, MutableMap<String, Any>>()\n        var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n\n        if (userMapJson != null) {\n            val userJsonArray = context.bodyAsJsonArray\n            for (i in 0 until userJsonArray.size()) {\n                var username = userJsonArray.getString(i)\n                if (username != null && userMapJson.containsKey(username)) {\n                    // 删除用户信息\n                    userMapJson.remove(username)\n                    // 移除用户目录\n                    var userHome = File(getWorkDir(\"storage\", \"data\", username))\n                    logger.info(\"delete userHome: {}\", userHome)\n                    if (userHome.exists()) {\n                        userHome.deleteRecursively()\n                    }\n                }\n            }\n            userMap = userMapJson.map as MutableMap<String, MutableMap<String, Any>>\n            saveStorage(\"data\", \"users\", value = userMap)\n        }\n\n        var userList = arrayListOf<Map<String, Any>>()\n        userMap.forEach{\n            userList.add(formatUser(it.value))\n        }\n        return returnData.setData(userList)\n    }\n\n    suspend fun updateUser(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (!appConfig.secure || appConfig.secureKey.isEmpty()) {\n            return returnData.setErrorMsg(\"不支持的操作\")\n        }\n        if (!checkManagerAuth(context)) {\n            return returnData.setData(\"NEED_SECURE_KEY\").setErrorMsg(\"请输入管理密码\")\n        }\n        val username = context.bodyAsJson.getString(\"username\") ?: \"\"\n        val enableWebdav = context.bodyAsJson.getBoolean(\"enableWebdav\")\n        val enableLocalStore = context.bodyAsJson.getBoolean(\"enableLocalStore\")\n        if (username.isEmpty()) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n\n        var userMap = mutableMapOf<String, MutableMap<String, Any>>()\n        var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n\n        if (userMapJson != null) {\n            userMap = userMapJson.map as MutableMap<String, MutableMap<String, Any>>\n            var existedUser = userMap.getOrDefault(username, null)\n            if (existedUser == null) {\n                return returnData.setErrorMsg(\"用户不存在\")\n            }\n            if (enableWebdav != null) {\n                existedUser.put(\"enable_webdav\", enableWebdav)\n            }\n            if (enableLocalStore != null) {\n                existedUser.put(\"enable_local_store\", enableLocalStore)\n            }\n            userMap.put(username, existedUser)\n            saveStorage(\"data\", \"users\", value = userMap)\n        }\n\n        var userList = arrayListOf<Map<String, Any>>()\n        userMap.forEach{\n            userList.add(formatUser(it.value))\n        }\n        return returnData.setData(userList)\n    }\n\n    suspend fun getUserInfo(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        checkAuth(context)\n        var username = context.session().get(\"username\") as String?\n        var secure = env.getProperty(\"reader.app.secure\", Boolean::class.java)\n        var secureKey = env.getProperty(\"reader.app.secureKey\")\n\n        var userInfo: Any? = null\n        if (username != null) {\n            var user = getUserInfoClass(username)\n            if (user != null) {\n                userInfo = formatUser(user)\n            }\n        }\n\n        return returnData.setData(mapOf(\n            \"userInfo\" to userInfo,\n            \"secure\" to secure,\n            \"secureKey\" to secureKey?.isNotEmpty()\n        ))\n    }\n\n    suspend fun saveUserConfig(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val content = context.bodyAsJson\n        if (content == null) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n        content.put(\"@updateTime\", System.currentTimeMillis())\n\n        val userNameSpace = getUserNameSpace(context)\n        saveUserStorage(userNameSpace, \"userConfig\", content)\n        return returnData.setData(\"\")\n    }\n\n    suspend fun getUserConfig(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        val userNameSpace = getUserNameSpace(context)\n        val userConfig = asJsonObject(getUserStorage(userNameSpace, \"userConfig\"))\n        if (userConfig == null) {\n            return returnData.setErrorMsg(\"没有备份文件\")\n        }\n        return returnData.setData(userConfig.map)\n    }\n\n    suspend fun uploadFile(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (context.fileUploads() == null || context.fileUploads().isEmpty()) {\n            return returnData.setErrorMsg(\"请上传文件\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        var fileList = JsonArray()\n        var type = context.request().getParam(\"type\")\n        if (type.isNullOrEmpty()) {\n            type = \"images\"\n        }\n        // logger.info(\"type: {}\", type)\n        context.fileUploads().forEach {\n            var file = File(it.uploadedFileName())\n            logger.info(\"uploadFile: {} {} {}\", it.uploadedFileName(), it.fileName(), file)\n            if (file.exists()) {\n                var fileName = it.fileName()\n                var newFile = File(getWorkDir(\"storage\", \"assets\", userNameSpace, type, fileName))\n                if (!newFile.parentFile.exists()) {\n                    newFile.parentFile.mkdirs()\n                }\n                if (newFile.exists()) {\n                    newFile.delete()\n                }\n                logger.info(\"moveTo: {}\", newFile)\n                if (file.copyRecursively(newFile)) {\n                    fileList.add(\"/assets/\" + userNameSpace + \"/\" + type + \"/\" + fileName)\n                }\n                file.deleteRecursively()\n            }\n        }\n        return returnData.setData(fileList.getList())\n    }\n\n    suspend fun deleteFile(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        var url: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            url = context.bodyAsJson.getString(\"url\") ?: \"\"\n        } else {\n            // get 请求\n            url = context.queryParam(\"url\").firstOrNull() ?: \"\"\n        }\n        if (url.isNullOrEmpty()) {\n            return returnData.setErrorMsg(\"请输入文件链接\")\n        }\n        var userNameSpace = getUserNameSpace(context)\n        if (!url.startsWith(\"/assets/\" + userNameSpace + \"/\")) {\n            return returnData.setErrorMsg(\"文件链接错误\")\n        }\n        var file = File(getWorkDir(\"storage\" + url))\n        logger.info(\"delete file: {}\", file)\n        file.deleteRecursively()\n        return returnData.setData(\"\")\n    }\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/api/controller/WebdavController.kt",
    "content": "package com.htmake.reader.api.controller\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.SearchBook\nimport io.legado.app.data.entities.BookGroup\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.RssSource\nimport io.legado.app.data.entities.RssArticle\nimport io.legado.app.model.webBook.WebBook\nimport io.vertx.ext.web.Route\nimport io.vertx.ext.web.Router\nimport io.vertx.ext.web.RoutingContext\nimport io.vertx.ext.web.handler.StaticHandler;\nimport mu.KotlinLogging\nimport com.htmake.reader.config.AppConfig\nimport com.htmake.reader.config.BookConfig\nimport io.legado.app.constant.DeepinkBookSource\nimport com.htmake.reader.utils.error\nimport com.htmake.reader.utils.success\nimport com.htmake.reader.utils.getStorage\nimport com.htmake.reader.utils.saveStorage\nimport com.htmake.reader.utils.asJsonArray\nimport com.htmake.reader.utils.asJsonObject\nimport com.htmake.reader.utils.toDataClass\nimport com.htmake.reader.utils.toMap\nimport com.htmake.reader.utils.fillData\nimport com.htmake.reader.utils.getWorkDir\nimport com.htmake.reader.utils.getRandomString\nimport com.htmake.reader.utils.genEncryptedPassword\nimport com.htmake.reader.entity.User\nimport com.htmake.reader.utils.SpringContextUtils\nimport com.htmake.reader.utils.deleteRecursively\nimport com.htmake.reader.utils.unzip\nimport com.htmake.reader.utils.zip\nimport com.htmake.reader.utils.jsonEncode\nimport com.htmake.reader.utils.getRelativePath\nimport com.htmake.reader.verticle.RestVerticle\nimport com.htmake.reader.SpringEvent\nimport org.springframework.stereotype.Component\nimport io.vertx.core.json.JsonObject\nimport io.vertx.core.json.JsonArray\nimport io.vertx.core.http.HttpMethod\nimport com.htmake.reader.api.ReturnData\nimport io.legado.app.utils.MD5Utils\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.net.URL;\nimport java.util.UUID;\nimport io.vertx.ext.web.client.WebClient\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.core.env.Environment\nimport java.io.File\nimport java.lang.Runtime\nimport kotlin.collections.mutableMapOf\nimport kotlin.system.measureTimeMillis\nimport kotlin.coroutines.CoroutineContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.text.SimpleDateFormat;\nimport io.legado.app.utils.EncoderUtils\nimport io.legado.app.model.rss.Rss\nimport org.springframework.scheduling.annotation.Scheduled\nimport io.legado.app.model.localBook.LocalBook\nimport java.nio.file.Paths\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.CoroutineScope\n// import io.legado.app.help.coroutine.Coroutine\n\nprivate val logger = KotlinLogging.logger {}\n\nclass WebdavController(coroutineContext: CoroutineContext, router: Router, onHandlerError: (RoutingContext, Exception) -> Unit): BaseController(coroutineContext) {\n\n    init {\n        // webdav 服务\n        router.route(\"/reader3/webdav*\").handler {\n            it.addHeadersEndHandler { _ ->\n                var res = it.response()\n                res.putHeader(\"DAV\", \"1,2\")\n                res.putHeader(\"Access-Control-Allow-Origin\", \"*\")\n                res.putHeader(\"Access-Control-Allow-Credentials\", \"true\")\n                res.putHeader(\"Access-Control-Expose-Headers\", \"DAV, content-length, Allow\")\n                res.putHeader(\"MS-Author-Via\", \"DAV\")\n                res.putHeader(\"Allow\", \"OPTIONS,DELETE,GET,PUT,PROPFIND,MKCOL,MOVE,COPY,LOCK,UNLOCK\")\n                if (appConfig.secure) {\n                    res.putHeader(\"WWW-Authenticate\", \"Basic realm=\\\"Default realm\\\"\")\n                }\n            }\n            val rawMethod = it.request().rawMethod()\n            if (!checkAuthorization(it)) {\n                if (\n                    rawMethod.equals(\"PROPFIND\") ||\n                    rawMethod.equals(\"MKCOL\") ||\n                    rawMethod.equals(\"PUT\") ||\n                    rawMethod.equals(\"GET\") ||\n                    rawMethod.equals(\"DELETE\") ||\n                    rawMethod.equals(\"MOVE\") ||\n                    rawMethod.equals(\"COPY\") ||\n                    rawMethod.equals(\"LOCK\") ||\n                    rawMethod.equals(\"UNLOCK\")\n                ) {\n                    it.response().setStatusCode(401).end()\n                    return@handler\n                } else if(rawMethod.equals(\"OPTIONS\")) {\n                    var authorization = it.request().getHeader(\"Authorization\")\n                    if (authorization != null) {\n                        it.response().setStatusCode(401).end()\n                        return@handler\n                    }\n                }\n            }\n            when (rawMethod) {\n                \"PROPFIND\" -> launch(Dispatchers.IO) {\n                    try {\n                        webdavList(it)\n                    } catch (e: Exception) {\n                        onHandlerError(it, e)\n                    }\n                }\n                \"MKCOL\" -> launch(Dispatchers.IO) {\n                    try {\n                        webdavMkdir(it)\n                    } catch (e: Exception) {\n                        onHandlerError(it, e)\n                    }\n                }\n                \"PUT\" -> launch(Dispatchers.IO) {\n                    try {\n                        webdavUpload(it)\n                    } catch (e: Exception) {\n                        onHandlerError(it, e)\n                    }\n                }\n                \"GET\" -> launch(Dispatchers.IO) {\n                    try {\n                        webdavDownload(it)\n                    } catch (e: Exception) {\n                        onHandlerError(it, e)\n                    }\n                }\n                \"DELETE\" -> launch(Dispatchers.IO) {\n                    try {\n                        webdavDelete(it)\n                    } catch (e: Exception) {\n                        onHandlerError(it, e)\n                    }\n                }\n                \"MOVE\" -> launch(Dispatchers.IO) {\n                    try {\n                        webdavMove(it)\n                    } catch (e: Exception) {\n                        onHandlerError(it, e)\n                    }\n                }\n                \"COPY\" -> launch(Dispatchers.IO) {\n                    try {\n                        webdavCopy(it)\n                    } catch (e: Exception) {\n                        onHandlerError(it, e)\n                    }\n                }\n                \"LOCK\" -> launch(Dispatchers.IO) {\n                    try {\n                        webdavLock(it)\n                    } catch (e: Exception) {\n                        onHandlerError(it, e)\n                    }\n                }\n                \"UNLOCK\" -> launch(Dispatchers.IO) {\n                    try {\n                        webdavUnLock(it)\n                    } catch (e: Exception) {\n                        onHandlerError(it, e)\n                    }\n                }\n                \"OPTIONS\" -> it.response().setStatusCode(200).end()\n                else -> it.response().setStatusCode(405).end()\n            }\n        }\n    }\n\n    fun checkAuthorization(context: RoutingContext): Boolean {\n        if (!appConfig.secure) {\n            return true\n        }\n        var authorization = context.request().getHeader(\"Authorization\")\n        logger.info(\"authorization: {}\", authorization)\n        if (authorization == null || authorization.isEmpty()) {\n            return false\n        }\n\n        // Basic YTox\n        val auth = EncoderUtils.base64Decode(authorization.replace(\"Basic \", \"\", true)).split(\":\", limit=2)\n        if (auth.size < 2) {\n            return false\n        }\n        val username = auth[0]\n        val password = auth[1]\n        var userMap = mutableMapOf<String, Map<String, Any>>()\n        var userMapJson: JsonObject? = asJsonObject(getStorage(\"data\", \"users\"))\n        if (userMapJson != null) {\n            userMap = userMapJson.map as MutableMap<String, Map<String, Any>>\n        }\n        var existedUser = userMap.getOrDefault(username, null)\n        if (existedUser == null) {\n            return false\n        }\n        var userInfo: User? = existedUser.toDataClass()\n        if (userInfo == null) {\n            return false\n        }\n        var passwordEncrypted = genEncryptedPassword(password, userInfo.salt)\n        if (passwordEncrypted != userInfo.password) {\n            logger.info(\"user: {} password error\", userInfo.username)\n            return false\n        }\n\n        if (!userInfo.enable_webdav) {\n            logger.info(\"user: {} enable_webdav: false\", userInfo.username)\n            return false\n        }\n\n        context.put(\"username\", userInfo.username)\n\n        return true\n    }\n\n    suspend fun webdavList(context: RoutingContext) {\n        var home = getUserWebdavHome(context)\n        var path = context.request().path().replace(\"/reader3/webdav/\", \"/\", true)\n        path = URLDecoder.decode(path, \"UTF-8\")\n        var file = File(home + path)\n        if (!file.exists()) {\n            context.response().setStatusCode(404).end()\n            return\n        }\n\n        var xml =\n        \"\"\"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n            <D:multistatus xmlns:D=\"DAV:\">\n                %s\n            </D:multistatus>\n        \"\"\"\n\n        var dirResponse =\n        \"\"\"<D:response>\n                <D:href>%s</D:href>\n                <D:propstat>\n                    <D:status>HTTP/1.1 200 OK</D:status>\n                    <D:prop>\n                        <D:getlastmodified>%s</D:getlastmodified>\n                        <D:creationdate>%s</D:creationdate>\n                        <D:resourcetype>\n                            <D:collection />\n                        </D:resourcetype>\n                        <D:displayname>%s</D:displayname>\n                    </D:prop>\n                </D:propstat>\n            </D:response>\n        \"\"\"\n\n        var fileResponse =\n        \"\"\"<D:response>\n                <D:href>%s</D:href>\n                <D:propstat>\n                    <D:status>HTTP/1.1 200 OK</D:status>\n                    <D:prop>\n                        <D:getlastmodified>%s</D:getlastmodified>\n                        <D:creationdate>%s</D:creationdate>\n                        <D:resourcetype />\n                        <D:displayname>%s</D:displayname>\n                        <D:getcontentlength>%s</D:getcontentlength>\n                        <D:getcontenttype>%s</D:getcontenttype>\n                    </D:prop>\n                </D:propstat>\n            </D:response>\n        \"\"\"\n\n        var fileUrl = context.request().absoluteURI()\n\n        // 只支持一级\n        var formatter = { f: File, url: String, showName: Boolean ->\n            var name = if(showName) f.name else \"\"\n            var modifiedDate = SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\").format(f.lastModified())\n            if (f.isFile()) {\n                String.format(fileResponse, url, modifiedDate, modifiedDate, name, f.length(), \"\")\n            } else {\n                String.format(dirResponse, url, modifiedDate, modifiedDate, name)\n            }\n        }\n\n        var response = \"\"\n        if (file.isFile()) {\n            response = String.format(xml, formatter(file, fileUrl, true))\n            context.response().setStatusCode(207).end(response)\n            return\n        }\n\n        if (file.isDirectory()) {\n            fileUrl = if (fileUrl.endsWith(\"/\")) fileUrl else fileUrl + \"/\"\n            response = formatter(file, fileUrl, false)\n            file.listFiles().forEach {\n                val fileName = URLEncoder.encode(it.name, \"UTF-8\")\n                response = response + formatter(it, fileUrl + fileName, true)\n            }\n            response = String.format(xml, response)\n            context.response().setStatusCode(207).end(response)\n            return\n        }\n\n        context.response().setStatusCode(404).end()\n    }\n\n    suspend fun webdavMkdir(context: RoutingContext) {\n        var home = getUserWebdavHome(context)\n        var path = context.request().path().replace(\"/reader3/webdav/\", \"/\", true)\n        path = URLDecoder.decode(path, \"UTF-8\")\n        var file = File(home + path)\n        if (file.exists()) {\n            // 文件夹存在时，返回成功\n            context.response().setStatusCode(201).end()\n            return\n        }\n        try {\n            file.mkdirs()\n            context.response().setStatusCode(201).end()\n        } catch(e: Exception) {\n            context.response().setStatusCode(500).end()\n        }\n    }\n\n    suspend fun webdavUpload(context: RoutingContext) {\n        var home = getUserWebdavHome(context)\n        var path = context.request().path().replace(\"/reader3/webdav/\", \"/\", true)\n        path = URLDecoder.decode(path, \"UTF-8\")\n        var file = File(home + path)\n        if (!file.parentFile.exists()) {\n            context.response().setStatusCode(409).end()\n            return\n        }\n        if (file.isDirectory()) {\n            context.response().setStatusCode(405).end()\n            return\n        }\n        if (file.exists()) {\n            file.delete();\n        }\n        try {\n            file.writeBytes(context.getBody().getBytes())\n            // 同步用户进度\n            if (file.toString().indexOf(\"/bookProgress/\") > 0 && file.toString().indexOf(\".json\") > 0) {\n                val userNameSpace = getUserNameSpace(context)\n                BookController(coroutineContext).syncBookProgressFromWebdav(file, userNameSpace)\n            }\n            context.response().setStatusCode(201).end()\n        } catch(e: Exception) {\n            context.response().setStatusCode(500).end()\n        }\n    }\n\n    suspend fun webdavDownload(context: RoutingContext) {\n        var home = getUserWebdavHome(context)\n        var path = context.request().path().replace(\"/reader3/webdav/\", \"/\", true)\n        path = URLDecoder.decode(path, \"UTF-8\")\n        var file = File(home + path)\n        if (!file.exists()) {\n            context.response().setStatusCode(404).end()\n            return\n        }\n        if (file.isDirectory()) {\n            context.response().setStatusCode(405).end()\n            return\n        }\n        context.response().putHeader(\"Cache-Control\", \"86400\")\n                        .putHeader(\"Content-Disposition\", \"attachment; filename=\" + URLEncoder.encode(file.name, \"UTF-8\"))\n                        .sendFile(file.toString())\n    }\n\n    suspend fun webdavDelete(context: RoutingContext) {\n        var home = getUserWebdavHome(context)\n        var path = context.request().path().replace(\"/reader3/webdav/\", \"/\", true)\n        path = URLDecoder.decode(path, \"UTF-8\")\n        var file = File(home + path)\n        if (!file.exists()) {\n            context.response().setStatusCode(404).end()\n            return\n        }\n        file.deleteRecursively()\n        context.response().setStatusCode(200).end()\n    }\n\n    suspend fun webdavMove(context: RoutingContext) {\n        var home = getUserWebdavHome(context)\n        var path = context.request().path().replace(\"/reader3/webdav/\", \"/\", true)\n        path = URLDecoder.decode(path, \"UTF-8\")\n\n        var file = File(home + path)\n        if (!file.exists()) {\n            context.response().setStatusCode(412).end()\n            return\n        }\n        var destination = context.request().getHeader(\"Destination\")\n        if (destination == null) {\n            context.response().setStatusCode(400).end()\n            return\n        }\n        var destinationUrl = URL(destination)\n        destination = destinationUrl.path?.replace(\"/reader3/webdav/\", \"/\", true)\n        if (destination == null) {\n            context.response().setStatusCode(400).end()\n            return\n        }\n\n        var overwrite = context.request().getHeader(\"Overwrite\")\n        var destinationFile = File(home + URLDecoder.decode(destination, \"UTF-8\"))\n        if (destinationFile.exists()) {\n            if (overwrite == null || overwrite.isEmpty()) {\n                context.response().setStatusCode(412).end()\n                return\n            }\n            destinationFile.deleteRecursively()\n        }\n        file.renameTo(destinationFile)\n\n        context.response().setStatusCode(201).end()\n    }\n\n    suspend fun webdavCopy(context: RoutingContext) {\n        var home = getUserWebdavHome(context)\n        var path = context.request().path().replace(\"/reader3/webdav/\", \"/\", true)\n        path = URLDecoder.decode(path, \"UTF-8\")\n\n        var file = File(home + path)\n        if (!file.exists()) {\n            context.response().setStatusCode(412).end()\n            return\n        }\n        var destination = context.request().getHeader(\"Destination\")\n        if (destination == null) {\n            context.response().setStatusCode(400).end()\n            return\n        }\n        var destinationUrl = URL(destination)\n        destination = destinationUrl.path?.replace(\"/reader3/webdav/\", \"/\", true)\n        if (destination == null) {\n            context.response().setStatusCode(400).end()\n            return\n        }\n\n        var overwrite = context.request().getHeader(\"Overwrite\")\n        var destinationFile = File(home + URLDecoder.decode(destination, \"UTF-8\"))\n        if (destinationFile.exists()) {\n            if (overwrite == null || overwrite.isEmpty()) {\n                context.response().setStatusCode(412).end()\n                return\n            }\n            destinationFile.deleteRecursively()\n        }\n        file.copyRecursively(destinationFile)\n\n        context.response().setStatusCode(201).end()\n    }\n\n    suspend fun webdavLock(context: RoutingContext) {\n        var response =\n        \"\"\"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n        <D:prop xmlns:D=\"DAV:\">\n            <D:lockdiscovery>\n                <D:activelock>\n                    <D:locktype>\n                        <write />\n                    </D:locktype>\n                    <D:lockscope>\n                        <exclusive />\n                    </D:lockscope>\n                    <D:locktoken>\n                        <D:href>%s</D:href>\n                    </D:locktoken>\n                    <D:lockroot>\n                        <D:href>%s</D:href>\n                    </D:lockroot>\n                    <D:depth>infinity</D:depth>\n                    <D:owner>\n                        <a:href xmlns:a=\"DAV:\">http://www.apple.com/webdav_fs/</a:href>\n                    </D:owner>\n                    <D:timeout>%s</D:timeout>\n                </D:activelock>\n            </D:lockdiscovery>\n        </D:prop>\n        \"\"\"\n        var lockToken = \"urn:uuid:\" + UUID.randomUUID().toString()\n\n        var timeout = context.request().getHeader(\"Timeout\")\n        if (timeout == null) {\n            timeout = \"Second-3600\"\n        }\n\n        var fileUrl = context.request().absoluteURI()\n\n        context.response().putHeader(\"Lock-Token\", lockToken).setStatusCode(200).end(String.format(response, lockToken, fileUrl, timeout))\n    }\n\n    suspend fun webdavUnLock(context: RoutingContext) {\n        var lockToken = context.request().getHeader(\"Lock-Token\")\n        if (lockToken == null) {\n            context.response().setStatusCode(400).end()\n            return\n        }\n        context.response().putHeader(\"Lock-Token\", lockToken).setStatusCode(204).end()\n    }\n\n\n    suspend fun getWebdavFileList(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n            }\n            if (!userInfo.enable_webdav) {\n                return returnData.setErrorMsg(\"未开启webdav功能\")\n            }\n        }\n        var path: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            path = context.bodyAsJson.getString(\"path\") ?: \"\"\n        } else {\n            // get 请求\n            path = context.queryParam(\"path\").firstOrNull() ?: \"\"\n            path = URLDecoder.decode(path, \"UTF-8\")\n        }\n        if (path.isEmpty()) {\n            path = \"/\"\n        }\n        var home = getUserWebdavHome(context)\n        var file = File(home + path)\n        logger.info(\"file: {} {}\", path, file)\n        if (!file.exists()) {\n            return returnData.setErrorMsg(\"路径不存在\")\n        }\n        if (!file.isDirectory()) {\n            return returnData.setErrorMsg(\"路径不是目录\")\n        }\n        var fileList = arrayListOf<Map<String, Any>>()\n        file.listFiles().forEach{\n            if (!it.name.startsWith(\".\")) {\n                fileList.add(mapOf(\n                    \"name\" to it.name,\n                    \"size\" to it.length(),\n                    \"path\" to it.toString().replace(home, \"\"),\n                    \"lastModified\" to it.lastModified(),\n                    \"isDirectory\" to it.isDirectory()\n                ))\n            }\n        }\n        return returnData.setData(fileList)\n    }\n\n    suspend fun getWebdavFile(context: RoutingContext) {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            context.success(returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\"))\n            return\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                context.success(returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\"))\n                return\n            }\n            if (!userInfo.enable_webdav) {\n                context.success(returnData.setErrorMsg(\"未开启webdav功能\"))\n                return\n            }\n        }\n        var path: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            path = context.bodyAsJson.getString(\"path\") ?: \"\"\n        } else {\n            // get 请求\n            path = context.queryParam(\"path\").firstOrNull() ?: \"\"\n            path = URLDecoder.decode(path, \"UTF-8\")\n        }\n        if (path.isEmpty()) {\n            context.success(returnData.setErrorMsg(\"参数错误\"))\n            return\n        }\n        var home = getUserWebdavHome(context)\n        var file = File(home + path)\n        logger.info(\"file: {} {}\", path, file)\n        if (!file.exists()) {\n            context.success(returnData.setErrorMsg(\"路径不存在\"))\n            return\n        }\n        context.response()\n                .putHeader(\"Cache-Control\", \"86400\")\n                .putHeader(\"Content-Disposition\", \"attachment; filename=\" + URLEncoder.encode(file.name, \"UTF-8\"))\n                .sendFile(file.toString())\n    }\n\n    suspend fun uploadFileToWebdav(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (context.fileUploads() == null || context.fileUploads().isEmpty()) {\n            return returnData.setErrorMsg(\"请上传文件\")\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n            }\n            if (!userInfo.enable_webdav) {\n                return returnData.setErrorMsg(\"未开启webdav功能\")\n            }\n        }\n        var path = context.request().getParam(\"path\")\n        if (path.isNullOrEmpty()) {\n            path = \"/\"\n        }\n        var fileList = arrayListOf<Map<String, Any>>()\n        var home = getUserWebdavHome(context) + path + File.separator\n\n        // logger.info(\"type: {}\", type)\n        context.fileUploads().forEach {\n            var file = File(it.uploadedFileName())\n            logger.info(\"uploadFile: {} {} {}\", it.uploadedFileName(), it.fileName(), file)\n            if (file.exists()) {\n                var fileName = it.fileName()\n                var newFile = File(home + fileName)\n                if (!newFile.parentFile.exists()) {\n                    newFile.parentFile.mkdirs()\n                }\n                if (newFile.exists()) {\n                    newFile.delete()\n                }\n                logger.info(\"moveTo: {}\", newFile)\n                if (file.copyRecursively(newFile)) {\n                    fileList.add(mapOf(\n                        \"name\" to newFile.name,\n                        \"size\" to newFile.length(),\n                        \"path\" to newFile.toString().replace(home, \"\"),\n                        \"lastModified\" to newFile.lastModified(),\n                        \"isDirectory\" to newFile.isDirectory()\n                    ))\n                }\n                file.deleteRecursively()\n            }\n        }\n        return returnData.setData(fileList)\n    }\n\n    suspend fun deleteWebdavFile(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n            }\n            if (!userInfo.enable_webdav) {\n                return returnData.setErrorMsg(\"未开启webdav功能\")\n            }\n        }\n        var path: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            path = context.bodyAsJson.getString(\"path\") ?: \"\"\n        } else {\n            // get 请求\n            path = context.queryParam(\"path\").firstOrNull() ?: \"\"\n            path = URLDecoder.decode(path, \"UTF-8\")\n        }\n        if (path.isEmpty()) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n        var home = getUserWebdavHome(context)\n        var file = File(home + path)\n        logger.info(\"file: {} {}\", path, file)\n        if (!file.exists()) {\n            return returnData.setErrorMsg(\"路径不存在\")\n        }\n        file.deleteRecursively()\n        return returnData.setData(\"\")\n    }\n\n    suspend fun deleteWebdavFileList(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n            }\n            if (!userInfo.enable_webdav) {\n                return returnData.setErrorMsg(\"未开启webdav功能\")\n            }\n        }\n        var path = context.bodyAsJson.getJsonArray(\"path\")\n        if (path == null) {\n            return returnData.setErrorMsg(\"参数错误\")\n        }\n        var home = getUserWebdavHome(context)\n        path.forEach {\n            var filePath = URLDecoder.decode(it as String? ?: \"\", \"UTF-8\")\n            if (filePath.isNotEmpty()) {\n                var file = File(home + filePath)\n                file.deleteRecursively()\n            }\n        }\n        return returnData.setData(\"\")\n    }\n\n    suspend fun restoreFromWebdav(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n            }\n            if (!userInfo.enable_webdav) {\n                return returnData.setErrorMsg(\"未开启webdav功能\")\n            }\n        }\n        var path: String\n        if (context.request().method() == HttpMethod.POST) {\n            // post 请求\n            path = context.bodyAsJson.getString(\"path\") ?: \"\"\n        } else {\n            // get 请求\n            path = context.queryParam(\"path\").firstOrNull() ?: \"\"\n            path = URLDecoder.decode(path, \"UTF-8\")\n        }\n        if (path.isEmpty()) {\n            path = \"/\"\n        }\n        var ext = getFileExt(path)\n        if (ext != \"zip\") {\n            return returnData.setErrorMsg(\"路径不是zip备份文件\")\n        }\n        var home = getUserWebdavHome(context)\n        var file = File(home + path)\n        logger.info(\"file: {} {}\", path, file)\n        if (!file.exists()) {\n            return returnData.setErrorMsg(\"路径不存在\")\n        }\n        val bookController = BookController(coroutineContext)\n        if (!bookController.syncFromWebdav(file.toString(), getUserNameSpace(context))) {\n            return returnData.setErrorMsg(\"恢复失败\")\n        }\n        return returnData.setData(\"\")\n    }\n\n    suspend fun backupToWebdav(context: RoutingContext): ReturnData {\n        val returnData = ReturnData()\n        if (!checkAuth(context)) {\n            return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n        }\n        if (appConfig.secure) {\n            var userInfo = context.get(\"userInfo\") as User?\n            if (userInfo == null) {\n                return returnData.setData(\"NEED_LOGIN\").setErrorMsg(\"请登录后使用\")\n            }\n            if (!userInfo.enable_webdav) {\n                return returnData.setErrorMsg(\"未开启webdav功能\")\n            }\n        }\n        val bookController = BookController(coroutineContext)\n\n        val userNameSpace = getUserNameSpace(context)\n        var latestZipFilePath = bookController.getLastBackFileFromWebdav(userNameSpace)\n        if (latestZipFilePath == null) {\n            return returnData.setErrorMsg(\"请先使用阅读App备份到webdav\")\n        }\n        if (!bookController.saveToWebdav(latestZipFilePath, userNameSpace)) {\n            return returnData.setErrorMsg(\"备份失败\")\n        }\n        return returnData.setData(\"\")\n    }\n\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/config/AppConfig.kt",
    "content": "package com.htmake.reader.config\n\nimport org.springframework.boot.context.properties.ConfigurationProperties\nimport org.springframework.stereotype.Component\n\n@Component\n@ConfigurationProperties(prefix = \"reader.app\")\nclass AppConfig {\n    lateinit var storagePath: String // 存储路径\n    var showUI = false // 是否显示UI\n    var debug = false  // 是否调试web\n    var packaged = false  // 是否打包为app\n    var secure = false    // 是否启用登录鉴权\n    var inviteCode = \"\"   // 注册邀请码\n    var secureKey = \"\"    // 管理密码\n    var cacheChapterContent = false // 是否缓存章节内容\n    var userLimit = 50    // 用户上限\n    var userBookLimit = 200    // 用户书籍上限\n    var debugLog = false  // 调试日志\n    var autoClearInactiveUser = 0  // 自动清理不活跃用户\n\n    var exportUseReplace = false // 导出不使用净化\n    var exportCharset = \"UTF-8\" // 导出字符集\n    var exportNoChapterName = false // 不添加章节名\n    var exportPictureFile = false // 导出图片\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/config/BookConfig.kt",
    "content": "package com.htmake.reader.config\n\nimport java.io.File\n\nobject BookConfig {\n    val javascriptVersion = \"reader-inject-javascript-1.1.0\"\n    val epubInjectJavascript = \"\"\"\n    //<![CDATA[\n    // ${javascriptVersion}\n    if (!window.reader_inited) {\n        function reader_notify(event, data, id) {\n            if (window.self !== window.top) {\n                window.top.postMessage(JSON.stringify({\n                    id: id,\n                    event: event,\n                    data: data\n                }), '*');\n            }\n        }\n\n        var reader_style_dom = document.createElement('style');\n        var head = document.head || document.getElementsByTagName('head')[0];\n        head.appendChild(reader_style_dom);\n\n        function reader_setStyle(style) {\n            reader_style_dom.innerText = style;\n            reader_notifyHeight();\n            setTimeout(reader_notifyHeight, 100);\n        }\n\n        function reader_notifyHeight() {\n            reader_notify(\"setHeight\", document.documentElement.scrollHeight || document.body.scrollHeight)\n        }\n\n        function reader_listenFromParent(event) {\n            reader_notify('received', {\n                data: event.data\n            })\n            let data;\n            try {\n                data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;\n            } catch (error) {\n                // console.error(error);\n                return;\n            }\n\n            if (!data) {\n                return;\n            }\n            reader_notify(\"data \", data);\n            if (data.event === 'setStyle') {\n                reader_setStyle(data.style);\n            } else if (data.event === 'execute') {\n                eval(data.script);\n            } else if (data.id) {\n                if (window.nativeCallback[data.id]) {\n                    window.nativeCallback[data.id](data);\n                    delete window.nativeCallback[data.id]\n                }\n            }\n        }\n\n\n        function reader_getLinkElement(element) {\n            if (!element || !element.nodeName) {\n                return false;\n            }\n            if (element.nodeName.toLowerCase() === \"a\") {\n                return element;\n            }\n            return reader_getLinkElement(element.parentNode)\n        }\n\n        function reader_getImageElement(element) {\n            if (!element || !element.nodeName) {\n                return false;\n            }\n            if (element.nodeName.toLowerCase() === \"img\") {\n                return element;\n            }\n        }\n\n        window.document.addEventListener('message', reader_listenFromParent);\n        window.addEventListener('message', reader_listenFromParent);\n        window.addEventListener('load', function() {\n            reader_notifyHeight();\n            reader_notify(\"load\", window.location.href);\n        });\n        window.addEventListener('resize', reader_notifyHeight);\n        document.addEventListener('DOMNodeInserted', reader_notifyHeight, false);\n        document.addEventListener('click', function(event) {\n            var linkElement = reader_getLinkElement(event.target)\n            var imageElement = reader_getImageElement(event.target)\n            if (linkElement) {\n                // 点击链接跳转\n                if (linkElement.pathname === window.location.pathname) {\n                    // 页内跳转\n                    var hashElement = document.querySelector(linkElement.hash)\n                    if (hashElement) {\n                        reader_notify(\"clickHash\", hashElement.getBoundingClientRect())\n                    }\n                } else {\n                    // 跳转其他页面\n                    reader_notify(\"clickA\", event.target.href);\n                }\n            } else if (imageElement) {\n                var imgList = document.querySelectorAll(\"img\");\n                if (imgList.length) {\n                    var imgUrlList = [];\n                    var index = 0;\n                    for (let i = 0; i < imgList.length; i++) {\n                        imgUrlList.push(imgList[i].src);\n                        if (imgList[i] === imageElement) {\n                            index = i;\n                        }\n                    }\n                    reader_notify(\"previewImageList\", {\n                        imageList: imgUrlList,\n                        imageIndex: index\n                    });\n                }\n            } else {\n                reader_notify(\"click\", {\n                    target: event.target.nodeName,\n                    clientX: event.clientX,\n                    clientY: event.clientY\n                });\n            }\n        });\n        window.addEventListener(\"keydown\", function(event) {\n            event.preventDefault();\n            event.stopPropagation();\n            reader_notify(\"keydown\", {\n                key: event.key,\n                keyCode: event.keyCode\n            });\n        });\n        reader_notify(\"inited\");\n\n        window.reader_inited = true;\n    }\n    //]]>\n    \"\"\"\n\n    fun injectJavascriptToEpubChapter(filePath: String) {\n        val file = File(filePath);\n        if (file.exists()) {\n            var content = file.readText()\n            if (content.indexOf(javascriptVersion) < 0) {\n                content = content.replace(\"<head>\", \"\"\"<head><script type=\"text/javascript\">${epubInjectJavascript}</script>\"\"\")\n                file.writeText(content)\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/entity/BasicError.kt",
    "content": "package com.htmake.reader.entity\n\ndata class BasicError(\n        val error: String,\n        val exception: String,\n        val message: String,\n        val path: String,\n        val status: Int,\n        val timestamp: Long\n)"
  },
  {
    "path": "src/main/java/com/htmake/reader/entity/Size.kt",
    "content": "package com.htmake.reader.entity\n\ndata class Size(\n        val width: Double,\n        val height: Double\n)"
  },
  {
    "path": "src/main/java/com/htmake/reader/entity/User.kt",
    "content": "package com.htmake.reader.entity\n\ndata class User(\n        var username: String=\"\",\n        var password: String=\"\",\n        var salt: String=\"\",\n        var token: String=\"\",\n        var last_login_at: Long = System.currentTimeMillis(),\n        var created_at: Long = System.currentTimeMillis(),\n        var enable_webdav: Boolean = false, // 是否开启 WebDAV 功能\n        var token_map: Map<String, Long>? = null,\n        var enable_local_store: Boolean = false // 是否开启本地书仓功能\n)"
  },
  {
    "path": "src/main/java/com/htmake/reader/init/appCtx.kt",
    "content": "package com.htmake.reader.init\n\nimport com.htmake.reader.utils.getWorkDir\n\n// 处理 appCtx\nobject appCtx {\n    val cacheDir: String by lazy {\n        getWorkDir(\"storage\", \"cache\")\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/htmake/reader/utils/Ext.kt",
    "content": "package com.htmake.reader.utils\n\nimport io.vertx.core.buffer.Buffer\nimport io.vertx.ext.web.client.HttpRequest\nimport io.vertx.ext.web.client.WebClient\nimport okhttp3.HttpUrl.Companion.toHttpUrl\nimport java.io.File\nimport java.io.OutputStream\nimport java.io.InputStream\nimport org.xml.sax.InputSource\nimport java.io.FileOutputStream\nimport java.io.FileInputStream\nimport java.util.zip.ZipFile\nimport java.util.zip.ZipEntry\nimport java.util.zip.ZipOutputStream\nimport org.w3c.dom.*;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\n\n/**\n * @Date: 2019-07-19 23:43\n * @Description:\n */\n\nfun String.url(): String {\n    if (this.startsWith(\"//\")) {\n        return (\"http:\" + this).toHttpUrl().toString()\n    } else if (this.startsWith(\"http\")) {\n        return this.toHttpUrl().toString()\n    }\n    return this\n}\n\nfun WebClient.getEncodeAbs(absoluteURI: String): HttpRequest<Buffer> {\n    return this.getAbs(absoluteURI.toHttpUrl().toString())\n}\n\nfun File.deleteRecursively() {\n    if (this.exists()) {\n        if (this.isFile() ) {\n            this.delete();\n        } else {\n            this.listFiles().forEach{\n                it.deleteRecursively()\n            }\n            this.delete()\n        }\n    }\n}\n\nfun File.unzip(descDir: String): Boolean {\n    if (!this.exists()) {\n        return false\n    }\n    val buffer = ByteArray(1024)\n    var outputStream: OutputStream? = null\n    var inputStream: InputStream? = null\n    try {\n        val zf = ZipFile(this.toString())\n        val entries = zf.entries()\n        while (entries.hasMoreElements()) {\n            val zipEntry: ZipEntry = entries.nextElement() as ZipEntry\n            val zipEntryName: String = zipEntry.name\n\n            val descFilePath: String = descDir + File.separator + zipEntryName\n            if (zipEntry.isDirectory) {\n                createDir(descFilePath)\n            } else {\n                inputStream = zf.getInputStream(zipEntry)\n                val descFile: File = createFile(descFilePath)\n                outputStream = FileOutputStream(descFile)\n\n                var len: Int\n                while (inputStream.read(buffer).also { len = it } > 0) {\n                    outputStream.write(buffer, 0, len)\n                }\n                inputStream.close()\n                outputStream.close()\n            }\n        }\n        return true\n    } catch(e: Exception) {\n        e.printStackTrace()\n    } finally {\n        inputStream?.close()\n        outputStream?.close()\n    }\n    return false\n}\n\nfun File.zip(zipFilePath: String): Boolean {\n    if (!this.exists()) {\n        return false\n    }\n    if (this.isDirectory()) {\n        val files = this.listFiles()\n        val filesList: List<File> = files.toList()\n        return zip(filesList, zipFilePath)\n    } else {\n        return zip(arrayListOf(this), zipFilePath)\n    }\n}\n\nfun zip(files: List<File>, zipFilePath: String): Boolean {\n    if (files.isEmpty()) {\n        return false\n    }\n\n    val zipFile = createFile(zipFilePath)\n    val buffer = ByteArray(1024)\n    var zipOutputStream: ZipOutputStream? = null\n    var inputStream: FileInputStream? = null\n    try {\n        zipOutputStream = ZipOutputStream(FileOutputStream(zipFile))\n        for (file in files) {\n            if (!file.exists()) continue\n            zipOutputStream.putNextEntry(ZipEntry(file.name))\n            inputStream = FileInputStream(file)\n            var len: Int\n            while (inputStream.read(buffer).also { len = it } > 0) {\n                zipOutputStream.write(buffer, 0, len)\n            }\n            zipOutputStream.closeEntry()\n        }\n        return true\n    } catch(e: Exception) {\n        e.printStackTrace()\n    } finally {\n        inputStream?.close()\n        zipOutputStream?.close()\n    }\n    return false\n}\n\nfun createDir(filePath: String): File {\n    val file = File(filePath)\n    if (!file.exists()) {\n        file.mkdirs()\n    }\n    return file\n}\n\nfun createFile(filePath: String): File {\n    val file = File(filePath)\n    val parentFile = file.parentFile!!\n    if (!parentFile.exists()) {\n        parentFile.mkdirs()\n    }\n    if (!file.exists()) {\n        file.createNewFile()\n    }\n    return file\n}\n\nfun getFileExtetion(url: String, defaultExt: String=\"\"): String {\n    try {\n        var seqs = url.split(\"?\", ignoreCase = true, limit = 2)\n        var file = seqs[0].split(\"/\").last()\n        val dotPos = file.lastIndexOf('.')\n        return if (0 <= dotPos) {\n            file.substring(dotPos + 1)\n        } else {\n            defaultExt\n        }\n    } catch (e: Exception) {\n        return defaultExt\n    }\n}\n\nfun xml2map(source: Any): MutableMap<String, Any> {\n    //1.创建DocumentBuilderFactory对象\n    val factory = DocumentBuilderFactory.newInstance()\n    //2.创建DocumentBuilder对象\n    var doc = mutableMapOf<String, Any>()\n    try {\n        val builder = factory.newDocumentBuilder()\n        // val document = builder.parse(filePath)\n        when {\n            source is String -> {\n                val document = builder.parse(source as String)\n                return parseNode(document.getChildNodes())\n            }\n            source is InputStream -> {\n                val document = builder.parse(source as InputStream)\n                return parseNode(document.getChildNodes())\n            }\n            source is InputSource -> {\n                val document = builder.parse(source as InputSource)\n                return parseNode(document.getChildNodes())\n            }\n            else -> {\n                return doc\n            }\n        }\n    } catch (e: Exception) {\n        e.printStackTrace()\n        return doc\n    }\n}\n\nfun parseNode(list: NodeList): MutableMap<String, Any> {\n    var doc = mutableMapOf<String, Any>()\n    for (i in 0 until list.getLength()) {\n        val node = list.item(i);\n        if (node.getNodeType() == Node.ELEMENT_NODE) {\n            val childNodes = node.getChildNodes()\n            // <Element><Text></Text><Element><Text></Text></Element></Element>\n            // logger.info(\"index: {} node: {} type: {} childNodesLength: {}\", i, node, node.getNodeType(), childNodes.getLength())\n            if (childNodes.getLength() == 1 && node.getFirstChild().getNodeType() == Node.TEXT_NODE) {\n                doc.put(node.getNodeName(), node.getFirstChild().getNodeValue())\n            } else if(childNodes.getLength() > 1) {\n                doc.put(node.getNodeName(), parseNode(childNodes))\n            }\n        }\n    }\n    return doc\n}\n\n\n\n\n\n"
  },
  {
    "path": "src/main/java/com/htmake/reader/utils/SpringContextUtils.java",
    "content": "package com.htmake.reader.utils;\n\nimport org.springframework.beans.BeansException;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class SpringContextUtils implements ApplicationContextAware {\n\n    /**\n     * 上下文对象实例\n     */\n    private static ApplicationContext applicationContext;\n\n    @Override\n    public void setApplicationContext(ApplicationContext context) throws BeansException {\n        applicationContext = context;\n    }\n\n    /**\n     * 获取applicationContext\n     *\n     * @return\n     */\n    public static ApplicationContext getApplicationContext() {\n        return applicationContext;\n    }\n\n    /**\n     * 通过name获取 Bean.\n     *\n     * @param name\n     * @return\n     */\n    public static Object getBean(String name) {\n        if (applicationContext != null) {\n            return getApplicationContext().getBean(name);\n        }\n        return null;\n    }\n\n    /**\n     * 通过class获取Bean.\n     *\n     * @param clazz\n     * @param <T>\n     * @return\n     */\n    public static <T> T getBean(Class<T> clazz) {\n        if (applicationContext != null) {\n            return getApplicationContext().getBean(clazz);\n        }\n        return null;\n    }\n\n    /**\n     * 通过name,以及Clazz返回指定的Bean\n     *\n     * @param name\n     * @param clazz\n     * @param <T>\n     * @return\n     */\n    public static <T> T getBean(String name, Class<T> clazz) {\n        if (applicationContext != null) {\n            return getApplicationContext().getBean(name, clazz);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/main/java/com/htmake/reader/utils/VertExt.kt",
    "content": "package com.htmake.reader.utils\n\nimport com.google.common.base.Throwables\nimport com.google.gson.Gson\nimport com.google.gson.GsonBuilder\nimport io.vertx.core.Handler\nimport io.vertx.core.json.JsonObject\nimport io.vertx.core.json.JsonArray\nimport io.vertx.ext.web.RoutingContext\nimport mu.KotlinLogging\nimport com.htmake.reader.entity.BasicError\nimport java.net.URLDecoder\nimport java.net.URLEncoder\nimport java.io.File\nimport java.nio.file.Paths\nimport com.htmake.reader.config.AppConfig\nimport com.google.gson.reflect.TypeToken\nimport kotlin.reflect.KProperty1\nimport kotlin.reflect.KMutableProperty\nimport kotlin.reflect.full.memberProperties\nimport io.legado.app.data.entities.Book\nimport io.legado.app.utils.MD5Utils\n\n/**\n * @Auther: zoharSoul\n * @Date: 2019-05-21 16:17\n * @Description:\n */\nval logger = KotlinLogging.logger {}\n\nval gson = GsonBuilder().disableHtmlEscaping().create()\nval prettyGson = GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create()\n\nvar storageFinalPath = \"\"\nvar workDirPath = \"\"\nvar workDirInit = false\n\nfun RoutingContext.success(any: Any?) {\n    val toJson: String = if (any is JsonObject) {\n        any.toString()\n    } else {\n        gson.toJson(any)\n    }\n    this.response()\n            .putHeader(\"content-type\", \"application/json; charset=utf-8\")\n            .end(toJson)\n}\n\nfun RoutingContext.error(throwable: Throwable) {\n    val path = URLDecoder.decode(this.request().absoluteURI(), \"UTF-8\")\n    val basicError = BasicError(\n            \"Internal Server Error\",\n            throwable.toString(),\n            throwable.message.toString(),\n            path,\n            500,\n            System.currentTimeMillis()\n    )\n\n    val errorJson = gson.toJson(basicError)\n    logger.error(\"Internal Server Error\", throwable)\n    logger.error { errorJson }\n\n    this.response()\n            .putHeader(\"content-type\", \"application/json; charset=utf-8\")\n            .setStatusCode(500)\n            .end(errorJson)\n}\n\nfun getWorkDir(subPath: String = \"\"): String {\n    if (!workDirInit && workDirPath.isEmpty()) {\n        var osName = System.getProperty(\"os.name\")\n        var currentDir = System.getProperty(\"user.dir\")\n        logger.info(\"osName: {} currentDir: {}\", osName, currentDir)\n        // MacOS 存放目录为用户目录\n        if (osName.startsWith(\"Mac OS\", true) && !currentDir.startsWith(\"/Users/\")) {\n            workDirPath = Paths.get(System.getProperty(\"user.home\"), \".reader\").toString()\n        } else {\n            workDirPath = currentDir\n        }\n        workDirInit = true\n    }\n    var path = Paths.get(workDirPath, subPath);\n\n    return path.toString();\n}\n\nfun getWorkDir(vararg subDirFiles: String): String {\n    return getWorkDir(getRelativePath(*subDirFiles))\n}\n\nfun getRelativePath(vararg subDirFiles: String): String {\n    val path = StringBuilder(\"\")\n    subDirFiles.forEach {\n        if (it.isNotEmpty()) {\n            path.append(File.separator).append(it)\n        }\n    }\n    return path.toString().let{\n        if (it.startsWith(\"/\")) {\n            it.substring(1)\n        } else {\n            it\n        }\n    }\n}\n\nfun getStoragePath(): String {\n    if (storageFinalPath.isNotEmpty()) {\n        return storageFinalPath;\n    }\n    var appConfig = SpringContextUtils.getBean(\"appConfig\", AppConfig::class.java)\n    var storageDir = File(\"storage\")\n    if (appConfig != null) {\n        // logger.info(\"storagePath from appConfig: {}\", appConfig.storagePath)\n        storageDir = File(appConfig.storagePath)\n    }\n    if (storageDir.isAbsolute()) {\n        return storageDir.toString();\n    }\n    var storagePath = getWorkDir(storageDir.toString())\n    if (appConfig != null) {\n        storageFinalPath = storagePath\n    }\n    return storagePath;\n}\n\nfun saveStorage(vararg name: String, value: Any, pretty: Boolean = false) {\n    val toJson: String = if (value is JsonObject || value is JsonArray) {\n        value.toString()\n    } else if (pretty) {\n        prettyGson.toJson(value)\n    } else {\n        gson.toJson(value)\n    }\n\n    var storagePath = getStoragePath()\n    var storageDir = File(storagePath)\n    if (!storageDir.exists()) {\n        storageDir.mkdirs()\n    }\n\n    val filename = name.last()\n    val file = File(getRelativePath(storagePath, *name.copyOfRange(0, name.size - 1), \"${filename}.json\"))\n    // val file = File(storagePath + \"/${name}.json\")\n    logger.info(\"Save file to storage name: {} path: {}\", name, file.absoluteFile)\n\n    if (!file.parentFile.exists()) {\n        file.parentFile.mkdirs()\n    }\n\n    if (!file.exists()) {\n        file.createNewFile()\n    }\n    file.writeText(toJson)\n}\n\nfun getStorage(vararg name: String): String?  {\n    var storagePath = getStoragePath()\n    var storageDir = File(storagePath)\n    if (!storageDir.exists()) {\n        storageDir.mkdirs()\n    }\n\n    val filename = name.last()\n    val file = File(getRelativePath(storagePath, *name.copyOfRange(0, name.size - 1), \"${filename}.json\"))\n    logger.info(\"Read file from storage name: {} path: {}\", name, file.absoluteFile)\n    if (!file.exists()) {\n        return null\n    }\n    return file.readText()\n}\n\nfun asJsonArray(value: Any?): JsonArray? {\n    if (value is JsonArray) {\n        return value\n    } else if (value is String) {\n        return JsonArray(value)\n    }\n    return null\n}\n\nfun asJsonObject(value: Any?): JsonObject? {\n    if (value is JsonObject) {\n        return value\n    } else if (value is String) {\n        return JsonObject(value)\n    }\n    return null\n}\n\n//convert a data class to a map\nfun <T> T.serializeToMap(): Map<String, Any> {\n    return convert()\n}\n\n//convert string to a map\nfun <T> T.toMap(): Map<String, Any> {\n    return convert()\n}\n\n//convert a map to a data class\ninline fun <reified T> Map<String, Any>.toDataClass(): T {\n    return convert()\n}\n\n//convert an object of type I to type O\ninline fun <I, reified O> I.convert(): O {\n    val json = if (this is String) {\n        this\n    } else {\n        gson.toJson(this)\n    }\n    return gson.fromJson(json, object : TypeToken<O>() {}.type)\n}\n\n@Suppress(\"UNCHECKED_CAST\")\nfun <R> readInstanceProperty(instance: Any, propertyName: String): R {\n    val property = instance::class.memberProperties\n                     // don't cast here to <Any, R>, it would succeed silently\n                     .first { it.name == propertyName } as KProperty1<Any, *>\n    // force a invalid cast exception if incorrect type here\n    return property.get(instance) as R\n}\n\n@Suppress(\"UNCHECKED_CAST\")\nfun setInstanceProperty(instance: Any, propertyName: String, propertyValue: Any) {\n    val property = instance::class.memberProperties\n                     .first { it.name == propertyName }\n    if(property is KMutableProperty<*>) {\n        property.setter.call(instance, propertyValue)\n    }\n}\n\nfun Book.fillData(newBook: Book, keys: List<String>): Book {\n    keys.let {\n        for (key in it) {\n            var current = readInstanceProperty<String>(this, key)\n            if (current.isNullOrEmpty()) {\n                var cacheValue = readInstanceProperty<String>(newBook, key)\n                if (!cacheValue.isNullOrEmpty()) {\n                    setInstanceProperty(this, key, cacheValue)\n                }\n            }\n        }\n    }\n    return this\n}\n\nfun getRandomString(length: Int) : String {\n    val allowedChars = \"ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz0123456789\"\n    return (1..length)\n        .map { allowedChars.random() }\n        .joinToString(\"\")\n}\n\nfun genEncryptedPassword(password: String, salt: String): String {\n    return MD5Utils.md5Encode(\n        MD5Utils.md5Encode(password + salt).toString() + salt\n    ).toString()\n}\n\nfun jsonEncode(value: Any, pretty: Boolean = false): String {\n    if (pretty) {\n        return prettyGson.toJson(value)\n    }\n    return gson.toJson(value)\n}"
  },
  {
    "path": "src/main/java/com/htmake/reader/verticle/RestVerticle.kt",
    "content": "package com.htmake.reader.verticle\n\nimport io.vertx.core.http.HttpMethod\nimport io.vertx.ext.web.Route\nimport io.vertx.ext.web.Router\nimport io.vertx.ext.web.RoutingContext\nimport io.vertx.ext.web.handler.BodyHandler\nimport io.vertx.ext.web.handler.CorsHandler\nimport io.vertx.ext.web.handler.LoggerFormat\nimport io.vertx.ext.web.handler.LoggerHandler\nimport io.vertx.ext.web.handler.SessionHandler\nimport io.vertx.ext.web.sstore.LocalSessionStore\nimport io.vertx.kotlin.coroutines.CoroutineVerticle\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport mu.KotlinLogging\nimport com.htmake.reader.utils.error\nimport com.htmake.reader.utils.success\nimport java.net.URLDecoder\n\n\nprivate val logger = KotlinLogging.logger {}\n\nabstract class RestVerticle : CoroutineVerticle() {\n\n    protected lateinit var router: Router\n\n    open var port: Int = 8080\n\n    override suspend fun start() {\n        super.start()\n        router = Router.router(vertx)\n        val cookieName = \"reader.session\"\n\t    router.route().handler(\n            SessionHandler.create(LocalSessionStore.create(vertx))\n                            .setSessionCookieName(cookieName)\n                            .setSessionTimeout(7L * 86400 * 1000)\n                            .setSessionCookiePath(\"/\")\n        );\n        router.route().handler {\n            it.addHeadersEndHandler { _ ->\n                val cookie = it.getCookie(cookieName)\n                if (cookie != null) {\n                    // 每次访问都延长cookie有效期\n                    cookie.setMaxAge(2L * 86400 * 1000)\n                    cookie.setPath(\"/\")\n                }\n            }\n            it.next()\n        }\n\n        // CORS support\n        router.route().handler {\n            it.addHeadersEndHandler { _ ->\n                val origin = it.request().getHeader(\"Origin\")\n                if (origin != null && origin.isNotEmpty()) {\n                    var res = it.response()\n                    res.putHeader(\"Access-Control-Allow-Origin\", origin)\n                    res.putHeader(\"Access-Control-Allow-Credentials\", \"true\")\n                    res.putHeader(\"Access-Control-Allow-Methods\", \"GET, POST, PATCH, PUT, DELETE\")\n                    res.putHeader(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With\")\n                }\n            }\n            val origin = it.request().getHeader(\"Origin\")\n            if (origin != null && origin.isNotEmpty() && it.request().method() == HttpMethod.OPTIONS) {\n                it.removeCookie(cookieName)\n                it.success(\"\")\n            } else {\n                it.next()\n            }\n        }\n\n        router.route().handler(BodyHandler.create())\n\n        router.route().handler(LoggerHandler.create(LoggerFormat.DEFAULT));\n        router.route(\"/reader3/*\").handler {\n            logger.info(\"{} {}\", it.request().rawMethod(), URLDecoder.decode(it.request().absoluteURI(), \"UTF-8\"))\n            if (!it.request().rawMethod().equals(\"PUT\") && (it.fileUploads() == null || it.fileUploads().isEmpty()) && it.bodyAsString.length > 0 && it.bodyAsString.length < 1000) {\n                logger.info(\"Request body: {}\", it.bodyAsString)\n            }\n            it.next()\n        }\n\n        router.get(\"/health\").handler { it.success(\"ok!\") }\n\n        initRouter(router)\n\n//        router.errorHandler(500) { routerContext ->\n//            logger.error { routerContext.failure().message }\n//            routerContext.error(routerContext.failure())\n//        }\n\n        router.route().last().failureHandler { ctx ->\n            ctx.error(ctx.failure())\n        }\n\n        logger.info(\"port: {}\", port)\n        vertx.createHttpServer().requestHandler(router).exceptionHandler{error ->\n            onException(error)\n        }.listen(port) { res ->\n            if (res.succeeded()) {\n                logger.info(\"Server running at: http://localhost:{}\", port);\n                logger.info(\"Web reader running at: http://localhost:{}\", port);\n                started();\n            } else {\n                onStartError();\n            }\n        }\n    }\n\n    abstract suspend fun initRouter(router: Router);\n\n    open fun onException(error: Throwable) {\n        logger.error(\"vertx exception: {}\", error)\n    }\n\n    open fun onStartError() {\n    }\n\n    open fun started() {\n\n    }\n\n    open fun onHandlerError(ctx: RoutingContext, error: Exception) {\n        logger.error(\"Error: {}\", error)\n        ctx.error(error)\n    }\n\n    /**\n     * An extension method for simplifying coroutines usage with Vert.x Web routers\n     */\n    fun Route.coroutineHandler(fn: suspend (RoutingContext) -> Any) {\n        handler { ctx ->\n            val job = launch(Dispatchers.IO) {\n                try {\n                    ctx.success(fn(ctx))\n//                    fn(ctx)\n                } catch (e: Exception) {\n                    onHandlerError(ctx, e)\n                }\n            }\n        }\n    }\n\n    fun Route.coroutineHandlerWithoutRes(fn: suspend (RoutingContext) -> Any) {\n        handler { ctx ->\n            val job = launch(Dispatchers.IO) {\n                try {\n                    fn(ctx)\n                } catch (e: Exception) {\n                    onHandlerError(ctx, e)\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/README.md",
    "content": "# 文件结构介绍\n\n* constant 常量\n* data 数据\n* help 帮助\n* lib 库\n* model 解析\n"
  },
  {
    "path": "src/main/java/io/legado/app/constant/Action.kt",
    "content": "package io.legado.app.constant\n\nobject Action {\n\n    const val play = \"play\"\n    const val stop = \"stop\"\n    const val resume = \"resume\"\n    const val pause = \"pause\"\n    const val addTimer = \"addTimer\"\n    const val setTimer = \"setTimer\"\n    const val prevParagraph = \"prevParagraph\"\n    const val nextParagraph = \"nextParagraph\"\n    const val upTtsSpeechRate = \"upTtsSpeechRate\"\n    const val adjustProgress = \"adjustProgress\"\n    const val prev = \"prev\"\n    const val next = \"next\"\n    const val moveTo = \"moveTo\"\n    const val init = \"init\"\n}"
  },
  {
    "path": "src/main/java/io/legado/app/constant/AppConst.kt",
    "content": "package io.legado.app.constant\n\nimport java.text.SimpleDateFormat\nimport com.script.javascript.RhinoScriptEngine\n\nobject AppConst {\n\n\n    const val UA_NAME = \"User-Agent\"\n\n    val userAgent: String by lazy {\n        \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36\"\n    }\n\n    val SCRIPT_ENGINE: RhinoScriptEngine by lazy {\n        RhinoScriptEngine()\n    }\n\n    val TIME_FORMAT: SimpleDateFormat by lazy {\n        SimpleDateFormat(\"HH:mm\")\n    }\n\n    val timeFormat: SimpleDateFormat by lazy {\n        SimpleDateFormat(\"HH:mm\")\n    }\n\n    val dateFormat: SimpleDateFormat by lazy {\n        SimpleDateFormat(\"yyyy/MM/dd HH:mm\")\n    }\n\n    val fileNameFormat: SimpleDateFormat by lazy {\n        SimpleDateFormat(\"yy-MM-dd-HH-mm-ss\")\n    }\n\n    val keyboardToolChars: List<String> by lazy {\n        arrayListOf(\n            \"@\", \"&\", \"|\", \"%\", \"/\", \":\", \"[\", \"]\", \"{\", \"}\", \"<\", \">\", \"\\\\\", \"$\", \"#\", \"!\", \".\",\n            \"href\", \"src\", \"textNodes\", \"xpath\", \"json\", \"css\", \"id\", \"class\", \"tag\"\n        )\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/constant/AppPattern.kt",
    "content": "package io.legado.app.constant\n\nimport java.util.regex.Pattern\n\nobject AppPattern {\n    val JS_PATTERN: Pattern =\n        Pattern.compile(\"<js>([\\\\w\\\\W]*?)</js>|@js:([\\\\w\\\\W]*)\", Pattern.CASE_INSENSITIVE)\n    val EXP_PATTERN: Pattern = Pattern.compile(\"\\\\{\\\\{([\\\\w\\\\W]*?)\\\\}\\\\}\")\n\n    //匹配格式化后的图片格式\n    val imgPattern: Pattern = Pattern.compile(\"<img[^>]*src=\\\"([^\\\"]*(?:\\\"[^>]+\\\\})?)\\\"[^>]*>\")\n\n    //dataURL图片类型\n    val dataUriRegex = Regex(\"data:.*?;base64,(.*)\")\n\n    val nameRegex = Regex(\"\\\\s+作\\\\s*者.*|\\\\s+\\\\S+\\\\s+著\")\n    val authorRegex = Regex(\"^\\\\s*作\\\\s*者[:：\\\\s]+|\\\\s+著\")\n    val fileNameRegex = Regex(\"[\\\\\\\\/:*?\\\"<>|.]\")\n    val splitGroupRegex = Regex(\"[,;，；]\")\n\n    //书源调试信息中的各种符号\n    val debugMessageSymbolRegex = Regex(\"[⇒◇┌└≡]\")\n\n    //本地书籍支持类型\n    val bookFileRegex = Regex(\".*\\\\.(txt|epub|umd)\", RegexOption.IGNORE_CASE)\n\n    /**\n     * 所有标点\n     */\n    val bdRegex = Regex(\"(\\\\p{P})+\")\n\n    /**\n     * 换行\n     */\n    val rnRegex = Regex(\"[\\\\r\\\\n]\")\n\n    /**\n     * 不发音段落判断\n     */\n    val notReadAloudRegex = Regex(\"^(\\\\s|\\\\p{C}|\\\\p{P}|\\\\p{Z}|\\\\p{S})+$\")\n}"
  },
  {
    "path": "src/main/java/io/legado/app/constant/BookType.kt",
    "content": "package io.legado.app.constant\n\nobject BookType {\n    const val default = 0           // 0 文本\n    const val audio = 1             // 1 音频\n    const val image = 2            // 2 图片\n    const val file = 3               // 3 只提供下载服务的网站\n    const val local = \"loc_book\"\n}"
  },
  {
    "path": "src/main/java/io/legado/app/constant/DeepinkBookSource.kt",
    "content": "package io.legado.app.constant\n\nimport java.io.File\n\nobject DeepinkBookSource {\n\n    fun generate(name: String, url: String, md5: String) {\n        val text = \"{\\n\" +\n                \"  \\\"name\\\": \\\"$name by [yuedu.best]\\\",\\n\" +\n                \"  \\\"url\\\": \\\"$url\\\",\\n\" +\n                \"  \\\"version\\\": 100,\\n\" +\n                \"  \\\"search\\\": {\\n\" +\n                \"    \\\"url\\\": \\\"http://api.yuedu.best/yuedu/searchBook@post->{\\\\\\\"key\\\\\\\":\\\\\\\"\\${key}\\\\\\\", \\\\\\\"bookSourceCode\\\\\\\":\\\\\\\"$md5\\\\\\\"}\\\",\\n\" +\n                \"    \\\"charset\\\": \\\"utf-8\\\",\\n\" +\n                \"    \\\"list\\\": \\\"\\$.[*]\\\",\\n\" +\n                \"    \\\"name\\\": \\\"\\$.name\\\",\\n\" +\n                \"    \\\"author\\\": \\\"\\$.author\\\",\\n\" +\n                \"    \\\"cover\\\": \\\"\\$.coverUrl\\\",\\n\" +\n                \"    \\\"summary\\\": \\\"\\$.intro\\\",\\n\" +\n                \"    \\\"detail\\\": \\\"http://api.yuedu.best/yuedu/getBookInfo@post->{\\\\\\\"searchBook\\\\\\\":\\${$}, \\\\\\\"bookSourceCode\\\\\\\":\\\\\\\"$md5\\\\\\\"}\\\"\\n\" +\n                \"  },\\n\" +\n                \"  \\\"detail\\\": {\\n\" +\n                \"    \\\"name\\\": \\\"\\$.name\\\",\\n\" +\n                \"    \\\"author\\\": \\\"\\$.author\\\",\\n\" +\n                \"    \\\"cover\\\": \\\"\\$.coverUrl\\\",\\n\" +\n                \"    \\\"summary\\\": \\\"\\$.intro\\\",\\n\" +\n                \"    \\\"status\\\": \\\"\\\",\\n\" +\n                \"    \\\"update\\\": \\\"\\$.latestChapterTime\\\",\\n\" +\n                \"    \\\"lastChapter\\\": \\\"\\$.latestChapterTitle\\\",\\n\" +\n                \"    \\\"catalog\\\": \\\"http://api.yuedu.best/yuedu/getChapterList@post->{\\\\\\\"book\\\\\\\":\\${$}, \\\\\\\"bookSourceCode\\\\\\\":\\\\\\\"$md5\\\\\\\"}\\\"\\n\" +\n                \"  },\\n\" +\n                \"  \\\"catalog\\\": {\\n\" +\n                \"    \\\"list\\\": \\\"\\$.[*]\\\",\\n\" +\n                \"    \\\"name\\\": \\\"\\$.title\\\",\\n\" +\n                \"    \\\"chapter\\\": \\\"http://api.yuedu.best/yuedu/getContent@post->{\\\\\\\"bookChapter\\\\\\\":\\${$}, \\\\\\\"bookSourceCode\\\\\\\":\\\\\\\"$md5\\\\\\\"}\\\"\\n\" +\n                \"  },\\n\" +\n                \"  \\\"chapter\\\": {\\n\" +\n                \"    \\\"content\\\": \\\"\\$.text\\\"\\n\" +\n                \"  }\\n\" +\n                \"}\"\n\n        val file = File(\"repo/${url.replace(\"https://\",\"\").replace(\"http://\",\"\")}.json\")\n        println(\"file path: \"+ file.absoluteFile)\n        file.createNewFile()\n        file.writeText(text)\n//        println(\"file path: \"+ file.absoluteFile)\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/constant/PreferKey.kt",
    "content": "package io.legado.app.constant\n\nobject PreferKey {\n\n    const val downloadPath = \"downloadPath\"\n    const val hideStatusBar = \"hideStatusBar\"\n    const val hideNavigationBar = \"hideNavigationBar\"\n    const val precisionSearch = \"precisionSearch\"\n    const val prevKey = \"prevKeyCode\"\n    const val nextKey = \"nextKeyCode\"\n}"
  },
  {
    "path": "src/main/java/io/legado/app/constant/RSSKeywords.kt",
    "content": "package io.legado.app.constant\n\nobject RSSKeywords {\n\n    const val RSS_ITEM = \"item\"\n    const val RSS_ITEM_TITLE = \"title\"\n    const val RSS_ITEM_LINK = \"link\"\n    const val RSS_ITEM_CATEGORY = \"category\"\n    const val RSS_ITEM_THUMBNAIL = \"media:thumbnail\"\n    const val RSS_ITEM_ENCLOSURE = \"enclosure\"\n    const val RSS_ITEM_DESCRIPTION = \"description\"\n    const val RSS_ITEM_CONTENT = \"content:encoded\"\n    const val RSS_ITEM_PUB_DATE = \"pubDate\"\n    const val RSS_ITEM_TIME = \"time\"\n    const val RSS_ITEM_URL = \"url\"\n    const val RSS_ITEM_TYPE = \"type\"\n}"
  },
  {
    "path": "src/main/java/io/legado/app/constant/Status.kt",
    "content": "package io.legado.app.constant\n\nobject Status {\n    const val STOP = 0\n    const val PLAY = 1\n    const val PAUSE = 3\n}"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/BaseBook.kt",
    "content": "package io.legado.app.data.entities\n\nimport io.legado.app.model.analyzeRule.RuleDataInterface\nimport io.legado.app.utils.splitNotBlank\n\ninterface BaseBook : RuleDataInterface {\n    var name: String\n    var author: String\n    var bookUrl: String\n    var kind: String?\n    var wordCount: String?\n\n    var infoHtml: String?\n    var tocHtml: String?\n\n    fun getKindList(): List<String> {\n        val kindList = arrayListOf<String>()\n        wordCount?.let {\n            if (it.isNotBlank()) kindList.add(it)\n        }\n        kind?.let {\n            val kinds = it.splitNotBlank(\",\", \"\\n\")\n            kindList.addAll(kinds)\n        }\n        return kindList\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/BaseSource.kt",
    "content": "package io.legado.app.data.entities\n\nimport com.script.SimpleBindings\nimport io.legado.app.utils.Base64\nimport io.legado.app.constant.AppConst\nimport io.legado.app.help.CacheManager\nimport io.legado.app.help.JsExtensions\nimport io.legado.app.help.http.CookieStore\nimport io.legado.app.utils.*\n\n/**\n * 可在js里调用,source.xxx()\n */\n@Suppress(\"unused\")\ninterface BaseSource : JsExtensions {\n\n    var concurrentRate: String? // 并发率\n    var loginUrl: String?       // 登录地址\n    // var loginUi: String?   // 登录UI\n    var header: String?         // 请求头\n\n    fun getTag(): String\n\n    fun getKey(): String\n\n    override fun getSource(): BaseSource? {\n        return this\n    }\n\n    fun getLoginJs(): String? {\n        val loginJs = loginUrl\n        return when {\n            loginJs == null -> null\n            loginJs.startsWith(\"@js:\") -> loginJs.substring(4)\n            loginJs.startsWith(\"<js>\") ->\n                loginJs.substring(4, loginJs.lastIndexOf(\"<\"))\n            else -> loginJs\n        }\n    }\n\n    fun login() {\n        getLoginJs()?.let {\n            evalJS(it)\n        }\n    }\n\n    /**\n     * 解析header规则\n     */\n    fun getHeaderMap(hasLoginHeader: Boolean = false) = HashMap<String, String>().apply {\n        this[AppConst.UA_NAME] = AppConst.userAgent\n        header?.let {\n            GSON.fromJsonObject<Map<String, String>>(\n                when {\n                    it.startsWith(\"@js:\", true) ->\n                        evalJS(it.substring(4)).toString()\n                    it.startsWith(\"<js>\", true) ->\n                        evalJS(it.substring(4, it.lastIndexOf(\"<\"))).toString()\n                    else -> it\n                }\n            ).getOrNull()?.let { map ->\n                putAll(map)\n            }\n        }\n        if (hasLoginHeader) {\n            getLoginHeaderMap()?.let {\n                putAll(it)\n            }\n        }\n    }\n\n    /**\n     * 获取用于登录的头部信息\n     */\n    fun getLoginHeader(): String? {\n        return CacheManager.get(\"loginHeader_${getKey()}\")\n    }\n\n    fun getLoginHeaderMap(): Map<String, String>? {\n        val cache = getLoginHeader() ?: return null\n        return GSON.fromJsonObject<Map<String, String>>(cache).getOrNull()\n    }\n\n    /**\n     * 保存登录头部信息,map格式,访问时自动添加\n     */\n    fun putLoginHeader(header: String) {\n        CacheManager.put(\"loginHeader_${getKey()}\", header)\n    }\n\n    fun removeLoginHeader() {\n        CacheManager.delete(\"loginHeader_${getKey()}\")\n    }\n\n    /**\n     * 获取用户信息,可以用来登录\n     * 用户信息采用aes加密存储\n     */\n    fun getLoginInfo(): String? {\n        try {\n            val key = AppConst.userAgent.encodeToByteArray(0, 8)\n            val cache = CacheManager.get(\"userInfo_${getKey()}\") ?: return null\n            val encodeBytes = EncoderUtils.base64Decode(cache, Base64.DEFAULT).toByteArray()\n            val decodeBytes = EncoderUtils.decryptAES(encodeBytes, key)\n                ?: return null\n            return String(decodeBytes)\n        } catch (e: Exception) {\n            log(\"获取登陆信息出错 \" + e.localizedMessage)\n            return null\n        }\n    }\n\n    fun getLoginInfoMap(): Map<String, String>? {\n        return GSON.fromJsonObject<Map<String, String>>(getLoginInfo()).getOrNull()\n    }\n\n    /**\n     * 保存用户信息,aes加密\n     */\n    fun putLoginInfo(info: String): Boolean {\n        return try {\n            val key = (AppConst.userAgent).encodeToByteArray(0, 8)\n            val encodeBytes = EncoderUtils.encryptAES(info.toByteArray(), key)\n            val encodeStr = Base64.encodeToString(encodeBytes, Base64.DEFAULT)\n            CacheManager.put(\"userInfo_${getKey()}\", encodeStr)\n            true\n        } catch (e: Exception) {\n            log(\"保存登陆信息出错 \" + e.localizedMessage)\n            false\n        }\n    }\n\n    fun removeLoginInfo() {\n        CacheManager.delete(\"userInfo_${getKey()}\")\n    }\n\n    fun setVariable(variable: String?) {\n        if (variable != null) {\n            CacheManager.put(\"sourceVariable_${getKey()}\", variable)\n        } else {\n            CacheManager.delete(\"sourceVariable_${getKey()}\")\n        }\n    }\n\n    fun getVariable(): String? {\n        return CacheManager.get(\"sourceVariable_${getKey()}\")\n    }\n\n    /**\n     * 执行JS\n     */\n    @Throws(Exception::class)\n    fun evalJS(jsStr: String, bindingsConfig: SimpleBindings.() -> Unit = {}): Any? {\n        val bindings = SimpleBindings()\n        bindings.apply(bindingsConfig)\n        bindings[\"java\"] = this\n        bindings[\"source\"] = this\n        bindings[\"baseUrl\"] = getKey()\n        bindings[\"cookie\"] = CookieStore\n        bindings[\"cache\"] = CacheManager\n        return AppConst.SCRIPT_ENGINE.eval(jsStr, bindings)\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/Book.kt",
    "content": "package io.legado.app.data.entities\n\n\nimport io.legado.app.constant.BookType\nimport io.legado.app.constant.AppPattern\nimport io.legado.app.utils.GSON\nimport io.legado.app.utils.fromJsonObject\nimport io.legado.app.utils.MD5Utils\nimport io.legado.app.utils.FileUtils\nimport io.legado.app.model.localBook.LocalBook\nimport io.legado.app.model.localBook.EpubFile\nimport io.legado.app.model.localBook.UmdFile\nimport io.legado.app.model.localBook.CbzFile\nimport java.nio.charset.Charset\nimport java.io.File\nimport kotlin.math.max\nimport kotlin.math.min\nimport org.jsoup.Jsoup\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\n\n@JsonIgnoreProperties(\"variableMap\", \"infoHtml\", \"tocHtml\", \"config\", \"rootDir\", \"readConfig\", \"localBook\", \"epub\", \"epubRootDir\", \"onLineTxt\", \"localTxt\", \"umd\", \"realAuthor\", \"unreadChapterNum\", \"folderName\", \"localFile\", \"kindList\", \"_userNameSpace\", \"bookDir\", \"userNameSpace\")\ndata class Book(\n        override var bookUrl: String = \"\",                   // 详情页Url(本地书源存储完整文件路径)\n        var tocUrl: String = \"\",                    // 目录页Url (toc=table of Contents)\n        var origin: String = BookType.local,        // 书源URL(默认BookType.local)\n        var originName: String = \"\",                //书源名称\n        override var name: String = \"\",                   // 书籍名称(书源获取)\n        override var author: String = \"\",                 // 作者名称(书源获取)\n        override var kind: String? = null,                    // 分类信息(书源获取)\n        var customTag: String? = null,              // 分类信息(用户修改)\n        var coverUrl: String? = null,               // 封面Url(书源获取)\n        var customCoverUrl: String? = null,         // 封面Url(用户修改)\n        var intro: String? = null,            // 简介内容(书源获取)\n       var customIntro: String? = null,      // 简介内容(用户修改)\n       var charset: String? = null,                // 自定义字符集名称(仅适用于本地书籍)\n        var type: Int = 0,                          // @BookType\n       var group: Int = 0,                         // 自定义分组索引号\n        var latestChapterTitle: String? = null,     // 最新章节标题\n        var latestChapterTime: Long = System.currentTimeMillis(),            // 最新章节标题更新时间\n        var lastCheckTime: Long = System.currentTimeMillis(),                // 最近一次更新书籍信息的时间\n        var lastCheckCount: Int = 0,                // 最近一次发现新章节的数量\n        var totalChapterNum: Int = 0,               // 书籍目录总数\n       var durChapterTitle: String? = null,        // 当前章节名称\n       var durChapterIndex: Int = 0,               // 当前章节索引\n       var durChapterPos: Int = 0,                 // 当前阅读的进度(首行字符的索引位置)\n       var durChapterTime: Long = System.currentTimeMillis(),               // 最近一次阅读书籍的时间(打开正文的时间)\n        override var wordCount: String? = null,\n       var canUpdate: Boolean = true,              // 刷新书架时更新书籍信息\n       var order: Int = 0,                         // 手动排序\n       var originOrder: Int = 0,                   //书源排序\n        var useReplaceRule: Boolean = true,         // 正文使用净化替换规则\n        var variable: String? = null,                // 自定义书籍变量信息(用于书源规则检索书籍信息)\n        var readConfig: ReadConfig? = null\n    ) : BaseBook {\n\n    fun isLocalBook(): Boolean {\n        return origin == BookType.local\n    }\n\n    fun isLocalTxt(): Boolean {\n        return isLocalBook() && originName.endsWith(\".txt\", true)\n    }\n\n    fun isLocalEpub(): Boolean {\n        return isLocalBook() && originName.endsWith(\".epub\", true)\n    }\n\n    fun isEpub(): Boolean {\n        return originName.endsWith(\".epub\", true)\n    }\n\n    fun isCbz(): Boolean {\n        return originName.endsWith(\".cbz\", true)\n    }\n\n    fun isUmd(): Boolean {\n        return originName.endsWith(\".umd\", true)\n    }\n\n    fun isOnLineTxt(): Boolean {\n        return !isLocalBook() && type == 0\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (other is Book) {\n            return other.bookUrl == bookUrl\n        }\n        return false\n    }\n\n    override fun hashCode(): Int {\n        return bookUrl.hashCode()\n    }\n\n    @delegate:Transient\n    override val variableMap: HashMap<String, String> by lazy {\n        GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: hashMapOf()\n    }\n\n    override fun putVariable(key: String, value: String?) {\n        if (value != null) {\n            variableMap[key] = value\n        } else {\n            variableMap.remove(key)\n        }\n        variable = GSON.toJson(variableMap)\n    }\n\n    override var infoHtml: String? = null\n\n    override var tocHtml: String? = null\n\n    fun getRealAuthor() = author.replace(AppPattern.authorRegex, \"\")\n\n    fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)\n\n    fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl\n\n    fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro\n\n    fun fileCharset(): Charset {\n        return charset(charset ?: \"UTF-8\")\n    }\n\n    private fun config(): ReadConfig {\n        if (readConfig == null) {\n            readConfig = ReadConfig()\n        }\n        return readConfig!!\n    }\n\n    fun setDelTag(tag: Long) {\n        config().delTag =\n            if ((config().delTag and tag) == tag) config().delTag and tag.inv() else config().delTag or tag\n    }\n\n    fun getDelTag(tag: Long): Boolean {\n        return config().delTag and tag == tag\n    }\n\n    fun getFolderName(): String {\n        //防止书名过长,只取9位\n        var folderName = name.replace(AppPattern.fileNameRegex, \"\")\n        folderName = folderName.substring(0, min(9, folderName.length))\n        return folderName + MD5Utils.md5Encode16(bookUrl)\n    }\n\n    @Transient\n    private var rootDir: String = \"\"\n\n    fun setRootDir(root: String) {\n        if (root.isNotEmpty() && !root.endsWith(File.separator)) {\n            rootDir = root + File.separator\n        } else {\n            rootDir = root\n        }\n    }\n\n    fun getLocalFile(): File {\n        if (isEpub() && originName.indexOf(\"localStore\") < 0 && originName.indexOf(\"webdav\") < 0) {\n            // 非本地/webdav书仓的 epub文件\n            return FileUtils.getFile(File(rootDir + originName), \"index.epub\")\n        }\n        if (isCbz() && originName.indexOf(\"localStore\") < 0 && originName.indexOf(\"webdav\") < 0) {\n            // 非本地/webdav书仓的 cbz文件\n            return FileUtils.getFile(File(rootDir + originName), \"index.cbz\")\n        }\n        return File(rootDir + originName)\n    }\n\n    @Transient\n    private var _userNameSpace: String = \"\"\n\n    fun setUserNameSpace(nameSpace: String) {\n        _userNameSpace = nameSpace\n    }\n\n    fun getUserNameSpace(): String {\n        return _userNameSpace\n    }\n\n    fun getBookDir(): String {\n        return FileUtils.getPath(File(rootDir), \"storage\", \"data\", _userNameSpace, name + \"_\" + author)\n    }\n\n    fun getSplitLongChapter(): Boolean {\n        return false\n    }\n\n    fun toSearchBook(): SearchBook {\n        return SearchBook(\n                name = name,\n                author = author,\n                kind = kind,\n                bookUrl = bookUrl,\n                origin = origin,\n                originName = originName,\n                type = type,\n                wordCount = wordCount,\n                latestChapterTitle = latestChapterTitle,\n                coverUrl = coverUrl,\n                intro = intro,\n                tocUrl = tocUrl,\n//                originOrder = originOrder,\n                variable = variable\n        ).apply {\n            this.infoHtml = this@Book.infoHtml\n            this.tocHtml = this@Book.tocHtml\n        }\n    }\n\n    fun getEpubRootDir(): String {\n        // 根据 content.opf 位置来确认root目录\n        // var contentOPF = \"OEBPS/content.opf\"\n\n        val defaultPath = \"OEBPS\"\n\n        // 根据 META-INF/container.xml 来获取 contentOPF 位置\n        val containerRes = File(bookUrl + File.separator + \"index\" + File.separator + \"META-INF\" + File.separator + \"container.xml\")\n        if (containerRes.exists()) {\n            try {\n                val document = Jsoup.parse(containerRes.readText())\n                val rootFileElement = document\n                        .getElementsByTag(\"rootfiles\").get(0)\n                        .getElementsByTag(\"rootfile\").get(0);\n                val result = rootFileElement.attr(\"full-path\");\n                System.out.println(\"result: \" + result)\n                if (result != null && result.isNotEmpty()) {\n                    return File(result).parentFile?.let{\n                        it.toString()\n                    } ?: \"\"\n                }\n            } catch (e: Exception) {\n                e.printStackTrace();\n                // Log.e(TAG, e.getMessage(), e);\n            }\n        }\n\n        // 返回默认位置\n        return defaultPath\n    }\n\n    fun updateFromLocal(onlyCover: Boolean = false) {\n        try {\n            if (isEpub()) {\n                EpubFile.upBookInfo(this, onlyCover)\n            } else if (isUmd()) {\n                UmdFile.upBookInfo(this, onlyCover)\n            } else if (isCbz()) {\n                CbzFile.upBookInfo(this, onlyCover)\n            }\n        } catch(e: Exception) {\n            e.printStackTrace()\n        }\n    }\n\n    fun workRoot(): String {\n        return rootDir\n    }\n\n    companion object {\n        const val hTag = 2L\n        const val rubyTag = 4L\n        const val imgTag = 8L\n        const val imgStyleDefault = \"DEFAULT\"\n        const val imgStyleFull = \"FULL\"\n        const val imgStyleText = \"TEXT\"\n\n        fun initLocalBook(bookUrl: String, localPath: String, rootDir: String = \"\"): Book {\n            val fileName = File(localPath).name\n            val nameAuthor = LocalBook.analyzeNameAuthor(fileName)\n            val book = Book(bookUrl, \"\", BookType.local, localPath, nameAuthor.first, nameAuthor.second).also {\n                it.canUpdate = false\n            }\n            book.setRootDir(rootDir)\n            book.updateFromLocal()\n            return book\n        }\n    }\n\n    data class ReadConfig(\n        var reverseToc: Boolean = false,\n        var pageAnim: Int = -1,\n        var reSegment: Boolean = false,\n        var imageStyle: String? = null,\n        var useReplaceRule: Boolean = false,   // 正文使用净化替换规则\n        var delTag: Long = 0L   //去除标签\n    )\n\n    class Converters {\n        fun readConfigToString(config: ReadConfig?): String = GSON.toJson(config)\n\n        fun stringToReadConfig(json: String?) = GSON.fromJsonObject<ReadConfig>(json)\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/BookChapter.kt",
    "content": "package io.legado.app.data.entities\n\n\nimport io.legado.app.utils.GSON\nimport io.legado.app.utils.fromJsonObject\nimport io.legado.app.utils.MD5Utils\nimport io.legado.app.model.analyzeRule.AnalyzeUrl\nimport io.legado.app.model.analyzeRule.RuleDataInterface\nimport io.legado.app.utils.NetworkUtils\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties\n\n@JsonIgnoreProperties(\"variableMap\")\ndata class BookChapter(\n        var url: String = \"\",               // 章节地址\n        var title: String = \"\",              // 章节标题\n        var isVolume: Boolean = false,      // 是否是卷名\n        var baseUrl: String = \"\",           //用来拼接相对url\n        var bookUrl: String = \"\",           // 书籍地址\n        var index: Int = 0,                 // 章节序号\n        var resourceUrl: String? = null,    // 音频真实URL\n        var tag: String? = null,            //\n        var start: Long? = null,            // 章节起始位置\n        var end: Long? = null,               // 章节终止位置\n        var startFragmentId: String? = null,  //EPUB书籍当前章节的fragmentId\n        var endFragmentId: String? = null,    //EPUB书籍下一章节的fragmentId\n        var variable: String? = null        //变量\n): RuleDataInterface {\n\n    @delegate:Transient\n    override val variableMap: HashMap<String, String> by lazy {\n        GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: hashMapOf()\n    }\n\n    override fun putVariable(key: String, value: String?) {\n        if (value != null) {\n            variableMap[key] = value\n        } else {\n            variableMap.remove(key)\n        }\n        variable = GSON.toJson(variableMap)\n    }\n\n    override fun hashCode() = url.hashCode()\n\n    override fun equals(other: Any?): Boolean {\n        if (other is BookChapter) {\n            return other.url == url\n        }\n        return false\n    }\n\n    fun getAbsoluteURL():String{\n        val urlMatcher = AnalyzeUrl.paramPattern.matcher(url)\n        val urlBefore = if(urlMatcher.find())url.substring(0,urlMatcher.start()) else url\n        val urlAbsoluteBefore = NetworkUtils.getAbsoluteURL(baseUrl,urlBefore)\n        return if(urlBefore.length == url.length) urlAbsoluteBefore else urlAbsoluteBefore + ',' + url.substring(urlMatcher.end())\n    }\n\n\n    fun getFileName(): String = String.format(\"%05d-%s.nb\", index, MD5Utils.md5Encode16(title))\n}\n\n"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/BookGroup.kt",
    "content": "package io.legado.app.data.entities\n\n\n\n\n//@Parcelize\n//@Entity(tableName = \"book_groups\")\ndata class BookGroup(\n//        @PrimaryKey\n        var groupId: Int = 0,\n        var groupName: String = \"\",\n        var order: Int = 0,\n        var show: Boolean = true\n)"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/BookSource.kt",
    "content": "package io.legado.app.data.entities\n\n\n//import io.legado.app.App\nimport io.legado.app.constant.AppConst\nimport io.legado.app.constant.AppConst.userAgent\nimport io.legado.app.data.entities.rule.*\nimport io.legado.app.help.JsExtensions\nimport io.legado.app.help.http.CookieStore\nimport io.legado.app.help.CacheManager\nimport io.legado.app.utils.GSON\nimport io.legado.app.utils.fromJsonObject\n//import io.legado.app.utils.getPrefString\nimport io.legado.app.help.SourceAnalyzer\nimport java.io.InputStream\n\n\nimport java.util.*\nimport javax.script.SimpleBindings\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties\n\n//@Parcelize\n//@Entity(\n//    tableName = \"book_sources\",\n//    indices = [(Index(value = [\"bookSourceUrl\"], unique = false))]\n//)\n@JsonIgnoreProperties(\"headerMap\", \"source\")\ndata class BookSource(\n    var bookSourceName: String = \"\",           // 名称\n    var bookSourceGroup: String? = null,       // 分组\n//    @PrimaryKey\n    var bookSourceUrl: String = \"\",           // 地址，包括 http/https\n    var bookSourceType: Int = 0,               // 类型，0 文本，1 音频\n    var bookUrlPattern: String? = null,       //详情页url正则\n    var customOrder: Int = 0,                 // 手动排序编号\n    var enabled: Boolean = true,            // 是否启用\n    var enabledExplore: Boolean = true,     //启用发现\n    override var concurrentRate: String? = null,    //并发率\n    override var header: String? = null,\n    override var loginUrl: String? = null,             // 登录地址\n    var loginCheckJs: String? = null,           // 登录检测js\n    var lastUpdateTime: Long = 0,             // 最后更新时间，用于排序\n    var weight: Int = 0,                      // 智能排序的权重\n    var exploreUrl: String? = null,                 // 发现url\n    var ruleExplore: ExploreRule? = null,           // 发现规则\n    var searchUrl: String? = null,                  // 搜索url\n    var ruleSearch: SearchRule? = null,             // 搜索规则\n    var ruleBookInfo: BookInfoRule? = null,         // 书籍信息页规则\n    var ruleToc: TocRule? = null,                   // 目录页规则\n    var ruleContent: ContentRule? = null,            // 正文页规则\n    var bookSourceComment: String? = null,           // 注释\n    var respondTime: Long = 180000L,               // 响应时间，用于排序\n) : BaseSource {\n//    @Ignore\n//    @IgnoredOnParcel\n    private var searchRuleV: SearchRule? = null\n\n//    @Ignore\n//    @IgnoredOnParcel\n    private var exploreRuleV: ExploreRule? = null\n\n//    @Ignore\n//    @IgnoredOnParcel\n    private var bookInfoRuleV: BookInfoRule? = null\n\n//    @Ignore\n//    @IgnoredOnParcel\n    private var tocRuleV: TocRule? = null\n\n//    @Ignore\n//    @IgnoredOnParcel\n    private var contentRuleV: ContentRule? = null\n\n    override fun getTag(): String {\n        return bookSourceName\n    }\n\n    override fun getKey(): String {\n        return bookSourceUrl\n    }\n\n    override fun hashCode(): Int {\n        return bookSourceUrl.hashCode()\n    }\n\n    override fun equals(other: Any?) =\n        if (other is BookSource) other.bookSourceUrl == bookSourceUrl else false\n\n\n    fun getSearchRule(): SearchRule {\n        return ruleSearch ?: SearchRule()\n    }\n\n    fun getExploreRule(): ExploreRule {\n        return ruleExplore ?: ExploreRule()\n    }\n\n    fun getBookInfoRule(): BookInfoRule {\n        return ruleBookInfo ?: BookInfoRule()\n    }\n\n    fun getTocRule(): TocRule {\n        return ruleToc ?: TocRule()\n    }\n\n    fun getContentRule(): ContentRule {\n        return ruleContent ?: ContentRule()\n    }\n\n//    fun getExploreKinds(): ArrayList<ExploreKind>? {\n//        val exploreKinds = arrayListOf<ExploreKind>()\n//        exploreUrl?.let {\n//            var a = it\n//            if (a.isNotBlank()) {\n//                try {\n//                    if (it.startsWith(\"<js>\", false)) {\n//                        val aCache = ACache.get(App.INSTANCE, \"explore\")\n//                        a = aCache.getAsString(bookSourceUrl) ?: \"\"\n//                        if (a.isBlank()) {\n//                            val bindings = SimpleBindings()\n//                            bindings[\"baseUrl\"] = bookSourceUrl\n//                            bindings[\"java\"] = JsExtensions\n//                            a = AppConst.SCRIPT_ENGINE.eval(\n//                                it.substring(4, it.lastIndexOf(\"<\")),\n//                                bindings\n//                            ).toString()\n//                            aCache.put(bookSourceUrl, a)\n//                        }\n//                    }\n//                    val b = a.split(\"(&&|\\n)+\".toRegex())\n//                    b.map { c ->\n//                        val d = c.split(\"::\")\n//                        if (d.size > 1)\n//                            exploreKinds.add(ExploreKind(d[0], d[1]))\n//                    }\n//                } catch (e: Exception) {\n//                    exploreKinds.add(ExploreKind(e.localizedMessage))\n//                }\n//            }\n//        }\n//        return exploreKinds\n//    }\n\n    fun equal(source: BookSource): Boolean {\n        return equal(bookSourceName, source.bookSourceName)\n                && equal(bookSourceUrl, source.bookSourceUrl)\n                && equal(bookSourceGroup, source.bookSourceGroup)\n                && bookSourceType == source.bookSourceType\n                && equal(bookUrlPattern, source.bookUrlPattern)\n                && enabled == source.enabled\n                && enabledExplore == source.enabledExplore\n                && equal(header, source.header)\n                && equal(loginUrl, source.loginUrl)\n                && equal(exploreUrl, source.exploreUrl)\n                && equal(searchUrl, source.searchUrl)\n                && getSearchRule() == source.getSearchRule()\n                && getExploreRule() == source.getExploreRule()\n                && getBookInfoRule() == source.getBookInfoRule()\n                && getTocRule() == source.getTocRule()\n                && getContentRule() == source.getContentRule()\n    }\n\n    private fun equal(a: String?, b: String?): Boolean {\n        return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())\n    }\n\n    data class ExploreKind(\n        var title: String,\n        var url: String? = null\n    )\n\n    companion object {\n\n        fun fromJson(json: String): Result<BookSource> {\n            return SourceAnalyzer.jsonToBookSource(json)\n        }\n\n        fun fromJsonArray(json: String): Result<MutableList<BookSource>> {\n            return SourceAnalyzer.jsonToBookSources(json)\n        }\n\n        fun fromJsonArray(inputStream: InputStream): Result<MutableList<BookSource>> {\n            return SourceAnalyzer.jsonToBookSources(inputStream)\n        }\n    }\n\n    class Converters {\n\n        fun exploreRuleToString(exploreRule: ExploreRule?): String =\n            GSON.toJson(exploreRule)\n\n        fun stringToExploreRule(json: String?) =\n            GSON.fromJsonObject<ExploreRule>(json).getOrNull()\n\n        fun searchRuleToString(searchRule: SearchRule?): String =\n            GSON.toJson(searchRule)\n\n        fun stringToSearchRule(json: String?) =\n            GSON.fromJsonObject<SearchRule>(json).getOrNull()\n\n        fun bookInfoRuleToString(bookInfoRule: BookInfoRule?): String =\n            GSON.toJson(bookInfoRule)\n\n        fun stringToBookInfoRule(json: String?) =\n            GSON.fromJsonObject<BookInfoRule>(json).getOrNull()\n\n        fun tocRuleToString(tocRule: TocRule?): String =\n            GSON.toJson(tocRule)\n\n        fun stringToTocRule(json: String?) =\n            GSON.fromJsonObject<TocRule>(json).getOrNull()\n\n        fun contentRuleToString(contentRule: ContentRule?): String =\n            GSON.toJson(contentRule)\n\n        fun stringToContentRule(json: String?) =\n            GSON.fromJsonObject<ContentRule>(json).getOrNull()\n\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/Bookmark.kt",
    "content": "package io.legado.app.data.entities\n\n\n\n\n//@Parcelize\n//@Entity(tableName = \"bookmarks\", indices = [(Index(value = [\"bookUrl\"], unique = true))])\ndata class Bookmark(\n//    @PrimaryKey\n    val time: Long = System.currentTimeMillis(),\n    val bookName: String = \"\",\n    val bookAuthor: String = \"\",\n    var chapterIndex: Int = 0,\n    var chapterPos: Int = 0,\n    var chapterName: String = \"\",\n    var bookText: String = \"\",\n    var content: String = \"\"\n\n)"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/Cache.kt",
    "content": "package io.legado.app.data.entities\n\n// @Entity(tableName = \"caches\", indices = [(Index(value = [\"key\"], unique = true))])\ndata class Cache(\n    // @PrimaryKey\n    val key: String = \"\",\n    var value: String? = null,\n    var deadline: Long = 0L\n)"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/Cookie.kt",
    "content": "package io.legado.app.data.entities\n\n// @Entity(tableName = \"cookies\", indices = [(Index(value = [\"url\"], unique = true))])\ndata class Cookie(\n    // @PrimaryKey\n    var url: String = \"\",\n    var cookie: String = \"\"\n)"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/ReplaceRule.kt",
    "content": "package io.legado.app.data.entities\n\nimport com.fasterxml.jackson.annotation.JsonProperty;\n\n\n\n//@Parcelize\n//@Entity(\n//    tableName = \"replace_rules\",\n//    indices = [(Index(value = [\"id\"]))]\n//)\ndata class ReplaceRule(\n//    @PrimaryKey(autoGenerate = true)\n    var id: Long = System.currentTimeMillis(),\n    var name: String = \"\",\n    var group: String? = null,\n    var pattern: String = \"\",\n    var replacement: String = \"\",\n    var scope: String? = null,\n    @get:JsonProperty(\"isEnabled\") var isEnabled: Boolean = true,\n    @get:JsonProperty(\"isRegex\") var isRegex: Boolean = false,\n//    @ColumnInfo(name = \"sortOrder\")\n    var order: Int = 0\n)"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/RssArticle.kt",
    "content": "package io.legado.app.data.entities\n\nimport io.legado.app.utils.GSON\nimport io.legado.app.utils.fromJsonObject\nimport io.legado.app.model.analyzeRule.RuleDataInterface\n\ndata class RssArticle(\n    var origin: String = \"\",\n    var sort: String = \"\",\n    var title: String = \"\",\n    var order: Long = 0,\n    var link: String = \"\",\n    var pubDate: String? = null,\n    var description: String? = null,\n    var content: String? = null,\n    var image: String? = null,\n    var read: Boolean = false,\n    var variable: String? = null\n): RuleDataInterface {\n\n    override fun hashCode() = link.hashCode()\n\n    override fun equals(other: Any?): Boolean {\n        other ?: return false\n        return if (other is RssArticle) origin == other.origin && link == other.link else false\n    }\n\n    @delegate:Transient\n    override val variableMap: HashMap<String, String> by lazy {\n        GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: hashMapOf()\n    }\n\n    override fun putVariable(key: String, value: String?) {\n        if (value != null) {\n            variableMap[key] = value\n        } else {\n            variableMap.remove(key)\n        }\n        variable = GSON.toJson(variableMap)\n    }\n\n    // fun toStar() = RssStar(\n    //     origin = origin,\n    //     sort = sort,\n    //     title = title,\n    //     starTime = System.currentTimeMillis(),\n    //     link = link,\n    //     pubDate = pubDate,\n    //     description = description,\n    //     content = content,\n    //     image = image\n    // )\n}"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/RssSource.kt",
    "content": "package io.legado.app.data.entities\n\nimport com.jayway.jsonpath.DocumentContext\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties\nimport io.legado.app.help.CacheManager\nimport io.legado.app.help.JsExtensions\nimport io.legado.app.help.http.CookieStore\nimport io.legado.app.constant.AppConst\nimport javax.script.SimpleBindings\nimport io.legado.app.utils.*\n\n@JsonIgnoreProperties(\"headerMap\", \"source\")\ndata class RssSource(\n    var sourceUrl: String = \"\",\n    var sourceName: String = \"\",\n    var sourceIcon: String = \"\",\n    var sourceGroup: String? = null,\n    var sourceComment: String? = null,\n    var enabled: Boolean = true,\n    override var concurrentRate: String? = null,    //并发率\n    override  var header: String? = null,            // 请求头\n    override var loginUrl: String? = null,          // 登录地址\n    // var loginUi: List<RowUi>? = null,               //登录UI\n    var loginCheckJs: String? = null,               //登录检测js\n    var sortUrl: String? = null,\n    var singleUrl: Boolean = false,\n    //列表规则\n    var articleStyle: Int = 0,                      //列表样式,0,1,2\n    var ruleArticles: String? = null,\n    var ruleNextPage: String? = null,\n    var ruleTitle: String? = null,\n    var rulePubDate: String? = null,\n    //webView规则\n    var ruleDescription: String? = null,\n    var ruleImage: String? = null,\n    var ruleLink: String? = null,\n    var ruleContent: String? = null,\n    var style: String? = null,\n    var enableJs: Boolean = true,\n    var loadWithBaseUrl: Boolean = true,\n    var customOrder: Int = 0\n): BaseSource {\n\n    override fun getTag(): String {\n        return sourceName\n    }\n\n    override fun getKey(): String {\n        return sourceUrl\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (other is RssSource) {\n            return other.sourceUrl == sourceUrl\n        }\n        return false\n    }\n\n    override fun hashCode() = sourceUrl.hashCode()\n\n    fun equal(source: RssSource): Boolean {\n        return equal(sourceUrl, source.sourceUrl)\n                && equal(sourceIcon, source.sourceIcon)\n                && enabled == source.enabled\n                && equal(sourceGroup, source.sourceGroup)\n                && equal(ruleArticles, source.ruleArticles)\n                && equal(ruleNextPage, source.ruleNextPage)\n                && equal(ruleTitle, source.ruleTitle)\n                && equal(rulePubDate, source.rulePubDate)\n                && equal(ruleDescription, source.ruleDescription)\n                && equal(ruleLink, source.ruleLink)\n                && equal(ruleContent, source.ruleContent)\n                && enableJs == source.enableJs\n                && loadWithBaseUrl == source.loadWithBaseUrl\n    }\n\n    private fun equal(a: String?, b: String?): Boolean {\n        return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())\n    }\n\n    fun sortUrls(): List<Pair<String, String>> = arrayListOf<Pair<String, String>>().apply {\n        kotlin.runCatching {\n            var a = sortUrl\n            if (sortUrl?.startsWith(\"<js>\", false) == true\n                || sortUrl?.startsWith(\"@js:\", false) == true\n            ) {\n                val jsStr = if (sortUrl!!.startsWith(\"@\")) {\n                    sortUrl!!.substring(4)\n                } else {\n                    sortUrl!!.substring(4, sortUrl!!.lastIndexOf(\"<\"))\n                }\n                a = evalJS(jsStr).toString()\n            }\n            a?.split(\"(&&|\\n)+\".toRegex())?.forEach { c ->\n                val d = c.split(\"::\")\n                if (d.size > 1)\n                    add(Pair(d[0], d[1]))\n            }\n            if (isEmpty()) {\n                add(Pair(\"\", sourceUrl))\n            }\n        }\n    }\n\n    @Suppress(\"MemberVisibilityCanBePrivate\")\n    companion object {\n\n        fun fromJsonDoc(doc: DocumentContext): Result<RssSource> {\n            return kotlin.runCatching {\n                // val loginUi = doc.read<Any>(\"$.loginUi\")\n                RssSource(\n                    sourceUrl = doc.readString(\"$.sourceUrl\")!!,\n                    sourceName = doc.readString(\"$.sourceName\")!!,\n                    sourceIcon = doc.readString(\"$.sourceIcon\") ?: \"\",\n                    sourceGroup = doc.readString(\"$.sourceGroup\"),\n                    sourceComment = doc.readString(\"$.sourceComment\"),\n                    enabled = doc.readBool(\"$.enabled\") ?: true,\n                    concurrentRate = doc.readString(\"$.concurrentRate\"),\n                    header = doc.readString(\"$.header\"),\n                    loginUrl = doc.readString(\"$.loginUrl\"),\n                    // loginUi = if (loginUi is List<*>) GSON.toJson(loginUi) else loginUi?.toString(),\n                    loginCheckJs = doc.readString(\"$.loginCheckJs\"),\n                    sortUrl = doc.readString(\"$.sortUrl\"),\n                    singleUrl = doc.readBool(\"$.singleUrl\") ?: false,\n                    articleStyle = doc.readInt(\"$.articleStyle\") ?: 0,\n                    ruleArticles = doc.readString(\"$.ruleArticles\"),\n                    ruleNextPage = doc.readString(\"$.ruleNextPage\"),\n                    ruleTitle = doc.readString(\"$.ruleTitle\"),\n                    rulePubDate = doc.readString(\"$.rulePubDate\"),\n                    ruleDescription = doc.readString(\"$.ruleDescription\"),\n                    ruleImage = doc.readString(\"$.ruleImage\"),\n                    ruleLink = doc.readString(\"$.ruleLink\"),\n                    ruleContent = doc.readString(\"$.ruleContent\"),\n                    style = doc.readString(\"$.style\"),\n                    enableJs = doc.readBool(\"$.enableJs\") ?: true,\n                    loadWithBaseUrl = doc.readBool(\"$.loadWithBaseUrl\") ?: true,\n                    customOrder = doc.readInt(\"$.customOrder\") ?: 0\n                )\n            }\n        }\n\n        fun fromJson(json: String): Result<RssSource> {\n            return fromJsonDoc(jsonPath.parse(json))\n        }\n\n        fun fromJsonArray(jsonArray: String): Result<ArrayList<RssSource>> {\n            return kotlin.runCatching {\n                val sources = arrayListOf<RssSource>()\n                val doc = jsonPath.parse(jsonArray).read<List<*>>(\"$\")\n                doc.forEach {\n                    val jsonItem = jsonPath.parse(it)\n                    fromJsonDoc(jsonItem).getOrThrow().let { source ->\n                        sources.add(source)\n                    }\n                }\n                sources\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/SearchBook.kt",
    "content": "package io.legado.app.data.entities\n\n//import android.os.Parcelable\n//import androidx.room.*\nimport io.legado.app.utils.GSON\nimport io.legado.app.utils.fromJsonObject\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\n\n//@Parcelize\n//@Entity(\n//    tableName = \"searchBooks\",\n//    indices = [(Index(value = [\"bookUrl\"], unique = true))],\n//    foreignKeys = [(ForeignKey(\n//        entity = BookSource::class,\n//        parentColumns = [\"bookSourceUrl\"],\n//        childColumns = [\"origin\"],\n//        onDelete = ForeignKey.CASCADE\n//    ))]\n//)\n@JsonIgnoreProperties(\"variableMap\", \"infoHtml\", \"tocHtml\", \"origins\", \"kindList\")\ndata class SearchBook(\n//    @PrimaryKey\n    override var bookUrl: String = \"\",\n    var origin: String = \"\",                     // 书源规则\n    var originName: String = \"\",\n    var type: Int = 0,                          // @BookType\n    override var name: String = \"\",\n    override var author: String = \"\",\n    override var kind: String? = null,\n    var coverUrl: String? = null,\n    var intro: String? = null,\n    override var wordCount: String? = null,\n    var latestChapterTitle: String? = null,\n    var tocUrl: String = \"\",                    // 目录页Url (toc=table of Contents)\n    var time: Long = 0,\n    var variable: String? = null,\n    var originOrder: Int = 0\n) :  BaseBook, Comparable<SearchBook> {\n\n//    @Ignore\n//    @IgnoredOnParcel\n    override var infoHtml: String? = null\n\n//    @Ignore\n//    @IgnoredOnParcel\n    override var tocHtml: String? = null\n\n    override fun equals(other: Any?): Boolean {\n        if (other is SearchBook) {\n            if (other.bookUrl == bookUrl) {\n                return true\n            }\n        }\n        return false\n    }\n\n    override fun hashCode(): Int {\n        return bookUrl.hashCode()\n    }\n\n    override fun compareTo(other: SearchBook): Int {\n        return other.originOrder - this.originOrder\n    }\n\n    @delegate:Transient\n    override val variableMap: HashMap<String, String> by lazy {\n        GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: hashMapOf()\n    }\n\n    override fun putVariable(key: String, value: String?) {\n        if (value != null) {\n            variableMap[key] = value\n        } else {\n            variableMap.remove(key)\n        }\n        variable = GSON.toJson(variableMap)\n    }\n\n//    @Ignore\n//    @IgnoredOnParcel\n    var origins: LinkedHashSet<String>? = null\n        private set\n\n    fun addOrigin(origin: String) {\n        if (origins == null) {\n            origins = linkedSetOf(this.origin)\n        }\n        origins?.add(origin)\n    }\n\n    fun toBook(): Book {\n        return Book(\n            name = name,\n            author = author,\n            kind = kind,\n            bookUrl = bookUrl,\n            origin = origin,\n            originName = originName,\n            type = type,\n            wordCount = wordCount,\n            latestChapterTitle = latestChapterTitle,\n            coverUrl = coverUrl,\n            intro = intro,\n            tocUrl = tocUrl,\n//            originOrder = originOrder,\n            variable = variable\n        ).apply {\n            this.infoHtml = this@SearchBook.infoHtml\n            this.tocUrl = this@SearchBook.tocUrl\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/SearchKeyword.kt",
    "content": "package io.legado.app.data.entities\n\n\n\n\n\n//@Parcelize\n//@Entity(tableName = \"search_keywords\", indices = [(Index(value = [\"word\"], unique = true))])\ndata class SearchKeyword(\n//    @PrimaryKey\n    var word: String = \"\",                      // 搜索关键词\n    var usage: Int = 1,                         // 使用次数\n    var lastUseTime: Long = System.currentTimeMillis()      // 最后一次使用时间\n)\n"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/SearchResult.kt",
    "content": "package io.legado.app.data.entities\n\ndata class SearchResult(\n    val resultCount: Int = 0,\n    val resultCountWithinChapter: Int = 0,\n    val resultText: String = \"\",\n    val chapterTitle: String = \"\",\n    val query: String = \"\",\n    val pageSize: Int = 0,\n    val chapterIndex: Int = 0,\n    val pageIndex: Int = 0,\n    val queryIndexInResult: Int = 0,\n    val queryIndexInChapter: Int = 0\n) {\n}"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/TxtTocRule.kt",
    "content": "package io.legado.app.data.entities\n\n// import androidx.room.Entity\n// import androidx.room.PrimaryKey\n\n\n// @Entity(tableName = \"txtTocRules\")\ndata class TxtTocRule(\n    // @PrimaryKey\n    var id: Long = System.currentTimeMillis(),\n    var name: String = \"\",\n    var rule: String = \"\",\n    var serialNumber: Int = -1,\n    var enable: Boolean = true\n)"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt",
    "content": "package io.legado.app.data.entities.rule\n\ndata class BookInfoRule(\n    var init: String? = null,\n    var name: String? = null,\n    var author: String? = null,\n    var intro: String? = null,\n    var kind: String? = null,\n    var lastChapter: String? = null,\n    var updateTime: String? = null,\n    var coverUrl: String? = null,\n    var tocUrl: String? = null,\n    var wordCount: String? = null,\n    var canReName: String? = null\n)"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/rule/BookListRule.kt",
    "content": "package io.legado.app.data.entities.rule\n\ninterface BookListRule {\n    var bookList: String?\n    var name: String?\n    var author: String?\n    var intro: String?\n    var kind: String?\n    var lastChapter: String?\n    var updateTime: String?\n    var bookUrl: String?\n    var coverUrl: String?\n    var wordCount: String?\n}"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/rule/ContentRule.kt",
    "content": "package io.legado.app.data.entities.rule\n\ndata class ContentRule(\n    var content: String? = null,\n    var nextContentUrl: String? = null,\n    var webJs: String? = null,\n    var sourceRegex: String? = null,\n    var replaceRegex: String? = null, //替换规则\n    var imageStyle: String? = null,  //默认大小居中,FULL最大宽度\n)"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt",
    "content": "package io.legado.app.data.entities.rule\n\ndata class ExploreRule(\n    override var bookList: String? = null,\n    override var name: String? = null,\n    override var author: String? = null,\n    override var intro: String? = null,\n    override var kind: String? = null,\n    override var lastChapter: String? = null,\n    override var updateTime: String? = null,\n    override var bookUrl: String? = null,\n    override var coverUrl: String? = null,\n    override var wordCount: String? = null\n) : BookListRule"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/rule/SearchRule.kt",
    "content": "package io.legado.app.data.entities.rule\n\ndata class SearchRule(\n        override var bookList: String? = null,\n        override var name: String? = null,\n        override var author: String? = null,\n        override var intro: String? = null,\n        override var kind: String? = null,\n        override var lastChapter: String? = null,\n        override var updateTime: String? = null,\n        override var bookUrl: String? = null,\n        override var coverUrl: String? = null,\n        override var wordCount: String? = null\n) : BookListRule"
  },
  {
    "path": "src/main/java/io/legado/app/data/entities/rule/TocRule.kt",
    "content": "package io.legado.app.data.entities.rule\n\ndata class TocRule(\n    var preUpdateJs: String? = null,\n    var chapterList: String? = null,\n    var chapterName: String? = null,\n    var chapterUrl: String? = null,\n    var isVolume: String? = null,\n    var isVip: String? = null,\n    var updateTime: String? = null,\n    var nextTocUrl: String? = null\n)"
  },
  {
    "path": "src/main/java/io/legado/app/exception/ConcurrentException.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage io.legado.app.exception\n\n/**\n * 并发限制\n */\nclass ConcurrentException(msg: String, val waitTime: Int) : NoStackTraceException(msg)"
  },
  {
    "path": "src/main/java/io/legado/app/exception/ContentEmptyException.kt",
    "content": "package io.legado.app.exception\n\n/**\n * 内容为空\n */\nclass ContentEmptyException(msg: String) : NoStackTraceException(msg)"
  },
  {
    "path": "src/main/java/io/legado/app/exception/NoStackTraceException.kt",
    "content": "package io.legado.app.exception\n\n/**\n * 不记录错误堆栈的报错\n */\nopen class NoStackTraceException(msg: String) : Exception(msg) {\n\n    override fun fillInStackTrace(): Throwable {\n        return this\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/exception/RegexTimeoutException.kt",
    "content": "package io.legado.app.exception\n\nclass RegexTimeoutException(msg: String) : NoStackTraceException(msg)"
  },
  {
    "path": "src/main/java/io/legado/app/exception/TocEmptyException.kt",
    "content": "package io.legado.app.exception\n\n/**\n * 目录为空\n */\nclass TocEmptyException(msg: String) : NoStackTraceException(msg)"
  },
  {
    "path": "src/main/java/io/legado/app/help/BookHelp.kt",
    "content": "package io.legado.app.help\n\nimport io.legado.app.constant.AppPattern\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.utils.FileUtils\nimport io.legado.app.utils.NetworkUtils\nimport io.legado.app.utils.MD5Utils\nimport io.legado.app.utils.getFile\nimport io.legado.app.model.analyzeRule.AnalyzeUrl\nimport io.legado.app.model.localBook.LocalBook\nimport java.io.File\nimport java.util.concurrent.CopyOnWriteArraySet\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.CoroutineScope\n\n//import org.apache.commons.text.similarity.JaccardSimilarity\n\nobject BookHelp {\n    private const val cacheImageFolderName = \"images\"\n    private val downloadImages = CopyOnWriteArraySet<String>()\n\n    private fun formatFolderName(folderName: String): String {\n        return folderName.replace(\"[\\\\\\\\/:*?\\\"<>|.]\".toRegex(), \"\")\n    }\n\n    fun formatAuthor(author: String?): String {\n        return author\n            ?.replace(\"作\\\\s*者[\\\\s:：]*\".toRegex(), \"\")\n            ?.replace(\"\\\\s+\".toRegex(), \" \")\n            ?.trim { it <= ' ' }\n            ?: \"\"\n    }\n\n    /**\n     * 格式化书名\n     */\n    fun formatBookName(name: String): String {\n        return name\n            .replace(AppPattern.nameRegex, \"\")\n            .trim { it <= ' ' }\n    }\n\n    /**\n     * 格式化作者\n     */\n    fun formatBookAuthor(author: String): String {\n        return author\n            .replace(AppPattern.authorRegex, \"\")\n            .trim { it <= ' ' }\n    }\n\n    fun getBookCacheDir(book: Book): File {\n        val md5Encode = MD5Utils.md5Encode(book.bookUrl).toString()\n        val bookDir = book.getBookDir()\n        if (bookDir.isEmpty()) {\n            throw Exception(\"bookDir不能为空\")\n        }\n        val localCacheDir = File(bookDir).getFile(md5Encode)\n        if (!localCacheDir.exists()) {\n            localCacheDir.mkdirs()\n        }\n        return localCacheDir\n    }\n\n    /**\n     * 读取章节内容\n     */\n    fun getContent(book: Book, bookChapter: BookChapter): String? {\n        val file = getBookCacheDir(book).getFile(\n            String.format(\"%d.txt\", bookChapter.index)\n        )\n        if (file.exists()) {\n            return file.readText()\n        }\n        if (book.isLocalBook()) {\n            val content = LocalBook.getContent(book, bookChapter)\n            if (content != null && book.isEpub()) {\n                saveText(book, bookChapter, content)\n            }\n            return content\n        }\n        return null\n    }\n\n    /**\n     * 删除章节内容\n     */\n    fun delContent(book: Book, bookChapter: BookChapter) {\n        FileUtils.createFileIfNotExist(\n            getBookCacheDir(book),\n            String.format(\"%d.txt\", bookChapter.index)\n        ).delete()\n    }\n\n    suspend fun saveContent(\n        scope: CoroutineScope,\n        bookSource: BookSource,\n        book: Book,\n        bookChapter: BookChapter,\n        content: String\n    ) {\n        saveText(book, bookChapter, content)\n        saveImages(scope, bookSource, book, bookChapter, content)\n    }\n\n    fun saveText(\n        book: Book,\n        bookChapter: BookChapter,\n        content: String\n    ) {\n        // if (content.isEmpty()) return\n        //保存文本\n        FileUtils.createFileIfNotExist(\n            getBookCacheDir(book),\n            String.format(\"%d.txt\", bookChapter.index)\n        ).writeText(content)\n    }\n\n    suspend fun saveImages(\n        scope: CoroutineScope,\n        bookSource: BookSource,\n        book: Book,\n        bookChapter: BookChapter,\n        content: String\n    ) {\n        val awaitList = arrayListOf<Deferred<Int>>()\n        content.split(\"\\n\").forEach {\n            val matcher = AppPattern.imgPattern.matcher(it)\n            if (matcher.find()) {\n                matcher.group(1)?.let { src ->\n                    val mSrc = NetworkUtils.getAbsoluteURL(bookChapter.url, src)\n                    val req: Deferred<Int> = scope.async {\n                        saveImage(bookSource, book, mSrc)\n                        return@async 1\n                    }\n                    awaitList.add(req)\n                }\n            }\n        }\n        awaitList.forEach {\n            it.await()\n        }\n    }\n\n    suspend fun saveImage(bookSource: BookSource?, book: Book, src: String) {\n        while (downloadImages.contains(src)) {\n            delay(100)\n        }\n        if (getImage(book, src).exists()) {\n            return\n        }\n        downloadImages.add(src)\n        val analyzeUrl = AnalyzeUrl(src, source = bookSource)\n        try {\n            analyzeUrl.getByteArrayAwait().let {\n                FileUtils.createFileIfNotExist(\n                    getBookCacheDir(book),\n                    cacheImageFolderName,\n                    \"${MD5Utils.md5Encode16(src)}.${getImageSuffix(src)}\"\n                ).writeBytes(it)\n            }\n        } catch (e: Exception) {\n            e.printStackTrace()\n        } finally {\n            downloadImages.remove(src)\n        }\n    }\n\n    fun getImage(book: Book, src: String): File {\n        return getBookCacheDir(book).getFile(\n            cacheImageFolderName,\n            \"${MD5Utils.md5Encode16(src)}.${getImageSuffix(src)}\"\n        )\n    }\n\n    fun getImageSuffix(src: String): String {\n        var suffix = src.substringAfterLast(\".\").substringBefore(\",\")\n        //检查截取的后缀字符是否合法 [a-zA-Z0-9]\n        val fileSuffixRegex = Regex(\"^[a-z0-9]+$\", RegexOption.IGNORE_CASE)\n        if (suffix.length > 5 || !suffix.matches(fileSuffixRegex)) {\n            suffix = \"jpg\"\n        }\n        return suffix\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/help/CacheManager.kt",
    "content": "package io.legado.app.help\n\nimport io.legado.app.data.entities.Cache\nimport io.legado.app.model.analyzeRule.QueryTTF\nimport io.legado.app.utils.ACache\n\n// TODO 处理缓存\n@Suppress(\"unused\")\nobject CacheManager {\n\n    private val queryTTFMap = hashMapOf<String, Pair<Long, QueryTTF>>()\n\n    /**\n     * saveTime 单位为秒\n     */\n    @JvmOverloads\n    fun put(key: String, value: Any, saveTime: Int = 0) {\n        val deadline =\n            if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000\n        when (value) {\n            is QueryTTF -> queryTTFMap[key] = Pair(deadline, value)\n            is ByteArray -> ACache.get().put(key, value, saveTime)\n            else -> {\n                val cache = Cache(key, value.toString(), deadline)\n                // appDb.cacheDao.insert(cache)\n            }\n        }\n    }\n\n    fun get(key: String): String? {\n        // return appDb.cacheDao.get(key, System.currentTimeMillis())\n        return null\n    }\n\n    fun getInt(key: String): Int? {\n        return get(key)?.toIntOrNull()\n    }\n\n    fun getLong(key: String): Long? {\n        return get(key)?.toLongOrNull()\n    }\n\n    fun getDouble(key: String): Double? {\n        return get(key)?.toDoubleOrNull()\n    }\n\n    fun getFloat(key: String): Float? {\n        return get(key)?.toFloatOrNull()\n    }\n\n    fun getByteArray(key: String): ByteArray? {\n        return ACache.get().getAsBinary(key)\n    }\n\n    fun getQueryTTF(key: String): QueryTTF? {\n        val cache = queryTTFMap[key] ?: return null\n        if (cache.first == 0L || cache.first > System.currentTimeMillis()) {\n            return cache.second\n        }\n        return null\n    }\n\n    fun putFile(key: String, value: String, saveTime: Int = 0) {\n        ACache.get().put(key, value, saveTime)\n    }\n\n    fun getFile(key: String): String? {\n        return ACache.get().getAsString(key)\n    }\n\n    fun delete(key: String) {\n        ACache.get().remove(key)\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/help/DefaultData.kt",
    "content": "package io.legado.app.help\n\n// import io.legado.app.data.entities.RssSource\nimport io.legado.app.data.entities.TxtTocRule\nimport io.legado.app.utils.GSON\nimport io.legado.app.utils.fromJsonArray\nimport java.io.File\n\nobject DefaultData {\n    const val txtTocRuleFileName = \"txtTocRule.json\"\n\n    val txtTocRules: List<TxtTocRule> by lazy {\n        val json = String(DefaultData::class.java.getResource(\"/defaultData/${txtTocRuleFileName}\").readBytes())\n        GSON.fromJsonArray<TxtTocRule>(json).getOrNull() ?: emptyList()\n    }\n\n    // val rssSources by lazy {\n    //     val json = String(\n    //         File(\"defaultData${File.separator}rssSources.json\")\n    //             .readBytes()\n    //     )\n    //     GSON.fromJsonArray<RssSource>(json)!!\n    // }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/help/EncodingDetectHelp.java",
    "content": "package io.legado.app.help;\n\n//import androidx.annotation.NonNull;\nimport org.jsoup.Jsoup;\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.select.Elements;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\n\nimport static io.legado.app.utils.TextUtils.isEmpty;\n\n/**\n * <Detect encoding .> Copyright (C) <2009> <Fluck,ACC http://androidos.cc/dev>\n * <p>\n * This program is free software: you can redistribute it and/or modify it under\n * the terms of the GNU General Public License as published by the Free Software\n * Foundation, either version 3 of the License, or (at your option) any later\n * version.\n * <p>\n * This program is distributed in the hope that it will be useful, but WITHOUT\n * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\n * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more\n * details.\n * <p>\n * EncodingDetect.java<br>\n * 自动获取文件的编码\n *\n * @author Billows.Van\n * @version 1.0\n * @since Create on 2010-01-27 11:19:00\n */\npublic class EncodingDetectHelp {\n\n    public static String getHtmlEncode( byte[] bytes) {\n        try {\n            Document doc = Jsoup.parse(new String(bytes, StandardCharsets.UTF_8));\n            Elements metaTags = doc.getElementsByTag(\"meta\");\n            String charsetStr;\n            for (Element metaTag : metaTags) {\n                charsetStr = metaTag.attr(\"charset\");\n                if (!isEmpty(charsetStr)) {\n                    return charsetStr;\n                }\n                String content = metaTag.attr(\"content\");\n                String http_equiv = metaTag.attr(\"http-equiv\");\n                if (http_equiv.toLowerCase().equals(\"content-type\")) {\n                    if (content.toLowerCase().contains(\"charset\")) {\n                        charsetStr = content.substring(content.toLowerCase().indexOf(\"charset\") + \"charset=\".length());\n                    } else {\n                        charsetStr = content.substring(content.toLowerCase().indexOf(\";\") + 1);\n                    }\n                    if (!isEmpty(charsetStr)) {\n                        return charsetStr;\n                    }\n                }\n            }\n        } catch (Exception ignored) {\n        }\n        return getJavaEncode(bytes);\n    }\n\n    public static String getJavaEncode(byte[] bytes) {\n        int len = bytes.length > 2000 ? 2000 : bytes.length;\n        byte[] cBytes = new byte[len];\n        System.arraycopy(bytes, 0, cBytes, 0, len);\n        BytesEncodingDetect bytesEncodingDetect = new BytesEncodingDetect();\n        String code = BytesEncodingDetect.javaname[bytesEncodingDetect.detectEncoding(cBytes)];\n        // UTF-16LE 特殊处理\n        if (\"Unicode\".equals(code)) {\n            if (cBytes[0] == -1) {\n                code = \"UTF-16LE\";\n            }\n        }\n        return code;\n    }\n\n    /**\n     * 得到文件的编码\n     */\n    public static String getJavaEncode( String filePath) {\n        BytesEncodingDetect s = new BytesEncodingDetect();\n        String fileCode = BytesEncodingDetect.javaname[s\n                .detectEncoding(new File(filePath))];\n\n        // UTF-16LE 特殊处理\n        if (\"Unicode\".equals(fileCode)) {\n            byte[] tempByte = BytesEncodingDetect.getFileBytes(new File(\n                    filePath));\n            if (tempByte[0] == -1) {\n                fileCode = \"UTF-16LE\";\n            }\n        }\n        return fileCode;\n    }\n\n    /**\n     * 得到文件的编码\n     */\n    public static String getJavaEncode( File file) {\n        BytesEncodingDetect s = new BytesEncodingDetect();\n        String fileCode = BytesEncodingDetect.javaname[s.detectEncoding(file)];\n        // UTF-16LE 特殊处理\n        if (\"Unicode\".equals(fileCode)) {\n            byte[] tempByte = BytesEncodingDetect.getFileBytes(file);\n            if (tempByte[0] == -1) {\n                fileCode = \"UTF-16LE\";\n            }\n        }\n        return fileCode;\n    }\n\n}\n\nclass BytesEncodingDetect extends Encoding {\n    // Frequency tables to hold the GB, Big5, and EUC-TW character\n    // frequencies\n    int GBFreq[][];\n\n    int GBKFreq[][];\n\n    int Big5Freq[][];\n\n    int Big5PFreq[][];\n\n    int EUC_TWFreq[][];\n\n    int KRFreq[][];\n\n    int JPFreq[][];\n\n    // int UnicodeFreq[94][128];\n    // public static String[] nicename;\n    // public static String[] codings;\n    public boolean debug;\n\n    public BytesEncodingDetect() {\n        super();\n        debug = false;\n        GBFreq = new int[94][94];\n        GBKFreq = new int[126][191];\n        Big5Freq = new int[94][158];\n        Big5PFreq = new int[126][191];\n        EUC_TWFreq = new int[94][94];\n        KRFreq = new int[94][94];\n        JPFreq = new int[94][94];\n        // Initialize the Frequency Table for GB, GBK, Big5, EUC-TW, KR, JP\n        initialize_frequencies();\n    }\n\n    /**\n     * Function : detectEncoding Aruguments: URL Returns : One of the encodings\n     * from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, or OTHER)\n     * Description: This function looks at the URL contents and assigns it a\n     * probability score for each encoding type. The encoding type with the\n     * highest probability is returned.\n     */\n    public int detectEncoding(URL testurl) {\n        byte[] rawtext = new byte[10000];\n        int bytesread = 0, byteoffset = 0;\n        int guess = OTHER;\n        InputStream chinesestream;\n        try {\n            chinesestream = testurl.openStream();\n            while ((bytesread = chinesestream.read(rawtext, byteoffset,\n                    rawtext.length - byteoffset)) > 0) {\n                byteoffset += bytesread;\n            }\n            ;\n            chinesestream.close();\n            guess = detectEncoding(rawtext);\n        } catch (Exception e) {\n            System.err.println(\"Error loading or using URL \" + e.toString());\n            guess = -1;\n        }\n        return guess;\n    }\n\n    /**\n     * Function : detectEncoding Aruguments: File Returns : One of the encodings\n     * from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, or OTHER)\n     * Description: This function looks at the file and assigns it a probability\n     * score for each encoding type. The encoding type with the highest\n     * probability is returned.\n     */\n    public int detectEncoding(File testfile) {\n        byte[] rawtext = getFileBytes(testfile);\n        return detectEncoding(rawtext);\n    }\n\n    public static byte[] getFileBytes(File testfile) {\n        FileInputStream chinesefile;\n        byte[] rawtext;\n        rawtext = new byte[2000];\n        try {\n            chinesefile = new FileInputStream(testfile);\n            chinesefile.read(rawtext);\n            chinesefile.close();\n        } catch (Exception e) {\n            System.err.println(\"Error: \" + e);\n        }\n        return rawtext;\n    }\n\n\n    /**\n     * Function : detectEncoding Aruguments: byte array Returns : One of the\n     * encodings from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII,\n     * or OTHER) Description: This function looks at the byte array and assigns\n     * it a probability score for each encoding type. The encoding type with the\n     * highest probability is returned.\n     */\n    public int detectEncoding(byte[] rawtext) {\n        int[] scores;\n        int index, maxscore = 0;\n        int encoding_guess = OTHER;\n        scores = new int[TOTALTYPES];\n        // Assign Scores\n        scores[GB2312] = gb2312_probability(rawtext);\n        scores[GBK] = gbk_probability(rawtext);\n        scores[GB18030] = gb18030_probability(rawtext);\n        scores[HZ] = hz_probability(rawtext);\n        scores[BIG5] = big5_probability(rawtext);\n        scores[CNS11643] = euc_tw_probability(rawtext);\n        scores[ISO2022CN] = iso_2022_cn_probability(rawtext);\n        scores[UTF8] = utf8_probability(rawtext);\n        scores[UNICODE] = utf16_probability(rawtext);\n        scores[EUC_KR] = euc_kr_probability(rawtext);\n        scores[CP949] = cp949_probability(rawtext);\n        scores[JOHAB] = 0;\n        scores[ISO2022KR] = iso_2022_kr_probability(rawtext);\n        scores[ASCII] = ascii_probability(rawtext);\n        scores[SJIS] = sjis_probability(rawtext);\n        scores[EUC_JP] = euc_jp_probability(rawtext);\n        scores[ISO2022JP] = iso_2022_jp_probability(rawtext);\n        scores[UNICODET] = 0;\n        scores[UNICODES] = 0;\n        scores[ISO2022CN_GB] = 0;\n        scores[ISO2022CN_CNS] = 0;\n        scores[OTHER] = 0;\n        // Tabulate Scores\n        for (index = 0; index < TOTALTYPES; index++) {\n            if (debug)\n                System.err.println(\"Encoding \" + nicename[index] + \" score \"\n                        + scores[index]);\n            if (scores[index] > maxscore) {\n                encoding_guess = index;\n                maxscore = scores[index];\n            }\n        }\n        // Return OTHER if nothing scored above 50\n        if (maxscore <= 50) {\n            encoding_guess = OTHER;\n        }\n        return encoding_guess;\n    }\n\n    /*\n     * Function: gb2312_probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses GB-2312\n     * encoding\n     */\n    int gb2312_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, gbchars = 1;\n        long gbfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7\n                        && (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE) {\n                    gbchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    column = rawtext[i + 1] + 256 - 0xA1;\n                    if (GBFreq[row][column] != 0) {\n                        gbfreq += GBFreq[row][column];\n                    } else if (15 <= row && row < 55) {\n                        // In GB high-freq character range\n                        gbfreq += 200;\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) gbchars / (float) dbchars);\n        freqval = 50 * ((float) gbfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    /*\n     * Function: gbk_probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses GBK\n     * encoding\n     */\n    int gbk_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, gbchars = 1;\n        long gbfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7\n                        && // Original GB range\n                        (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE) {\n                    gbchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    column = rawtext[i + 1] + 256 - 0xA1;\n                    // System.out.println(\"original row \" + row + \" column \" +\n                    // column);\n                    if (GBFreq[row][column] != 0) {\n                        gbfreq += GBFreq[row][column];\n                    } else if (15 <= row && row < 55) {\n                        gbfreq += 200;\n                    }\n                } else if ((byte) 0x81 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xFE && // Extended GB range\n                        (((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) || ((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E))) {\n                    gbchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0x81;\n                    if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) {\n                        column = rawtext[i + 1] - 0x40;\n                    } else {\n                        column = rawtext[i + 1] + 256 - 0x40;\n                    }\n                    // System.out.println(\"extended row \" + row + \" column \" +\n                    // column + \" rawtext[i] \" + rawtext[i]);\n                    if (GBKFreq[row][column] != 0) {\n                        gbfreq += GBKFreq[row][column];\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) gbchars / (float) dbchars);\n        freqval = 50 * ((float) gbfreq / (float) totalfreq);\n        // For regular GB files, this would give the same score, so I handicap\n        // it slightly\n        return (int) (rangeval + freqval) - 1;\n    }\n\n    /*\n     * Function: gb18030_probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses GBK\n     * encoding\n     */\n    int gb18030_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, gbchars = 1;\n        long gbfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7\n                        && // Original GB range\n                        i + 1 < rawtextlen && (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE) {\n                    gbchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    column = rawtext[i + 1] + 256 - 0xA1;\n                    // System.out.println(\"original row \" + row + \" column \" +\n                    // column);\n                    if (GBFreq[row][column] != 0) {\n                        gbfreq += GBFreq[row][column];\n                    } else if (15 <= row && row < 55) {\n                        gbfreq += 200;\n                    }\n                } else if ((byte) 0x81 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xFE\n                        && // Extended GB range\n                        i + 1 < rawtextlen\n                        && (((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) || ((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E))) {\n                    gbchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0x81;\n                    if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) {\n                        column = rawtext[i + 1] - 0x40;\n                    } else {\n                        column = rawtext[i + 1] + 256 - 0x40;\n                    }\n                    // System.out.println(\"extended row \" + row + \" column \" +\n                    // column + \" rawtext[i] \" + rawtext[i]);\n                    if (GBKFreq[row][column] != 0) {\n                        gbfreq += GBKFreq[row][column];\n                    }\n                } else if ((byte) 0x81 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xFE\n                        && // Extended GB range\n                        i + 3 < rawtextlen && (byte) 0x30 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0x39\n                        && (byte) 0x81 <= rawtext[i + 2]\n                        && rawtext[i + 2] <= (byte) 0xFE\n                        && (byte) 0x30 <= rawtext[i + 3]\n                        && rawtext[i + 3] <= (byte) 0x39) {\n                    gbchars++;\n                    /*\n                     * totalfreq += 500; row = rawtext[i] + 256 - 0x81; if (0x40\n                     * <= rawtext[i+1] && rawtext[i+1] <= 0x7E) { column =\n                     * rawtext[i+1] - 0x40; } else { column = rawtext[i+1] + 256\n                     * - 0x40; } //System.out.println(\"extended row \" + row + \"\n                     * column \" + column + \" rawtext[i] \" + rawtext[i]); if\n                     * (GBKFreq[row][column] != 0) { gbfreq +=\n                     * GBKFreq[row][column]; }\n                     */\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) gbchars / (float) dbchars);\n        freqval = 50 * ((float) gbfreq / (float) totalfreq);\n        // For regular GB files, this would give the same score, so I handicap\n        // it slightly\n        return (int) (rangeval + freqval) - 1;\n    }\n\n    /*\n     * Function: hz_probability Argument: byte array Returns : number from 0 to\n     * 100 representing probability text in array uses HZ encoding\n     */\n    int hz_probability(byte[] rawtext) {\n        int i, rawtextlen;\n        int hzchars = 0, dbchars = 1;\n        long hzfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int hzstart = 0, hzend = 0;\n        int row, column;\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen; i++) {\n            if (rawtext[i] == '~') {\n                if (rawtext[i + 1] == '{') {\n                    hzstart++;\n                    i += 2;\n                    while (i < rawtextlen - 1) {\n                        if (rawtext[i] == 0x0A || rawtext[i] == 0x0D) {\n                            break;\n                        } else if (rawtext[i] == '~' && rawtext[i + 1] == '}') {\n                            hzend++;\n                            i++;\n                            break;\n                        } else if ((0x21 <= rawtext[i] && rawtext[i] <= 0x77)\n                                && (0x21 <= rawtext[i + 1] && rawtext[i + 1] <= 0x77)) {\n                            hzchars += 2;\n                            row = rawtext[i] - 0x21;\n                            column = rawtext[i + 1] - 0x21;\n                            totalfreq += 500;\n                            if (GBFreq[row][column] != 0) {\n                                hzfreq += GBFreq[row][column];\n                            } else if (15 <= row && row < 55) {\n                                hzfreq += 200;\n                            }\n                        } else if ((0xA1 <= rawtext[i] && rawtext[i] <= 0xF7)\n                                && (0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= 0xF7)) {\n                            hzchars += 2;\n                            row = rawtext[i] + 256 - 0xA1;\n                            column = rawtext[i + 1] + 256 - 0xA1;\n                            totalfreq += 500;\n                            if (GBFreq[row][column] != 0) {\n                                hzfreq += GBFreq[row][column];\n                            } else if (15 <= row && row < 55) {\n                                hzfreq += 200;\n                            }\n                        }\n                        dbchars += 2;\n                        i += 2;\n                    }\n                } else if (rawtext[i + 1] == '}') {\n                    hzend++;\n                    i++;\n                } else if (rawtext[i + 1] == '~') {\n                    i++;\n                }\n            }\n        }\n        if (hzstart > 4) {\n            rangeval = 50;\n        } else if (hzstart > 1) {\n            rangeval = 41;\n        } else if (hzstart > 0) { // Only 39 in case the sequence happened to\n            // occur\n            rangeval = 39; // in otherwise non-Hz text\n        } else {\n            rangeval = 0;\n        }\n        freqval = 50 * ((float) hzfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    /**\n     * Function: big5_probability Argument: byte array Returns : number from 0\n     * to 100 representing probability text in array uses Big5 encoding\n     */\n    int big5_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, bfchars = 1;\n        float rangeval = 0, freqval = 0;\n        long bffreq = 0, totalfreq = 1;\n        int row, column;\n        // Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if ((byte) 0xA1 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xF9\n                        && (((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) || ((byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE))) {\n                    bfchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) {\n                        column = rawtext[i + 1] - 0x40;\n                    } else {\n                        column = rawtext[i + 1] + 256 - 0x61;\n                    }\n                    if (Big5Freq[row][column] != 0) {\n                        bffreq += Big5Freq[row][column];\n                    } else if (3 <= row && row <= 37) {\n                        bffreq += 200;\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) bfchars / (float) dbchars);\n        freqval = 50 * ((float) bffreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    /*\n     * Function: big5plus_probability Argument: pointer to unsigned char array\n     * Returns : number from 0 to 100 representing probability text in array\n     * uses Big5+ encoding\n     */\n    int big5plus_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, bfchars = 1;\n        long bffreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 128) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if (0xA1 <= rawtext[i]\n                        && rawtext[i] <= 0xF9\n                        && // Original Big5 range\n                        ((0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) || (0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= 0xFE))) {\n                    bfchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] - 0xA1;\n                    if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) {\n                        column = rawtext[i + 1] - 0x40;\n                    } else {\n                        column = rawtext[i + 1] - 0x61;\n                    }\n                    // System.out.println(\"original row \" + row + \" column \" +\n                    // column);\n                    if (Big5Freq[row][column] != 0) {\n                        bffreq += Big5Freq[row][column];\n                    } else if (3 <= row && row < 37) {\n                        bffreq += 200;\n                    }\n                } else if (0x81 <= rawtext[i]\n                        && rawtext[i] <= 0xFE\n                        && // Extended Big5 range\n                        ((0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) || (0x80 <= rawtext[i + 1] && rawtext[i + 1] <= 0xFE))) {\n                    bfchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] - 0x81;\n                    if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) {\n                        column = rawtext[i + 1] - 0x40;\n                    } else {\n                        column = rawtext[i + 1] - 0x40;\n                    }\n                    // System.out.println(\"extended row \" + row + \" column \" +\n                    // column + \" rawtext[i] \" + rawtext[i]);\n                    if (Big5PFreq[row][column] != 0) {\n                        bffreq += Big5PFreq[row][column];\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) bfchars / (float) dbchars);\n        freqval = 50 * ((float) bffreq / (float) totalfreq);\n        // For regular Big5 files, this would give the same score, so I handicap\n        // it slightly\n        return (int) (rangeval + freqval) - 1;\n    }\n\n    /*\n     * Function: euc_tw_probability Argument: byte array Returns : number from 0\n     * to 100 representing probability text in array uses EUC-TW (CNS 11643)\n     * encoding\n     */\n    int euc_tw_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, cnschars = 1;\n        long cnsfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Check to see if characters fit into acceptable ranges\n        // and have expected frequency of use\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            if (rawtext[i] >= 0) { // in ASCII range\n                // asciichars++;\n            } else { // high bit set\n                dbchars++;\n                if (i + 3 < rawtextlen && (byte) 0x8E == rawtext[i]\n                        && (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xB0\n                        && (byte) 0xA1 <= rawtext[i + 2]\n                        && rawtext[i + 2] <= (byte) 0xFE\n                        && (byte) 0xA1 <= rawtext[i + 3]\n                        && rawtext[i + 3] <= (byte) 0xFE) { // Planes 1 - 16\n                    cnschars++;\n                    // System.out.println(\"plane 2 or above CNS char\");\n                    // These are all less frequent chars so just ignore freq\n                    i += 3;\n                } else if ((byte) 0xA1 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xFE\n                        && // Plane 1\n                        (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE) {\n                    cnschars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    column = rawtext[i + 1] + 256 - 0xA1;\n                    if (EUC_TWFreq[row][column] != 0) {\n                        cnsfreq += EUC_TWFreq[row][column];\n                    } else if (35 <= row && row <= 92) {\n                        cnsfreq += 150;\n                    }\n                    i++;\n                }\n            }\n        }\n        rangeval = 50 * ((float) cnschars / (float) dbchars);\n        freqval = 50 * ((float) cnsfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    /*\n     * Function: iso_2022_cn_probability Argument: byte array Returns : number\n     * from 0 to 100 representing probability text in array uses ISO 2022-CN\n     * encoding WORKS FOR BASIC CASES, BUT STILL NEEDS MORE WORK\n     */\n    int iso_2022_cn_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, isochars = 1;\n        long isofreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Check to see if characters fit into acceptable ranges\n        // and have expected frequency of use\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            if (rawtext[i] == (byte) 0x1B && i + 3 < rawtextlen) { // Escape\n                // char ESC\n                if (rawtext[i + 1] == (byte) 0x24 && rawtext[i + 2] == 0x29\n                        && rawtext[i + 3] == (byte) 0x41) { // GB Escape $ ) A\n                    i += 4;\n                    while (rawtext[i] != (byte) 0x1B) {\n                        dbchars++;\n                        if ((0x21 <= rawtext[i] && rawtext[i] <= 0x77)\n                                && (0x21 <= rawtext[i + 1] && rawtext[i + 1] <= 0x77)) {\n                            isochars++;\n                            row = rawtext[i] - 0x21;\n                            column = rawtext[i + 1] - 0x21;\n                            totalfreq += 500;\n                            if (GBFreq[row][column] != 0) {\n                                isofreq += GBFreq[row][column];\n                            } else if (15 <= row && row < 55) {\n                                isofreq += 200;\n                            }\n                            i++;\n                        }\n                        i++;\n                    }\n                } else if (i + 3 < rawtextlen && rawtext[i + 1] == (byte) 0x24\n                        && rawtext[i + 2] == (byte) 0x29\n                        && rawtext[i + 3] == (byte) 0x47) {\n                    // CNS Escape $ ) G\n                    i += 4;\n                    while (rawtext[i] != (byte) 0x1B) {\n                        dbchars++;\n                        if ((byte) 0x21 <= rawtext[i]\n                                && rawtext[i] <= (byte) 0x7E\n                                && (byte) 0x21 <= rawtext[i + 1]\n                                && rawtext[i + 1] <= (byte) 0x7E) {\n                            isochars++;\n                            totalfreq += 500;\n                            row = rawtext[i] - 0x21;\n                            column = rawtext[i + 1] - 0x21;\n                            if (EUC_TWFreq[row][column] != 0) {\n                                isofreq += EUC_TWFreq[row][column];\n                            } else if (35 <= row && row <= 92) {\n                                isofreq += 150;\n                            }\n                            i++;\n                        }\n                        i++;\n                    }\n                }\n                if (rawtext[i] == (byte) 0x1B && i + 2 < rawtextlen\n                        && rawtext[i + 1] == (byte) 0x28\n                        && rawtext[i + 2] == (byte) 0x42) { // ASCII:\n                    // ESC\n                    // ( B\n                    i += 2;\n                }\n            }\n        }\n        rangeval = 50 * ((float) isochars / (float) dbchars);\n        freqval = 50 * ((float) isofreq / (float) totalfreq);\n        // System.out.println(\"isochars dbchars isofreq totalfreq \" + isochars +\n        // \" \" + dbchars + \" \" + isofreq + \" \" + totalfreq + \"\n        // \" + rangeval + \" \" + freqval);\n        return (int) (rangeval + freqval);\n        // return 0;\n    }\n\n    /*\n     * Function: utf8_probability Argument: byte array Returns : number from 0\n     * to 100 representing probability text in array uses UTF-8 encoding of\n     * Unicode\n     */\n    int utf8_probability(byte[] rawtext) {\n        int score = 0;\n        int i, rawtextlen = 0;\n        int goodbytes = 0, asciibytes = 0;\n        // Maybe also use UTF8 Byte Order Mark: EF BB BF\n        // Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen; i++) {\n            if ((rawtext[i] & (byte) 0x7F) == rawtext[i]) { // One byte\n                asciibytes++;\n                // Ignore ASCII, can throw off count\n            } else if (-64 <= rawtext[i] && rawtext[i] <= -33\n                    && // Two bytes\n                    i + 1 < rawtextlen && -128 <= rawtext[i + 1]\n                    && rawtext[i + 1] <= -65) {\n                goodbytes += 2;\n                i++;\n            } else if (-32 <= rawtext[i]\n                    && rawtext[i] <= -17\n                    && // Three bytes\n                    i + 2 < rawtextlen && -128 <= rawtext[i + 1]\n                    && rawtext[i + 1] <= -65 && -128 <= rawtext[i + 2]\n                    && rawtext[i + 2] <= -65) {\n                goodbytes += 3;\n                i += 2;\n            }\n        }\n        if (asciibytes == rawtextlen) {\n            return 0;\n        }\n        score = (int) (100 * ((float) goodbytes / (float) (rawtextlen - asciibytes)));\n        // System.out.println(\"rawtextlen \" + rawtextlen + \" goodbytes \" +\n        // goodbytes + \" asciibytes \" + asciibytes + \" score \" +\n        // score);\n        // If not above 98, reduce to zero to prevent coincidental matches\n        // Allows for some (few) bad formed sequences\n        if (score > 98) {\n            return score;\n        } else if (score > 95 && goodbytes > 30) {\n            return score;\n        } else {\n            return 0;\n        }\n    }\n\n    /*\n     * Function: utf16_probability Argument: byte array Returns : number from 0\n     * to 100 representing probability text in array uses UTF-16 encoding of\n     * Unicode, guess based on BOM // NOT VERY GENERAL, NEEDS MUCH MORE WORK\n     */\n    int utf16_probability(byte[] rawtext) {\n        // int score = 0;\n        // int i, rawtextlen = 0;\n        // int goodbytes = 0, asciibytes = 0;\n        if (rawtext.length > 1\n                && ((byte) 0xFE == rawtext[0] && (byte) 0xFF == rawtext[1]) || // Big-endian\n                ((byte) 0xFF == rawtext[0] && (byte) 0xFE == rawtext[1])) { // Little-endian\n            return 100;\n        }\n        return 0;\n        /*\n         * // Check to see if characters fit into acceptable ranges rawtextlen =\n         * rawtext.length; for (i = 0; i < rawtextlen; i++) { if ((rawtext[i] &\n         * (byte)0x7F) == rawtext[i]) { // One byte goodbytes += 1;\n         * asciibytes++; } else if ((rawtext[i] & (byte)0xDF) == rawtext[i]) {\n         * // Two bytes if (i+1 < rawtextlen && (rawtext[i+1] & (byte)0xBF) ==\n         * rawtext[i+1]) { goodbytes += 2; i++; } } else if ((rawtext[i] &\n         * (byte)0xEF) == rawtext[i]) { // Three bytes if (i+2 < rawtextlen &&\n         * (rawtext[i+1] & (byte)0xBF) == rawtext[i+1] && (rawtext[i+2] &\n         * (byte)0xBF) == rawtext[i+2]) { goodbytes += 3; i+=2; } } }\n         *\n         * score = (int)(100 * ((float)goodbytes/(float)rawtext.length)); // An\n         * all ASCII file is also a good UTF8 file, but I'd rather it // get\n         * identified as ASCII. Can delete following 3 lines otherwise if\n         * (goodbytes == asciibytes) { score = 0; } // If not above 90, reduce\n         * to zero to prevent coincidental matches if (score > 90) { return\n         * score; } else { return 0; }\n         */\n    }\n\n    /*\n     * Function: ascii_probability Argument: byte array Returns : number from 0\n     * to 100 representing probability text in array uses all ASCII Description:\n     * Sees if array has any characters not in ASCII range, if so, score is\n     * reduced\n     */\n    int ascii_probability(byte[] rawtext) {\n        int score = 75;\n        int i, rawtextlen;\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen; i++) {\n            if (rawtext[i] < 0) {\n                score = score - 5;\n            } else if (rawtext[i] == (byte) 0x1B) { // ESC (used by ISO 2022)\n                score = score - 5;\n            }\n            if (score <= 0) {\n                return 0;\n            }\n        }\n        return score;\n    }\n\n    /*\n     * Function: euc_kr__probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses EUC-KR\n     * encoding\n     */\n    int euc_kr_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, krchars = 1;\n        long krfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE\n                        && (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE) {\n                    krchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    column = rawtext[i + 1] + 256 - 0xA1;\n                    if (KRFreq[row][column] != 0) {\n                        krfreq += KRFreq[row][column];\n                    } else if (15 <= row && row < 55) {\n                        krfreq += 0;\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) krchars / (float) dbchars);\n        freqval = 50 * ((float) krfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    /*\n     * Function: cp949__probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses Cp949\n     * encoding\n     */\n    int cp949_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, krchars = 1;\n        long krfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if ((byte) 0x81 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xFE\n                        && ((byte) 0x41 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0x5A\n                        || (byte) 0x61 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0x7A || (byte) 0x81 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE)) {\n                    krchars++;\n                    totalfreq += 500;\n                    if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE\n                            && (byte) 0xA1 <= rawtext[i + 1]\n                            && rawtext[i + 1] <= (byte) 0xFE) {\n                        row = rawtext[i] + 256 - 0xA1;\n                        column = rawtext[i + 1] + 256 - 0xA1;\n                        if (KRFreq[row][column] != 0) {\n                            krfreq += KRFreq[row][column];\n                        }\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) krchars / (float) dbchars);\n        freqval = 50 * ((float) krfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    int iso_2022_kr_probability(byte[] rawtext) {\n        int i;\n        for (i = 0; i < rawtext.length; i++) {\n            if (i + 3 < rawtext.length && rawtext[i] == 0x1b\n                    && (char) rawtext[i + 1] == '$'\n                    && (char) rawtext[i + 2] == ')'\n                    && (char) rawtext[i + 3] == 'C') {\n                return 100;\n            }\n        }\n        return 0;\n    }\n\n    /*\n     * Function: euc_jp_probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses EUC-JP\n     * encoding\n     */\n    int euc_jp_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, jpchars = 1;\n        long jpfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE\n                        && (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE) {\n                    jpchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    column = rawtext[i + 1] + 256 - 0xA1;\n                    if (JPFreq[row][column] != 0) {\n                        jpfreq += JPFreq[row][column];\n                    } else if (15 <= row && row < 55) {\n                        jpfreq += 0;\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) jpchars / (float) dbchars);\n        freqval = 50 * ((float) jpfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    int iso_2022_jp_probability(byte[] rawtext) {\n        int i;\n        for (i = 0; i < rawtext.length; i++) {\n            if (i + 2 < rawtext.length && rawtext[i] == 0x1b\n                    && (char) rawtext[i + 1] == '$'\n                    && (char) rawtext[i + 2] == 'B') {\n                return 100;\n            }\n        }\n        return 0;\n    }\n\n    /*\n     * Function: sjis_probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses\n     * Shift-JIS encoding\n     */\n    int sjis_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, jpchars = 1;\n        long jpfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column, adjust;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if (i + 1 < rawtext.length\n                        && (((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0x9F) || ((byte) 0xE0 <= rawtext[i] && rawtext[i] <= (byte) 0xEF))\n                        && (((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) || ((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFC))) {\n                    jpchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256;\n                    column = rawtext[i + 1] + 256;\n                    if (column < 0x9f) {\n                        adjust = 1;\n                        if (column > 0x7f) {\n                            column -= 0x20;\n                        } else {\n                            column -= 0x19;\n                        }\n                    } else {\n                        adjust = 0;\n                        column -= 0x7e;\n                    }\n                    if (row < 0xa0) {\n                        row = ((row - 0x70) << 1) - adjust;\n                    } else {\n                        row = ((row - 0xb0) << 1) - adjust;\n                    }\n                    row -= 0x20;\n                    column = 0x20;\n                    // System.out.println(\"original row \" + row + \" column \" +\n                    // column);\n                    if (row < JPFreq.length && column < JPFreq[row].length\n                            && JPFreq[row][column] != 0) {\n                        jpfreq += JPFreq[row][column];\n                    }\n                    i++;\n                } else if ((byte) 0xA1 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xDF) {\n                    // half-width katakana, convert to full-width\n                }\n            }\n        }\n        rangeval = 50 * ((float) jpchars / (float) dbchars);\n        freqval = 50 * ((float) jpfreq / (float) totalfreq);\n        // For regular GB files, this would give the same score, so I handicap\n        // it slightly\n        return (int) (rangeval + freqval) - 1;\n    }\n\n    void initialize_frequencies() {\n        int i, j;\n        for (i = 93; i >= 0; i--) {\n            for (j = 93; j >= 0; j--) {\n                GBFreq[i][j] = 0;\n            }\n        }\n        for (i = 125; i >= 0; i--) {\n            for (j = 190; j >= 0; j--) {\n                GBKFreq[i][j] = 0;\n            }\n        }\n        // for (i = 0; i < 94; i++) {\n        // for (j = 0; j < 158; j++) {\n        for (i = 93; i >= 0; i--) {\n            for (j = 157; j >= 0; j--) {\n                Big5Freq[i][j] = 0;\n            }\n        }\n        // for (i = 0; i < 126; i++) {\n        // for (j = 0; j < 191; j++) {\n        for (i = 125; i >= 0; i--) {\n            for (j = 190; j >= 0; j--) {\n                Big5PFreq[i][j] = 0;\n            }\n        }\n        // for (i = 0; i < 94; i++) {\n        // for (j = 0; j < 94; j++) {\n        for (i = 93; i >= 0; i--) {\n            for (j = 93; j >= 0; j--) {\n                EUC_TWFreq[i][j] = 0;\n            }\n        }\n        for (i = 93; i >= 0; i--) {\n            for (j = 93; j >= 0; j--) {\n                JPFreq[i][j] = 0;\n            }\n        }\n        GBFreq[20][35] = 599;\n        GBFreq[49][26] = 598;\n        GBFreq[41][38] = 597;\n        GBFreq[17][26] = 596;\n        GBFreq[32][42] = 595;\n        GBFreq[39][42] = 594;\n        GBFreq[45][49] = 593;\n        GBFreq[51][57] = 592;\n        GBFreq[50][47] = 591;\n        GBFreq[42][90] = 590;\n        GBFreq[52][65] = 589;\n        GBFreq[53][47] = 588;\n        GBFreq[19][82] = 587;\n        GBFreq[31][19] = 586;\n        GBFreq[40][46] = 585;\n        GBFreq[24][89] = 584;\n        GBFreq[23][85] = 583;\n        GBFreq[20][28] = 582;\n        GBFreq[42][20] = 581;\n        GBFreq[34][38] = 580;\n        GBFreq[45][9] = 579;\n        GBFreq[54][50] = 578;\n        GBFreq[25][44] = 577;\n        GBFreq[35][66] = 576;\n        GBFreq[20][55] = 575;\n        GBFreq[18][85] = 574;\n        GBFreq[20][31] = 573;\n        GBFreq[49][17] = 572;\n        GBFreq[41][16] = 571;\n        GBFreq[35][73] = 570;\n        GBFreq[20][34] = 569;\n        GBFreq[29][44] = 568;\n        GBFreq[35][38] = 567;\n        GBFreq[49][9] = 566;\n        GBFreq[46][33] = 565;\n        GBFreq[49][51] = 564;\n        GBFreq[40][89] = 563;\n        GBFreq[26][64] = 562;\n        GBFreq[54][51] = 561;\n        GBFreq[54][36] = 560;\n        GBFreq[39][4] = 559;\n        GBFreq[53][13] = 558;\n        GBFreq[24][92] = 557;\n        GBFreq[27][49] = 556;\n        GBFreq[48][6] = 555;\n        GBFreq[21][51] = 554;\n        GBFreq[30][40] = 553;\n        GBFreq[42][92] = 552;\n        GBFreq[31][78] = 551;\n        GBFreq[25][82] = 550;\n        GBFreq[47][0] = 549;\n        GBFreq[34][19] = 548;\n        GBFreq[47][35] = 547;\n        GBFreq[21][63] = 546;\n        GBFreq[43][75] = 545;\n        GBFreq[21][87] = 544;\n        GBFreq[35][59] = 543;\n        GBFreq[25][34] = 542;\n        GBFreq[21][27] = 541;\n        GBFreq[39][26] = 540;\n        GBFreq[34][26] = 539;\n        GBFreq[39][52] = 538;\n        GBFreq[50][57] = 537;\n        GBFreq[37][79] = 536;\n        GBFreq[26][24] = 535;\n        GBFreq[22][1] = 534;\n        GBFreq[18][40] = 533;\n        GBFreq[41][33] = 532;\n        GBFreq[53][26] = 531;\n        GBFreq[54][86] = 530;\n        GBFreq[20][16] = 529;\n        GBFreq[46][74] = 528;\n        GBFreq[30][19] = 527;\n        GBFreq[45][35] = 526;\n        GBFreq[45][61] = 525;\n        GBFreq[30][9] = 524;\n        GBFreq[41][53] = 523;\n        GBFreq[41][13] = 522;\n        GBFreq[50][34] = 521;\n        GBFreq[53][86] = 520;\n        GBFreq[47][47] = 519;\n        GBFreq[22][28] = 518;\n        GBFreq[50][53] = 517;\n        GBFreq[39][70] = 516;\n        GBFreq[38][15] = 515;\n        GBFreq[42][88] = 514;\n        GBFreq[16][29] = 513;\n        GBFreq[27][90] = 512;\n        GBFreq[29][12] = 511;\n        GBFreq[44][22] = 510;\n        GBFreq[34][69] = 509;\n        GBFreq[24][10] = 508;\n        GBFreq[44][11] = 507;\n        GBFreq[39][92] = 506;\n        GBFreq[49][48] = 505;\n        GBFreq[31][46] = 504;\n        GBFreq[19][50] = 503;\n        GBFreq[21][14] = 502;\n        GBFreq[32][28] = 501;\n        GBFreq[18][3] = 500;\n        GBFreq[53][9] = 499;\n        GBFreq[34][80] = 498;\n        GBFreq[48][88] = 497;\n        GBFreq[46][53] = 496;\n        GBFreq[22][53] = 495;\n        GBFreq[28][10] = 494;\n        GBFreq[44][65] = 493;\n        GBFreq[20][10] = 492;\n        GBFreq[40][76] = 491;\n        GBFreq[47][8] = 490;\n        GBFreq[50][74] = 489;\n        GBFreq[23][62] = 488;\n        GBFreq[49][65] = 487;\n        GBFreq[28][87] = 486;\n        GBFreq[15][48] = 485;\n        GBFreq[22][7] = 484;\n        GBFreq[19][42] = 483;\n        GBFreq[41][20] = 482;\n        GBFreq[26][55] = 481;\n        GBFreq[21][93] = 480;\n        GBFreq[31][76] = 479;\n        GBFreq[34][31] = 478;\n        GBFreq[20][66] = 477;\n        GBFreq[51][33] = 476;\n        GBFreq[34][86] = 475;\n        GBFreq[37][67] = 474;\n        GBFreq[53][53] = 473;\n        GBFreq[40][88] = 472;\n        GBFreq[39][10] = 471;\n        GBFreq[24][3] = 470;\n        GBFreq[27][25] = 469;\n        GBFreq[26][15] = 468;\n        GBFreq[21][88] = 467;\n        GBFreq[52][62] = 466;\n        GBFreq[46][81] = 465;\n        GBFreq[38][72] = 464;\n        GBFreq[17][30] = 463;\n        GBFreq[52][92] = 462;\n        GBFreq[34][90] = 461;\n        GBFreq[21][7] = 460;\n        GBFreq[36][13] = 459;\n        GBFreq[45][41] = 458;\n        GBFreq[32][5] = 457;\n        GBFreq[26][89] = 456;\n        GBFreq[23][87] = 455;\n        GBFreq[20][39] = 454;\n        GBFreq[27][23] = 453;\n        GBFreq[25][59] = 452;\n        GBFreq[49][20] = 451;\n        GBFreq[54][77] = 450;\n        GBFreq[27][67] = 449;\n        GBFreq[47][33] = 448;\n        GBFreq[41][17] = 447;\n        GBFreq[19][81] = 446;\n        GBFreq[16][66] = 445;\n        GBFreq[45][26] = 444;\n        GBFreq[49][81] = 443;\n        GBFreq[53][55] = 442;\n        GBFreq[16][26] = 441;\n        GBFreq[54][62] = 440;\n        GBFreq[20][70] = 439;\n        GBFreq[42][35] = 438;\n        GBFreq[20][57] = 437;\n        GBFreq[34][36] = 436;\n        GBFreq[46][63] = 435;\n        GBFreq[19][45] = 434;\n        GBFreq[21][10] = 433;\n        GBFreq[52][93] = 432;\n        GBFreq[25][2] = 431;\n        GBFreq[30][57] = 430;\n        GBFreq[41][24] = 429;\n        GBFreq[28][43] = 428;\n        GBFreq[45][86] = 427;\n        GBFreq[51][56] = 426;\n        GBFreq[37][28] = 425;\n        GBFreq[52][69] = 424;\n        GBFreq[43][92] = 423;\n        GBFreq[41][31] = 422;\n        GBFreq[37][87] = 421;\n        GBFreq[47][36] = 420;\n        GBFreq[16][16] = 419;\n        GBFreq[40][56] = 418;\n        GBFreq[24][55] = 417;\n        GBFreq[17][1] = 416;\n        GBFreq[35][57] = 415;\n        GBFreq[27][50] = 414;\n        GBFreq[26][14] = 413;\n        GBFreq[50][40] = 412;\n        GBFreq[39][19] = 411;\n        GBFreq[19][89] = 410;\n        GBFreq[29][91] = 409;\n        GBFreq[17][89] = 408;\n        GBFreq[39][74] = 407;\n        GBFreq[46][39] = 406;\n        GBFreq[40][28] = 405;\n        GBFreq[45][68] = 404;\n        GBFreq[43][10] = 403;\n        GBFreq[42][13] = 402;\n        GBFreq[44][81] = 401;\n        GBFreq[41][47] = 400;\n        GBFreq[48][58] = 399;\n        GBFreq[43][68] = 398;\n        GBFreq[16][79] = 397;\n        GBFreq[19][5] = 396;\n        GBFreq[54][59] = 395;\n        GBFreq[17][36] = 394;\n        GBFreq[18][0] = 393;\n        GBFreq[41][5] = 392;\n        GBFreq[41][72] = 391;\n        GBFreq[16][39] = 390;\n        GBFreq[54][0] = 389;\n        GBFreq[51][16] = 388;\n        GBFreq[29][36] = 387;\n        GBFreq[47][5] = 386;\n        GBFreq[47][51] = 385;\n        GBFreq[44][7] = 384;\n        GBFreq[35][30] = 383;\n        GBFreq[26][9] = 382;\n        GBFreq[16][7] = 381;\n        GBFreq[32][1] = 380;\n        GBFreq[33][76] = 379;\n        GBFreq[34][91] = 378;\n        GBFreq[52][36] = 377;\n        GBFreq[26][77] = 376;\n        GBFreq[35][48] = 375;\n        GBFreq[40][80] = 374;\n        GBFreq[41][92] = 373;\n        GBFreq[27][93] = 372;\n        GBFreq[15][17] = 371;\n        GBFreq[16][76] = 370;\n        GBFreq[51][12] = 369;\n        GBFreq[18][20] = 368;\n        GBFreq[15][54] = 367;\n        GBFreq[50][5] = 366;\n        GBFreq[33][22] = 365;\n        GBFreq[37][57] = 364;\n        GBFreq[28][47] = 363;\n        GBFreq[42][31] = 362;\n        GBFreq[18][2] = 361;\n        GBFreq[43][64] = 360;\n        GBFreq[23][47] = 359;\n        GBFreq[28][79] = 358;\n        GBFreq[25][45] = 357;\n        GBFreq[23][91] = 356;\n        GBFreq[22][19] = 355;\n        GBFreq[25][46] = 354;\n        GBFreq[22][36] = 353;\n        GBFreq[54][85] = 352;\n        GBFreq[46][20] = 351;\n        GBFreq[27][37] = 350;\n        GBFreq[26][81] = 349;\n        GBFreq[42][29] = 348;\n        GBFreq[31][90] = 347;\n        GBFreq[41][59] = 346;\n        GBFreq[24][65] = 345;\n        GBFreq[44][84] = 344;\n        GBFreq[24][90] = 343;\n        GBFreq[38][54] = 342;\n        GBFreq[28][70] = 341;\n        GBFreq[27][15] = 340;\n        GBFreq[28][80] = 339;\n        GBFreq[29][8] = 338;\n        GBFreq[45][80] = 337;\n        GBFreq[53][37] = 336;\n        GBFreq[28][65] = 335;\n        GBFreq[23][86] = 334;\n        GBFreq[39][45] = 333;\n        GBFreq[53][32] = 332;\n        GBFreq[38][68] = 331;\n        GBFreq[45][78] = 330;\n        GBFreq[43][7] = 329;\n        GBFreq[46][82] = 328;\n        GBFreq[27][38] = 327;\n        GBFreq[16][62] = 326;\n        GBFreq[24][17] = 325;\n        GBFreq[22][70] = 324;\n        GBFreq[52][28] = 323;\n        GBFreq[23][40] = 322;\n        GBFreq[28][50] = 321;\n        GBFreq[42][91] = 320;\n        GBFreq[47][76] = 319;\n        GBFreq[15][42] = 318;\n        GBFreq[43][55] = 317;\n        GBFreq[29][84] = 316;\n        GBFreq[44][90] = 315;\n        GBFreq[53][16] = 314;\n        GBFreq[22][93] = 313;\n        GBFreq[34][10] = 312;\n        GBFreq[32][53] = 311;\n        GBFreq[43][65] = 310;\n        GBFreq[28][7] = 309;\n        GBFreq[35][46] = 308;\n        GBFreq[21][39] = 307;\n        GBFreq[44][18] = 306;\n        GBFreq[40][10] = 305;\n        GBFreq[54][53] = 304;\n        GBFreq[38][74] = 303;\n        GBFreq[28][26] = 302;\n        GBFreq[15][13] = 301;\n        GBFreq[39][34] = 300;\n        GBFreq[39][46] = 299;\n        GBFreq[42][66] = 298;\n        GBFreq[33][58] = 297;\n        GBFreq[15][56] = 296;\n        GBFreq[18][51] = 295;\n        GBFreq[49][68] = 294;\n        GBFreq[30][37] = 293;\n        GBFreq[51][84] = 292;\n        GBFreq[51][9] = 291;\n        GBFreq[40][70] = 290;\n        GBFreq[41][84] = 289;\n        GBFreq[28][64] = 288;\n        GBFreq[32][88] = 287;\n        GBFreq[24][5] = 286;\n        GBFreq[53][23] = 285;\n        GBFreq[42][27] = 284;\n        GBFreq[22][38] = 283;\n        GBFreq[32][86] = 282;\n        GBFreq[34][30] = 281;\n        GBFreq[38][63] = 280;\n        GBFreq[24][59] = 279;\n        GBFreq[22][81] = 278;\n        GBFreq[32][11] = 277;\n        GBFreq[51][21] = 276;\n        GBFreq[54][41] = 275;\n        GBFreq[21][50] = 274;\n        GBFreq[23][89] = 273;\n        GBFreq[19][87] = 272;\n        GBFreq[26][7] = 271;\n        GBFreq[30][75] = 270;\n        GBFreq[43][84] = 269;\n        GBFreq[51][25] = 268;\n        GBFreq[16][67] = 267;\n        GBFreq[32][9] = 266;\n        GBFreq[48][51] = 265;\n        GBFreq[39][7] = 264;\n        GBFreq[44][88] = 263;\n        GBFreq[52][24] = 262;\n        GBFreq[23][34] = 261;\n        GBFreq[32][75] = 260;\n        GBFreq[19][10] = 259;\n        GBFreq[28][91] = 258;\n        GBFreq[32][83] = 257;\n        GBFreq[25][75] = 256;\n        GBFreq[53][45] = 255;\n        GBFreq[29][85] = 254;\n        GBFreq[53][59] = 253;\n        GBFreq[16][2] = 252;\n        GBFreq[19][78] = 251;\n        GBFreq[15][75] = 250;\n        GBFreq[51][42] = 249;\n        GBFreq[45][67] = 248;\n        GBFreq[15][74] = 247;\n        GBFreq[25][81] = 246;\n        GBFreq[37][62] = 245;\n        GBFreq[16][55] = 244;\n        GBFreq[18][38] = 243;\n        GBFreq[23][23] = 242;\n        GBFreq[38][30] = 241;\n        GBFreq[17][28] = 240;\n        GBFreq[44][73] = 239;\n        GBFreq[23][78] = 238;\n        GBFreq[40][77] = 237;\n        GBFreq[38][87] = 236;\n        GBFreq[27][19] = 235;\n        GBFreq[38][82] = 234;\n        GBFreq[37][22] = 233;\n        GBFreq[41][30] = 232;\n        GBFreq[54][9] = 231;\n        GBFreq[32][30] = 230;\n        GBFreq[30][52] = 229;\n        GBFreq[40][84] = 228;\n        GBFreq[53][57] = 227;\n        GBFreq[27][27] = 226;\n        GBFreq[38][64] = 225;\n        GBFreq[18][43] = 224;\n        GBFreq[23][69] = 223;\n        GBFreq[28][12] = 222;\n        GBFreq[50][78] = 221;\n        GBFreq[50][1] = 220;\n        GBFreq[26][88] = 219;\n        GBFreq[36][40] = 218;\n        GBFreq[33][89] = 217;\n        GBFreq[41][28] = 216;\n        GBFreq[31][77] = 215;\n        GBFreq[46][1] = 214;\n        GBFreq[47][19] = 213;\n        GBFreq[35][55] = 212;\n        GBFreq[41][21] = 211;\n        GBFreq[27][10] = 210;\n        GBFreq[32][77] = 209;\n        GBFreq[26][37] = 208;\n        GBFreq[20][33] = 207;\n        GBFreq[41][52] = 206;\n        GBFreq[32][18] = 205;\n        GBFreq[38][13] = 204;\n        GBFreq[20][18] = 203;\n        GBFreq[20][24] = 202;\n        GBFreq[45][19] = 201;\n        GBFreq[18][53] = 200;\n        /*\n         * GBFreq[39][0] = 199; GBFreq[40][71] = 198; GBFreq[41][27] = 197;\n         * GBFreq[15][69] = 196; GBFreq[42][10] = 195; GBFreq[31][89] = 194;\n         * GBFreq[51][28] = 193; GBFreq[41][22] = 192; GBFreq[40][43] = 191;\n         * GBFreq[38][6] = 190; GBFreq[37][11] = 189; GBFreq[39][60] = 188;\n         * GBFreq[48][47] = 187; GBFreq[46][80] = 186; GBFreq[52][49] = 185;\n         * GBFreq[50][48] = 184; GBFreq[25][1] = 183; GBFreq[52][29] = 182;\n         * GBFreq[24][66] = 181; GBFreq[23][35] = 180; GBFreq[49][72] = 179;\n         * GBFreq[47][45] = 178; GBFreq[45][14] = 177; GBFreq[51][70] = 176;\n         * GBFreq[22][30] = 175; GBFreq[49][83] = 174; GBFreq[26][79] = 173;\n         * GBFreq[27][41] = 172; GBFreq[51][81] = 171; GBFreq[41][54] = 170;\n         * GBFreq[20][4] = 169; GBFreq[29][60] = 168; GBFreq[20][27] = 167;\n         * GBFreq[50][15] = 166; GBFreq[41][6] = 165; GBFreq[35][34] = 164;\n         * GBFreq[44][87] = 163; GBFreq[46][66] = 162; GBFreq[42][37] = 161;\n         * GBFreq[42][24] = 160; GBFreq[54][7] = 159; GBFreq[41][14] = 158;\n         * GBFreq[39][83] = 157; GBFreq[16][87] = 156; GBFreq[20][59] = 155;\n         * GBFreq[42][12] = 154; GBFreq[47][2] = 153; GBFreq[21][32] = 152;\n         * GBFreq[53][29] = 151; GBFreq[22][40] = 150; GBFreq[24][58] = 149;\n         * GBFreq[52][88] = 148; GBFreq[29][30] = 147; GBFreq[15][91] = 146;\n         * GBFreq[54][72] = 145; GBFreq[51][75] = 144; GBFreq[33][67] = 143;\n         * GBFreq[41][50] = 142; GBFreq[27][34] = 141; GBFreq[46][17] = 140;\n         * GBFreq[31][74] = 139; GBFreq[42][67] = 138; GBFreq[54][87] = 137;\n         * GBFreq[27][14] = 136; GBFreq[16][63] = 135; GBFreq[16][5] = 134;\n         * GBFreq[43][23] = 133; GBFreq[23][13] = 132; GBFreq[31][12] = 131;\n         * GBFreq[25][57] = 130; GBFreq[38][49] = 129; GBFreq[42][69] = 128;\n         * GBFreq[23][80] = 127; GBFreq[29][0] = 126; GBFreq[28][2] = 125;\n         * GBFreq[28][17] = 124; GBFreq[17][27] = 123; GBFreq[40][16] = 122;\n         * GBFreq[45][1] = 121; GBFreq[36][33] = 120; GBFreq[35][23] = 119;\n         * GBFreq[20][86] = 118; GBFreq[29][53] = 117; GBFreq[23][88] = 116;\n         * GBFreq[51][87] = 115; GBFreq[54][27] = 114; GBFreq[44][36] = 113;\n         * GBFreq[21][45] = 112; GBFreq[53][52] = 111; GBFreq[31][53] = 110;\n         * GBFreq[38][47] = 109; GBFreq[27][21] = 108; GBFreq[30][42] = 107;\n         * GBFreq[29][10] = 106; GBFreq[35][35] = 105; GBFreq[24][56] = 104;\n         * GBFreq[41][29] = 103; GBFreq[18][68] = 102; GBFreq[29][24] = 101;\n         * GBFreq[25][84] = 100; GBFreq[35][47] = 99; GBFreq[29][56] = 98;\n         * GBFreq[30][44] = 97; GBFreq[53][3] = 96; GBFreq[30][63] = 95;\n         * GBFreq[52][52] = 94; GBFreq[54][1] = 93; GBFreq[22][48] = 92;\n         * GBFreq[54][66] = 91; GBFreq[21][90] = 90; GBFreq[52][47] = 89;\n         * GBFreq[39][25] = 88; GBFreq[39][39] = 87; GBFreq[44][37] = 86;\n         * GBFreq[44][76] = 85; GBFreq[46][75] = 84; GBFreq[18][37] = 83;\n         * GBFreq[47][42] = 82; GBFreq[19][92] = 81; GBFreq[51][27] = 80;\n         * GBFreq[48][83] = 79; GBFreq[23][70] = 78; GBFreq[29][9] = 77;\n         * GBFreq[33][79] = 76; GBFreq[52][90] = 75; GBFreq[53][6] = 74;\n         * GBFreq[24][36] = 73; GBFreq[25][25] = 72; GBFreq[44][26] = 71;\n         * GBFreq[25][36] = 70; GBFreq[29][87] = 69; GBFreq[48][0] = 68;\n         * GBFreq[15][40] = 67; GBFreq[17][45] = 66; GBFreq[30][14] = 65;\n         * GBFreq[48][38] = 64; GBFreq[23][19] = 63; GBFreq[40][42] = 62;\n         * GBFreq[31][63] = 61; GBFreq[16][23] = 60; GBFreq[26][21] = 59;\n         * GBFreq[32][76] = 58; GBFreq[23][58] = 57; GBFreq[41][37] = 56;\n         * GBFreq[30][43] = 55; GBFreq[47][38] = 54; GBFreq[21][46] = 53;\n         * GBFreq[18][33] = 52; GBFreq[52][37] = 51; GBFreq[36][8] = 50;\n         * GBFreq[49][24] = 49; GBFreq[15][66] = 48; GBFreq[35][77] = 47;\n         * GBFreq[27][58] = 46; GBFreq[35][51] = 45; GBFreq[24][69] = 44;\n         * GBFreq[20][54] = 43; GBFreq[24][41] = 42; GBFreq[41][0] = 41;\n         * GBFreq[33][71] = 40; GBFreq[23][52] = 39; GBFreq[29][67] = 38;\n         * GBFreq[46][51] = 37; GBFreq[46][90] = 36; GBFreq[49][33] = 35;\n         * GBFreq[33][28] = 34; GBFreq[37][86] = 33; GBFreq[39][22] = 32;\n         * GBFreq[37][37] = 31; GBFreq[29][62] = 30; GBFreq[29][50] = 29;\n         * GBFreq[36][89] = 28; GBFreq[42][44] = 27; GBFreq[51][82] = 26;\n         * GBFreq[28][83] = 25; GBFreq[15][78] = 24; GBFreq[46][62] = 23;\n         * GBFreq[19][69] = 22; GBFreq[51][23] = 21; GBFreq[37][69] = 20;\n         * GBFreq[25][5] = 19; GBFreq[51][85] = 18; GBFreq[48][77] = 17;\n         * GBFreq[32][46] = 16; GBFreq[53][60] = 15; GBFreq[28][57] = 14;\n         * GBFreq[54][82] = 13; GBFreq[54][15] = 12; GBFreq[49][54] = 11;\n         * GBFreq[53][87] = 10; GBFreq[27][16] = 9; GBFreq[29][34] = 8;\n         * GBFreq[20][44] = 7; GBFreq[42][73] = 6; GBFreq[47][71] = 5;\n         * GBFreq[29][37] = 4; GBFreq[25][50] = 3; GBFreq[18][84] = 2;\n         * GBFreq[50][45] = 1; GBFreq[48][46] = 0;\n         */\n        // GBFreq[43][89] = -1; GBFreq[54][68] = -2;\n        Big5Freq[9][89] = 600;\n        Big5Freq[11][15] = 599;\n        Big5Freq[3][66] = 598;\n        Big5Freq[6][121] = 597;\n        Big5Freq[3][0] = 596;\n        Big5Freq[5][82] = 595;\n        Big5Freq[3][42] = 594;\n        Big5Freq[5][34] = 593;\n        Big5Freq[3][8] = 592;\n        Big5Freq[3][6] = 591;\n        Big5Freq[3][67] = 590;\n        Big5Freq[7][139] = 589;\n        Big5Freq[23][137] = 588;\n        Big5Freq[12][46] = 587;\n        Big5Freq[4][8] = 586;\n        Big5Freq[4][41] = 585;\n        Big5Freq[18][47] = 584;\n        Big5Freq[12][114] = 583;\n        Big5Freq[6][1] = 582;\n        Big5Freq[22][60] = 581;\n        Big5Freq[5][46] = 580;\n        Big5Freq[11][79] = 579;\n        Big5Freq[3][23] = 578;\n        Big5Freq[7][114] = 577;\n        Big5Freq[29][102] = 576;\n        Big5Freq[19][14] = 575;\n        Big5Freq[4][133] = 574;\n        Big5Freq[3][29] = 573;\n        Big5Freq[4][109] = 572;\n        Big5Freq[14][127] = 571;\n        Big5Freq[5][48] = 570;\n        Big5Freq[13][104] = 569;\n        Big5Freq[3][132] = 568;\n        Big5Freq[26][64] = 567;\n        Big5Freq[7][19] = 566;\n        Big5Freq[4][12] = 565;\n        Big5Freq[11][124] = 564;\n        Big5Freq[7][89] = 563;\n        Big5Freq[15][124] = 562;\n        Big5Freq[4][108] = 561;\n        Big5Freq[19][66] = 560;\n        Big5Freq[3][21] = 559;\n        Big5Freq[24][12] = 558;\n        Big5Freq[28][111] = 557;\n        Big5Freq[12][107] = 556;\n        Big5Freq[3][112] = 555;\n        Big5Freq[8][113] = 554;\n        Big5Freq[5][40] = 553;\n        Big5Freq[26][145] = 552;\n        Big5Freq[3][48] = 551;\n        Big5Freq[3][70] = 550;\n        Big5Freq[22][17] = 549;\n        Big5Freq[16][47] = 548;\n        Big5Freq[3][53] = 547;\n        Big5Freq[4][24] = 546;\n        Big5Freq[32][120] = 545;\n        Big5Freq[24][49] = 544;\n        Big5Freq[24][142] = 543;\n        Big5Freq[18][66] = 542;\n        Big5Freq[29][150] = 541;\n        Big5Freq[5][122] = 540;\n        Big5Freq[5][114] = 539;\n        Big5Freq[3][44] = 538;\n        Big5Freq[10][128] = 537;\n        Big5Freq[15][20] = 536;\n        Big5Freq[13][33] = 535;\n        Big5Freq[14][87] = 534;\n        Big5Freq[3][126] = 533;\n        Big5Freq[4][53] = 532;\n        Big5Freq[4][40] = 531;\n        Big5Freq[9][93] = 530;\n        Big5Freq[15][137] = 529;\n        Big5Freq[10][123] = 528;\n        Big5Freq[4][56] = 527;\n        Big5Freq[5][71] = 526;\n        Big5Freq[10][8] = 525;\n        Big5Freq[5][16] = 524;\n        Big5Freq[5][146] = 523;\n        Big5Freq[18][88] = 522;\n        Big5Freq[24][4] = 521;\n        Big5Freq[20][47] = 520;\n        Big5Freq[5][33] = 519;\n        Big5Freq[9][43] = 518;\n        Big5Freq[20][12] = 517;\n        Big5Freq[20][13] = 516;\n        Big5Freq[5][156] = 515;\n        Big5Freq[22][140] = 514;\n        Big5Freq[8][146] = 513;\n        Big5Freq[21][123] = 512;\n        Big5Freq[4][90] = 511;\n        Big5Freq[5][62] = 510;\n        Big5Freq[17][59] = 509;\n        Big5Freq[10][37] = 508;\n        Big5Freq[18][107] = 507;\n        Big5Freq[14][53] = 506;\n        Big5Freq[22][51] = 505;\n        Big5Freq[8][13] = 504;\n        Big5Freq[5][29] = 503;\n        Big5Freq[9][7] = 502;\n        Big5Freq[22][14] = 501;\n        Big5Freq[8][55] = 500;\n        Big5Freq[33][9] = 499;\n        Big5Freq[16][64] = 498;\n        Big5Freq[7][131] = 497;\n        Big5Freq[34][4] = 496;\n        Big5Freq[7][101] = 495;\n        Big5Freq[11][139] = 494;\n        Big5Freq[3][135] = 493;\n        Big5Freq[7][102] = 492;\n        Big5Freq[17][13] = 491;\n        Big5Freq[3][20] = 490;\n        Big5Freq[27][106] = 489;\n        Big5Freq[5][88] = 488;\n        Big5Freq[6][33] = 487;\n        Big5Freq[5][139] = 486;\n        Big5Freq[6][0] = 485;\n        Big5Freq[17][58] = 484;\n        Big5Freq[5][133] = 483;\n        Big5Freq[9][107] = 482;\n        Big5Freq[23][39] = 481;\n        Big5Freq[5][23] = 480;\n        Big5Freq[3][79] = 479;\n        Big5Freq[32][97] = 478;\n        Big5Freq[3][136] = 477;\n        Big5Freq[4][94] = 476;\n        Big5Freq[21][61] = 475;\n        Big5Freq[23][123] = 474;\n        Big5Freq[26][16] = 473;\n        Big5Freq[24][137] = 472;\n        Big5Freq[22][18] = 471;\n        Big5Freq[5][1] = 470;\n        Big5Freq[20][119] = 469;\n        Big5Freq[3][7] = 468;\n        Big5Freq[10][79] = 467;\n        Big5Freq[15][105] = 466;\n        Big5Freq[3][144] = 465;\n        Big5Freq[12][80] = 464;\n        Big5Freq[15][73] = 463;\n        Big5Freq[3][19] = 462;\n        Big5Freq[8][109] = 461;\n        Big5Freq[3][15] = 460;\n        Big5Freq[31][82] = 459;\n        Big5Freq[3][43] = 458;\n        Big5Freq[25][119] = 457;\n        Big5Freq[16][111] = 456;\n        Big5Freq[7][77] = 455;\n        Big5Freq[3][95] = 454;\n        Big5Freq[24][82] = 453;\n        Big5Freq[7][52] = 452;\n        Big5Freq[9][151] = 451;\n        Big5Freq[3][129] = 450;\n        Big5Freq[5][87] = 449;\n        Big5Freq[3][55] = 448;\n        Big5Freq[8][153] = 447;\n        Big5Freq[4][83] = 446;\n        Big5Freq[3][114] = 445;\n        Big5Freq[23][147] = 444;\n        Big5Freq[15][31] = 443;\n        Big5Freq[3][54] = 442;\n        Big5Freq[11][122] = 441;\n        Big5Freq[4][4] = 440;\n        Big5Freq[34][149] = 439;\n        Big5Freq[3][17] = 438;\n        Big5Freq[21][64] = 437;\n        Big5Freq[26][144] = 436;\n        Big5Freq[4][62] = 435;\n        Big5Freq[8][15] = 434;\n        Big5Freq[35][80] = 433;\n        Big5Freq[7][110] = 432;\n        Big5Freq[23][114] = 431;\n        Big5Freq[3][108] = 430;\n        Big5Freq[3][62] = 429;\n        Big5Freq[21][41] = 428;\n        Big5Freq[15][99] = 427;\n        Big5Freq[5][47] = 426;\n        Big5Freq[4][96] = 425;\n        Big5Freq[20][122] = 424;\n        Big5Freq[5][21] = 423;\n        Big5Freq[4][157] = 422;\n        Big5Freq[16][14] = 421;\n        Big5Freq[3][117] = 420;\n        Big5Freq[7][129] = 419;\n        Big5Freq[4][27] = 418;\n        Big5Freq[5][30] = 417;\n        Big5Freq[22][16] = 416;\n        Big5Freq[5][64] = 415;\n        Big5Freq[17][99] = 414;\n        Big5Freq[17][57] = 413;\n        Big5Freq[8][105] = 412;\n        Big5Freq[5][112] = 411;\n        Big5Freq[20][59] = 410;\n        Big5Freq[6][129] = 409;\n        Big5Freq[18][17] = 408;\n        Big5Freq[3][92] = 407;\n        Big5Freq[28][118] = 406;\n        Big5Freq[3][109] = 405;\n        Big5Freq[31][51] = 404;\n        Big5Freq[13][116] = 403;\n        Big5Freq[6][15] = 402;\n        Big5Freq[36][136] = 401;\n        Big5Freq[12][74] = 400;\n        Big5Freq[20][88] = 399;\n        Big5Freq[36][68] = 398;\n        Big5Freq[3][147] = 397;\n        Big5Freq[15][84] = 396;\n        Big5Freq[16][32] = 395;\n        Big5Freq[16][58] = 394;\n        Big5Freq[7][66] = 393;\n        Big5Freq[23][107] = 392;\n        Big5Freq[9][6] = 391;\n        Big5Freq[12][86] = 390;\n        Big5Freq[23][112] = 389;\n        Big5Freq[37][23] = 388;\n        Big5Freq[3][138] = 387;\n        Big5Freq[20][68] = 386;\n        Big5Freq[15][116] = 385;\n        Big5Freq[18][64] = 384;\n        Big5Freq[12][139] = 383;\n        Big5Freq[11][155] = 382;\n        Big5Freq[4][156] = 381;\n        Big5Freq[12][84] = 380;\n        Big5Freq[18][49] = 379;\n        Big5Freq[25][125] = 378;\n        Big5Freq[25][147] = 377;\n        Big5Freq[15][110] = 376;\n        Big5Freq[19][96] = 375;\n        Big5Freq[30][152] = 374;\n        Big5Freq[6][31] = 373;\n        Big5Freq[27][117] = 372;\n        Big5Freq[3][10] = 371;\n        Big5Freq[6][131] = 370;\n        Big5Freq[13][112] = 369;\n        Big5Freq[36][156] = 368;\n        Big5Freq[4][60] = 367;\n        Big5Freq[15][121] = 366;\n        Big5Freq[4][112] = 365;\n        Big5Freq[30][142] = 364;\n        Big5Freq[23][154] = 363;\n        Big5Freq[27][101] = 362;\n        Big5Freq[9][140] = 361;\n        Big5Freq[3][89] = 360;\n        Big5Freq[18][148] = 359;\n        Big5Freq[4][69] = 358;\n        Big5Freq[16][49] = 357;\n        Big5Freq[6][117] = 356;\n        Big5Freq[36][55] = 355;\n        Big5Freq[5][123] = 354;\n        Big5Freq[4][126] = 353;\n        Big5Freq[4][119] = 352;\n        Big5Freq[9][95] = 351;\n        Big5Freq[5][24] = 350;\n        Big5Freq[16][133] = 349;\n        Big5Freq[10][134] = 348;\n        Big5Freq[26][59] = 347;\n        Big5Freq[6][41] = 346;\n        Big5Freq[6][146] = 345;\n        Big5Freq[19][24] = 344;\n        Big5Freq[5][113] = 343;\n        Big5Freq[10][118] = 342;\n        Big5Freq[34][151] = 341;\n        Big5Freq[9][72] = 340;\n        Big5Freq[31][25] = 339;\n        Big5Freq[18][126] = 338;\n        Big5Freq[18][28] = 337;\n        Big5Freq[4][153] = 336;\n        Big5Freq[3][84] = 335;\n        Big5Freq[21][18] = 334;\n        Big5Freq[25][129] = 333;\n        Big5Freq[6][107] = 332;\n        Big5Freq[12][25] = 331;\n        Big5Freq[17][109] = 330;\n        Big5Freq[7][76] = 329;\n        Big5Freq[15][15] = 328;\n        Big5Freq[4][14] = 327;\n        Big5Freq[23][88] = 326;\n        Big5Freq[18][2] = 325;\n        Big5Freq[6][88] = 324;\n        Big5Freq[16][84] = 323;\n        Big5Freq[12][48] = 322;\n        Big5Freq[7][68] = 321;\n        Big5Freq[5][50] = 320;\n        Big5Freq[13][54] = 319;\n        Big5Freq[7][98] = 318;\n        Big5Freq[11][6] = 317;\n        Big5Freq[9][80] = 316;\n        Big5Freq[16][41] = 315;\n        Big5Freq[7][43] = 314;\n        Big5Freq[28][117] = 313;\n        Big5Freq[3][51] = 312;\n        Big5Freq[7][3] = 311;\n        Big5Freq[20][81] = 310;\n        Big5Freq[4][2] = 309;\n        Big5Freq[11][16] = 308;\n        Big5Freq[10][4] = 307;\n        Big5Freq[10][119] = 306;\n        Big5Freq[6][142] = 305;\n        Big5Freq[18][51] = 304;\n        Big5Freq[8][144] = 303;\n        Big5Freq[10][65] = 302;\n        Big5Freq[11][64] = 301;\n        Big5Freq[11][130] = 300;\n        Big5Freq[9][92] = 299;\n        Big5Freq[18][29] = 298;\n        Big5Freq[18][78] = 297;\n        Big5Freq[18][151] = 296;\n        Big5Freq[33][127] = 295;\n        Big5Freq[35][113] = 294;\n        Big5Freq[10][155] = 293;\n        Big5Freq[3][76] = 292;\n        Big5Freq[36][123] = 291;\n        Big5Freq[13][143] = 290;\n        Big5Freq[5][135] = 289;\n        Big5Freq[23][116] = 288;\n        Big5Freq[6][101] = 287;\n        Big5Freq[14][74] = 286;\n        Big5Freq[7][153] = 285;\n        Big5Freq[3][101] = 284;\n        Big5Freq[9][74] = 283;\n        Big5Freq[3][156] = 282;\n        Big5Freq[4][147] = 281;\n        Big5Freq[9][12] = 280;\n        Big5Freq[18][133] = 279;\n        Big5Freq[4][0] = 278;\n        Big5Freq[7][155] = 277;\n        Big5Freq[9][144] = 276;\n        Big5Freq[23][49] = 275;\n        Big5Freq[5][89] = 274;\n        Big5Freq[10][11] = 273;\n        Big5Freq[3][110] = 272;\n        Big5Freq[3][40] = 271;\n        Big5Freq[29][115] = 270;\n        Big5Freq[9][100] = 269;\n        Big5Freq[21][67] = 268;\n        Big5Freq[23][145] = 267;\n        Big5Freq[10][47] = 266;\n        Big5Freq[4][31] = 265;\n        Big5Freq[4][81] = 264;\n        Big5Freq[22][62] = 263;\n        Big5Freq[4][28] = 262;\n        Big5Freq[27][39] = 261;\n        Big5Freq[27][54] = 260;\n        Big5Freq[32][46] = 259;\n        Big5Freq[4][76] = 258;\n        Big5Freq[26][15] = 257;\n        Big5Freq[12][154] = 256;\n        Big5Freq[9][150] = 255;\n        Big5Freq[15][17] = 254;\n        Big5Freq[5][129] = 253;\n        Big5Freq[10][40] = 252;\n        Big5Freq[13][37] = 251;\n        Big5Freq[31][104] = 250;\n        Big5Freq[3][152] = 249;\n        Big5Freq[5][22] = 248;\n        Big5Freq[8][48] = 247;\n        Big5Freq[4][74] = 246;\n        Big5Freq[6][17] = 245;\n        Big5Freq[30][82] = 244;\n        Big5Freq[4][116] = 243;\n        Big5Freq[16][42] = 242;\n        Big5Freq[5][55] = 241;\n        Big5Freq[4][64] = 240;\n        Big5Freq[14][19] = 239;\n        Big5Freq[35][82] = 238;\n        Big5Freq[30][139] = 237;\n        Big5Freq[26][152] = 236;\n        Big5Freq[32][32] = 235;\n        Big5Freq[21][102] = 234;\n        Big5Freq[10][131] = 233;\n        Big5Freq[9][128] = 232;\n        Big5Freq[3][87] = 231;\n        Big5Freq[4][51] = 230;\n        Big5Freq[10][15] = 229;\n        Big5Freq[4][150] = 228;\n        Big5Freq[7][4] = 227;\n        Big5Freq[7][51] = 226;\n        Big5Freq[7][157] = 225;\n        Big5Freq[4][146] = 224;\n        Big5Freq[4][91] = 223;\n        Big5Freq[7][13] = 222;\n        Big5Freq[17][116] = 221;\n        Big5Freq[23][21] = 220;\n        Big5Freq[5][106] = 219;\n        Big5Freq[14][100] = 218;\n        Big5Freq[10][152] = 217;\n        Big5Freq[14][89] = 216;\n        Big5Freq[6][138] = 215;\n        Big5Freq[12][157] = 214;\n        Big5Freq[10][102] = 213;\n        Big5Freq[19][94] = 212;\n        Big5Freq[7][74] = 211;\n        Big5Freq[18][128] = 210;\n        Big5Freq[27][111] = 209;\n        Big5Freq[11][57] = 208;\n        Big5Freq[3][131] = 207;\n        Big5Freq[30][23] = 206;\n        Big5Freq[30][126] = 205;\n        Big5Freq[4][36] = 204;\n        Big5Freq[26][124] = 203;\n        Big5Freq[4][19] = 202;\n        Big5Freq[9][152] = 201;\n        /*\n         * Big5Freq[5][0] = 200; Big5Freq[26][57] = 199; Big5Freq[13][155] =\n         * 198; Big5Freq[3][38] = 197; Big5Freq[9][155] = 196; Big5Freq[28][53]\n         * = 195; Big5Freq[15][71] = 194; Big5Freq[21][95] = 193;\n         * Big5Freq[15][112] = 192; Big5Freq[14][138] = 191; Big5Freq[8][18] =\n         * 190; Big5Freq[20][151] = 189; Big5Freq[37][27] = 188;\n         * Big5Freq[32][48] = 187; Big5Freq[23][66] = 186; Big5Freq[9][2] = 185;\n         * Big5Freq[13][133] = 184; Big5Freq[7][127] = 183; Big5Freq[3][11] =\n         * 182; Big5Freq[12][118] = 181; Big5Freq[13][101] = 180;\n         * Big5Freq[30][153] = 179; Big5Freq[4][65] = 178; Big5Freq[5][25] =\n         * 177; Big5Freq[5][140] = 176; Big5Freq[6][25] = 175; Big5Freq[4][52] =\n         * 174; Big5Freq[30][156] = 173; Big5Freq[16][13] = 172; Big5Freq[21][8]\n         * = 171; Big5Freq[19][74] = 170; Big5Freq[15][145] = 169;\n         * Big5Freq[9][15] = 168; Big5Freq[13][82] = 167; Big5Freq[26][86] =\n         * 166; Big5Freq[18][52] = 165; Big5Freq[6][109] = 164; Big5Freq[10][99]\n         * = 163; Big5Freq[18][101] = 162; Big5Freq[25][49] = 161;\n         * Big5Freq[31][79] = 160; Big5Freq[28][20] = 159; Big5Freq[12][115] =\n         * 158; Big5Freq[15][66] = 157; Big5Freq[11][104] = 156;\n         * Big5Freq[23][106] = 155; Big5Freq[34][157] = 154; Big5Freq[32][94] =\n         * 153; Big5Freq[29][88] = 152; Big5Freq[10][46] = 151;\n         * Big5Freq[13][118] = 150; Big5Freq[20][37] = 149; Big5Freq[12][30] =\n         * 148; Big5Freq[21][4] = 147; Big5Freq[16][33] = 146; Big5Freq[13][52]\n         * = 145; Big5Freq[4][7] = 144; Big5Freq[21][49] = 143; Big5Freq[3][27]\n         * = 142; Big5Freq[16][91] = 141; Big5Freq[5][155] = 140;\n         * Big5Freq[29][130] = 139; Big5Freq[3][125] = 138; Big5Freq[14][26] =\n         * 137; Big5Freq[15][39] = 136; Big5Freq[24][110] = 135;\n         * Big5Freq[7][141] = 134; Big5Freq[21][15] = 133; Big5Freq[32][104] =\n         * 132; Big5Freq[8][31] = 131; Big5Freq[34][112] = 130; Big5Freq[10][75]\n         * = 129; Big5Freq[21][23] = 128; Big5Freq[34][131] = 127;\n         * Big5Freq[12][3] = 126; Big5Freq[10][62] = 125; Big5Freq[9][120] =\n         * 124; Big5Freq[32][149] = 123; Big5Freq[8][44] = 122; Big5Freq[24][2]\n         * = 121; Big5Freq[6][148] = 120; Big5Freq[15][103] = 119;\n         * Big5Freq[36][54] = 118; Big5Freq[36][134] = 117; Big5Freq[11][7] =\n         * 116; Big5Freq[3][90] = 115; Big5Freq[36][73] = 114; Big5Freq[8][102]\n         * = 113; Big5Freq[12][87] = 112; Big5Freq[25][64] = 111; Big5Freq[9][1]\n         * = 110; Big5Freq[24][121] = 109; Big5Freq[5][75] = 108;\n         * Big5Freq[17][83] = 107; Big5Freq[18][57] = 106; Big5Freq[8][95] =\n         * 105; Big5Freq[14][36] = 104; Big5Freq[28][113] = 103;\n         * Big5Freq[12][56] = 102; Big5Freq[14][61] = 101; Big5Freq[25][138] =\n         * 100; Big5Freq[4][34] = 99; Big5Freq[11][152] = 98; Big5Freq[35][0] =\n         * 97; Big5Freq[4][15] = 96; Big5Freq[8][82] = 95; Big5Freq[20][73] =\n         * 94; Big5Freq[25][52] = 93; Big5Freq[24][6] = 92; Big5Freq[21][78] =\n         * 91; Big5Freq[17][32] = 90; Big5Freq[17][91] = 89; Big5Freq[5][76] =\n         * 88; Big5Freq[15][60] = 87; Big5Freq[15][150] = 86; Big5Freq[5][80] =\n         * 85; Big5Freq[15][81] = 84; Big5Freq[28][108] = 83; Big5Freq[18][14] =\n         * 82; Big5Freq[19][109] = 81; Big5Freq[28][133] = 80; Big5Freq[21][97]\n         * = 79; Big5Freq[5][105] = 78; Big5Freq[18][114] = 77; Big5Freq[16][95]\n         * = 76; Big5Freq[5][51] = 75; Big5Freq[3][148] = 74; Big5Freq[22][102]\n         * = 73; Big5Freq[4][123] = 72; Big5Freq[8][88] = 71; Big5Freq[25][111]\n         * = 70; Big5Freq[8][149] = 69; Big5Freq[9][48] = 68; Big5Freq[16][126]\n         * = 67; Big5Freq[33][150] = 66; Big5Freq[9][54] = 65; Big5Freq[29][104]\n         * = 64; Big5Freq[3][3] = 63; Big5Freq[11][49] = 62; Big5Freq[24][109] =\n         * 61; Big5Freq[28][116] = 60; Big5Freq[34][113] = 59; Big5Freq[5][3] =\n         * 58; Big5Freq[21][106] = 57; Big5Freq[4][98] = 56; Big5Freq[12][135] =\n         * 55; Big5Freq[16][101] = 54; Big5Freq[12][147] = 53; Big5Freq[27][55]\n         * = 52; Big5Freq[3][5] = 51; Big5Freq[11][101] = 50; Big5Freq[16][157]\n         * = 49; Big5Freq[22][114] = 48; Big5Freq[18][46] = 47; Big5Freq[4][29]\n         * = 46; Big5Freq[8][103] = 45; Big5Freq[16][151] = 44; Big5Freq[8][29]\n         * = 43; Big5Freq[15][114] = 42; Big5Freq[22][70] = 41;\n         * Big5Freq[13][121] = 40; Big5Freq[7][112] = 39; Big5Freq[20][83] = 38;\n         * Big5Freq[3][36] = 37; Big5Freq[10][103] = 36; Big5Freq[3][96] = 35;\n         * Big5Freq[21][79] = 34; Big5Freq[25][120] = 33; Big5Freq[29][121] =\n         * 32; Big5Freq[23][71] = 31; Big5Freq[21][22] = 30; Big5Freq[18][89] =\n         * 29; Big5Freq[25][104] = 28; Big5Freq[10][124] = 27; Big5Freq[26][4] =\n         * 26; Big5Freq[21][136] = 25; Big5Freq[6][112] = 24; Big5Freq[12][103]\n         * = 23; Big5Freq[17][66] = 22; Big5Freq[13][151] = 21;\n         * Big5Freq[33][152] = 20; Big5Freq[11][148] = 19; Big5Freq[13][57] =\n         * 18; Big5Freq[13][41] = 17; Big5Freq[7][60] = 16; Big5Freq[21][29] =\n         * 15; Big5Freq[9][157] = 14; Big5Freq[24][95] = 13; Big5Freq[15][148] =\n         * 12; Big5Freq[15][122] = 11; Big5Freq[6][125] = 10; Big5Freq[11][25] =\n         * 9; Big5Freq[20][55] = 8; Big5Freq[19][84] = 7; Big5Freq[21][82] = 6;\n         * Big5Freq[24][3] = 5; Big5Freq[13][70] = 4; Big5Freq[6][21] = 3;\n         * Big5Freq[21][86] = 2; Big5Freq[12][23] = 1; Big5Freq[3][85] = 0;\n         * EUC_TWFreq[45][90] = 600;\n         */\n        Big5PFreq[41][122] = 600;\n        Big5PFreq[35][0] = 599;\n        Big5PFreq[43][15] = 598;\n        Big5PFreq[35][99] = 597;\n        Big5PFreq[35][6] = 596;\n        Big5PFreq[35][8] = 595;\n        Big5PFreq[38][154] = 594;\n        Big5PFreq[37][34] = 593;\n        Big5PFreq[37][115] = 592;\n        Big5PFreq[36][12] = 591;\n        Big5PFreq[18][77] = 590;\n        Big5PFreq[35][100] = 589;\n        Big5PFreq[35][42] = 588;\n        Big5PFreq[120][75] = 587;\n        Big5PFreq[35][23] = 586;\n        Big5PFreq[13][72] = 585;\n        Big5PFreq[0][67] = 584;\n        Big5PFreq[39][172] = 583;\n        Big5PFreq[22][182] = 582;\n        Big5PFreq[15][186] = 581;\n        Big5PFreq[15][165] = 580;\n        Big5PFreq[35][44] = 579;\n        Big5PFreq[40][13] = 578;\n        Big5PFreq[38][1] = 577;\n        Big5PFreq[37][33] = 576;\n        Big5PFreq[36][24] = 575;\n        Big5PFreq[56][4] = 574;\n        Big5PFreq[35][29] = 573;\n        Big5PFreq[9][96] = 572;\n        Big5PFreq[37][62] = 571;\n        Big5PFreq[48][47] = 570;\n        Big5PFreq[51][14] = 569;\n        Big5PFreq[39][122] = 568;\n        Big5PFreq[44][46] = 567;\n        Big5PFreq[35][21] = 566;\n        Big5PFreq[36][8] = 565;\n        Big5PFreq[36][141] = 564;\n        Big5PFreq[3][81] = 563;\n        Big5PFreq[37][155] = 562;\n        Big5PFreq[42][84] = 561;\n        Big5PFreq[36][40] = 560;\n        Big5PFreq[35][103] = 559;\n        Big5PFreq[11][84] = 558;\n        Big5PFreq[45][33] = 557;\n        Big5PFreq[121][79] = 556;\n        Big5PFreq[2][77] = 555;\n        Big5PFreq[36][41] = 554;\n        Big5PFreq[37][47] = 553;\n        Big5PFreq[39][125] = 552;\n        Big5PFreq[37][26] = 551;\n        Big5PFreq[35][48] = 550;\n        Big5PFreq[35][28] = 549;\n        Big5PFreq[35][159] = 548;\n        Big5PFreq[37][40] = 547;\n        Big5PFreq[35][145] = 546;\n        Big5PFreq[37][147] = 545;\n        Big5PFreq[46][160] = 544;\n        Big5PFreq[37][46] = 543;\n        Big5PFreq[50][99] = 542;\n        Big5PFreq[52][13] = 541;\n        Big5PFreq[10][82] = 540;\n        Big5PFreq[35][169] = 539;\n        Big5PFreq[35][31] = 538;\n        Big5PFreq[47][31] = 537;\n        Big5PFreq[18][79] = 536;\n        Big5PFreq[16][113] = 535;\n        Big5PFreq[37][104] = 534;\n        Big5PFreq[39][134] = 533;\n        Big5PFreq[36][53] = 532;\n        Big5PFreq[38][0] = 531;\n        Big5PFreq[4][86] = 530;\n        Big5PFreq[54][17] = 529;\n        Big5PFreq[43][157] = 528;\n        Big5PFreq[35][165] = 527;\n        Big5PFreq[69][147] = 526;\n        Big5PFreq[117][95] = 525;\n        Big5PFreq[35][162] = 524;\n        Big5PFreq[35][17] = 523;\n        Big5PFreq[36][142] = 522;\n        Big5PFreq[36][4] = 521;\n        Big5PFreq[37][166] = 520;\n        Big5PFreq[35][168] = 519;\n        Big5PFreq[35][19] = 518;\n        Big5PFreq[37][48] = 517;\n        Big5PFreq[42][37] = 516;\n        Big5PFreq[40][146] = 515;\n        Big5PFreq[36][123] = 514;\n        Big5PFreq[22][41] = 513;\n        Big5PFreq[20][119] = 512;\n        Big5PFreq[2][74] = 511;\n        Big5PFreq[44][113] = 510;\n        Big5PFreq[35][125] = 509;\n        Big5PFreq[37][16] = 508;\n        Big5PFreq[35][20] = 507;\n        Big5PFreq[35][55] = 506;\n        Big5PFreq[37][145] = 505;\n        Big5PFreq[0][88] = 504;\n        Big5PFreq[3][94] = 503;\n        Big5PFreq[6][65] = 502;\n        Big5PFreq[26][15] = 501;\n        Big5PFreq[41][126] = 500;\n        Big5PFreq[36][129] = 499;\n        Big5PFreq[31][75] = 498;\n        Big5PFreq[19][61] = 497;\n        Big5PFreq[35][128] = 496;\n        Big5PFreq[29][79] = 495;\n        Big5PFreq[36][62] = 494;\n        Big5PFreq[37][189] = 493;\n        Big5PFreq[39][109] = 492;\n        Big5PFreq[39][135] = 491;\n        Big5PFreq[72][15] = 490;\n        Big5PFreq[47][106] = 489;\n        Big5PFreq[54][14] = 488;\n        Big5PFreq[24][52] = 487;\n        Big5PFreq[38][162] = 486;\n        Big5PFreq[41][43] = 485;\n        Big5PFreq[37][121] = 484;\n        Big5PFreq[14][66] = 483;\n        Big5PFreq[37][30] = 482;\n        Big5PFreq[35][7] = 481;\n        Big5PFreq[49][58] = 480;\n        Big5PFreq[43][188] = 479;\n        Big5PFreq[24][66] = 478;\n        Big5PFreq[35][171] = 477;\n        Big5PFreq[40][186] = 476;\n        Big5PFreq[39][164] = 475;\n        Big5PFreq[78][186] = 474;\n        Big5PFreq[8][72] = 473;\n        Big5PFreq[36][190] = 472;\n        Big5PFreq[35][53] = 471;\n        Big5PFreq[35][54] = 470;\n        Big5PFreq[22][159] = 469;\n        Big5PFreq[35][9] = 468;\n        Big5PFreq[41][140] = 467;\n        Big5PFreq[37][22] = 466;\n        Big5PFreq[48][97] = 465;\n        Big5PFreq[50][97] = 464;\n        Big5PFreq[36][127] = 463;\n        Big5PFreq[37][23] = 462;\n        Big5PFreq[40][55] = 461;\n        Big5PFreq[35][43] = 460;\n        Big5PFreq[26][22] = 459;\n        Big5PFreq[35][15] = 458;\n        Big5PFreq[72][179] = 457;\n        Big5PFreq[20][129] = 456;\n        Big5PFreq[52][101] = 455;\n        Big5PFreq[35][12] = 454;\n        Big5PFreq[42][156] = 453;\n        Big5PFreq[15][157] = 452;\n        Big5PFreq[50][140] = 451;\n        Big5PFreq[26][28] = 450;\n        Big5PFreq[54][51] = 449;\n        Big5PFreq[35][112] = 448;\n        Big5PFreq[36][116] = 447;\n        Big5PFreq[42][11] = 446;\n        Big5PFreq[37][172] = 445;\n        Big5PFreq[37][29] = 444;\n        Big5PFreq[44][107] = 443;\n        Big5PFreq[50][17] = 442;\n        Big5PFreq[39][107] = 441;\n        Big5PFreq[19][109] = 440;\n        Big5PFreq[36][60] = 439;\n        Big5PFreq[49][132] = 438;\n        Big5PFreq[26][16] = 437;\n        Big5PFreq[43][155] = 436;\n        Big5PFreq[37][120] = 435;\n        Big5PFreq[15][159] = 434;\n        Big5PFreq[43][6] = 433;\n        Big5PFreq[45][188] = 432;\n        Big5PFreq[35][38] = 431;\n        Big5PFreq[39][143] = 430;\n        Big5PFreq[48][144] = 429;\n        Big5PFreq[37][168] = 428;\n        Big5PFreq[37][1] = 427;\n        Big5PFreq[36][109] = 426;\n        Big5PFreq[46][53] = 425;\n        Big5PFreq[38][54] = 424;\n        Big5PFreq[36][0] = 423;\n        Big5PFreq[72][33] = 422;\n        Big5PFreq[42][8] = 421;\n        Big5PFreq[36][31] = 420;\n        Big5PFreq[35][150] = 419;\n        Big5PFreq[118][93] = 418;\n        Big5PFreq[37][61] = 417;\n        Big5PFreq[0][85] = 416;\n        Big5PFreq[36][27] = 415;\n        Big5PFreq[35][134] = 414;\n        Big5PFreq[36][145] = 413;\n        Big5PFreq[6][96] = 412;\n        Big5PFreq[36][14] = 411;\n        Big5PFreq[16][36] = 410;\n        Big5PFreq[15][175] = 409;\n        Big5PFreq[35][10] = 408;\n        Big5PFreq[36][189] = 407;\n        Big5PFreq[35][51] = 406;\n        Big5PFreq[35][109] = 405;\n        Big5PFreq[35][147] = 404;\n        Big5PFreq[35][180] = 403;\n        Big5PFreq[72][5] = 402;\n        Big5PFreq[36][107] = 401;\n        Big5PFreq[49][116] = 400;\n        Big5PFreq[73][30] = 399;\n        Big5PFreq[6][90] = 398;\n        Big5PFreq[2][70] = 397;\n        Big5PFreq[17][141] = 396;\n        Big5PFreq[35][62] = 395;\n        Big5PFreq[16][180] = 394;\n        Big5PFreq[4][91] = 393;\n        Big5PFreq[15][171] = 392;\n        Big5PFreq[35][177] = 391;\n        Big5PFreq[37][173] = 390;\n        Big5PFreq[16][121] = 389;\n        Big5PFreq[35][5] = 388;\n        Big5PFreq[46][122] = 387;\n        Big5PFreq[40][138] = 386;\n        Big5PFreq[50][49] = 385;\n        Big5PFreq[36][152] = 384;\n        Big5PFreq[13][43] = 383;\n        Big5PFreq[9][88] = 382;\n        Big5PFreq[36][159] = 381;\n        Big5PFreq[27][62] = 380;\n        Big5PFreq[40][18] = 379;\n        Big5PFreq[17][129] = 378;\n        Big5PFreq[43][97] = 377;\n        Big5PFreq[13][131] = 376;\n        Big5PFreq[46][107] = 375;\n        Big5PFreq[60][64] = 374;\n        Big5PFreq[36][179] = 373;\n        Big5PFreq[37][55] = 372;\n        Big5PFreq[41][173] = 371;\n        Big5PFreq[44][172] = 370;\n        Big5PFreq[23][187] = 369;\n        Big5PFreq[36][149] = 368;\n        Big5PFreq[17][125] = 367;\n        Big5PFreq[55][180] = 366;\n        Big5PFreq[51][129] = 365;\n        Big5PFreq[36][51] = 364;\n        Big5PFreq[37][122] = 363;\n        Big5PFreq[48][32] = 362;\n        Big5PFreq[51][99] = 361;\n        Big5PFreq[54][16] = 360;\n        Big5PFreq[41][183] = 359;\n        Big5PFreq[37][179] = 358;\n        Big5PFreq[38][179] = 357;\n        Big5PFreq[35][143] = 356;\n        Big5PFreq[37][24] = 355;\n        Big5PFreq[40][177] = 354;\n        Big5PFreq[47][117] = 353;\n        Big5PFreq[39][52] = 352;\n        Big5PFreq[22][99] = 351;\n        Big5PFreq[40][142] = 350;\n        Big5PFreq[36][49] = 349;\n        Big5PFreq[38][17] = 348;\n        Big5PFreq[39][188] = 347;\n        Big5PFreq[36][186] = 346;\n        Big5PFreq[35][189] = 345;\n        Big5PFreq[41][7] = 344;\n        Big5PFreq[18][91] = 343;\n        Big5PFreq[43][137] = 342;\n        Big5PFreq[35][142] = 341;\n        Big5PFreq[35][117] = 340;\n        Big5PFreq[39][138] = 339;\n        Big5PFreq[16][59] = 338;\n        Big5PFreq[39][174] = 337;\n        Big5PFreq[55][145] = 336;\n        Big5PFreq[37][21] = 335;\n        Big5PFreq[36][180] = 334;\n        Big5PFreq[37][156] = 333;\n        Big5PFreq[49][13] = 332;\n        Big5PFreq[41][107] = 331;\n        Big5PFreq[36][56] = 330;\n        Big5PFreq[53][8] = 329;\n        Big5PFreq[22][114] = 328;\n        Big5PFreq[5][95] = 327;\n        Big5PFreq[37][0] = 326;\n        Big5PFreq[26][183] = 325;\n        Big5PFreq[22][66] = 324;\n        Big5PFreq[35][58] = 323;\n        Big5PFreq[48][117] = 322;\n        Big5PFreq[36][102] = 321;\n        Big5PFreq[22][122] = 320;\n        Big5PFreq[35][11] = 319;\n        Big5PFreq[46][19] = 318;\n        Big5PFreq[22][49] = 317;\n        Big5PFreq[48][166] = 316;\n        Big5PFreq[41][125] = 315;\n        Big5PFreq[41][1] = 314;\n        Big5PFreq[35][178] = 313;\n        Big5PFreq[41][12] = 312;\n        Big5PFreq[26][167] = 311;\n        Big5PFreq[42][152] = 310;\n        Big5PFreq[42][46] = 309;\n        Big5PFreq[42][151] = 308;\n        Big5PFreq[20][135] = 307;\n        Big5PFreq[37][162] = 306;\n        Big5PFreq[37][50] = 305;\n        Big5PFreq[22][185] = 304;\n        Big5PFreq[36][166] = 303;\n        Big5PFreq[19][40] = 302;\n        Big5PFreq[22][107] = 301;\n        Big5PFreq[22][102] = 300;\n        Big5PFreq[57][162] = 299;\n        Big5PFreq[22][124] = 298;\n        Big5PFreq[37][138] = 297;\n        Big5PFreq[37][25] = 296;\n        Big5PFreq[0][69] = 295;\n        Big5PFreq[43][172] = 294;\n        Big5PFreq[42][167] = 293;\n        Big5PFreq[35][120] = 292;\n        Big5PFreq[41][128] = 291;\n        Big5PFreq[2][88] = 290;\n        Big5PFreq[20][123] = 289;\n        Big5PFreq[35][123] = 288;\n        Big5PFreq[36][28] = 287;\n        Big5PFreq[42][188] = 286;\n        Big5PFreq[42][164] = 285;\n        Big5PFreq[42][4] = 284;\n        Big5PFreq[43][57] = 283;\n        Big5PFreq[39][3] = 282;\n        Big5PFreq[42][3] = 281;\n        Big5PFreq[57][158] = 280;\n        Big5PFreq[35][146] = 279;\n        Big5PFreq[24][54] = 278;\n        Big5PFreq[13][110] = 277;\n        Big5PFreq[23][132] = 276;\n        Big5PFreq[26][102] = 275;\n        Big5PFreq[55][178] = 274;\n        Big5PFreq[17][117] = 273;\n        Big5PFreq[41][161] = 272;\n        Big5PFreq[38][150] = 271;\n        Big5PFreq[10][71] = 270;\n        Big5PFreq[47][60] = 269;\n        Big5PFreq[16][114] = 268;\n        Big5PFreq[21][47] = 267;\n        Big5PFreq[39][101] = 266;\n        Big5PFreq[18][45] = 265;\n        Big5PFreq[40][121] = 264;\n        Big5PFreq[45][41] = 263;\n        Big5PFreq[22][167] = 262;\n        Big5PFreq[26][149] = 261;\n        Big5PFreq[15][189] = 260;\n        Big5PFreq[41][177] = 259;\n        Big5PFreq[46][36] = 258;\n        Big5PFreq[20][40] = 257;\n        Big5PFreq[41][54] = 256;\n        Big5PFreq[3][87] = 255;\n        Big5PFreq[40][16] = 254;\n        Big5PFreq[42][15] = 253;\n        Big5PFreq[11][83] = 252;\n        Big5PFreq[0][94] = 251;\n        Big5PFreq[122][81] = 250;\n        Big5PFreq[41][26] = 249;\n        Big5PFreq[36][34] = 248;\n        Big5PFreq[44][148] = 247;\n        Big5PFreq[35][3] = 246;\n        Big5PFreq[36][114] = 245;\n        Big5PFreq[42][112] = 244;\n        Big5PFreq[35][183] = 243;\n        Big5PFreq[49][73] = 242;\n        Big5PFreq[39][2] = 241;\n        Big5PFreq[38][121] = 240;\n        Big5PFreq[44][114] = 239;\n        Big5PFreq[49][32] = 238;\n        Big5PFreq[1][65] = 237;\n        Big5PFreq[38][25] = 236;\n        Big5PFreq[39][4] = 235;\n        Big5PFreq[42][62] = 234;\n        Big5PFreq[35][40] = 233;\n        Big5PFreq[24][2] = 232;\n        Big5PFreq[53][49] = 231;\n        Big5PFreq[41][133] = 230;\n        Big5PFreq[43][134] = 229;\n        Big5PFreq[3][83] = 228;\n        Big5PFreq[38][158] = 227;\n        Big5PFreq[24][17] = 226;\n        Big5PFreq[52][59] = 225;\n        Big5PFreq[38][41] = 224;\n        Big5PFreq[37][127] = 223;\n        Big5PFreq[22][175] = 222;\n        Big5PFreq[44][30] = 221;\n        Big5PFreq[47][178] = 220;\n        Big5PFreq[43][99] = 219;\n        Big5PFreq[19][4] = 218;\n        Big5PFreq[37][97] = 217;\n        Big5PFreq[38][181] = 216;\n        Big5PFreq[45][103] = 215;\n        Big5PFreq[1][86] = 214;\n        Big5PFreq[40][15] = 213;\n        Big5PFreq[22][136] = 212;\n        Big5PFreq[75][165] = 211;\n        Big5PFreq[36][15] = 210;\n        Big5PFreq[46][80] = 209;\n        Big5PFreq[59][55] = 208;\n        Big5PFreq[37][108] = 207;\n        Big5PFreq[21][109] = 206;\n        Big5PFreq[24][165] = 205;\n        Big5PFreq[79][158] = 204;\n        Big5PFreq[44][139] = 203;\n        Big5PFreq[36][124] = 202;\n        Big5PFreq[42][185] = 201;\n        Big5PFreq[39][186] = 200;\n        Big5PFreq[22][128] = 199;\n        Big5PFreq[40][44] = 198;\n        Big5PFreq[41][105] = 197;\n        Big5PFreq[1][70] = 196;\n        Big5PFreq[1][68] = 195;\n        Big5PFreq[53][22] = 194;\n        Big5PFreq[36][54] = 193;\n        Big5PFreq[47][147] = 192;\n        Big5PFreq[35][36] = 191;\n        Big5PFreq[35][185] = 190;\n        Big5PFreq[45][37] = 189;\n        Big5PFreq[43][163] = 188;\n        Big5PFreq[56][115] = 187;\n        Big5PFreq[38][164] = 186;\n        Big5PFreq[35][141] = 185;\n        Big5PFreq[42][132] = 184;\n        Big5PFreq[46][120] = 183;\n        Big5PFreq[69][142] = 182;\n        Big5PFreq[38][175] = 181;\n        Big5PFreq[22][112] = 180;\n        Big5PFreq[38][142] = 179;\n        Big5PFreq[40][37] = 178;\n        Big5PFreq[37][109] = 177;\n        Big5PFreq[40][144] = 176;\n        Big5PFreq[44][117] = 175;\n        Big5PFreq[35][181] = 174;\n        Big5PFreq[26][105] = 173;\n        Big5PFreq[16][48] = 172;\n        Big5PFreq[44][122] = 171;\n        Big5PFreq[12][86] = 170;\n        Big5PFreq[84][53] = 169;\n        Big5PFreq[17][44] = 168;\n        Big5PFreq[59][54] = 167;\n        Big5PFreq[36][98] = 166;\n        Big5PFreq[45][115] = 165;\n        Big5PFreq[73][9] = 164;\n        Big5PFreq[44][123] = 163;\n        Big5PFreq[37][188] = 162;\n        Big5PFreq[51][117] = 161;\n        Big5PFreq[15][156] = 160;\n        Big5PFreq[36][155] = 159;\n        Big5PFreq[44][25] = 158;\n        Big5PFreq[38][12] = 157;\n        Big5PFreq[38][140] = 156;\n        Big5PFreq[23][4] = 155;\n        Big5PFreq[45][149] = 154;\n        Big5PFreq[22][189] = 153;\n        Big5PFreq[38][147] = 152;\n        Big5PFreq[27][5] = 151;\n        Big5PFreq[22][42] = 150;\n        Big5PFreq[3][68] = 149;\n        Big5PFreq[39][51] = 148;\n        Big5PFreq[36][29] = 147;\n        Big5PFreq[20][108] = 146;\n        Big5PFreq[50][57] = 145;\n        Big5PFreq[55][104] = 144;\n        Big5PFreq[22][46] = 143;\n        Big5PFreq[18][164] = 142;\n        Big5PFreq[50][159] = 141;\n        Big5PFreq[85][131] = 140;\n        Big5PFreq[26][79] = 139;\n        Big5PFreq[38][100] = 138;\n        Big5PFreq[53][112] = 137;\n        Big5PFreq[20][190] = 136;\n        Big5PFreq[14][69] = 135;\n        Big5PFreq[23][11] = 134;\n        Big5PFreq[40][114] = 133;\n        Big5PFreq[40][148] = 132;\n        Big5PFreq[53][130] = 131;\n        Big5PFreq[36][2] = 130;\n        Big5PFreq[66][82] = 129;\n        Big5PFreq[45][166] = 128;\n        Big5PFreq[4][88] = 127;\n        Big5PFreq[16][57] = 126;\n        Big5PFreq[22][116] = 125;\n        Big5PFreq[36][108] = 124;\n        Big5PFreq[13][48] = 123;\n        Big5PFreq[54][12] = 122;\n        Big5PFreq[40][136] = 121;\n        Big5PFreq[36][128] = 120;\n        Big5PFreq[23][6] = 119;\n        Big5PFreq[38][125] = 118;\n        Big5PFreq[45][154] = 117;\n        Big5PFreq[51][127] = 116;\n        Big5PFreq[44][163] = 115;\n        Big5PFreq[16][173] = 114;\n        Big5PFreq[43][49] = 113;\n        Big5PFreq[20][112] = 112;\n        Big5PFreq[15][168] = 111;\n        Big5PFreq[35][129] = 110;\n        Big5PFreq[20][45] = 109;\n        Big5PFreq[38][10] = 108;\n        Big5PFreq[57][171] = 107;\n        Big5PFreq[44][190] = 106;\n        Big5PFreq[40][56] = 105;\n        Big5PFreq[36][156] = 104;\n        Big5PFreq[3][88] = 103;\n        Big5PFreq[50][122] = 102;\n        Big5PFreq[36][7] = 101;\n        Big5PFreq[39][43] = 100;\n        Big5PFreq[15][166] = 99;\n        Big5PFreq[42][136] = 98;\n        Big5PFreq[22][131] = 97;\n        Big5PFreq[44][23] = 96;\n        Big5PFreq[54][147] = 95;\n        Big5PFreq[41][32] = 94;\n        Big5PFreq[23][121] = 93;\n        Big5PFreq[39][108] = 92;\n        Big5PFreq[2][78] = 91;\n        Big5PFreq[40][155] = 90;\n        Big5PFreq[55][51] = 89;\n        Big5PFreq[19][34] = 88;\n        Big5PFreq[48][128] = 87;\n        Big5PFreq[48][159] = 86;\n        Big5PFreq[20][70] = 85;\n        Big5PFreq[34][71] = 84;\n        Big5PFreq[16][31] = 83;\n        Big5PFreq[42][157] = 82;\n        Big5PFreq[20][44] = 81;\n        Big5PFreq[11][92] = 80;\n        Big5PFreq[44][180] = 79;\n        Big5PFreq[84][33] = 78;\n        Big5PFreq[16][116] = 77;\n        Big5PFreq[61][163] = 76;\n        Big5PFreq[35][164] = 75;\n        Big5PFreq[36][42] = 74;\n        Big5PFreq[13][40] = 73;\n        Big5PFreq[43][176] = 72;\n        Big5PFreq[2][66] = 71;\n        Big5PFreq[20][133] = 70;\n        Big5PFreq[36][65] = 69;\n        Big5PFreq[38][33] = 68;\n        Big5PFreq[12][91] = 67;\n        Big5PFreq[36][26] = 66;\n        Big5PFreq[15][174] = 65;\n        Big5PFreq[77][32] = 64;\n        Big5PFreq[16][1] = 63;\n        Big5PFreq[25][86] = 62;\n        Big5PFreq[17][13] = 61;\n        Big5PFreq[5][75] = 60;\n        Big5PFreq[36][52] = 59;\n        Big5PFreq[51][164] = 58;\n        Big5PFreq[12][85] = 57;\n        Big5PFreq[39][168] = 56;\n        Big5PFreq[43][16] = 55;\n        Big5PFreq[40][69] = 54;\n        Big5PFreq[26][108] = 53;\n        Big5PFreq[51][56] = 52;\n        Big5PFreq[16][37] = 51;\n        Big5PFreq[40][29] = 50;\n        Big5PFreq[46][171] = 49;\n        Big5PFreq[40][128] = 48;\n        Big5PFreq[72][114] = 47;\n        Big5PFreq[21][103] = 46;\n        Big5PFreq[22][44] = 45;\n        Big5PFreq[40][115] = 44;\n        Big5PFreq[43][7] = 43;\n        Big5PFreq[43][153] = 42;\n        Big5PFreq[17][20] = 41;\n        Big5PFreq[16][49] = 40;\n        Big5PFreq[36][57] = 39;\n        Big5PFreq[18][38] = 38;\n        Big5PFreq[45][184] = 37;\n        Big5PFreq[37][167] = 36;\n        Big5PFreq[26][106] = 35;\n        Big5PFreq[61][121] = 34;\n        Big5PFreq[89][140] = 33;\n        Big5PFreq[46][61] = 32;\n        Big5PFreq[39][163] = 31;\n        Big5PFreq[40][62] = 30;\n        Big5PFreq[38][165] = 29;\n        Big5PFreq[47][37] = 28;\n        Big5PFreq[18][155] = 27;\n        Big5PFreq[20][33] = 26;\n        Big5PFreq[29][90] = 25;\n        Big5PFreq[20][103] = 24;\n        Big5PFreq[37][51] = 23;\n        Big5PFreq[57][0] = 22;\n        Big5PFreq[40][31] = 21;\n        Big5PFreq[45][32] = 20;\n        Big5PFreq[59][23] = 19;\n        Big5PFreq[18][47] = 18;\n        Big5PFreq[45][134] = 17;\n        Big5PFreq[37][59] = 16;\n        Big5PFreq[21][128] = 15;\n        Big5PFreq[36][106] = 14;\n        Big5PFreq[31][39] = 13;\n        Big5PFreq[40][182] = 12;\n        Big5PFreq[52][155] = 11;\n        Big5PFreq[42][166] = 10;\n        Big5PFreq[35][27] = 9;\n        Big5PFreq[38][3] = 8;\n        Big5PFreq[13][44] = 7;\n        Big5PFreq[58][157] = 6;\n        Big5PFreq[47][51] = 5;\n        Big5PFreq[41][37] = 4;\n        Big5PFreq[41][172] = 3;\n        Big5PFreq[51][165] = 2;\n        Big5PFreq[15][161] = 1;\n        Big5PFreq[24][181] = 0;\n        EUC_TWFreq[48][49] = 599;\n        EUC_TWFreq[35][65] = 598;\n        EUC_TWFreq[41][27] = 597;\n        EUC_TWFreq[35][0] = 596;\n        EUC_TWFreq[39][19] = 595;\n        EUC_TWFreq[35][42] = 594;\n        EUC_TWFreq[38][66] = 593;\n        EUC_TWFreq[35][8] = 592;\n        EUC_TWFreq[35][6] = 591;\n        EUC_TWFreq[35][66] = 590;\n        EUC_TWFreq[43][14] = 589;\n        EUC_TWFreq[69][80] = 588;\n        EUC_TWFreq[50][48] = 587;\n        EUC_TWFreq[36][71] = 586;\n        EUC_TWFreq[37][10] = 585;\n        EUC_TWFreq[60][52] = 584;\n        EUC_TWFreq[51][21] = 583;\n        EUC_TWFreq[40][2] = 582;\n        EUC_TWFreq[67][35] = 581;\n        EUC_TWFreq[38][78] = 580;\n        EUC_TWFreq[49][18] = 579;\n        EUC_TWFreq[35][23] = 578;\n        EUC_TWFreq[42][83] = 577;\n        EUC_TWFreq[79][47] = 576;\n        EUC_TWFreq[61][82] = 575;\n        EUC_TWFreq[38][7] = 574;\n        EUC_TWFreq[35][29] = 573;\n        EUC_TWFreq[37][77] = 572;\n        EUC_TWFreq[54][67] = 571;\n        EUC_TWFreq[38][80] = 570;\n        EUC_TWFreq[52][74] = 569;\n        EUC_TWFreq[36][37] = 568;\n        EUC_TWFreq[74][8] = 567;\n        EUC_TWFreq[41][83] = 566;\n        EUC_TWFreq[36][75] = 565;\n        EUC_TWFreq[49][63] = 564;\n        EUC_TWFreq[42][58] = 563;\n        EUC_TWFreq[56][33] = 562;\n        EUC_TWFreq[37][76] = 561;\n        EUC_TWFreq[62][39] = 560;\n        EUC_TWFreq[35][21] = 559;\n        EUC_TWFreq[70][19] = 558;\n        EUC_TWFreq[77][88] = 557;\n        EUC_TWFreq[51][14] = 556;\n        EUC_TWFreq[36][17] = 555;\n        EUC_TWFreq[44][51] = 554;\n        EUC_TWFreq[38][72] = 553;\n        EUC_TWFreq[74][90] = 552;\n        EUC_TWFreq[35][48] = 551;\n        EUC_TWFreq[35][69] = 550;\n        EUC_TWFreq[66][86] = 549;\n        EUC_TWFreq[57][20] = 548;\n        EUC_TWFreq[35][53] = 547;\n        EUC_TWFreq[36][87] = 546;\n        EUC_TWFreq[84][67] = 545;\n        EUC_TWFreq[70][56] = 544;\n        EUC_TWFreq[71][54] = 543;\n        EUC_TWFreq[60][70] = 542;\n        EUC_TWFreq[80][1] = 541;\n        EUC_TWFreq[39][59] = 540;\n        EUC_TWFreq[39][51] = 539;\n        EUC_TWFreq[35][44] = 538;\n        EUC_TWFreq[48][4] = 537;\n        EUC_TWFreq[55][24] = 536;\n        EUC_TWFreq[52][4] = 535;\n        EUC_TWFreq[54][26] = 534;\n        EUC_TWFreq[36][31] = 533;\n        EUC_TWFreq[37][22] = 532;\n        EUC_TWFreq[37][9] = 531;\n        EUC_TWFreq[46][0] = 530;\n        EUC_TWFreq[56][46] = 529;\n        EUC_TWFreq[47][93] = 528;\n        EUC_TWFreq[37][25] = 527;\n        EUC_TWFreq[39][8] = 526;\n        EUC_TWFreq[46][73] = 525;\n        EUC_TWFreq[38][48] = 524;\n        EUC_TWFreq[39][83] = 523;\n        EUC_TWFreq[60][92] = 522;\n        EUC_TWFreq[70][11] = 521;\n        EUC_TWFreq[63][84] = 520;\n        EUC_TWFreq[38][65] = 519;\n        EUC_TWFreq[45][45] = 518;\n        EUC_TWFreq[63][49] = 517;\n        EUC_TWFreq[63][50] = 516;\n        EUC_TWFreq[39][93] = 515;\n        EUC_TWFreq[68][20] = 514;\n        EUC_TWFreq[44][84] = 513;\n        EUC_TWFreq[66][34] = 512;\n        EUC_TWFreq[37][58] = 511;\n        EUC_TWFreq[39][0] = 510;\n        EUC_TWFreq[59][1] = 509;\n        EUC_TWFreq[47][8] = 508;\n        EUC_TWFreq[61][17] = 507;\n        EUC_TWFreq[53][87] = 506;\n        EUC_TWFreq[67][26] = 505;\n        EUC_TWFreq[43][46] = 504;\n        EUC_TWFreq[38][61] = 503;\n        EUC_TWFreq[45][9] = 502;\n        EUC_TWFreq[66][83] = 501;\n        EUC_TWFreq[43][88] = 500;\n        EUC_TWFreq[85][20] = 499;\n        EUC_TWFreq[57][36] = 498;\n        EUC_TWFreq[43][6] = 497;\n        EUC_TWFreq[86][77] = 496;\n        EUC_TWFreq[42][70] = 495;\n        EUC_TWFreq[49][78] = 494;\n        EUC_TWFreq[36][40] = 493;\n        EUC_TWFreq[42][71] = 492;\n        EUC_TWFreq[58][49] = 491;\n        EUC_TWFreq[35][20] = 490;\n        EUC_TWFreq[76][20] = 489;\n        EUC_TWFreq[39][25] = 488;\n        EUC_TWFreq[40][34] = 487;\n        EUC_TWFreq[39][76] = 486;\n        EUC_TWFreq[40][1] = 485;\n        EUC_TWFreq[59][0] = 484;\n        EUC_TWFreq[39][70] = 483;\n        EUC_TWFreq[46][14] = 482;\n        EUC_TWFreq[68][77] = 481;\n        EUC_TWFreq[38][55] = 480;\n        EUC_TWFreq[35][78] = 479;\n        EUC_TWFreq[84][44] = 478;\n        EUC_TWFreq[36][41] = 477;\n        EUC_TWFreq[37][62] = 476;\n        EUC_TWFreq[65][67] = 475;\n        EUC_TWFreq[69][66] = 474;\n        EUC_TWFreq[73][55] = 473;\n        EUC_TWFreq[71][49] = 472;\n        EUC_TWFreq[66][87] = 471;\n        EUC_TWFreq[38][33] = 470;\n        EUC_TWFreq[64][61] = 469;\n        EUC_TWFreq[35][7] = 468;\n        EUC_TWFreq[47][49] = 467;\n        EUC_TWFreq[56][14] = 466;\n        EUC_TWFreq[36][49] = 465;\n        EUC_TWFreq[50][81] = 464;\n        EUC_TWFreq[55][76] = 463;\n        EUC_TWFreq[35][19] = 462;\n        EUC_TWFreq[44][47] = 461;\n        EUC_TWFreq[35][15] = 460;\n        EUC_TWFreq[82][59] = 459;\n        EUC_TWFreq[35][43] = 458;\n        EUC_TWFreq[73][0] = 457;\n        EUC_TWFreq[57][83] = 456;\n        EUC_TWFreq[42][46] = 455;\n        EUC_TWFreq[36][0] = 454;\n        EUC_TWFreq[70][88] = 453;\n        EUC_TWFreq[42][22] = 452;\n        EUC_TWFreq[46][58] = 451;\n        EUC_TWFreq[36][34] = 450;\n        EUC_TWFreq[39][24] = 449;\n        EUC_TWFreq[35][55] = 448;\n        EUC_TWFreq[44][91] = 447;\n        EUC_TWFreq[37][51] = 446;\n        EUC_TWFreq[36][19] = 445;\n        EUC_TWFreq[69][90] = 444;\n        EUC_TWFreq[55][35] = 443;\n        EUC_TWFreq[35][54] = 442;\n        EUC_TWFreq[49][61] = 441;\n        EUC_TWFreq[36][67] = 440;\n        EUC_TWFreq[88][34] = 439;\n        EUC_TWFreq[35][17] = 438;\n        EUC_TWFreq[65][69] = 437;\n        EUC_TWFreq[74][89] = 436;\n        EUC_TWFreq[37][31] = 435;\n        EUC_TWFreq[43][48] = 434;\n        EUC_TWFreq[89][27] = 433;\n        EUC_TWFreq[42][79] = 432;\n        EUC_TWFreq[69][57] = 431;\n        EUC_TWFreq[36][13] = 430;\n        EUC_TWFreq[35][62] = 429;\n        EUC_TWFreq[65][47] = 428;\n        EUC_TWFreq[56][8] = 427;\n        EUC_TWFreq[38][79] = 426;\n        EUC_TWFreq[37][64] = 425;\n        EUC_TWFreq[64][64] = 424;\n        EUC_TWFreq[38][53] = 423;\n        EUC_TWFreq[38][31] = 422;\n        EUC_TWFreq[56][81] = 421;\n        EUC_TWFreq[36][22] = 420;\n        EUC_TWFreq[43][4] = 419;\n        EUC_TWFreq[36][90] = 418;\n        EUC_TWFreq[38][62] = 417;\n        EUC_TWFreq[66][85] = 416;\n        EUC_TWFreq[39][1] = 415;\n        EUC_TWFreq[59][40] = 414;\n        EUC_TWFreq[58][93] = 413;\n        EUC_TWFreq[44][43] = 412;\n        EUC_TWFreq[39][49] = 411;\n        EUC_TWFreq[64][2] = 410;\n        EUC_TWFreq[41][35] = 409;\n        EUC_TWFreq[60][22] = 408;\n        EUC_TWFreq[35][91] = 407;\n        EUC_TWFreq[78][1] = 406;\n        EUC_TWFreq[36][14] = 405;\n        EUC_TWFreq[82][29] = 404;\n        EUC_TWFreq[52][86] = 403;\n        EUC_TWFreq[40][16] = 402;\n        EUC_TWFreq[91][52] = 401;\n        EUC_TWFreq[50][75] = 400;\n        EUC_TWFreq[64][30] = 399;\n        EUC_TWFreq[90][78] = 398;\n        EUC_TWFreq[36][52] = 397;\n        EUC_TWFreq[55][87] = 396;\n        EUC_TWFreq[57][5] = 395;\n        EUC_TWFreq[57][31] = 394;\n        EUC_TWFreq[42][35] = 393;\n        EUC_TWFreq[69][50] = 392;\n        EUC_TWFreq[45][8] = 391;\n        EUC_TWFreq[50][87] = 390;\n        EUC_TWFreq[69][55] = 389;\n        EUC_TWFreq[92][3] = 388;\n        EUC_TWFreq[36][43] = 387;\n        EUC_TWFreq[64][10] = 386;\n        EUC_TWFreq[56][25] = 385;\n        EUC_TWFreq[60][68] = 384;\n        EUC_TWFreq[51][46] = 383;\n        EUC_TWFreq[50][0] = 382;\n        EUC_TWFreq[38][30] = 381;\n        EUC_TWFreq[50][85] = 380;\n        EUC_TWFreq[60][54] = 379;\n        EUC_TWFreq[73][6] = 378;\n        EUC_TWFreq[73][28] = 377;\n        EUC_TWFreq[56][19] = 376;\n        EUC_TWFreq[62][69] = 375;\n        EUC_TWFreq[81][66] = 374;\n        EUC_TWFreq[40][32] = 373;\n        EUC_TWFreq[76][31] = 372;\n        EUC_TWFreq[35][10] = 371;\n        EUC_TWFreq[41][37] = 370;\n        EUC_TWFreq[52][82] = 369;\n        EUC_TWFreq[91][72] = 368;\n        EUC_TWFreq[37][29] = 367;\n        EUC_TWFreq[56][30] = 366;\n        EUC_TWFreq[37][80] = 365;\n        EUC_TWFreq[81][56] = 364;\n        EUC_TWFreq[70][3] = 363;\n        EUC_TWFreq[76][15] = 362;\n        EUC_TWFreq[46][47] = 361;\n        EUC_TWFreq[35][88] = 360;\n        EUC_TWFreq[61][58] = 359;\n        EUC_TWFreq[37][37] = 358;\n        EUC_TWFreq[57][22] = 357;\n        EUC_TWFreq[41][23] = 356;\n        EUC_TWFreq[90][66] = 355;\n        EUC_TWFreq[39][60] = 354;\n        EUC_TWFreq[38][0] = 353;\n        EUC_TWFreq[37][87] = 352;\n        EUC_TWFreq[46][2] = 351;\n        EUC_TWFreq[38][56] = 350;\n        EUC_TWFreq[58][11] = 349;\n        EUC_TWFreq[48][10] = 348;\n        EUC_TWFreq[74][4] = 347;\n        EUC_TWFreq[40][42] = 346;\n        EUC_TWFreq[41][52] = 345;\n        EUC_TWFreq[61][92] = 344;\n        EUC_TWFreq[39][50] = 343;\n        EUC_TWFreq[47][88] = 342;\n        EUC_TWFreq[88][36] = 341;\n        EUC_TWFreq[45][73] = 340;\n        EUC_TWFreq[82][3] = 339;\n        EUC_TWFreq[61][36] = 338;\n        EUC_TWFreq[60][33] = 337;\n        EUC_TWFreq[38][27] = 336;\n        EUC_TWFreq[35][83] = 335;\n        EUC_TWFreq[65][24] = 334;\n        EUC_TWFreq[73][10] = 333;\n        EUC_TWFreq[41][13] = 332;\n        EUC_TWFreq[50][27] = 331;\n        EUC_TWFreq[59][50] = 330;\n        EUC_TWFreq[42][45] = 329;\n        EUC_TWFreq[55][19] = 328;\n        EUC_TWFreq[36][77] = 327;\n        EUC_TWFreq[69][31] = 326;\n        EUC_TWFreq[60][7] = 325;\n        EUC_TWFreq[40][88] = 324;\n        EUC_TWFreq[57][56] = 323;\n        EUC_TWFreq[50][50] = 322;\n        EUC_TWFreq[42][37] = 321;\n        EUC_TWFreq[38][82] = 320;\n        EUC_TWFreq[52][25] = 319;\n        EUC_TWFreq[42][67] = 318;\n        EUC_TWFreq[48][40] = 317;\n        EUC_TWFreq[45][81] = 316;\n        EUC_TWFreq[57][14] = 315;\n        EUC_TWFreq[42][13] = 314;\n        EUC_TWFreq[78][0] = 313;\n        EUC_TWFreq[35][51] = 312;\n        EUC_TWFreq[41][67] = 311;\n        EUC_TWFreq[64][23] = 310;\n        EUC_TWFreq[36][65] = 309;\n        EUC_TWFreq[48][50] = 308;\n        EUC_TWFreq[46][69] = 307;\n        EUC_TWFreq[47][89] = 306;\n        EUC_TWFreq[41][48] = 305;\n        EUC_TWFreq[60][56] = 304;\n        EUC_TWFreq[44][82] = 303;\n        EUC_TWFreq[47][35] = 302;\n        EUC_TWFreq[49][3] = 301;\n        EUC_TWFreq[49][69] = 300;\n        EUC_TWFreq[45][93] = 299;\n        EUC_TWFreq[60][34] = 298;\n        EUC_TWFreq[60][82] = 297;\n        EUC_TWFreq[61][61] = 296;\n        EUC_TWFreq[86][42] = 295;\n        EUC_TWFreq[89][60] = 294;\n        EUC_TWFreq[48][31] = 293;\n        EUC_TWFreq[35][75] = 292;\n        EUC_TWFreq[91][39] = 291;\n        EUC_TWFreq[53][19] = 290;\n        EUC_TWFreq[39][72] = 289;\n        EUC_TWFreq[69][59] = 288;\n        EUC_TWFreq[41][7] = 287;\n        EUC_TWFreq[54][13] = 286;\n        EUC_TWFreq[43][28] = 285;\n        EUC_TWFreq[36][6] = 284;\n        EUC_TWFreq[45][75] = 283;\n        EUC_TWFreq[36][61] = 282;\n        EUC_TWFreq[38][21] = 281;\n        EUC_TWFreq[45][14] = 280;\n        EUC_TWFreq[61][43] = 279;\n        EUC_TWFreq[36][63] = 278;\n        EUC_TWFreq[43][30] = 277;\n        EUC_TWFreq[46][51] = 276;\n        EUC_TWFreq[68][87] = 275;\n        EUC_TWFreq[39][26] = 274;\n        EUC_TWFreq[46][76] = 273;\n        EUC_TWFreq[36][15] = 272;\n        EUC_TWFreq[35][40] = 271;\n        EUC_TWFreq[79][60] = 270;\n        EUC_TWFreq[46][7] = 269;\n        EUC_TWFreq[65][72] = 268;\n        EUC_TWFreq[69][88] = 267;\n        EUC_TWFreq[47][18] = 266;\n        EUC_TWFreq[37][0] = 265;\n        EUC_TWFreq[37][49] = 264;\n        EUC_TWFreq[67][37] = 263;\n        EUC_TWFreq[36][91] = 262;\n        EUC_TWFreq[75][48] = 261;\n        EUC_TWFreq[75][63] = 260;\n        EUC_TWFreq[83][87] = 259;\n        EUC_TWFreq[37][44] = 258;\n        EUC_TWFreq[73][54] = 257;\n        EUC_TWFreq[51][61] = 256;\n        EUC_TWFreq[46][57] = 255;\n        EUC_TWFreq[55][21] = 254;\n        EUC_TWFreq[39][66] = 253;\n        EUC_TWFreq[47][11] = 252;\n        EUC_TWFreq[52][8] = 251;\n        EUC_TWFreq[82][81] = 250;\n        EUC_TWFreq[36][57] = 249;\n        EUC_TWFreq[38][54] = 248;\n        EUC_TWFreq[43][81] = 247;\n        EUC_TWFreq[37][42] = 246;\n        EUC_TWFreq[40][18] = 245;\n        EUC_TWFreq[80][90] = 244;\n        EUC_TWFreq[37][84] = 243;\n        EUC_TWFreq[57][15] = 242;\n        EUC_TWFreq[38][87] = 241;\n        EUC_TWFreq[37][32] = 240;\n        EUC_TWFreq[53][53] = 239;\n        EUC_TWFreq[89][29] = 238;\n        EUC_TWFreq[81][53] = 237;\n        EUC_TWFreq[75][3] = 236;\n        EUC_TWFreq[83][73] = 235;\n        EUC_TWFreq[66][13] = 234;\n        EUC_TWFreq[48][7] = 233;\n        EUC_TWFreq[46][35] = 232;\n        EUC_TWFreq[35][86] = 231;\n        EUC_TWFreq[37][20] = 230;\n        EUC_TWFreq[46][80] = 229;\n        EUC_TWFreq[38][24] = 228;\n        EUC_TWFreq[41][68] = 227;\n        EUC_TWFreq[42][21] = 226;\n        EUC_TWFreq[43][32] = 225;\n        EUC_TWFreq[38][20] = 224;\n        EUC_TWFreq[37][59] = 223;\n        EUC_TWFreq[41][77] = 222;\n        EUC_TWFreq[59][57] = 221;\n        EUC_TWFreq[68][59] = 220;\n        EUC_TWFreq[39][43] = 219;\n        EUC_TWFreq[54][39] = 218;\n        EUC_TWFreq[48][28] = 217;\n        EUC_TWFreq[54][28] = 216;\n        EUC_TWFreq[41][44] = 215;\n        EUC_TWFreq[51][64] = 214;\n        EUC_TWFreq[47][72] = 213;\n        EUC_TWFreq[62][67] = 212;\n        EUC_TWFreq[42][43] = 211;\n        EUC_TWFreq[61][38] = 210;\n        EUC_TWFreq[76][25] = 209;\n        EUC_TWFreq[48][91] = 208;\n        EUC_TWFreq[36][36] = 207;\n        EUC_TWFreq[80][32] = 206;\n        EUC_TWFreq[81][40] = 205;\n        EUC_TWFreq[37][5] = 204;\n        EUC_TWFreq[74][69] = 203;\n        EUC_TWFreq[36][82] = 202;\n        EUC_TWFreq[46][59] = 201;\n        /*\n         * EUC_TWFreq[38][32] = 200; EUC_TWFreq[74][2] = 199; EUC_TWFreq[53][31]\n         * = 198; EUC_TWFreq[35][38] = 197; EUC_TWFreq[46][62] = 196;\n         * EUC_TWFreq[77][31] = 195; EUC_TWFreq[55][74] = 194; EUC_TWFreq[66][6]\n         * = 193; EUC_TWFreq[56][21] = 192; EUC_TWFreq[54][78] = 191;\n         * EUC_TWFreq[43][51] = 190; EUC_TWFreq[64][93] = 189; EUC_TWFreq[92][7]\n         * = 188; EUC_TWFreq[83][89] = 187; EUC_TWFreq[69][9] = 186;\n         * EUC_TWFreq[45][4] = 185; EUC_TWFreq[53][9] = 184; EUC_TWFreq[43][2] =\n         * 183; EUC_TWFreq[35][11] = 182; EUC_TWFreq[51][25] = 181;\n         * EUC_TWFreq[52][71] = 180; EUC_TWFreq[81][67] = 179;\n         * EUC_TWFreq[37][33] = 178; EUC_TWFreq[38][57] = 177;\n         * EUC_TWFreq[39][77] = 176; EUC_TWFreq[40][26] = 175;\n         * EUC_TWFreq[37][21] = 174; EUC_TWFreq[81][70] = 173;\n         * EUC_TWFreq[56][80] = 172; EUC_TWFreq[65][14] = 171;\n         * EUC_TWFreq[62][47] = 170; EUC_TWFreq[56][54] = 169;\n         * EUC_TWFreq[45][17] = 168; EUC_TWFreq[52][52] = 167;\n         * EUC_TWFreq[74][30] = 166; EUC_TWFreq[60][57] = 165;\n         * EUC_TWFreq[41][15] = 164; EUC_TWFreq[47][69] = 163;\n         * EUC_TWFreq[61][11] = 162; EUC_TWFreq[72][25] = 161;\n         * EUC_TWFreq[82][56] = 160; EUC_TWFreq[76][92] = 159;\n         * EUC_TWFreq[51][22] = 158; EUC_TWFreq[55][69] = 157;\n         * EUC_TWFreq[49][43] = 156; EUC_TWFreq[69][49] = 155;\n         * EUC_TWFreq[88][42] = 154; EUC_TWFreq[84][41] = 153;\n         * EUC_TWFreq[79][33] = 152; EUC_TWFreq[47][17] = 151;\n         * EUC_TWFreq[52][88] = 150; EUC_TWFreq[63][74] = 149;\n         * EUC_TWFreq[50][32] = 148; EUC_TWFreq[65][10] = 147; EUC_TWFreq[57][6]\n         * = 146; EUC_TWFreq[52][23] = 145; EUC_TWFreq[36][70] = 144;\n         * EUC_TWFreq[65][55] = 143; EUC_TWFreq[35][27] = 142;\n         * EUC_TWFreq[57][63] = 141; EUC_TWFreq[39][92] = 140;\n         * EUC_TWFreq[79][75] = 139; EUC_TWFreq[36][30] = 138;\n         * EUC_TWFreq[53][60] = 137; EUC_TWFreq[55][43] = 136;\n         * EUC_TWFreq[71][22] = 135; EUC_TWFreq[43][16] = 134;\n         * EUC_TWFreq[65][21] = 133; EUC_TWFreq[84][51] = 132;\n         * EUC_TWFreq[43][64] = 131; EUC_TWFreq[87][91] = 130;\n         * EUC_TWFreq[47][45] = 129; EUC_TWFreq[65][29] = 128;\n         * EUC_TWFreq[88][16] = 127; EUC_TWFreq[50][5] = 126; EUC_TWFreq[47][33]\n         * = 125; EUC_TWFreq[46][27] = 124; EUC_TWFreq[85][2] = 123;\n         * EUC_TWFreq[43][77] = 122; EUC_TWFreq[70][9] = 121; EUC_TWFreq[41][54]\n         * = 120; EUC_TWFreq[56][12] = 119; EUC_TWFreq[90][65] = 118;\n         * EUC_TWFreq[91][50] = 117; EUC_TWFreq[48][41] = 116;\n         * EUC_TWFreq[35][89] = 115; EUC_TWFreq[90][83] = 114;\n         * EUC_TWFreq[44][40] = 113; EUC_TWFreq[50][88] = 112;\n         * EUC_TWFreq[72][39] = 111; EUC_TWFreq[45][3] = 110; EUC_TWFreq[71][33]\n         * = 109; EUC_TWFreq[39][12] = 108; EUC_TWFreq[59][24] = 107;\n         * EUC_TWFreq[60][62] = 106; EUC_TWFreq[44][33] = 105;\n         * EUC_TWFreq[53][70] = 104; EUC_TWFreq[77][90] = 103;\n         * EUC_TWFreq[50][58] = 102; EUC_TWFreq[54][1] = 101; EUC_TWFreq[73][19]\n         * = 100; EUC_TWFreq[37][3] = 99; EUC_TWFreq[49][91] = 98;\n         * EUC_TWFreq[88][43] = 97; EUC_TWFreq[36][78] = 96; EUC_TWFreq[44][20]\n         * = 95; EUC_TWFreq[64][15] = 94; EUC_TWFreq[72][28] = 93;\n         * EUC_TWFreq[70][13] = 92; EUC_TWFreq[65][83] = 91; EUC_TWFreq[58][68]\n         * = 90; EUC_TWFreq[59][32] = 89; EUC_TWFreq[39][13] = 88;\n         * EUC_TWFreq[55][64] = 87; EUC_TWFreq[56][59] = 86; EUC_TWFreq[39][17]\n         * = 85; EUC_TWFreq[55][84] = 84; EUC_TWFreq[77][85] = 83;\n         * EUC_TWFreq[60][19] = 82; EUC_TWFreq[62][82] = 81; EUC_TWFreq[78][16]\n         * = 80; EUC_TWFreq[66][8] = 79; EUC_TWFreq[39][42] = 78;\n         * EUC_TWFreq[61][24] = 77; EUC_TWFreq[57][67] = 76; EUC_TWFreq[38][83]\n         * = 75; EUC_TWFreq[36][53] = 74; EUC_TWFreq[67][76] = 73;\n         * EUC_TWFreq[37][91] = 72; EUC_TWFreq[44][26] = 71; EUC_TWFreq[72][86]\n         * = 70; EUC_TWFreq[44][87] = 69; EUC_TWFreq[45][50] = 68;\n         * EUC_TWFreq[58][4] = 67; EUC_TWFreq[86][65] = 66; EUC_TWFreq[45][56] =\n         * 65; EUC_TWFreq[79][49] = 64; EUC_TWFreq[35][3] = 63;\n         * EUC_TWFreq[48][83] = 62; EUC_TWFreq[71][21] = 61; EUC_TWFreq[77][93]\n         * = 60; EUC_TWFreq[87][92] = 59; EUC_TWFreq[38][35] = 58;\n         * EUC_TWFreq[66][17] = 57; EUC_TWFreq[37][66] = 56; EUC_TWFreq[51][42]\n         * = 55; EUC_TWFreq[57][73] = 54; EUC_TWFreq[51][54] = 53;\n         * EUC_TWFreq[75][64] = 52; EUC_TWFreq[35][5] = 51; EUC_TWFreq[49][40] =\n         * 50; EUC_TWFreq[58][35] = 49; EUC_TWFreq[67][88] = 48;\n         * EUC_TWFreq[60][51] = 47; EUC_TWFreq[36][92] = 46; EUC_TWFreq[44][41]\n         * = 45; EUC_TWFreq[58][29] = 44; EUC_TWFreq[43][62] = 43;\n         * EUC_TWFreq[56][23] = 42; EUC_TWFreq[67][44] = 41; EUC_TWFreq[52][91]\n         * = 40; EUC_TWFreq[42][81] = 39; EUC_TWFreq[64][25] = 38;\n         * EUC_TWFreq[35][36] = 37; EUC_TWFreq[47][73] = 36; EUC_TWFreq[36][1] =\n         * 35; EUC_TWFreq[65][84] = 34; EUC_TWFreq[73][1] = 33;\n         * EUC_TWFreq[79][66] = 32; EUC_TWFreq[69][14] = 31; EUC_TWFreq[65][28]\n         * = 30; EUC_TWFreq[60][93] = 29; EUC_TWFreq[72][79] = 28;\n         * EUC_TWFreq[48][0] = 27; EUC_TWFreq[73][43] = 26; EUC_TWFreq[66][47] =\n         * 25; EUC_TWFreq[41][18] = 24; EUC_TWFreq[51][10] = 23;\n         * EUC_TWFreq[59][7] = 22; EUC_TWFreq[53][27] = 21; EUC_TWFreq[86][67] =\n         * 20; EUC_TWFreq[49][87] = 19; EUC_TWFreq[52][28] = 18;\n         * EUC_TWFreq[52][12] = 17; EUC_TWFreq[42][30] = 16; EUC_TWFreq[65][35]\n         * = 15; EUC_TWFreq[46][64] = 14; EUC_TWFreq[71][7] = 13;\n         * EUC_TWFreq[56][57] = 12; EUC_TWFreq[56][31] = 11; EUC_TWFreq[41][31]\n         * = 10; EUC_TWFreq[48][59] = 9; EUC_TWFreq[63][92] = 8;\n         * EUC_TWFreq[62][57] = 7; EUC_TWFreq[65][87] = 6; EUC_TWFreq[70][10] =\n         * 5; EUC_TWFreq[52][40] = 4; EUC_TWFreq[40][22] = 3; EUC_TWFreq[65][91]\n         * = 2; EUC_TWFreq[50][25] = 1; EUC_TWFreq[35][84] = 0;\n         */\n        GBKFreq[52][132] = 600;\n        GBKFreq[73][135] = 599;\n        GBKFreq[49][123] = 598;\n        GBKFreq[77][146] = 597;\n        GBKFreq[81][123] = 596;\n        GBKFreq[82][144] = 595;\n        GBKFreq[51][179] = 594;\n        GBKFreq[83][154] = 593;\n        GBKFreq[71][139] = 592;\n        GBKFreq[64][139] = 591;\n        GBKFreq[85][144] = 590;\n        GBKFreq[52][125] = 589;\n        GBKFreq[88][25] = 588;\n        GBKFreq[81][106] = 587;\n        GBKFreq[81][148] = 586;\n        GBKFreq[62][137] = 585;\n        GBKFreq[94][0] = 584;\n        GBKFreq[1][64] = 583;\n        GBKFreq[67][163] = 582;\n        GBKFreq[20][190] = 581;\n        GBKFreq[57][131] = 580;\n        GBKFreq[29][169] = 579;\n        GBKFreq[72][143] = 578;\n        GBKFreq[0][173] = 577;\n        GBKFreq[11][23] = 576;\n        GBKFreq[61][141] = 575;\n        GBKFreq[60][123] = 574;\n        GBKFreq[81][114] = 573;\n        GBKFreq[82][131] = 572;\n        GBKFreq[67][156] = 571;\n        GBKFreq[71][167] = 570;\n        GBKFreq[20][50] = 569;\n        GBKFreq[77][132] = 568;\n        GBKFreq[84][38] = 567;\n        GBKFreq[26][29] = 566;\n        GBKFreq[74][187] = 565;\n        GBKFreq[62][116] = 564;\n        GBKFreq[67][135] = 563;\n        GBKFreq[5][86] = 562;\n        GBKFreq[72][186] = 561;\n        GBKFreq[75][161] = 560;\n        GBKFreq[78][130] = 559;\n        GBKFreq[94][30] = 558;\n        GBKFreq[84][72] = 557;\n        GBKFreq[1][67] = 556;\n        GBKFreq[75][172] = 555;\n        GBKFreq[74][185] = 554;\n        GBKFreq[53][160] = 553;\n        GBKFreq[123][14] = 552;\n        GBKFreq[79][97] = 551;\n        GBKFreq[85][110] = 550;\n        GBKFreq[78][171] = 549;\n        GBKFreq[52][131] = 548;\n        GBKFreq[56][100] = 547;\n        GBKFreq[50][182] = 546;\n        GBKFreq[94][64] = 545;\n        GBKFreq[106][74] = 544;\n        GBKFreq[11][102] = 543;\n        GBKFreq[53][124] = 542;\n        GBKFreq[24][3] = 541;\n        GBKFreq[86][148] = 540;\n        GBKFreq[53][184] = 539;\n        GBKFreq[86][147] = 538;\n        GBKFreq[96][161] = 537;\n        GBKFreq[82][77] = 536;\n        GBKFreq[59][146] = 535;\n        GBKFreq[84][126] = 534;\n        GBKFreq[79][132] = 533;\n        GBKFreq[85][123] = 532;\n        GBKFreq[71][101] = 531;\n        GBKFreq[85][106] = 530;\n        GBKFreq[6][184] = 529;\n        GBKFreq[57][156] = 528;\n        GBKFreq[75][104] = 527;\n        GBKFreq[50][137] = 526;\n        GBKFreq[79][133] = 525;\n        GBKFreq[76][108] = 524;\n        GBKFreq[57][142] = 523;\n        GBKFreq[84][130] = 522;\n        GBKFreq[52][128] = 521;\n        GBKFreq[47][44] = 520;\n        GBKFreq[52][152] = 519;\n        GBKFreq[54][104] = 518;\n        GBKFreq[30][47] = 517;\n        GBKFreq[71][123] = 516;\n        GBKFreq[52][107] = 515;\n        GBKFreq[45][84] = 514;\n        GBKFreq[107][118] = 513;\n        GBKFreq[5][161] = 512;\n        GBKFreq[48][126] = 511;\n        GBKFreq[67][170] = 510;\n        GBKFreq[43][6] = 509;\n        GBKFreq[70][112] = 508;\n        GBKFreq[86][174] = 507;\n        GBKFreq[84][166] = 506;\n        GBKFreq[79][130] = 505;\n        GBKFreq[57][141] = 504;\n        GBKFreq[81][178] = 503;\n        GBKFreq[56][187] = 502;\n        GBKFreq[81][162] = 501;\n        GBKFreq[53][104] = 500;\n        GBKFreq[123][35] = 499;\n        GBKFreq[70][169] = 498;\n        GBKFreq[69][164] = 497;\n        GBKFreq[109][61] = 496;\n        GBKFreq[73][130] = 495;\n        GBKFreq[62][134] = 494;\n        GBKFreq[54][125] = 493;\n        GBKFreq[79][105] = 492;\n        GBKFreq[70][165] = 491;\n        GBKFreq[71][189] = 490;\n        GBKFreq[23][147] = 489;\n        GBKFreq[51][139] = 488;\n        GBKFreq[47][137] = 487;\n        GBKFreq[77][123] = 486;\n        GBKFreq[86][183] = 485;\n        GBKFreq[63][173] = 484;\n        GBKFreq[79][144] = 483;\n        GBKFreq[84][159] = 482;\n        GBKFreq[60][91] = 481;\n        GBKFreq[66][187] = 480;\n        GBKFreq[73][114] = 479;\n        GBKFreq[85][56] = 478;\n        GBKFreq[71][149] = 477;\n        GBKFreq[84][189] = 476;\n        GBKFreq[104][31] = 475;\n        GBKFreq[83][82] = 474;\n        GBKFreq[68][35] = 473;\n        GBKFreq[11][77] = 472;\n        GBKFreq[15][155] = 471;\n        GBKFreq[83][153] = 470;\n        GBKFreq[71][1] = 469;\n        GBKFreq[53][190] = 468;\n        GBKFreq[50][135] = 467;\n        GBKFreq[3][147] = 466;\n        GBKFreq[48][136] = 465;\n        GBKFreq[66][166] = 464;\n        GBKFreq[55][159] = 463;\n        GBKFreq[82][150] = 462;\n        GBKFreq[58][178] = 461;\n        GBKFreq[64][102] = 460;\n        GBKFreq[16][106] = 459;\n        GBKFreq[68][110] = 458;\n        GBKFreq[54][14] = 457;\n        GBKFreq[60][140] = 456;\n        GBKFreq[91][71] = 455;\n        GBKFreq[54][150] = 454;\n        GBKFreq[78][177] = 453;\n        GBKFreq[78][117] = 452;\n        GBKFreq[104][12] = 451;\n        GBKFreq[73][150] = 450;\n        GBKFreq[51][142] = 449;\n        GBKFreq[81][145] = 448;\n        GBKFreq[66][183] = 447;\n        GBKFreq[51][178] = 446;\n        GBKFreq[75][107] = 445;\n        GBKFreq[65][119] = 444;\n        GBKFreq[69][176] = 443;\n        GBKFreq[59][122] = 442;\n        GBKFreq[78][160] = 441;\n        GBKFreq[85][183] = 440;\n        GBKFreq[105][16] = 439;\n        GBKFreq[73][110] = 438;\n        GBKFreq[104][39] = 437;\n        GBKFreq[119][16] = 436;\n        GBKFreq[76][162] = 435;\n        GBKFreq[67][152] = 434;\n        GBKFreq[82][24] = 433;\n        GBKFreq[73][121] = 432;\n        GBKFreq[83][83] = 431;\n        GBKFreq[82][145] = 430;\n        GBKFreq[49][133] = 429;\n        GBKFreq[94][13] = 428;\n        GBKFreq[58][139] = 427;\n        GBKFreq[74][189] = 426;\n        GBKFreq[66][177] = 425;\n        GBKFreq[85][184] = 424;\n        GBKFreq[55][183] = 423;\n        GBKFreq[71][107] = 422;\n        GBKFreq[11][98] = 421;\n        GBKFreq[72][153] = 420;\n        GBKFreq[2][137] = 419;\n        GBKFreq[59][147] = 418;\n        GBKFreq[58][152] = 417;\n        GBKFreq[55][144] = 416;\n        GBKFreq[73][125] = 415;\n        GBKFreq[52][154] = 414;\n        GBKFreq[70][178] = 413;\n        GBKFreq[79][148] = 412;\n        GBKFreq[63][143] = 411;\n        GBKFreq[50][140] = 410;\n        GBKFreq[47][145] = 409;\n        GBKFreq[48][123] = 408;\n        GBKFreq[56][107] = 407;\n        GBKFreq[84][83] = 406;\n        GBKFreq[59][112] = 405;\n        GBKFreq[124][72] = 404;\n        GBKFreq[79][99] = 403;\n        GBKFreq[3][37] = 402;\n        GBKFreq[114][55] = 401;\n        GBKFreq[85][152] = 400;\n        GBKFreq[60][47] = 399;\n        GBKFreq[65][96] = 398;\n        GBKFreq[74][110] = 397;\n        GBKFreq[86][182] = 396;\n        GBKFreq[50][99] = 395;\n        GBKFreq[67][186] = 394;\n        GBKFreq[81][74] = 393;\n        GBKFreq[80][37] = 392;\n        GBKFreq[21][60] = 391;\n        GBKFreq[110][12] = 390;\n        GBKFreq[60][162] = 389;\n        GBKFreq[29][115] = 388;\n        GBKFreq[83][130] = 387;\n        GBKFreq[52][136] = 386;\n        GBKFreq[63][114] = 385;\n        GBKFreq[49][127] = 384;\n        GBKFreq[83][109] = 383;\n        GBKFreq[66][128] = 382;\n        GBKFreq[78][136] = 381;\n        GBKFreq[81][180] = 380;\n        GBKFreq[76][104] = 379;\n        GBKFreq[56][156] = 378;\n        GBKFreq[61][23] = 377;\n        GBKFreq[4][30] = 376;\n        GBKFreq[69][154] = 375;\n        GBKFreq[100][37] = 374;\n        GBKFreq[54][177] = 373;\n        GBKFreq[23][119] = 372;\n        GBKFreq[71][171] = 371;\n        GBKFreq[84][146] = 370;\n        GBKFreq[20][184] = 369;\n        GBKFreq[86][76] = 368;\n        GBKFreq[74][132] = 367;\n        GBKFreq[47][97] = 366;\n        GBKFreq[82][137] = 365;\n        GBKFreq[94][56] = 364;\n        GBKFreq[92][30] = 363;\n        GBKFreq[19][117] = 362;\n        GBKFreq[48][173] = 361;\n        GBKFreq[2][136] = 360;\n        GBKFreq[7][182] = 359;\n        GBKFreq[74][188] = 358;\n        GBKFreq[14][132] = 357;\n        GBKFreq[62][172] = 356;\n        GBKFreq[25][39] = 355;\n        GBKFreq[85][129] = 354;\n        GBKFreq[64][98] = 353;\n        GBKFreq[67][127] = 352;\n        GBKFreq[72][167] = 351;\n        GBKFreq[57][143] = 350;\n        GBKFreq[76][187] = 349;\n        GBKFreq[83][181] = 348;\n        GBKFreq[84][10] = 347;\n        GBKFreq[55][166] = 346;\n        GBKFreq[55][188] = 345;\n        GBKFreq[13][151] = 344;\n        GBKFreq[62][124] = 343;\n        GBKFreq[53][136] = 342;\n        GBKFreq[106][57] = 341;\n        GBKFreq[47][166] = 340;\n        GBKFreq[109][30] = 339;\n        GBKFreq[78][114] = 338;\n        GBKFreq[83][19] = 337;\n        GBKFreq[56][162] = 336;\n        GBKFreq[60][177] = 335;\n        GBKFreq[88][9] = 334;\n        GBKFreq[74][163] = 333;\n        GBKFreq[52][156] = 332;\n        GBKFreq[71][180] = 331;\n        GBKFreq[60][57] = 330;\n        GBKFreq[72][173] = 329;\n        GBKFreq[82][91] = 328;\n        GBKFreq[51][186] = 327;\n        GBKFreq[75][86] = 326;\n        GBKFreq[75][78] = 325;\n        GBKFreq[76][170] = 324;\n        GBKFreq[60][147] = 323;\n        GBKFreq[82][75] = 322;\n        GBKFreq[80][148] = 321;\n        GBKFreq[86][150] = 320;\n        GBKFreq[13][95] = 319;\n        GBKFreq[0][11] = 318;\n        GBKFreq[84][190] = 317;\n        GBKFreq[76][166] = 316;\n        GBKFreq[14][72] = 315;\n        GBKFreq[67][144] = 314;\n        GBKFreq[84][44] = 313;\n        GBKFreq[72][125] = 312;\n        GBKFreq[66][127] = 311;\n        GBKFreq[60][25] = 310;\n        GBKFreq[70][146] = 309;\n        GBKFreq[79][135] = 308;\n        GBKFreq[54][135] = 307;\n        GBKFreq[60][104] = 306;\n        GBKFreq[55][132] = 305;\n        GBKFreq[94][2] = 304;\n        GBKFreq[54][133] = 303;\n        GBKFreq[56][190] = 302;\n        GBKFreq[58][174] = 301;\n        GBKFreq[80][144] = 300;\n        GBKFreq[85][113] = 299;\n        /*\n         * GBKFreq[83][15] = 298; GBKFreq[105][80] = 297; GBKFreq[7][179] = 296;\n         * GBKFreq[93][4] = 295; GBKFreq[123][40] = 294; GBKFreq[85][120] = 293;\n         * GBKFreq[77][165] = 292; GBKFreq[86][67] = 291; GBKFreq[25][162] =\n         * 290; GBKFreq[77][183] = 289; GBKFreq[83][71] = 288; GBKFreq[78][99] =\n         * 287; GBKFreq[72][177] = 286; GBKFreq[71][97] = 285; GBKFreq[58][111]\n         * = 284; GBKFreq[77][175] = 283; GBKFreq[76][181] = 282;\n         * GBKFreq[71][142] = 281; GBKFreq[64][150] = 280; GBKFreq[5][142] =\n         * 279; GBKFreq[73][128] = 278; GBKFreq[73][156] = 277; GBKFreq[60][188]\n         * = 276; GBKFreq[64][56] = 275; GBKFreq[74][128] = 274;\n         * GBKFreq[48][163] = 273; GBKFreq[54][116] = 272; GBKFreq[73][127] =\n         * 271; GBKFreq[16][176] = 270; GBKFreq[62][149] = 269; GBKFreq[105][96]\n         * = 268; GBKFreq[55][186] = 267; GBKFreq[4][51] = 266; GBKFreq[48][113]\n         * = 265; GBKFreq[48][152] = 264; GBKFreq[23][9] = 263; GBKFreq[56][102]\n         * = 262; GBKFreq[11][81] = 261; GBKFreq[82][112] = 260; GBKFreq[65][85]\n         * = 259; GBKFreq[69][125] = 258; GBKFreq[68][31] = 257; GBKFreq[5][20]\n         * = 256; GBKFreq[60][176] = 255; GBKFreq[82][81] = 254;\n         * GBKFreq[72][107] = 253; GBKFreq[3][52] = 252; GBKFreq[71][157] = 251;\n         * GBKFreq[24][46] = 250; GBKFreq[69][108] = 249; GBKFreq[78][178] =\n         * 248; GBKFreq[9][69] = 247; GBKFreq[73][144] = 246; GBKFreq[63][187] =\n         * 245; GBKFreq[68][36] = 244; GBKFreq[47][151] = 243; GBKFreq[14][74] =\n         * 242; GBKFreq[47][114] = 241; GBKFreq[80][171] = 240; GBKFreq[75][152]\n         * = 239; GBKFreq[86][40] = 238; GBKFreq[93][43] = 237; GBKFreq[2][50] =\n         * 236; GBKFreq[62][66] = 235; GBKFreq[1][183] = 234; GBKFreq[74][124] =\n         * 233; GBKFreq[58][104] = 232; GBKFreq[83][106] = 231; GBKFreq[60][144]\n         * = 230; GBKFreq[48][99] = 229; GBKFreq[54][157] = 228;\n         * GBKFreq[70][179] = 227; GBKFreq[61][127] = 226; GBKFreq[57][135] =\n         * 225; GBKFreq[59][190] = 224; GBKFreq[77][116] = 223; GBKFreq[26][17]\n         * = 222; GBKFreq[60][13] = 221; GBKFreq[71][38] = 220; GBKFreq[85][177]\n         * = 219; GBKFreq[59][73] = 218; GBKFreq[50][150] = 217;\n         * GBKFreq[79][102] = 216; GBKFreq[76][118] = 215; GBKFreq[67][132] =\n         * 214; GBKFreq[73][146] = 213; GBKFreq[83][184] = 212; GBKFreq[86][159]\n         * = 211; GBKFreq[95][120] = 210; GBKFreq[23][139] = 209;\n         * GBKFreq[64][183] = 208; GBKFreq[85][103] = 207; GBKFreq[41][90] =\n         * 206; GBKFreq[87][72] = 205; GBKFreq[62][104] = 204; GBKFreq[79][168]\n         * = 203; GBKFreq[79][150] = 202; GBKFreq[104][20] = 201;\n         * GBKFreq[56][114] = 200; GBKFreq[84][26] = 199; GBKFreq[57][99] = 198;\n         * GBKFreq[62][154] = 197; GBKFreq[47][98] = 196; GBKFreq[61][64] = 195;\n         * GBKFreq[112][18] = 194; GBKFreq[123][19] = 193; GBKFreq[4][98] = 192;\n         * GBKFreq[47][163] = 191; GBKFreq[66][188] = 190; GBKFreq[81][85] =\n         * 189; GBKFreq[82][30] = 188; GBKFreq[65][83] = 187; GBKFreq[67][24] =\n         * 186; GBKFreq[68][179] = 185; GBKFreq[55][177] = 184; GBKFreq[2][122]\n         * = 183; GBKFreq[47][139] = 182; GBKFreq[79][158] = 181;\n         * GBKFreq[64][143] = 180; GBKFreq[100][24] = 179; GBKFreq[73][103] =\n         * 178; GBKFreq[50][148] = 177; GBKFreq[86][97] = 176; GBKFreq[59][116]\n         * = 175; GBKFreq[64][173] = 174; GBKFreq[99][91] = 173; GBKFreq[11][99]\n         * = 172; GBKFreq[78][179] = 171; GBKFreq[18][17] = 170;\n         * GBKFreq[58][185] = 169; GBKFreq[47][165] = 168; GBKFreq[67][131] =\n         * 167; GBKFreq[94][40] = 166; GBKFreq[74][153] = 165; GBKFreq[79][142]\n         * = 164; GBKFreq[57][98] = 163; GBKFreq[1][164] = 162; GBKFreq[55][168]\n         * = 161; GBKFreq[13][141] = 160; GBKFreq[51][31] = 159;\n         * GBKFreq[57][178] = 158; GBKFreq[50][189] = 157; GBKFreq[60][167] =\n         * 156; GBKFreq[80][34] = 155; GBKFreq[109][80] = 154; GBKFreq[85][54] =\n         * 153; GBKFreq[69][183] = 152; GBKFreq[67][143] = 151; GBKFreq[47][120]\n         * = 150; GBKFreq[45][75] = 149; GBKFreq[82][98] = 148; GBKFreq[83][22]\n         * = 147; GBKFreq[13][103] = 146; GBKFreq[49][174] = 145;\n         * GBKFreq[57][181] = 144; GBKFreq[64][127] = 143; GBKFreq[61][131] =\n         * 142; GBKFreq[52][180] = 141; GBKFreq[74][134] = 140; GBKFreq[84][187]\n         * = 139; GBKFreq[81][189] = 138; GBKFreq[47][160] = 137;\n         * GBKFreq[66][148] = 136; GBKFreq[7][4] = 135; GBKFreq[85][134] = 134;\n         * GBKFreq[88][13] = 133; GBKFreq[88][80] = 132; GBKFreq[69][166] = 131;\n         * GBKFreq[86][18] = 130; GBKFreq[79][141] = 129; GBKFreq[50][108] =\n         * 128; GBKFreq[94][69] = 127; GBKFreq[81][110] = 126; GBKFreq[69][119]\n         * = 125; GBKFreq[72][161] = 124; GBKFreq[106][45] = 123;\n         * GBKFreq[73][124] = 122; GBKFreq[94][28] = 121; GBKFreq[63][174] =\n         * 120; GBKFreq[3][149] = 119; GBKFreq[24][160] = 118; GBKFreq[113][94]\n         * = 117; GBKFreq[56][138] = 116; GBKFreq[64][185] = 115;\n         * GBKFreq[86][56] = 114; GBKFreq[56][150] = 113; GBKFreq[110][55] =\n         * 112; GBKFreq[28][13] = 111; GBKFreq[54][190] = 110; GBKFreq[8][180] =\n         * 109; GBKFreq[73][149] = 108; GBKFreq[80][155] = 107; GBKFreq[83][172]\n         * = 106; GBKFreq[67][174] = 105; GBKFreq[64][180] = 104;\n         * GBKFreq[84][46] = 103; GBKFreq[91][74] = 102; GBKFreq[69][134] = 101;\n         * GBKFreq[61][107] = 100; GBKFreq[47][171] = 99; GBKFreq[59][51] = 98;\n         * GBKFreq[109][74] = 97; GBKFreq[64][174] = 96; GBKFreq[52][151] = 95;\n         * GBKFreq[51][176] = 94; GBKFreq[80][157] = 93; GBKFreq[94][31] = 92;\n         * GBKFreq[79][155] = 91; GBKFreq[72][174] = 90; GBKFreq[69][113] = 89;\n         * GBKFreq[83][167] = 88; GBKFreq[83][122] = 87; GBKFreq[8][178] = 86;\n         * GBKFreq[70][186] = 85; GBKFreq[59][153] = 84; GBKFreq[84][68] = 83;\n         * GBKFreq[79][39] = 82; GBKFreq[47][180] = 81; GBKFreq[88][53] = 80;\n         * GBKFreq[57][154] = 79; GBKFreq[47][153] = 78; GBKFreq[3][153] = 77;\n         * GBKFreq[76][134] = 76; GBKFreq[51][166] = 75; GBKFreq[58][176] = 74;\n         * GBKFreq[27][138] = 73; GBKFreq[73][126] = 72; GBKFreq[76][185] = 71;\n         * GBKFreq[52][186] = 70; GBKFreq[81][151] = 69; GBKFreq[26][50] = 68;\n         * GBKFreq[76][173] = 67; GBKFreq[106][56] = 66; GBKFreq[85][142] = 65;\n         * GBKFreq[11][103] = 64; GBKFreq[69][159] = 63; GBKFreq[53][142] = 62;\n         * GBKFreq[7][6] = 61; GBKFreq[84][59] = 60; GBKFreq[86][3] = 59;\n         * GBKFreq[64][144] = 58; GBKFreq[1][187] = 57; GBKFreq[82][128] = 56;\n         * GBKFreq[3][66] = 55; GBKFreq[68][133] = 54; GBKFreq[55][167] = 53;\n         * GBKFreq[52][130] = 52; GBKFreq[61][133] = 51; GBKFreq[72][181] = 50;\n         * GBKFreq[25][98] = 49; GBKFreq[84][149] = 48; GBKFreq[91][91] = 47;\n         * GBKFreq[47][188] = 46; GBKFreq[68][130] = 45; GBKFreq[22][44] = 44;\n         * GBKFreq[81][121] = 43; GBKFreq[72][140] = 42; GBKFreq[55][133] = 41;\n         * GBKFreq[55][185] = 40; GBKFreq[56][105] = 39; GBKFreq[60][30] = 38;\n         * GBKFreq[70][103] = 37; GBKFreq[62][141] = 36; GBKFreq[70][144] = 35;\n         * GBKFreq[59][111] = 34; GBKFreq[54][17] = 33; GBKFreq[18][190] = 32;\n         * GBKFreq[65][164] = 31; GBKFreq[83][125] = 30; GBKFreq[61][121] = 29;\n         * GBKFreq[48][13] = 28; GBKFreq[51][189] = 27; GBKFreq[65][68] = 26;\n         * GBKFreq[7][0] = 25; GBKFreq[76][188] = 24; GBKFreq[85][117] = 23;\n         * GBKFreq[45][33] = 22; GBKFreq[78][187] = 21; GBKFreq[106][48] = 20;\n         * GBKFreq[59][52] = 19; GBKFreq[86][185] = 18; GBKFreq[84][121] = 17;\n         * GBKFreq[82][189] = 16; GBKFreq[68][156] = 15; GBKFreq[55][125] = 14;\n         * GBKFreq[65][175] = 13; GBKFreq[7][140] = 12; GBKFreq[50][106] = 11;\n         * GBKFreq[59][124] = 10; GBKFreq[67][115] = 9; GBKFreq[82][114] = 8;\n         * GBKFreq[74][121] = 7; GBKFreq[106][69] = 6; GBKFreq[94][27] = 5;\n         * GBKFreq[78][98] = 4; GBKFreq[85][186] = 3; GBKFreq[108][90] = 2;\n         * GBKFreq[62][160] = 1; GBKFreq[60][169] = 0;\n         */\n        KRFreq[31][43] = 600;\n        KRFreq[19][56] = 599;\n        KRFreq[38][46] = 598;\n        KRFreq[3][3] = 597;\n        KRFreq[29][77] = 596;\n        KRFreq[19][33] = 595;\n        KRFreq[30][0] = 594;\n        KRFreq[29][89] = 593;\n        KRFreq[31][26] = 592;\n        KRFreq[31][38] = 591;\n        KRFreq[32][85] = 590;\n        KRFreq[15][0] = 589;\n        KRFreq[16][54] = 588;\n        KRFreq[15][76] = 587;\n        KRFreq[31][25] = 586;\n        KRFreq[23][13] = 585;\n        KRFreq[28][34] = 584;\n        KRFreq[18][9] = 583;\n        KRFreq[29][37] = 582;\n        KRFreq[22][45] = 581;\n        KRFreq[19][46] = 580;\n        KRFreq[16][65] = 579;\n        KRFreq[23][5] = 578;\n        KRFreq[26][70] = 577;\n        KRFreq[31][53] = 576;\n        KRFreq[27][12] = 575;\n        KRFreq[30][67] = 574;\n        KRFreq[31][57] = 573;\n        KRFreq[20][20] = 572;\n        KRFreq[30][31] = 571;\n        KRFreq[20][72] = 570;\n        KRFreq[15][51] = 569;\n        KRFreq[3][8] = 568;\n        KRFreq[32][53] = 567;\n        KRFreq[27][85] = 566;\n        KRFreq[25][23] = 565;\n        KRFreq[15][44] = 564;\n        KRFreq[32][3] = 563;\n        KRFreq[31][68] = 562;\n        KRFreq[30][24] = 561;\n        KRFreq[29][49] = 560;\n        KRFreq[27][49] = 559;\n        KRFreq[23][23] = 558;\n        KRFreq[31][91] = 557;\n        KRFreq[31][46] = 556;\n        KRFreq[19][74] = 555;\n        KRFreq[27][27] = 554;\n        KRFreq[3][17] = 553;\n        KRFreq[20][38] = 552;\n        KRFreq[21][82] = 551;\n        KRFreq[28][25] = 550;\n        KRFreq[32][5] = 549;\n        KRFreq[31][23] = 548;\n        KRFreq[25][45] = 547;\n        KRFreq[32][87] = 546;\n        KRFreq[18][26] = 545;\n        KRFreq[24][10] = 544;\n        KRFreq[26][82] = 543;\n        KRFreq[15][89] = 542;\n        KRFreq[28][36] = 541;\n        KRFreq[28][31] = 540;\n        KRFreq[16][23] = 539;\n        KRFreq[16][77] = 538;\n        KRFreq[19][84] = 537;\n        KRFreq[23][72] = 536;\n        KRFreq[38][48] = 535;\n        KRFreq[23][2] = 534;\n        KRFreq[30][20] = 533;\n        KRFreq[38][47] = 532;\n        KRFreq[39][12] = 531;\n        KRFreq[23][21] = 530;\n        KRFreq[18][17] = 529;\n        KRFreq[30][87] = 528;\n        KRFreq[29][62] = 527;\n        KRFreq[29][87] = 526;\n        KRFreq[34][53] = 525;\n        KRFreq[32][29] = 524;\n        KRFreq[35][0] = 523;\n        KRFreq[24][43] = 522;\n        KRFreq[36][44] = 521;\n        KRFreq[20][30] = 520;\n        KRFreq[39][86] = 519;\n        KRFreq[22][14] = 518;\n        KRFreq[29][39] = 517;\n        KRFreq[28][38] = 516;\n        KRFreq[23][79] = 515;\n        KRFreq[24][56] = 514;\n        KRFreq[29][63] = 513;\n        KRFreq[31][45] = 512;\n        KRFreq[23][26] = 511;\n        KRFreq[15][87] = 510;\n        KRFreq[30][74] = 509;\n        KRFreq[24][69] = 508;\n        KRFreq[20][4] = 507;\n        KRFreq[27][50] = 506;\n        KRFreq[30][75] = 505;\n        KRFreq[24][13] = 504;\n        KRFreq[30][8] = 503;\n        KRFreq[31][6] = 502;\n        KRFreq[25][80] = 501;\n        KRFreq[36][8] = 500;\n        KRFreq[15][18] = 499;\n        KRFreq[39][23] = 498;\n        KRFreq[16][24] = 497;\n        KRFreq[31][89] = 496;\n        KRFreq[15][71] = 495;\n        KRFreq[15][57] = 494;\n        KRFreq[30][11] = 493;\n        KRFreq[15][36] = 492;\n        KRFreq[16][60] = 491;\n        KRFreq[24][45] = 490;\n        KRFreq[37][35] = 489;\n        KRFreq[24][87] = 488;\n        KRFreq[20][45] = 487;\n        KRFreq[31][90] = 486;\n        KRFreq[32][21] = 485;\n        KRFreq[19][70] = 484;\n        KRFreq[24][15] = 483;\n        KRFreq[26][92] = 482;\n        KRFreq[37][13] = 481;\n        KRFreq[39][2] = 480;\n        KRFreq[23][70] = 479;\n        KRFreq[27][25] = 478;\n        KRFreq[15][69] = 477;\n        KRFreq[19][61] = 476;\n        KRFreq[31][58] = 475;\n        KRFreq[24][57] = 474;\n        KRFreq[36][74] = 473;\n        KRFreq[21][6] = 472;\n        KRFreq[30][44] = 471;\n        KRFreq[15][91] = 470;\n        KRFreq[27][16] = 469;\n        KRFreq[29][42] = 468;\n        KRFreq[33][86] = 467;\n        KRFreq[29][41] = 466;\n        KRFreq[20][68] = 465;\n        KRFreq[25][47] = 464;\n        KRFreq[22][0] = 463;\n        KRFreq[18][14] = 462;\n        KRFreq[31][28] = 461;\n        KRFreq[15][2] = 460;\n        KRFreq[23][76] = 459;\n        KRFreq[38][32] = 458;\n        KRFreq[29][82] = 457;\n        KRFreq[21][86] = 456;\n        KRFreq[24][62] = 455;\n        KRFreq[31][64] = 454;\n        KRFreq[38][26] = 453;\n        KRFreq[32][86] = 452;\n        KRFreq[22][32] = 451;\n        KRFreq[19][59] = 450;\n        KRFreq[34][18] = 449;\n        KRFreq[18][54] = 448;\n        KRFreq[38][63] = 447;\n        KRFreq[36][23] = 446;\n        KRFreq[35][35] = 445;\n        KRFreq[32][62] = 444;\n        KRFreq[28][35] = 443;\n        KRFreq[27][13] = 442;\n        KRFreq[31][59] = 441;\n        KRFreq[29][29] = 440;\n        KRFreq[15][64] = 439;\n        KRFreq[26][84] = 438;\n        KRFreq[21][90] = 437;\n        KRFreq[20][24] = 436;\n        KRFreq[16][18] = 435;\n        KRFreq[22][23] = 434;\n        KRFreq[31][14] = 433;\n        KRFreq[15][1] = 432;\n        KRFreq[18][63] = 431;\n        KRFreq[19][10] = 430;\n        KRFreq[25][49] = 429;\n        KRFreq[36][57] = 428;\n        KRFreq[20][22] = 427;\n        KRFreq[15][15] = 426;\n        KRFreq[31][51] = 425;\n        KRFreq[24][60] = 424;\n        KRFreq[31][70] = 423;\n        KRFreq[15][7] = 422;\n        KRFreq[28][40] = 421;\n        KRFreq[18][41] = 420;\n        KRFreq[15][38] = 419;\n        KRFreq[32][0] = 418;\n        KRFreq[19][51] = 417;\n        KRFreq[34][62] = 416;\n        KRFreq[16][27] = 415;\n        KRFreq[20][70] = 414;\n        KRFreq[22][33] = 413;\n        KRFreq[26][73] = 412;\n        KRFreq[20][79] = 411;\n        KRFreq[23][6] = 410;\n        KRFreq[24][85] = 409;\n        KRFreq[38][51] = 408;\n        KRFreq[29][88] = 407;\n        KRFreq[38][55] = 406;\n        KRFreq[32][32] = 405;\n        KRFreq[27][18] = 404;\n        KRFreq[23][87] = 403;\n        KRFreq[35][6] = 402;\n        KRFreq[34][27] = 401;\n        KRFreq[39][35] = 400;\n        KRFreq[30][88] = 399;\n        KRFreq[32][92] = 398;\n        KRFreq[32][49] = 397;\n        KRFreq[24][61] = 396;\n        KRFreq[18][74] = 395;\n        KRFreq[23][77] = 394;\n        KRFreq[23][50] = 393;\n        KRFreq[23][32] = 392;\n        KRFreq[23][36] = 391;\n        KRFreq[38][38] = 390;\n        KRFreq[29][86] = 389;\n        KRFreq[36][15] = 388;\n        KRFreq[31][50] = 387;\n        KRFreq[15][86] = 386;\n        KRFreq[39][13] = 385;\n        KRFreq[34][26] = 384;\n        KRFreq[19][34] = 383;\n        KRFreq[16][3] = 382;\n        KRFreq[26][93] = 381;\n        KRFreq[19][67] = 380;\n        KRFreq[24][72] = 379;\n        KRFreq[29][17] = 378;\n        KRFreq[23][24] = 377;\n        KRFreq[25][19] = 376;\n        KRFreq[18][65] = 375;\n        KRFreq[30][78] = 374;\n        KRFreq[27][52] = 373;\n        KRFreq[22][18] = 372;\n        KRFreq[16][38] = 371;\n        KRFreq[21][26] = 370;\n        KRFreq[34][20] = 369;\n        KRFreq[15][42] = 368;\n        KRFreq[16][71] = 367;\n        KRFreq[17][17] = 366;\n        KRFreq[24][71] = 365;\n        KRFreq[18][84] = 364;\n        KRFreq[15][40] = 363;\n        KRFreq[31][62] = 362;\n        KRFreq[15][8] = 361;\n        KRFreq[16][69] = 360;\n        KRFreq[29][79] = 359;\n        KRFreq[38][91] = 358;\n        KRFreq[31][92] = 357;\n        KRFreq[20][77] = 356;\n        KRFreq[3][16] = 355;\n        KRFreq[27][87] = 354;\n        KRFreq[16][25] = 353;\n        KRFreq[36][33] = 352;\n        KRFreq[37][76] = 351;\n        KRFreq[30][12] = 350;\n        KRFreq[26][75] = 349;\n        KRFreq[25][14] = 348;\n        KRFreq[32][26] = 347;\n        KRFreq[23][22] = 346;\n        KRFreq[20][90] = 345;\n        KRFreq[19][8] = 344;\n        KRFreq[38][41] = 343;\n        KRFreq[34][2] = 342;\n        KRFreq[39][4] = 341;\n        KRFreq[27][89] = 340;\n        KRFreq[28][41] = 339;\n        KRFreq[28][44] = 338;\n        KRFreq[24][92] = 337;\n        KRFreq[34][65] = 336;\n        KRFreq[39][14] = 335;\n        KRFreq[21][38] = 334;\n        KRFreq[19][31] = 333;\n        KRFreq[37][39] = 332;\n        KRFreq[33][41] = 331;\n        KRFreq[38][4] = 330;\n        KRFreq[23][80] = 329;\n        KRFreq[25][24] = 328;\n        KRFreq[37][17] = 327;\n        KRFreq[22][16] = 326;\n        KRFreq[22][46] = 325;\n        KRFreq[33][91] = 324;\n        KRFreq[24][89] = 323;\n        KRFreq[30][52] = 322;\n        KRFreq[29][38] = 321;\n        KRFreq[38][85] = 320;\n        KRFreq[15][12] = 319;\n        KRFreq[27][58] = 318;\n        KRFreq[29][52] = 317;\n        KRFreq[37][38] = 316;\n        KRFreq[34][41] = 315;\n        KRFreq[31][65] = 314;\n        KRFreq[29][53] = 313;\n        KRFreq[22][47] = 312;\n        KRFreq[22][19] = 311;\n        KRFreq[26][0] = 310;\n        KRFreq[37][86] = 309;\n        KRFreq[35][4] = 308;\n        KRFreq[36][54] = 307;\n        KRFreq[20][76] = 306;\n        KRFreq[30][9] = 305;\n        KRFreq[30][33] = 304;\n        KRFreq[23][17] = 303;\n        KRFreq[23][33] = 302;\n        KRFreq[38][52] = 301;\n        KRFreq[15][19] = 300;\n        KRFreq[28][45] = 299;\n        KRFreq[29][78] = 298;\n        KRFreq[23][15] = 297;\n        KRFreq[33][5] = 296;\n        KRFreq[17][40] = 295;\n        KRFreq[30][83] = 294;\n        KRFreq[18][1] = 293;\n        KRFreq[30][81] = 292;\n        KRFreq[19][40] = 291;\n        KRFreq[24][47] = 290;\n        KRFreq[17][56] = 289;\n        KRFreq[39][80] = 288;\n        KRFreq[30][46] = 287;\n        KRFreq[16][61] = 286;\n        KRFreq[26][78] = 285;\n        KRFreq[26][57] = 284;\n        KRFreq[20][46] = 283;\n        KRFreq[25][15] = 282;\n        KRFreq[25][91] = 281;\n        KRFreq[21][83] = 280;\n        KRFreq[30][77] = 279;\n        KRFreq[35][30] = 278;\n        KRFreq[30][34] = 277;\n        KRFreq[20][69] = 276;\n        KRFreq[35][10] = 275;\n        KRFreq[29][70] = 274;\n        KRFreq[22][50] = 273;\n        KRFreq[18][0] = 272;\n        KRFreq[22][64] = 271;\n        KRFreq[38][65] = 270;\n        KRFreq[22][70] = 269;\n        KRFreq[24][58] = 268;\n        KRFreq[19][66] = 267;\n        KRFreq[30][59] = 266;\n        KRFreq[37][14] = 265;\n        KRFreq[16][56] = 264;\n        KRFreq[29][85] = 263;\n        KRFreq[31][15] = 262;\n        KRFreq[36][84] = 261;\n        KRFreq[39][15] = 260;\n        KRFreq[39][90] = 259;\n        KRFreq[18][12] = 258;\n        KRFreq[21][93] = 257;\n        KRFreq[24][66] = 256;\n        KRFreq[27][90] = 255;\n        KRFreq[25][90] = 254;\n        KRFreq[22][24] = 253;\n        KRFreq[36][67] = 252;\n        KRFreq[33][90] = 251;\n        KRFreq[15][60] = 250;\n        KRFreq[23][85] = 249;\n        KRFreq[34][1] = 248;\n        KRFreq[39][37] = 247;\n        KRFreq[21][18] = 246;\n        KRFreq[34][4] = 245;\n        KRFreq[28][33] = 244;\n        KRFreq[15][13] = 243;\n        KRFreq[32][22] = 242;\n        KRFreq[30][76] = 241;\n        KRFreq[20][21] = 240;\n        KRFreq[38][66] = 239;\n        KRFreq[32][55] = 238;\n        KRFreq[32][89] = 237;\n        KRFreq[25][26] = 236;\n        KRFreq[16][80] = 235;\n        KRFreq[15][43] = 234;\n        KRFreq[38][54] = 233;\n        KRFreq[39][68] = 232;\n        KRFreq[22][88] = 231;\n        KRFreq[21][84] = 230;\n        KRFreq[21][17] = 229;\n        KRFreq[20][28] = 228;\n        KRFreq[32][1] = 227;\n        KRFreq[33][87] = 226;\n        KRFreq[38][71] = 225;\n        KRFreq[37][47] = 224;\n        KRFreq[18][77] = 223;\n        KRFreq[37][58] = 222;\n        KRFreq[34][74] = 221;\n        KRFreq[32][54] = 220;\n        KRFreq[27][33] = 219;\n        KRFreq[32][93] = 218;\n        KRFreq[23][51] = 217;\n        KRFreq[20][57] = 216;\n        KRFreq[22][37] = 215;\n        KRFreq[39][10] = 214;\n        KRFreq[39][17] = 213;\n        KRFreq[33][4] = 212;\n        KRFreq[32][84] = 211;\n        KRFreq[34][3] = 210;\n        KRFreq[28][27] = 209;\n        KRFreq[15][79] = 208;\n        KRFreq[34][21] = 207;\n        KRFreq[34][69] = 206;\n        KRFreq[21][62] = 205;\n        KRFreq[36][24] = 204;\n        KRFreq[16][89] = 203;\n        KRFreq[18][48] = 202;\n        KRFreq[38][15] = 201;\n        KRFreq[36][58] = 200;\n        KRFreq[21][56] = 199;\n        KRFreq[34][48] = 198;\n        KRFreq[21][15] = 197;\n        KRFreq[39][3] = 196;\n        KRFreq[16][44] = 195;\n        KRFreq[18][79] = 194;\n        KRFreq[25][13] = 193;\n        KRFreq[29][47] = 192;\n        KRFreq[38][88] = 191;\n        KRFreq[20][71] = 190;\n        KRFreq[16][58] = 189;\n        KRFreq[35][57] = 188;\n        KRFreq[29][30] = 187;\n        KRFreq[29][23] = 186;\n        KRFreq[34][93] = 185;\n        KRFreq[30][85] = 184;\n        KRFreq[15][80] = 183;\n        KRFreq[32][78] = 182;\n        KRFreq[37][82] = 181;\n        KRFreq[22][40] = 180;\n        KRFreq[21][69] = 179;\n        KRFreq[26][85] = 178;\n        KRFreq[31][31] = 177;\n        KRFreq[28][64] = 176;\n        KRFreq[38][13] = 175;\n        KRFreq[25][2] = 174;\n        KRFreq[22][34] = 173;\n        KRFreq[28][28] = 172;\n        KRFreq[24][91] = 171;\n        KRFreq[33][74] = 170;\n        KRFreq[29][40] = 169;\n        KRFreq[15][77] = 168;\n        KRFreq[32][80] = 167;\n        KRFreq[30][41] = 166;\n        KRFreq[23][30] = 165;\n        KRFreq[24][63] = 164;\n        KRFreq[30][53] = 163;\n        KRFreq[39][70] = 162;\n        KRFreq[23][61] = 161;\n        KRFreq[37][27] = 160;\n        KRFreq[16][55] = 159;\n        KRFreq[22][74] = 158;\n        KRFreq[26][50] = 157;\n        KRFreq[16][10] = 156;\n        KRFreq[34][63] = 155;\n        KRFreq[35][14] = 154;\n        KRFreq[17][7] = 153;\n        KRFreq[15][59] = 152;\n        KRFreq[27][23] = 151;\n        KRFreq[18][70] = 150;\n        KRFreq[32][56] = 149;\n        KRFreq[37][87] = 148;\n        KRFreq[17][61] = 147;\n        KRFreq[18][83] = 146;\n        KRFreq[23][86] = 145;\n        KRFreq[17][31] = 144;\n        KRFreq[23][83] = 143;\n        KRFreq[35][2] = 142;\n        KRFreq[18][64] = 141;\n        KRFreq[27][43] = 140;\n        KRFreq[32][42] = 139;\n        KRFreq[25][76] = 138;\n        KRFreq[19][85] = 137;\n        KRFreq[37][81] = 136;\n        KRFreq[38][83] = 135;\n        KRFreq[35][7] = 134;\n        KRFreq[16][51] = 133;\n        KRFreq[27][22] = 132;\n        KRFreq[16][76] = 131;\n        KRFreq[22][4] = 130;\n        KRFreq[38][84] = 129;\n        KRFreq[17][83] = 128;\n        KRFreq[24][46] = 127;\n        KRFreq[33][15] = 126;\n        KRFreq[20][48] = 125;\n        KRFreq[17][30] = 124;\n        KRFreq[30][93] = 123;\n        KRFreq[28][11] = 122;\n        KRFreq[28][30] = 121;\n        KRFreq[15][62] = 120;\n        KRFreq[17][87] = 119;\n        KRFreq[32][81] = 118;\n        KRFreq[23][37] = 117;\n        KRFreq[30][22] = 116;\n        KRFreq[32][66] = 115;\n        KRFreq[33][78] = 114;\n        KRFreq[21][4] = 113;\n        KRFreq[31][17] = 112;\n        KRFreq[39][61] = 111;\n        KRFreq[18][76] = 110;\n        KRFreq[15][85] = 109;\n        KRFreq[31][47] = 108;\n        KRFreq[19][57] = 107;\n        KRFreq[23][55] = 106;\n        KRFreq[27][29] = 105;\n        KRFreq[29][46] = 104;\n        KRFreq[33][0] = 103;\n        KRFreq[16][83] = 102;\n        KRFreq[39][78] = 101;\n        KRFreq[32][77] = 100;\n        KRFreq[36][25] = 99;\n        KRFreq[34][19] = 98;\n        KRFreq[38][49] = 97;\n        KRFreq[19][25] = 96;\n        KRFreq[23][53] = 95;\n        KRFreq[28][43] = 94;\n        KRFreq[31][44] = 93;\n        KRFreq[36][34] = 92;\n        KRFreq[16][34] = 91;\n        KRFreq[35][1] = 90;\n        KRFreq[19][87] = 89;\n        KRFreq[18][53] = 88;\n        KRFreq[29][54] = 87;\n        KRFreq[22][41] = 86;\n        KRFreq[38][18] = 85;\n        KRFreq[22][2] = 84;\n        KRFreq[20][3] = 83;\n        KRFreq[39][69] = 82;\n        KRFreq[30][29] = 81;\n        KRFreq[28][19] = 80;\n        KRFreq[29][90] = 79;\n        KRFreq[17][86] = 78;\n        KRFreq[15][9] = 77;\n        KRFreq[39][73] = 76;\n        KRFreq[15][37] = 75;\n        KRFreq[35][40] = 74;\n        KRFreq[33][77] = 73;\n        KRFreq[27][86] = 72;\n        KRFreq[36][79] = 71;\n        KRFreq[23][18] = 70;\n        KRFreq[34][87] = 69;\n        KRFreq[39][24] = 68;\n        KRFreq[26][8] = 67;\n        KRFreq[33][48] = 66;\n        KRFreq[39][30] = 65;\n        KRFreq[33][28] = 64;\n        KRFreq[16][67] = 63;\n        KRFreq[31][78] = 62;\n        KRFreq[32][23] = 61;\n        KRFreq[24][55] = 60;\n        KRFreq[30][68] = 59;\n        KRFreq[18][60] = 58;\n        KRFreq[15][17] = 57;\n        KRFreq[23][34] = 56;\n        KRFreq[20][49] = 55;\n        KRFreq[15][78] = 54;\n        KRFreq[24][14] = 53;\n        KRFreq[19][41] = 52;\n        KRFreq[31][55] = 51;\n        KRFreq[21][39] = 50;\n        KRFreq[35][9] = 49;\n        KRFreq[30][15] = 48;\n        KRFreq[20][52] = 47;\n        KRFreq[35][71] = 46;\n        KRFreq[20][7] = 45;\n        KRFreq[29][72] = 44;\n        KRFreq[37][77] = 43;\n        KRFreq[22][35] = 42;\n        KRFreq[20][61] = 41;\n        KRFreq[31][60] = 40;\n        KRFreq[20][93] = 39;\n        KRFreq[27][92] = 38;\n        KRFreq[28][16] = 37;\n        KRFreq[36][26] = 36;\n        KRFreq[18][89] = 35;\n        KRFreq[21][63] = 34;\n        KRFreq[22][52] = 33;\n        KRFreq[24][65] = 32;\n        KRFreq[31][8] = 31;\n        KRFreq[31][49] = 30;\n        KRFreq[33][30] = 29;\n        KRFreq[37][15] = 28;\n        KRFreq[18][18] = 27;\n        KRFreq[25][50] = 26;\n        KRFreq[29][20] = 25;\n        KRFreq[35][48] = 24;\n        KRFreq[38][75] = 23;\n        KRFreq[26][83] = 22;\n        KRFreq[21][87] = 21;\n        KRFreq[27][71] = 20;\n        KRFreq[32][91] = 19;\n        KRFreq[25][73] = 18;\n        KRFreq[16][84] = 17;\n        KRFreq[25][31] = 16;\n        KRFreq[17][90] = 15;\n        KRFreq[18][40] = 14;\n        KRFreq[17][77] = 13;\n        KRFreq[17][35] = 12;\n        KRFreq[23][52] = 11;\n        KRFreq[23][35] = 10;\n        KRFreq[16][5] = 9;\n        KRFreq[23][58] = 8;\n        KRFreq[19][60] = 7;\n        KRFreq[30][32] = 6;\n        KRFreq[38][34] = 5;\n        KRFreq[23][4] = 4;\n        KRFreq[23][1] = 3;\n        KRFreq[27][57] = 2;\n        KRFreq[39][38] = 1;\n        KRFreq[32][33] = 0;\n        JPFreq[3][74] = 600;\n        JPFreq[3][45] = 599;\n        JPFreq[3][3] = 598;\n        JPFreq[3][24] = 597;\n        JPFreq[3][30] = 596;\n        JPFreq[3][42] = 595;\n        JPFreq[3][46] = 594;\n        JPFreq[3][39] = 593;\n        JPFreq[3][11] = 592;\n        JPFreq[3][37] = 591;\n        JPFreq[3][38] = 590;\n        JPFreq[3][31] = 589;\n        JPFreq[3][41] = 588;\n        JPFreq[3][5] = 587;\n        JPFreq[3][10] = 586;\n        JPFreq[3][75] = 585;\n        JPFreq[3][65] = 584;\n        JPFreq[3][72] = 583;\n        JPFreq[37][91] = 582;\n        JPFreq[0][27] = 581;\n        JPFreq[3][18] = 580;\n        JPFreq[3][22] = 579;\n        JPFreq[3][61] = 578;\n        JPFreq[3][14] = 577;\n        JPFreq[24][80] = 576;\n        JPFreq[4][82] = 575;\n        JPFreq[17][80] = 574;\n        JPFreq[30][44] = 573;\n        JPFreq[3][73] = 572;\n        JPFreq[3][64] = 571;\n        JPFreq[38][14] = 570;\n        JPFreq[33][70] = 569;\n        JPFreq[3][1] = 568;\n        JPFreq[3][16] = 567;\n        JPFreq[3][35] = 566;\n        JPFreq[3][40] = 565;\n        JPFreq[4][74] = 564;\n        JPFreq[4][24] = 563;\n        JPFreq[42][59] = 562;\n        JPFreq[3][7] = 561;\n        JPFreq[3][71] = 560;\n        JPFreq[3][12] = 559;\n        JPFreq[15][75] = 558;\n        JPFreq[3][20] = 557;\n        JPFreq[4][39] = 556;\n        JPFreq[34][69] = 555;\n        JPFreq[3][28] = 554;\n        JPFreq[35][24] = 553;\n        JPFreq[3][82] = 552;\n        JPFreq[28][47] = 551;\n        JPFreq[3][67] = 550;\n        JPFreq[37][16] = 549;\n        JPFreq[26][93] = 548;\n        JPFreq[4][1] = 547;\n        JPFreq[26][85] = 546;\n        JPFreq[31][14] = 545;\n        JPFreq[4][3] = 544;\n        JPFreq[4][72] = 543;\n        JPFreq[24][51] = 542;\n        JPFreq[27][51] = 541;\n        JPFreq[27][49] = 540;\n        JPFreq[22][77] = 539;\n        JPFreq[27][10] = 538;\n        JPFreq[29][68] = 537;\n        JPFreq[20][35] = 536;\n        JPFreq[41][11] = 535;\n        JPFreq[24][70] = 534;\n        JPFreq[36][61] = 533;\n        JPFreq[31][23] = 532;\n        JPFreq[43][16] = 531;\n        JPFreq[23][68] = 530;\n        JPFreq[32][15] = 529;\n        JPFreq[3][32] = 528;\n        JPFreq[19][53] = 527;\n        JPFreq[40][83] = 526;\n        JPFreq[4][14] = 525;\n        JPFreq[36][9] = 524;\n        JPFreq[4][73] = 523;\n        JPFreq[23][10] = 522;\n        JPFreq[3][63] = 521;\n        JPFreq[39][14] = 520;\n        JPFreq[3][78] = 519;\n        JPFreq[33][47] = 518;\n        JPFreq[21][39] = 517;\n        JPFreq[34][46] = 516;\n        JPFreq[36][75] = 515;\n        JPFreq[41][92] = 514;\n        JPFreq[37][93] = 513;\n        JPFreq[4][34] = 512;\n        JPFreq[15][86] = 511;\n        JPFreq[46][1] = 510;\n        JPFreq[37][65] = 509;\n        JPFreq[3][62] = 508;\n        JPFreq[32][73] = 507;\n        JPFreq[21][65] = 506;\n        JPFreq[29][75] = 505;\n        JPFreq[26][51] = 504;\n        JPFreq[3][34] = 503;\n        JPFreq[4][10] = 502;\n        JPFreq[30][22] = 501;\n        JPFreq[35][73] = 500;\n        JPFreq[17][82] = 499;\n        JPFreq[45][8] = 498;\n        JPFreq[27][73] = 497;\n        JPFreq[18][55] = 496;\n        JPFreq[25][2] = 495;\n        JPFreq[3][26] = 494;\n        JPFreq[45][46] = 493;\n        JPFreq[4][22] = 492;\n        JPFreq[4][40] = 491;\n        JPFreq[18][10] = 490;\n        JPFreq[32][9] = 489;\n        JPFreq[26][49] = 488;\n        JPFreq[3][47] = 487;\n        JPFreq[24][65] = 486;\n        JPFreq[4][76] = 485;\n        JPFreq[43][67] = 484;\n        JPFreq[3][9] = 483;\n        JPFreq[41][37] = 482;\n        JPFreq[33][68] = 481;\n        JPFreq[43][31] = 480;\n        JPFreq[19][55] = 479;\n        JPFreq[4][30] = 478;\n        JPFreq[27][33] = 477;\n        JPFreq[16][62] = 476;\n        JPFreq[36][35] = 475;\n        JPFreq[37][15] = 474;\n        JPFreq[27][70] = 473;\n        JPFreq[22][71] = 472;\n        JPFreq[33][45] = 471;\n        JPFreq[31][78] = 470;\n        JPFreq[43][59] = 469;\n        JPFreq[32][19] = 468;\n        JPFreq[17][28] = 467;\n        JPFreq[40][28] = 466;\n        JPFreq[20][93] = 465;\n        JPFreq[18][15] = 464;\n        JPFreq[4][23] = 463;\n        JPFreq[3][23] = 462;\n        JPFreq[26][64] = 461;\n        JPFreq[44][92] = 460;\n        JPFreq[17][27] = 459;\n        JPFreq[3][56] = 458;\n        JPFreq[25][38] = 457;\n        JPFreq[23][31] = 456;\n        JPFreq[35][43] = 455;\n        JPFreq[4][54] = 454;\n        JPFreq[35][19] = 453;\n        JPFreq[22][47] = 452;\n        JPFreq[42][0] = 451;\n        JPFreq[23][28] = 450;\n        JPFreq[46][33] = 449;\n        JPFreq[36][85] = 448;\n        JPFreq[31][12] = 447;\n        JPFreq[3][76] = 446;\n        JPFreq[4][75] = 445;\n        JPFreq[36][56] = 444;\n        JPFreq[4][64] = 443;\n        JPFreq[25][77] = 442;\n        JPFreq[15][52] = 441;\n        JPFreq[33][73] = 440;\n        JPFreq[3][55] = 439;\n        JPFreq[43][82] = 438;\n        JPFreq[27][82] = 437;\n        JPFreq[20][3] = 436;\n        JPFreq[40][51] = 435;\n        JPFreq[3][17] = 434;\n        JPFreq[27][71] = 433;\n        JPFreq[4][52] = 432;\n        JPFreq[44][48] = 431;\n        JPFreq[27][2] = 430;\n        JPFreq[17][39] = 429;\n        JPFreq[31][8] = 428;\n        JPFreq[44][54] = 427;\n        JPFreq[43][18] = 426;\n        JPFreq[43][77] = 425;\n        JPFreq[4][61] = 424;\n        JPFreq[19][91] = 423;\n        JPFreq[31][13] = 422;\n        JPFreq[44][71] = 421;\n        JPFreq[20][0] = 420;\n        JPFreq[23][87] = 419;\n        JPFreq[21][14] = 418;\n        JPFreq[29][13] = 417;\n        JPFreq[3][58] = 416;\n        JPFreq[26][18] = 415;\n        JPFreq[4][47] = 414;\n        JPFreq[4][18] = 413;\n        JPFreq[3][53] = 412;\n        JPFreq[26][92] = 411;\n        JPFreq[21][7] = 410;\n        JPFreq[4][37] = 409;\n        JPFreq[4][63] = 408;\n        JPFreq[36][51] = 407;\n        JPFreq[4][32] = 406;\n        JPFreq[28][73] = 405;\n        JPFreq[4][50] = 404;\n        JPFreq[41][60] = 403;\n        JPFreq[23][1] = 402;\n        JPFreq[36][92] = 401;\n        JPFreq[15][41] = 400;\n        JPFreq[21][71] = 399;\n        JPFreq[41][30] = 398;\n        JPFreq[32][76] = 397;\n        JPFreq[17][34] = 396;\n        JPFreq[26][15] = 395;\n        JPFreq[26][25] = 394;\n        JPFreq[31][77] = 393;\n        JPFreq[31][3] = 392;\n        JPFreq[46][34] = 391;\n        JPFreq[27][84] = 390;\n        JPFreq[23][8] = 389;\n        JPFreq[16][0] = 388;\n        JPFreq[28][80] = 387;\n        JPFreq[26][54] = 386;\n        JPFreq[33][18] = 385;\n        JPFreq[31][20] = 384;\n        JPFreq[31][62] = 383;\n        JPFreq[30][41] = 382;\n        JPFreq[33][30] = 381;\n        JPFreq[45][45] = 380;\n        JPFreq[37][82] = 379;\n        JPFreq[15][33] = 378;\n        JPFreq[20][12] = 377;\n        JPFreq[18][5] = 376;\n        JPFreq[28][86] = 375;\n        JPFreq[30][19] = 374;\n        JPFreq[42][43] = 373;\n        JPFreq[36][31] = 372;\n        JPFreq[17][93] = 371;\n        JPFreq[4][15] = 370;\n        JPFreq[21][20] = 369;\n        JPFreq[23][21] = 368;\n        JPFreq[28][72] = 367;\n        JPFreq[4][20] = 366;\n        JPFreq[26][55] = 365;\n        JPFreq[21][5] = 364;\n        JPFreq[19][16] = 363;\n        JPFreq[23][64] = 362;\n        JPFreq[40][59] = 361;\n        JPFreq[37][26] = 360;\n        JPFreq[26][56] = 359;\n        JPFreq[4][12] = 358;\n        JPFreq[33][71] = 357;\n        JPFreq[32][39] = 356;\n        JPFreq[38][40] = 355;\n        JPFreq[22][74] = 354;\n        JPFreq[3][25] = 353;\n        JPFreq[15][48] = 352;\n        JPFreq[41][82] = 351;\n        JPFreq[41][9] = 350;\n        JPFreq[25][48] = 349;\n        JPFreq[31][71] = 348;\n        JPFreq[43][29] = 347;\n        JPFreq[26][80] = 346;\n        JPFreq[4][5] = 345;\n        JPFreq[18][71] = 344;\n        JPFreq[29][0] = 343;\n        JPFreq[43][43] = 342;\n        JPFreq[23][81] = 341;\n        JPFreq[4][42] = 340;\n        JPFreq[44][28] = 339;\n        JPFreq[23][93] = 338;\n        JPFreq[17][81] = 337;\n        JPFreq[25][25] = 336;\n        JPFreq[41][23] = 335;\n        JPFreq[34][35] = 334;\n        JPFreq[4][53] = 333;\n        JPFreq[28][36] = 332;\n        JPFreq[4][41] = 331;\n        JPFreq[25][60] = 330;\n        JPFreq[23][20] = 329;\n        JPFreq[3][43] = 328;\n        JPFreq[24][79] = 327;\n        JPFreq[29][41] = 326;\n        JPFreq[30][83] = 325;\n        JPFreq[3][50] = 324;\n        JPFreq[22][18] = 323;\n        JPFreq[18][3] = 322;\n        JPFreq[39][30] = 321;\n        JPFreq[4][28] = 320;\n        JPFreq[21][64] = 319;\n        JPFreq[4][68] = 318;\n        JPFreq[17][71] = 317;\n        JPFreq[27][0] = 316;\n        JPFreq[39][28] = 315;\n        JPFreq[30][13] = 314;\n        JPFreq[36][70] = 313;\n        JPFreq[20][82] = 312;\n        JPFreq[33][38] = 311;\n        JPFreq[44][87] = 310;\n        JPFreq[34][45] = 309;\n        JPFreq[4][26] = 308;\n        JPFreq[24][44] = 307;\n        JPFreq[38][67] = 306;\n        JPFreq[38][6] = 305;\n        JPFreq[30][68] = 304;\n        JPFreq[15][89] = 303;\n        JPFreq[24][93] = 302;\n        JPFreq[40][41] = 301;\n        JPFreq[38][3] = 300;\n        JPFreq[28][23] = 299;\n        JPFreq[26][17] = 298;\n        JPFreq[4][38] = 297;\n        JPFreq[22][78] = 296;\n        JPFreq[15][37] = 295;\n        JPFreq[25][85] = 294;\n        JPFreq[4][9] = 293;\n        JPFreq[4][7] = 292;\n        JPFreq[27][53] = 291;\n        JPFreq[39][29] = 290;\n        JPFreq[41][43] = 289;\n        JPFreq[25][62] = 288;\n        JPFreq[4][48] = 287;\n        JPFreq[28][28] = 286;\n        JPFreq[21][40] = 285;\n        JPFreq[36][73] = 284;\n        JPFreq[26][39] = 283;\n        JPFreq[22][54] = 282;\n        JPFreq[33][5] = 281;\n        JPFreq[19][21] = 280;\n        JPFreq[46][31] = 279;\n        JPFreq[20][64] = 278;\n        JPFreq[26][63] = 277;\n        JPFreq[22][23] = 276;\n        JPFreq[25][81] = 275;\n        JPFreq[4][62] = 274;\n        JPFreq[37][31] = 273;\n        JPFreq[40][52] = 272;\n        JPFreq[29][79] = 271;\n        JPFreq[41][48] = 270;\n        JPFreq[31][57] = 269;\n        JPFreq[32][92] = 268;\n        JPFreq[36][36] = 267;\n        JPFreq[27][7] = 266;\n        JPFreq[35][29] = 265;\n        JPFreq[37][34] = 264;\n        JPFreq[34][42] = 263;\n        JPFreq[27][15] = 262;\n        JPFreq[33][27] = 261;\n        JPFreq[31][38] = 260;\n        JPFreq[19][79] = 259;\n        JPFreq[4][31] = 258;\n        JPFreq[4][66] = 257;\n        JPFreq[17][32] = 256;\n        JPFreq[26][67] = 255;\n        JPFreq[16][30] = 254;\n        JPFreq[26][46] = 253;\n        JPFreq[24][26] = 252;\n        JPFreq[35][10] = 251;\n        JPFreq[18][37] = 250;\n        JPFreq[3][19] = 249;\n        JPFreq[33][69] = 248;\n        JPFreq[31][9] = 247;\n        JPFreq[45][29] = 246;\n        JPFreq[3][15] = 245;\n        JPFreq[18][54] = 244;\n        JPFreq[3][44] = 243;\n        JPFreq[31][29] = 242;\n        JPFreq[18][45] = 241;\n        JPFreq[38][28] = 240;\n        JPFreq[24][12] = 239;\n        JPFreq[35][82] = 238;\n        JPFreq[17][43] = 237;\n        JPFreq[28][9] = 236;\n        JPFreq[23][25] = 235;\n        JPFreq[44][37] = 234;\n        JPFreq[23][75] = 233;\n        JPFreq[23][92] = 232;\n        JPFreq[0][24] = 231;\n        JPFreq[19][74] = 230;\n        JPFreq[45][32] = 229;\n        JPFreq[16][72] = 228;\n        JPFreq[16][93] = 227;\n        JPFreq[45][13] = 226;\n        JPFreq[24][8] = 225;\n        JPFreq[25][47] = 224;\n        JPFreq[28][26] = 223;\n        JPFreq[43][81] = 222;\n        JPFreq[32][71] = 221;\n        JPFreq[18][41] = 220;\n        JPFreq[26][62] = 219;\n        JPFreq[41][24] = 218;\n        JPFreq[40][11] = 217;\n        JPFreq[43][57] = 216;\n        JPFreq[34][53] = 215;\n        JPFreq[20][32] = 214;\n        JPFreq[34][43] = 213;\n        JPFreq[41][91] = 212;\n        JPFreq[29][57] = 211;\n        JPFreq[15][43] = 210;\n        JPFreq[22][89] = 209;\n        JPFreq[33][83] = 208;\n        JPFreq[43][20] = 207;\n        JPFreq[25][58] = 206;\n        JPFreq[30][30] = 205;\n        JPFreq[4][56] = 204;\n        JPFreq[17][64] = 203;\n        JPFreq[23][0] = 202;\n        JPFreq[44][12] = 201;\n        JPFreq[25][37] = 200;\n        JPFreq[35][13] = 199;\n        JPFreq[20][30] = 198;\n        JPFreq[21][84] = 197;\n        JPFreq[29][14] = 196;\n        JPFreq[30][5] = 195;\n        JPFreq[37][2] = 194;\n        JPFreq[4][78] = 193;\n        JPFreq[29][78] = 192;\n        JPFreq[29][84] = 191;\n        JPFreq[32][86] = 190;\n        JPFreq[20][68] = 189;\n        JPFreq[30][39] = 188;\n        JPFreq[15][69] = 187;\n        JPFreq[4][60] = 186;\n        JPFreq[20][61] = 185;\n        JPFreq[41][67] = 184;\n        JPFreq[16][35] = 183;\n        JPFreq[36][57] = 182;\n        JPFreq[39][80] = 181;\n        JPFreq[4][59] = 180;\n        JPFreq[4][44] = 179;\n        JPFreq[40][54] = 178;\n        JPFreq[30][8] = 177;\n        JPFreq[44][30] = 176;\n        JPFreq[31][93] = 175;\n        JPFreq[31][47] = 174;\n        JPFreq[16][70] = 173;\n        JPFreq[21][0] = 172;\n        JPFreq[17][35] = 171;\n        JPFreq[21][67] = 170;\n        JPFreq[44][18] = 169;\n        JPFreq[36][29] = 168;\n        JPFreq[18][67] = 167;\n        JPFreq[24][28] = 166;\n        JPFreq[36][24] = 165;\n        JPFreq[23][5] = 164;\n        JPFreq[31][65] = 163;\n        JPFreq[26][59] = 162;\n        JPFreq[28][2] = 161;\n        JPFreq[39][69] = 160;\n        JPFreq[42][40] = 159;\n        JPFreq[37][80] = 158;\n        JPFreq[15][66] = 157;\n        JPFreq[34][38] = 156;\n        JPFreq[28][48] = 155;\n        JPFreq[37][77] = 154;\n        JPFreq[29][34] = 153;\n        JPFreq[33][12] = 152;\n        JPFreq[4][65] = 151;\n        JPFreq[30][31] = 150;\n        JPFreq[27][92] = 149;\n        JPFreq[4][2] = 148;\n        JPFreq[4][51] = 147;\n        JPFreq[23][77] = 146;\n        JPFreq[4][35] = 145;\n        JPFreq[3][13] = 144;\n        JPFreq[26][26] = 143;\n        JPFreq[44][4] = 142;\n        JPFreq[39][53] = 141;\n        JPFreq[20][11] = 140;\n        JPFreq[40][33] = 139;\n        JPFreq[45][7] = 138;\n        JPFreq[4][70] = 137;\n        JPFreq[3][49] = 136;\n        JPFreq[20][59] = 135;\n        JPFreq[21][12] = 134;\n        JPFreq[33][53] = 133;\n        JPFreq[20][14] = 132;\n        JPFreq[37][18] = 131;\n        JPFreq[18][17] = 130;\n        JPFreq[36][23] = 129;\n        JPFreq[18][57] = 128;\n        JPFreq[26][74] = 127;\n        JPFreq[35][2] = 126;\n        JPFreq[38][58] = 125;\n        JPFreq[34][68] = 124;\n        JPFreq[29][81] = 123;\n        JPFreq[20][69] = 122;\n        JPFreq[39][86] = 121;\n        JPFreq[4][16] = 120;\n        JPFreq[16][49] = 119;\n        JPFreq[15][72] = 118;\n        JPFreq[26][35] = 117;\n        JPFreq[32][14] = 116;\n        JPFreq[40][90] = 115;\n        JPFreq[33][79] = 114;\n        JPFreq[35][4] = 113;\n        JPFreq[23][33] = 112;\n        JPFreq[19][19] = 111;\n        JPFreq[31][41] = 110;\n        JPFreq[44][1] = 109;\n        JPFreq[22][56] = 108;\n        JPFreq[31][27] = 107;\n        JPFreq[32][18] = 106;\n        JPFreq[27][32] = 105;\n        JPFreq[37][39] = 104;\n        JPFreq[42][11] = 103;\n        JPFreq[29][71] = 102;\n        JPFreq[32][58] = 101;\n        JPFreq[46][10] = 100;\n        JPFreq[17][30] = 99;\n        JPFreq[38][15] = 98;\n        JPFreq[29][60] = 97;\n        JPFreq[4][11] = 96;\n        JPFreq[38][31] = 95;\n        JPFreq[40][79] = 94;\n        JPFreq[28][49] = 93;\n        JPFreq[28][84] = 92;\n        JPFreq[26][77] = 91;\n        JPFreq[22][32] = 90;\n        JPFreq[33][17] = 89;\n        JPFreq[23][18] = 88;\n        JPFreq[32][64] = 87;\n        JPFreq[4][6] = 86;\n        JPFreq[33][51] = 85;\n        JPFreq[44][77] = 84;\n        JPFreq[29][5] = 83;\n        JPFreq[46][25] = 82;\n        JPFreq[19][58] = 81;\n        JPFreq[4][46] = 80;\n        JPFreq[15][71] = 79;\n        JPFreq[18][58] = 78;\n        JPFreq[26][45] = 77;\n        JPFreq[45][66] = 76;\n        JPFreq[34][10] = 75;\n        JPFreq[19][37] = 74;\n        JPFreq[33][65] = 73;\n        JPFreq[44][52] = 72;\n        JPFreq[16][38] = 71;\n        JPFreq[36][46] = 70;\n        JPFreq[20][26] = 69;\n        JPFreq[30][37] = 68;\n        JPFreq[4][58] = 67;\n        JPFreq[43][2] = 66;\n        JPFreq[30][18] = 65;\n        JPFreq[19][35] = 64;\n        JPFreq[15][68] = 63;\n        JPFreq[3][36] = 62;\n        JPFreq[35][40] = 61;\n        JPFreq[36][32] = 60;\n        JPFreq[37][14] = 59;\n        JPFreq[17][11] = 58;\n        JPFreq[19][78] = 57;\n        JPFreq[37][11] = 56;\n        JPFreq[28][63] = 55;\n        JPFreq[29][61] = 54;\n        JPFreq[33][3] = 53;\n        JPFreq[41][52] = 52;\n        JPFreq[33][63] = 51;\n        JPFreq[22][41] = 50;\n        JPFreq[4][19] = 49;\n        JPFreq[32][41] = 48;\n        JPFreq[24][4] = 47;\n        JPFreq[31][28] = 46;\n        JPFreq[43][30] = 45;\n        JPFreq[17][3] = 44;\n        JPFreq[43][70] = 43;\n        JPFreq[34][19] = 42;\n        JPFreq[20][77] = 41;\n        JPFreq[18][83] = 40;\n        JPFreq[17][15] = 39;\n        JPFreq[23][61] = 38;\n        JPFreq[40][27] = 37;\n        JPFreq[16][48] = 36;\n        JPFreq[39][78] = 35;\n        JPFreq[41][53] = 34;\n        JPFreq[40][91] = 33;\n        JPFreq[40][72] = 32;\n        JPFreq[18][52] = 31;\n        JPFreq[35][66] = 30;\n        JPFreq[39][93] = 29;\n        JPFreq[19][48] = 28;\n        JPFreq[26][36] = 27;\n        JPFreq[27][25] = 26;\n        JPFreq[42][71] = 25;\n        JPFreq[42][85] = 24;\n        JPFreq[26][48] = 23;\n        JPFreq[28][15] = 22;\n        JPFreq[3][66] = 21;\n        JPFreq[25][24] = 20;\n        JPFreq[27][43] = 19;\n        JPFreq[27][78] = 18;\n        JPFreq[45][43] = 17;\n        JPFreq[27][72] = 16;\n        JPFreq[40][29] = 15;\n        JPFreq[41][0] = 14;\n        JPFreq[19][57] = 13;\n        JPFreq[15][59] = 12;\n        JPFreq[29][29] = 11;\n        JPFreq[4][25] = 10;\n        JPFreq[21][42] = 9;\n        JPFreq[23][35] = 8;\n        JPFreq[33][1] = 7;\n        JPFreq[4][57] = 6;\n        JPFreq[17][60] = 5;\n        JPFreq[25][19] = 4;\n        JPFreq[22][65] = 3;\n        JPFreq[42][29] = 2;\n        JPFreq[27][66] = 1;\n        JPFreq[26][89] = 0;\n    }\n}\n\nclass Encoding {\n    // Supported Encoding Types\n    public static int GB2312 = 0;\n\n    public static int GBK = 1;\n\n    public static int GB18030 = 2;\n\n    public static int HZ = 3;\n\n    public static int BIG5 = 4;\n\n    public static int CNS11643 = 5;\n\n    public static int UTF8 = 6;\n\n    public static int UTF8T = 7;\n\n    public static int UTF8S = 8;\n\n    public static int UNICODE = 9;\n\n    public static int UNICODET = 10;\n\n    public static int UNICODES = 11;\n\n    public static int ISO2022CN = 12;\n\n    public static int ISO2022CN_CNS = 13;\n\n    public static int ISO2022CN_GB = 14;\n\n    public static int EUC_KR = 15;\n\n    public static int CP949 = 16;\n\n    public static int ISO2022KR = 17;\n\n    public static int JOHAB = 18;\n\n    public static int SJIS = 19;\n\n    public static int EUC_JP = 20;\n\n    public static int ISO2022JP = 21;\n\n    public static int ASCII = 22;\n\n    public static int OTHER = 23;\n\n    public static int TOTALTYPES = 24;\n\n    public final static int SIMP = 0;\n\n    public final static int TRAD = 1;\n\n    // Names of the encodings as understood by Java\n    public static String[] javaname;\n\n    // Names of the encodings for human viewing\n    public static String[] nicename;\n\n    // Names of charsets as used in charset parameter of HTML Meta tag\n    public static String[] htmlname;\n\n    // Constructor\n    public Encoding() {\n        javaname = new String[TOTALTYPES];\n        nicename = new String[TOTALTYPES];\n        htmlname = new String[TOTALTYPES];\n        // Assign encoding names\n        javaname[GB2312] = \"GB2312\";\n        javaname[GBK] = \"GBK\";\n        javaname[GB18030] = \"GB18030\";\n        javaname[HZ] = \"ASCII\"; // What to put here? Sun doesn't support HZ\n        javaname[ISO2022CN_GB] = \"ISO2022CN_GB\";\n        javaname[BIG5] = \"BIG5\";\n        javaname[CNS11643] = \"EUC-TW\";\n        javaname[ISO2022CN_CNS] = \"ISO2022CN_CNS\";\n        javaname[ISO2022CN] = \"ISO2022CN\";\n        javaname[UTF8] = \"UTF-8\";\n        javaname[UTF8T] = \"UTF-8\";\n        javaname[UTF8S] = \"UTF-8\";\n        javaname[UNICODE] = \"Unicode\";\n        javaname[UNICODET] = \"Unicode\";\n        javaname[UNICODES] = \"Unicode\";\n        javaname[EUC_KR] = \"EUC_KR\";\n        javaname[CP949] = \"MS949\";\n        javaname[ISO2022KR] = \"ISO2022KR\";\n        javaname[JOHAB] = \"Johab\";\n        javaname[SJIS] = \"SJIS\";\n        javaname[EUC_JP] = \"EUC_JP\";\n        javaname[ISO2022JP] = \"ISO2022JP\";\n        javaname[ASCII] = \"ASCII\";\n        javaname[OTHER] = \"ISO8859_1\";\n        // Assign encoding names\n        htmlname[GB2312] = \"GB2312\";\n        htmlname[GBK] = \"GBK\";\n        htmlname[GB18030] = \"GB18030\";\n        htmlname[HZ] = \"HZ-GB-2312\";\n        htmlname[ISO2022CN_GB] = \"ISO-2022-CN-EXT\";\n        htmlname[BIG5] = \"BIG5\";\n        htmlname[CNS11643] = \"EUC-TW\";\n        htmlname[ISO2022CN_CNS] = \"ISO-2022-CN-EXT\";\n        htmlname[ISO2022CN] = \"ISO-2022-CN\";\n        htmlname[UTF8] = \"UTF-8\";\n        htmlname[UTF8T] = \"UTF-8\";\n        htmlname[UTF8S] = \"UTF-8\";\n        htmlname[UNICODE] = \"UTF-16\";\n        htmlname[UNICODET] = \"UTF-16\";\n        htmlname[UNICODES] = \"UTF-16\";\n        htmlname[EUC_KR] = \"EUC-KR\";\n        htmlname[CP949] = \"x-windows-949\";\n        htmlname[ISO2022KR] = \"ISO-2022-KR\";\n        htmlname[JOHAB] = \"x-Johab\";\n        htmlname[SJIS] = \"Shift_JIS\";\n        htmlname[EUC_JP] = \"EUC-JP\";\n        htmlname[ISO2022JP] = \"ISO-2022-JP\";\n        htmlname[ASCII] = \"ASCII\";\n        htmlname[OTHER] = \"ISO8859-1\";\n        // Assign Human readable names\n        nicename[GB2312] = \"GB-2312\";\n        nicename[GBK] = \"GBK\";\n        nicename[GB18030] = \"GB18030\";\n        nicename[HZ] = \"HZ\";\n        nicename[ISO2022CN_GB] = \"ISO2022CN-GB\";\n        nicename[BIG5] = \"Big5\";\n        nicename[CNS11643] = \"CNS11643\";\n        nicename[ISO2022CN_CNS] = \"ISO2022CN-CNS\";\n        nicename[ISO2022CN] = \"ISO2022 CN\";\n        nicename[UTF8] = \"UTF-8\";\n        nicename[UTF8T] = \"UTF-8 (Trad)\";\n        nicename[UTF8S] = \"UTF-8 (Simp)\";\n        nicename[UNICODE] = \"Unicode\";\n        nicename[UNICODET] = \"Unicode (Trad)\";\n        nicename[UNICODES] = \"Unicode (Simp)\";\n        nicename[EUC_KR] = \"EUC-KR\";\n        nicename[CP949] = \"CP949\";\n        nicename[ISO2022KR] = \"ISO 2022 KR\";\n        nicename[JOHAB] = \"Johab\";\n        nicename[SJIS] = \"Shift-JIS\";\n        nicename[EUC_JP] = \"EUC-JP\";\n        nicename[ISO2022JP] = \"ISO 2022 JP\";\n        nicename[ASCII] = \"ASCII\";\n        nicename[OTHER] = \"OTHER\";\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/help/JsExtensions.kt",
    "content": "package io.legado.app.help\n\nimport cn.hutool.crypto.digest.DigestUtil\nimport cn.hutool.crypto.symmetric.AES\nimport cn.hutool.crypto.symmetric.DESede\nimport io.legado.app.utils.Base64\nimport io.legado.app.constant.AppConst.dateFormat\nimport io.legado.app.help.http.*\nimport io.legado.app.model.Debug\nimport io.legado.app.model.analyzeRule.AnalyzeUrl\nimport io.legado.app.model.analyzeRule.QueryTTF\nimport io.legado.app.utils.*\nimport io.legado.app.data.entities.BaseSource\nimport io.legado.app.exception.NoStackTraceException\nimport kotlinx.coroutines.Dispatchers.IO\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.runBlocking\nimport org.jsoup.Connection\nimport org.jsoup.Jsoup\nimport com.htmake.reader.init.appCtx\nimport java.io.ByteArrayInputStream\nimport java.io.ByteArrayOutputStream\nimport java.io.File\nimport java.net.URLEncoder\nimport java.nio.charset.Charset\nimport java.util.*\nimport java.util.zip.ZipEntry\nimport java.util.zip.ZipInputStream\nimport java.text.SimpleDateFormat\n\n/**\n * js扩展类, 在js中通过java变量调用\n * 所有对于文件的读写删操作都是相对路径,只能操作阅读缓存内的文件\n * /android/data/{package}/cache/...\n */\n@Suppress(\"unused\")\ninterface JsExtensions {\n\n    fun getSource(): BaseSource?\n\n    /**\n     * 访问网络,返回String\n     */\n    fun ajax(urlStr: String): String? {\n        return runBlocking {\n            kotlin.runCatching {\n                val analyzeUrl = AnalyzeUrl(urlStr, source = getSource())\n                analyzeUrl.getStrResponse(urlStr).body\n            }.onFailure {\n                it.printOnDebug()\n            }.getOrElse {\n                it.msg\n            }\n        }\n    }\n\n    /**\n     * 并发访问网络\n     */\n    fun ajaxAll(urlList: Array<String>): Array<StrResponse?> {\n        return runBlocking {\n            val asyncArray = Array(urlList.size) {\n                async(IO) {\n                    val url = urlList[it]\n                    val analyzeUrl = AnalyzeUrl(url, source = getSource())\n                    analyzeUrl.getStrResponse(url)\n                }\n            }\n            val resArray = Array<StrResponse?>(urlList.size) {\n                asyncArray[it].await()\n            }\n            resArray\n        }\n    }\n\n    /**\n     * 访问网络,返回Response<String>\n     */\n    fun connect(urlStr: String): StrResponse {\n        return runBlocking {\n            val analyzeUrl = AnalyzeUrl(urlStr, source = getSource())\n            kotlin.runCatching {\n                analyzeUrl.getStrResponseAwait()\n            }.onFailure {\n                it.printOnDebug()\n            }.getOrElse {\n                StrResponse(analyzeUrl.url, it.localizedMessage)\n            }\n        }\n    }\n\n    fun connect(urlStr: String, header: String?): StrResponse {\n        return runBlocking {\n            val headerMap = GSON.fromJsonObject<Map<String, String>>(header).getOrNull()\n            val analyzeUrl = AnalyzeUrl(urlStr, headerMapF = headerMap, source = getSource())\n            kotlin.runCatching {\n                analyzeUrl.getStrResponseAwait()\n            }.onFailure {\n                it.printOnDebug()\n            }.getOrElse {\n                StrResponse(analyzeUrl.url, it.localizedMessage)\n            }\n        }\n    }\n\n    /**\n     * 使用webView访问网络\n     * @param html 直接用webView载入的html, 如果html为空直接访问url\n     * @param url html内如果有相对路径的资源不传入url访问不了\n     * @param js 用来取返回值的js语句, 没有就返回整个源代码\n     * @return 返回js获取的内容\n     */\n    fun webView(html: String?, url: String?, js: String?): String? {\n        return null\n    }\n\n    /**\n     * 可从网络，本地文件(阅读私有缓存目录和书籍保存位置支持相对路径)导入JavaScript脚本\n     */\n    fun importScript(path: String): String {\n        val result = when {\n            path.startsWith(\"http\") -> cacheFile(path) ?: \"\"\n            path.startsWith(\"/storage\") -> FileUtils.readText(path)\n            else -> readTxtFile(path)\n        }\n        if (result.isBlank()) throw NoStackTraceException(\"$path 内容获取失败或者为空\")\n        return result\n    }\n\n    /**\n     * 缓存以文本方式保存的文件 如.js .txt等\n     */\n    fun cacheFile(urlStr: String): String? {\n        return cacheFile(urlStr, 0)\n    }\n\n    /**\n     * 缓存以文本方式保存的文件 如.js .txt等\n     * @param urlStr 网络文件的链接\n     * @param saveTime 缓存时间，单位：秒\n     * @return 返回缓存后的文件内容\n     */\n    fun cacheFile(urlStr: String, saveTime: Int = 0): String? {\n        val key = md5Encode16(urlStr)\n        val cache = CacheManager.getFile(key)\n        if (cache.isNullOrBlank()) {\n            log(\"首次下载 $urlStr\")\n            val value = ajax(urlStr) ?: return null\n            CacheManager.putFile(key, value, saveTime)\n            return value\n        }\n        return cache\n    }\n\n    /**\n     *js实现读取cookie\n     */\n    fun getCookie(tag: String, key: String? = null): String {\n        val cookie = CookieStore.getCookie(tag)\n        val cookieMap = CookieStore.cookieToMap(cookie)\n        return if (key != null) {\n            cookieMap[key] ?: \"\"\n        } else {\n            cookie\n        }\n    }\n\n    /**\n     * 实现16进制字符串转文件\n     * @param content 需要转成文件的16进制字符串\n     * @param url 通过url里的参数来判断文件类型\n     * @return 相对路径\n     */\n    fun downloadFile(content: String, url: String): String {\n        val type = AnalyzeUrl(url).type ?: return \"\"\n        val zipPath = FileUtils.getPath(\n            FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),\n            \"${MD5Utils.md5Encode16(url)}.${type}\"\n        )\n        FileUtils.deleteFile(zipPath)\n        val zipFile = FileUtils.createFileIfNotExist(zipPath)\n        StringUtils.hexStringToByte(content).let {\n            if (it.isNotEmpty()) {\n                zipFile.writeBytes(it)\n            }\n        }\n        return zipPath.substring(FileUtils.getCachePath().length)\n    }\n\n    /**\n     * js实现重定向拦截,网络访问get\n     */\n    fun get(urlStr: String, headers: Map<String, String>): Connection.Response {\n        return Jsoup.connect(urlStr)\n            .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)\n            .ignoreContentType(true)\n            .followRedirects(false)\n            .headers(headers)\n            .method(Connection.Method.GET)\n            .execute()\n    }\n\n    /**\n     * 网络访问post\n     */\n    fun post(urlStr: String, body: String, headers: Map<String, String>): Connection.Response {\n        return Jsoup.connect(urlStr)\n            .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)\n            .ignoreContentType(true)\n            .followRedirects(false)\n            .requestBody(body)\n            .headers(headers)\n            .method(Connection.Method.POST)\n            .execute()\n    }\n\n    /**\n     * js实现解码,不能删\n     */\n    fun base64Decode(str: String): String {\n        return EncoderUtils.base64Decode(str, Base64.NO_WRAP)\n    }\n\n    fun base64Decode(str: String, flags: Int): String {\n        return EncoderUtils.base64Decode(str, flags)\n    }\n\n    fun base64DecodeToByteArray(str: String?): ByteArray? {\n        if (str.isNullOrBlank()) {\n            return null\n        }\n        return Base64.decode(str, Base64.DEFAULT)\n    }\n\n    fun base64DecodeToByteArray(str: String?, flags: Int): ByteArray? {\n        if (str.isNullOrBlank()) {\n            return null\n        }\n        return Base64.decode(str, flags)\n    }\n\n    fun base64Encode(str: String): String? {\n        return EncoderUtils.base64Encode(str, Base64.NO_WRAP)\n    }\n\n    fun base64Encode(str: String, flags: Int): String? {\n        return EncoderUtils.base64Encode(str, flags)\n    }\n\n    fun md5Encode(str: String): String {\n        return MD5Utils.md5Encode(str)\n    }\n\n    fun md5Encode16(str: String): String {\n        return MD5Utils.md5Encode16(str)\n    }\n\n    /**\n     * 格式化时间\n     */\n    fun timeFormatUTC(time: Long, format: String, sh: Int): String? {\n        val utc = SimpleTimeZone(sh, \"UTC\")\n        return SimpleDateFormat(format, Locale.getDefault()).run {\n            timeZone = utc\n            format(Date(time))\n        }\n    }\n\n    /**\n     * 时间格式化\n     */\n    fun timeFormat(time: Long): String {\n        return dateFormat.format(Date(time))\n    }\n\n    /**\n     * utf8编码转gbk编码\n     */\n    fun utf8ToGbk(str: String): String {\n        val utf8 = String(str.toByteArray(charset(\"UTF-8\")))\n        val unicode = String(utf8.toByteArray(), charset(\"UTF-8\"))\n        return String(unicode.toByteArray(charset(\"GBK\")))\n    }\n\n    fun encodeURI(str: String): String {\n        return try {\n            URLEncoder.encode(str, \"UTF-8\")\n        } catch (e: Exception) {\n            \"\"\n        }\n    }\n\n    fun encodeURI(str: String, enc: String): String {\n        return try {\n            URLEncoder.encode(str, enc)\n        } catch (e: Exception) {\n            \"\"\n        }\n    }\n\n    fun htmlFormat(str: String): String {\n        return HtmlFormatter.formatKeepImg(str)\n    }\n\n    //****************文件操作******************//\n\n    /**\n     * 获取本地文件\n     * @param path 相对路径\n     * @return File\n     */\n    fun getFile(path: String): File {\n        val cachePath = appCtx.cacheDir\n        val aPath: String = if (path.startsWith(File.separator)) {\n            cachePath + path\n        } else {\n            cachePath + File.separator + path\n        }\n        return File(aPath)\n    }\n\n    fun readFile(path: String): ByteArray? {\n        val file = getFile(path)\n        if (file.exists()) {\n            return file.readBytes()\n        }\n        return null\n    }\n\n    fun readTxtFile(path: String): String {\n        val file = getFile(path)\n        if (file.exists()) {\n            val charsetName = EncodingDetect.getEncode(file)\n            return String(file.readBytes(), charset(charsetName))\n        }\n        return \"\"\n    }\n\n    fun readTxtFile(path: String, charsetName: String): String {\n        val file = getFile(path)\n        if (file.exists()) {\n            return String(file.readBytes(), charset(charsetName))\n        }\n        return \"\"\n    }\n\n    /**\n     * 删除本地文件\n     */\n    fun deleteFile(path: String) {\n        val file = getFile(path)\n        FileUtils.delete(file, true)\n    }\n\n    /**\n     * js实现压缩文件解压\n     * @param zipPath 相对路径\n     * @return 相对路径\n     */\n    fun unzipFile(zipPath: String): String {\n        if (zipPath.isEmpty()) return \"\"\n        val unzipPath = FileUtils.getPath(\n            FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),\n            FileUtils.getNameExcludeExtension(zipPath)\n        )\n        FileUtils.deleteFile(unzipPath)\n        val zipFile = getFile(zipPath)\n        val unzipFolder = FileUtils.createFolderIfNotExist(unzipPath)\n        ZipUtils.unzipFile(zipFile, unzipFolder)\n        FileUtils.deleteFile(zipFile.absolutePath)\n        return unzipPath.substring(FileUtils.getCachePath().length)\n    }\n\n    /**\n     * js实现文件夹内所有文件读取\n     */\n    fun getTxtInFolder(unzipPath: String): String {\n        if (unzipPath.isEmpty()) return \"\"\n        val unzipFolder = getFile(unzipPath)\n        val contents = StringBuilder()\n        unzipFolder.listFiles().let {\n            if (it != null) {\n                for (f in it) {\n                    val charsetName = EncodingDetect.getEncode(f)\n                    contents.append(String(f.readBytes(), charset(charsetName)))\n                        .append(\"\\n\")\n                }\n                contents.deleteCharAt(contents.length - 1)\n            }\n        }\n        FileUtils.deleteFile(unzipFolder.absolutePath)\n        return contents.toString()\n    }\n\n    /**\n     * 获取网络zip文件里面的数据\n     * @param url zip文件的链接或十六进制字符串\n     * @param path 所需获取文件在zip内的路径\n     * @return zip指定文件的数据\n     */\n    fun getZipStringContent(url: String, path: String): String {\n        val byteArray = getZipByteArrayContent(url, path) ?: return \"\"\n        val charsetName = EncodingDetect.getEncode(byteArray)\n        return String(byteArray, Charset.forName(charsetName))\n    }\n\n    fun getZipStringContent(url: String, path: String, charsetName: String): String {\n        val byteArray = getZipByteArrayContent(url, path) ?: return \"\"\n        return String(byteArray, Charset.forName(charsetName))\n    }\n\n    /**\n     * 获取网络zip文件里面的数据\n     * @param url zip文件的链接或十六进制字符串\n     * @param path 所需获取文件在zip内的路径\n     * @return zip指定文件的数据\n     */\n    fun getZipByteArrayContent(url: String, path: String): ByteArray? {\n        val bytes = if (url.startsWith(\"http://\") || url.startsWith(\"https://\")) {\n            runBlocking {\n                return@runBlocking okHttpClient.newCall { url(url) }.bytes()\n            }\n        } else {\n            StringUtils.hexStringToByte(url)\n        }\n        val bos = ByteArrayOutputStream()\n        val zis = ZipInputStream(ByteArrayInputStream(bytes))\n        var entry: ZipEntry? = zis.nextEntry\n        while (entry != null) {\n            if (entry.name.equals(path)) {\n                zis.use { it.copyTo(bos) }\n                return bos.toByteArray()\n            }\n            entry = zis.nextEntry\n        }\n        Debug.log(\"getZipContent 未发现内容\")\n\n        return null\n    }\n\n    //******************文件操作************************//\n\n    /**\n     * 解析字体,返回字体解析类\n     */\n    fun queryBase64TTF(base64: String?): QueryTTF? {\n        base64DecodeToByteArray(base64)?.let {\n            return QueryTTF(it)\n        }\n        return null\n    }\n\n    /**\n     * 返回字体解析类\n     * @param str 支持url,本地文件,base64,自动判断,自动缓存\n     */\n    fun queryTTF(str: String?): QueryTTF? {\n        str ?: return null\n        val key = md5Encode16(str)\n        var qTTF = CacheManager.getQueryTTF(key)\n        if (qTTF != null) return qTTF\n        val font: ByteArray? = when {\n            str.isAbsUrl() -> runBlocking {\n                var x = CacheManager.getByteArray(key)\n                if (x == null) {\n                    x = okHttpClient.newCall { url(str) }.bytes()\n                    x.let {\n                        CacheManager.put(key, it)\n                    }\n                }\n                return@runBlocking x\n            }\n            str.indexOf(\"storage/\") > 0 -> File(str).readBytes()\n            else -> base64DecodeToByteArray(str)\n        }\n        font ?: return null\n        qTTF = QueryTTF(font)\n        CacheManager.put(key, qTTF)\n        return qTTF\n    }\n\n    /**\n     * @param text 包含错误字体的内容\n     * @param font1 错误的字体\n     * @param font2 正确的字体\n     */\n    fun replaceFont(\n        text: String,\n        font1: QueryTTF?,\n        font2: QueryTTF?\n    ): String {\n        if (font1 == null || font2 == null) return text\n        val contentArray = text.toCharArray()\n        contentArray.forEachIndexed { index, s ->\n            val oldCode = s.code\n            if (font1.inLimit(s)) {\n                val code = font2.getCodeByGlyf(font1.getGlyfByCode(oldCode))\n                if (code != 0) contentArray[index] = code.toChar()\n            }\n        }\n        return contentArray.joinToString(\"\")\n    }\n\n    /**\n     * 弹窗提示\n     */\n    fun toast(msg: Any?) {\n        Debug.log(\"toast: \" + msg.toString())\n    }\n\n    /**\n     * 弹窗提示 停留时间较长\n     */\n    fun longToast(msg: Any?) {\n        Debug.log(\"longToast: \" + msg.toString())\n    }\n\n    /**\n     * 输出调试日志\n     */\n    fun log(msg: String): String {\n        Debug.log(msg)\n        return msg\n    }\n\n    /**\n     * 输出对象类型\n     */\n    fun logType(any: Any?) {\n        if (any == null) {\n            log(\"null\")\n        } else {\n            log(any.javaClass.name)\n        }\n    }\n\n    /**\n     * 生成UUID\n     */\n    fun randomUUID(): String {\n        return UUID.randomUUID().toString()\n    }\n\n    /**\n     * AES 解码为 ByteArray\n     * @param str 传入的AES加密的数据\n     * @param key AES 解密的key\n     * @param transformation AES加密的方式\n     * @param iv ECB模式的偏移向量\n     */\n    fun aesDecodeToByteArray(\n        str: String, key: String, transformation: String, iv: String\n    ): ByteArray? {\n        return try {\n            EncoderUtils.decryptAES(\n                data = str.encodeToByteArray(),\n                key = key.encodeToByteArray(),\n                transformation,\n                iv.encodeToByteArray()\n            )\n        } catch (e: Exception) {\n            e.printOnDebug()\n            log(e.localizedMessage ?: \"aesDecodeToByteArrayERROR\")\n            null\n        }\n    }\n\n    /**\n     * AES 解码为 String\n     * @param str 传入的AES加密的数据\n     * @param key AES 解密的key\n     * @param transformation AES加密的方式\n     * @param iv ECB模式的偏移向量\n     */\n\n    fun aesDecodeToString(\n        str: String, key: String, transformation: String, iv: String\n    ): String? {\n        return aesDecodeToByteArray(str, key, transformation, iv)?.let { String(it) }\n    }\n\n    /**\n     * 已经base64的AES 解码为 ByteArray\n     * @param str 传入的AES Base64加密的数据\n     * @param key AES 解密的key\n     * @param transformation AES加密的方式\n     * @param iv ECB模式的偏移向量\n     */\n\n    fun aesBase64DecodeToByteArray(\n        str: String, key: String, transformation: String, iv: String\n    ): ByteArray? {\n        return try {\n            EncoderUtils.decryptBase64AES(\n                str.encodeToByteArray(),\n                key.encodeToByteArray(),\n                transformation,\n                iv.encodeToByteArray()\n            )\n        } catch (e: Exception) {\n            e.printOnDebug()\n            log(e.localizedMessage ?: \"aesDecodeToByteArrayERROR\")\n            null\n        }\n    }\n\n    /**\n     * 已经base64的AES 解码为 String\n     * @param str 传入的AES Base64加密的数据\n     * @param key AES 解密的key\n     * @param transformation AES加密的方式\n     * @param iv ECB模式的偏移向量\n     */\n\n    fun aesBase64DecodeToString(\n        str: String, key: String, transformation: String, iv: String\n    ): String? {\n        return aesBase64DecodeToByteArray(str, key, transformation, iv)?.let { String(it) }\n    }\n\n    /**\n     * 加密aes为ByteArray\n     * @param data 传入的原始数据\n     * @param key AES加密的key\n     * @param transformation AES加密的方式\n     * @param iv ECB模式的偏移向量\n     */\n    fun aesEncodeToByteArray(\n        data: String, key: String, transformation: String, iv: String\n    ): ByteArray? {\n        return try {\n            EncoderUtils.encryptAES(\n                data.encodeToByteArray(),\n                key = key.encodeToByteArray(),\n                transformation,\n                iv.encodeToByteArray()\n            )\n        } catch (e: Exception) {\n            e.printOnDebug()\n            log(e.localizedMessage ?: \"aesEncodeToByteArrayERROR\")\n            null\n        }\n    }\n\n    /**\n     * 加密aes为String\n     * @param data 传入的原始数据\n     * @param key AES加密的key\n     * @param transformation AES加密的方式\n     * @param iv ECB模式的偏移向量\n     */\n    fun aesEncodeToString(\n        data: String, key: String, transformation: String, iv: String\n    ): String? {\n        return aesEncodeToByteArray(data, key, transformation, iv)?.let { String(it) }\n    }\n\n    /**\n     * 加密aes后Base64化的ByteArray\n     * @param data 传入的原始数据\n     * @param key AES加密的key\n     * @param transformation AES加密的方式\n     * @param iv ECB模式的偏移向量\n     */\n    fun aesEncodeToBase64ByteArray(\n        data: String, key: String, transformation: String, iv: String\n    ): ByteArray? {\n        return try {\n            EncoderUtils.encryptAES2Base64(\n                data.encodeToByteArray(),\n                key.encodeToByteArray(),\n                transformation,\n                iv.encodeToByteArray()\n            )\n        } catch (e: Exception) {\n            e.printOnDebug()\n            log(e.localizedMessage ?: \"aesEncodeToBase64ByteArrayERROR\")\n            null\n        }\n    }\n\n    /**\n     * 加密aes后Base64化的String\n     * @param data 传入的原始数据\n     * @param key AES加密的key\n     * @param transformation AES加密的方式\n     * @param iv ECB模式的偏移向量\n     */\n    fun aesEncodeToBase64String(\n        data: String, key: String, transformation: String, iv: String\n    ): String? {\n        return aesEncodeToBase64ByteArray(data, key, transformation, iv)?.let { String(it) }\n    }\n\n    fun androidId(): String {\n        return \"\"\n    }\n\n    /**\n     * AES解密，算法参数经过Base64加密\n     *\n     * @param data 加密的字符串\n     * @param key Base64后的密钥\n     * @param mode 模式\n     * @param padding 补码方式\n     * @param iv Base64后的加盐\n     * @return 解密后的字符串\n     */\n    fun aesDecodeArgsBase64Str(\n        data: String,\n        key: String,\n        mode: String,\n        padding: String,\n        iv: String\n    ): String? {\n        return AES(\n            mode,\n            padding,\n            Base64.decode(key, Base64.NO_WRAP),\n            Base64.decode(iv, Base64.NO_WRAP)\n        ).decryptStr(data)\n    }\n\n    /**\n     * 3DES解密\n     *\n     * @param data 加密的字符串\n     * @param key 密钥\n     * @param mode 模式\n     * @param padding 补码方式\n     * @param iv 加盐\n     * @return 解密后的字符串\n     */\n    fun tripleDESDecodeStr(\n        data: String,\n        key: String,\n        mode: String,\n        padding: String,\n        iv: String\n    ): String? {\n        return DESede(mode, padding, key.toByteArray(), iv.toByteArray()).decryptStr(data)\n    }\n\n    /**\n     * 3DES解密，算法参数经过Base64加密\n     *\n     * @param data 加密的字符串\n     * @param key Base64后的密钥\n     * @param mode 模式\n     * @param padding 补码方式\n     * @param iv Base64后的加盐\n     * @return 解密后的字符串\n     */\n    fun tripleDESDecodeArgsBase64Str(\n        data: String,\n        key: String,\n        mode: String,\n        padding: String,\n        iv: String\n    ): String? {\n        return DESede(\n            mode,\n            padding,\n            Base64.decode(key, Base64.NO_WRAP),\n            Base64.decode(iv, Base64.NO_WRAP)\n        ).decryptStr(data)\n    }\n\n    /**\n     * AES加密并转为Base64，算法参数经过Base64加密\n     *\n     * @param data 被加密的字符串\n     * @param key Base64后的密钥\n     * @param mode 模式\n     * @param padding 补码方式\n     * @param iv Base64后的加盐\n     * @return 加密后的Base64\n     */\n    fun aesEncodeArgsBase64Str(\n        data: String,\n        key: String,\n        mode: String,\n        padding: String,\n        iv: String\n    ): String? {\n        return AES(\n            mode,\n            padding,\n            Base64.decode(key, Base64.NO_WRAP),\n            Base64.decode(iv, Base64.NO_WRAP)\n        ).encryptBase64(data)\n    }\n    /////DES\n    fun desDecodeToString(\n        data: String, key: String, transformation: String, iv: String\n    ): String? {\n        return EncoderUtils.decryptDES(\n            data.encodeToByteArray(),\n            key.encodeToByteArray(),\n            transformation,\n            iv.encodeToByteArray()\n        )?.let { String(it) }\n    }\n\n    fun desBase64DecodeToString(\n       data: String, key: String, transformation: String, iv: String\n    ): String? {\n        return EncoderUtils.decryptBase64DES(\n            data.encodeToByteArray(),\n            key.encodeToByteArray(),\n            transformation,\n            iv.encodeToByteArray()\n        )?.let { String(it) }\n    }\n\n    fun desEncodeToString(\n        data: String, key: String, transformation: String, iv: String\n    ): String? {\n        return EncoderUtils.encryptDES(\n            data.encodeToByteArray(),\n            key.encodeToByteArray(),\n            transformation,\n            iv.encodeToByteArray()\n        )?.let { String(it) }\n    }\n\n    fun desEncodeToBase64String(\n        data: String, key: String, transformation: String, iv: String\n    ): String? {\n        return EncoderUtils.encryptDES2Base64(\n            data.encodeToByteArray(),\n            key.encodeToByteArray(),\n            transformation,\n            iv.encodeToByteArray()\n        )?.let { String(it) }\n    }\n    /**\n     * 3DES加密并转为Base64\n     *\n     * @param data 被加密的字符串\n     * @param key 密钥\n     * @param mode 模式\n     * @param padding 补码方式\n     * @param iv 加盐\n     * @return 加密后的Base64\n     */\n    fun tripleDESEncodeBase64Str(\n        data: String,\n        key: String,\n        mode: String,\n        padding: String,\n        iv: String\n    ): String? {\n        return DESede(mode, padding, key.toByteArray(), iv.toByteArray()).encryptBase64(data)\n    }\n\n    /**\n     * 3DES加密并转为Base64，算法参数经过Base64加密\n     *\n     * @param data 被加密的字符串\n     * @param key Base64后的密钥\n     * @param mode 模式\n     * @param padding 补码方式\n     * @param iv Base64后的加盐\n     * @return 加密后的Base64\n     */\n    fun tripleDESEncodeArgsBase64Str(\n        data: String,\n        key: String,\n        mode: String,\n        padding: String,\n        iv: String\n    ): String? {\n        return DESede(\n            mode,\n            padding,\n            Base64.decode(key, Base64.NO_WRAP),\n            Base64.decode(iv, Base64.NO_WRAP)\n        ).encryptBase64(data)\n    }\n\n    /**\n     * 生成摘要，并转为16进制字符串\n     *\n     * @param data 被摘要数据\n     * @param algorithm 签名算法\n     * @return 16进制字符串\n     */\n    fun digestHex(\n        data: String,\n        algorithm: String,\n    ): String? {\n        return DigestUtil.digester(algorithm).digestHex(data)\n    }\n\n    /**\n     * 生成摘要，并转为Base64字符串\n     *\n     * @param data 被摘要数据\n     * @param algorithm 签名算法\n     * @return Base64字符串\n     */\n    fun digestBase64Str(\n        data: String,\n        algorithm: String,\n    ): String? {\n        return Base64.encodeToString(DigestUtil.digester(algorithm).digest(data), Base64.NO_WRAP)\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt",
    "content": "package io.legado.app.help.coroutine\n\nclass CompositeCoroutine : CoroutineContainer {\n\n    private var resources: HashSet<Coroutine<*>>? = null\n\n    val size: Int\n        get() = resources?.size ?: 0\n\n    val isEmpty: Boolean\n        get() = size == 0\n\n    constructor()\n\n    constructor(vararg coroutines: Coroutine<*>) {\n        this.resources = hashSetOf(*coroutines)\n    }\n\n    constructor(coroutines: Iterable<Coroutine<*>>) {\n        this.resources = hashSetOf()\n        for (d in coroutines) {\n            this.resources?.add(d)\n        }\n    }\n\n    override fun add(coroutine: Coroutine<*>): Boolean {\n        synchronized(this) {\n            var set: HashSet<Coroutine<*>>? = resources\n            if (resources == null) {\n                set = hashSetOf()\n                resources = set\n            }\n            return set!!.add(coroutine)\n        }\n    }\n\n    override fun addAll(vararg coroutines: Coroutine<*>): Boolean {\n        synchronized(this) {\n            var set: HashSet<Coroutine<*>>? = resources\n            if (resources == null) {\n                set = hashSetOf()\n                resources = set\n            }\n            for (coroutine in coroutines) {\n                val add = set!!.add(coroutine)\n                if (!add) {\n                    return false\n                }\n            }\n        }\n        return true\n    }\n\n    override fun remove(coroutine: Coroutine<*>): Boolean {\n        if (delete(coroutine)) {\n            coroutine.cancel()\n            return true\n        }\n        return false\n    }\n\n    override fun delete(coroutine: Coroutine<*>): Boolean {\n        synchronized(this) {\n            val set = resources\n            if (set == null || !set.remove(coroutine)) {\n                return false\n            }\n        }\n        return true\n    }\n\n    override fun clear() {\n        val set: HashSet<Coroutine<*>>?\n        synchronized(this) {\n            set = resources\n            resources = null\n        }\n\n        set?.forEachIndexed { _, coroutine ->\n            coroutine.cancel()\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/help/coroutine/Coroutine.kt",
    "content": "package io.legado.app.help.coroutine\n\nimport kotlinx.coroutines.*\nimport kotlin.coroutines.CoroutineContext\n\n\nclass Coroutine<T>(\n    val scope: CoroutineScope,\n    context: CoroutineContext = Dispatchers.IO,\n    block: suspend CoroutineScope.() -> T\n) {\n\n    companion object {\n\n        private val DEFAULT = MainScope()\n\n        fun <T> async(\n            scope: CoroutineScope = DEFAULT,\n            context: CoroutineContext = Dispatchers.IO,\n            block: suspend CoroutineScope.() -> T\n        ): Coroutine<T> {\n            return Coroutine(scope, context, block)\n        }\n\n    }\n\n    private val job: Job\n\n    private var start: VoidCallback? = null\n    private var success: Callback<T>? = null\n    private var error: Callback<Throwable>? = null\n    private var finally: VoidCallback? = null\n    private var cancel: VoidCallback? = null\n\n    private var timeMillis: Long? = null\n    private var errorReturn: Result<T>? = null\n\n    val isCancelled: Boolean\n        get() = job.isCancelled\n\n    val isActive: Boolean\n        get() = job.isActive\n\n    val isCompleted: Boolean\n        get() = job.isCompleted\n\n    init {\n        this.job = executeInternal(context, block)\n    }\n\n    fun timeout(timeMillis: () -> Long): Coroutine<T> {\n        this.timeMillis = timeMillis()\n        return this@Coroutine\n    }\n\n    fun timeout(timeMillis: Long): Coroutine<T> {\n        this.timeMillis = timeMillis\n        return this@Coroutine\n    }\n\n    fun onErrorReturn(value: () -> T?): Coroutine<T> {\n        this.errorReturn = Result(value())\n        return this@Coroutine\n    }\n\n    fun onErrorReturn(value: T?): Coroutine<T> {\n        this.errorReturn = Result(value)\n        return this@Coroutine\n    }\n\n    fun onStart(\n        context: CoroutineContext? = null,\n        block: (suspend CoroutineScope.() -> Unit)\n    ): Coroutine<T> {\n        this.start = VoidCallback(context, block)\n        return this@Coroutine\n    }\n\n    fun onSuccess(\n        context: CoroutineContext? = null,\n        block: suspend CoroutineScope.(T) -> Unit\n    ): Coroutine<T> {\n        this.success = Callback(context, block)\n        return this@Coroutine\n    }\n\n    fun onError(\n        context: CoroutineContext? = null,\n        block: suspend CoroutineScope.(Throwable) -> Unit\n    ): Coroutine<T> {\n        this.error = Callback(context, block)\n        return this@Coroutine\n    }\n\n    fun onFinally(\n        context: CoroutineContext? = null,\n        block: suspend CoroutineScope.() -> Unit\n    ): Coroutine<T> {\n        this.finally = VoidCallback(context, block)\n        return this@Coroutine\n    }\n\n    fun onCancel(\n        context: CoroutineContext? = null,\n        block: suspend CoroutineScope.() -> Unit\n    ): Coroutine<T> {\n        this.cancel = VoidCallback(context, block)\n        return this@Coroutine\n    }\n\n    //取消当前任务\n    fun cancel(cause: CancellationException? = null) {\n        job.cancel(cause)\n        cancel?.let {\n            MainScope().launch {\n                if (null == it.context) {\n                    it.block.invoke(scope)\n                } else {\n                    withContext(scope.coroutineContext.plus(it.context)) {\n                        it.block.invoke(this)\n                    }\n                }\n            }\n        }\n    }\n\n    fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle {\n        return job.invokeOnCompletion(handler)\n    }\n\n    private fun executeInternal(\n        context: CoroutineContext,\n        block: suspend CoroutineScope.() -> T\n    ): Job {\n        return scope.plus(Dispatchers.IO).launch {\n            try {\n                start?.let { dispatchVoidCallback(this, it) }\n                val value = executeBlock(scope, context, timeMillis ?: 0L, block)\n                if (isActive) {\n                    success?.let { dispatchCallback(this, value, it) }\n                }\n            } catch (e: Throwable) {\n                e.printStackTrace()\n                val consume: Boolean = errorReturn?.value?.let { value ->\n                    if (isActive) {\n                        success?.let { dispatchCallback(this, value, it) }\n                    }\n                    true\n                } ?: false\n\n                if (!consume && isActive) {\n                    error?.let { dispatchCallback(this, e, it) }\n                }\n            } finally {\n                if (isActive) {\n                    finally?.let { dispatchVoidCallback(this, it) }\n                }\n            }\n        }\n    }\n\n    private suspend inline fun dispatchVoidCallback(scope: CoroutineScope, callback: VoidCallback) {\n        if (null == callback.context) {\n            callback.block.invoke(scope)\n        } else {\n            withContext(scope.coroutineContext.plus(callback.context)) {\n                callback.block.invoke(this)\n            }\n        }\n    }\n\n    private suspend inline fun <R> dispatchCallback(\n        scope: CoroutineScope,\n        value: R,\n        callback: Callback<R>\n    ) {\n        if (!scope.isActive) return\n        if (null == callback.context) {\n            callback.block.invoke(scope, value)\n        } else {\n            withContext(scope.coroutineContext.plus(callback.context)) {\n                callback.block.invoke(this, value)\n            }\n        }\n    }\n\n    private suspend inline fun executeBlock(\n        scope: CoroutineScope,\n        context: CoroutineContext,\n        timeMillis: Long,\n        noinline block: suspend CoroutineScope.() -> T\n    ): T {\n        return withContext(scope.coroutineContext.plus(context)) {\n            if (timeMillis > 0L) withTimeout(timeMillis) {\n                block()\n            } else {\n                block()\n            }\n        }\n    }\n\n    private data class Result<out T>(val value: T?)\n\n    private inner class VoidCallback(\n        val context: CoroutineContext?,\n        val block: suspend CoroutineScope.() -> Unit\n    )\n\n    private inner class Callback<VALUE>(\n        val context: CoroutineContext?,\n        val block: suspend CoroutineScope.(VALUE) -> Unit\n    )\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/help/coroutine/CoroutineContainer.kt",
    "content": "package io.legado.app.help.coroutine\n\ninternal interface CoroutineContainer {\n\n    fun add(coroutine: Coroutine<*>): Boolean\n\n    fun addAll(vararg coroutines: Coroutine<*>): Boolean\n\n    fun remove(coroutine: Coroutine<*>): Boolean\n\n    fun delete(coroutine: Coroutine<*>): Boolean\n\n    fun clear()\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/AjaxWebView.kt",
    "content": "//package io.legado.app.help.http\n//\n//import android.annotation.SuppressLint\n//import android.net.http.SslError\n//import android.os.Build\n//import android.os.Handler\n//import android.os.Looper\n//import android.os.Message\n//import android.text.TextUtils\n//import android.webkit.*\n//import io.legado.app.App\n//import io.legado.app.constant.AppConst\n//import org.apache.commons.text.StringEscapeUtils\n//import io.legado.app.utils.TextUtils\n//import java.lang.ref.WeakReference\n//\n//\n//class AjaxWebView {\n//    var callback: Callback? = null\n//    private var mHandler: AjaxHandler\n//\n//    init {\n//        mHandler = AjaxHandler(this)\n//    }\n//\n//    class AjaxHandler(private val ajaxWebView: AjaxWebView) : Handler(Looper.getMainLooper()) {\n//\n//        private var mWebView: WebView? = null\n//\n//        override fun handleMessage(msg: Message) {\n//            val params: AjaxParams\n//            when (msg.what) {\n//                MSG_AJAX_START -> {\n//                    params = msg.obj as AjaxParams\n//                    mWebView = createAjaxWebView(params, this)\n//                }\n//                MSG_SNIFF_START -> {\n//                    params = msg.obj as AjaxParams\n//                    mWebView = createAjaxWebView(params, this)\n//                }\n//                MSG_SUCCESS -> {\n//                    ajaxWebView.callback?.onResult(msg.obj as Res)\n//                    destroyWebView()\n//                }\n//                MSG_ERROR -> {\n//                    ajaxWebView.callback?.onError(msg.obj as Throwable)\n//                    destroyWebView()\n//                }\n//            }\n//        }\n//\n//        @SuppressLint(\"SetJavaScriptEnabled\", \"JavascriptInterface\")\n//        fun createAjaxWebView(params: AjaxParams, handler: Handler): WebView {\n//            val webView = WebView(App.INSTANCE)\n//            val settings = webView.settings\n//            settings.javaScriptEnabled = true\n//            settings.domStorageEnabled = true\n//            settings.blockNetworkImage = true\n//            settings.userAgentString = params.userAgent\n//            settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW\n//            if (params.isSniff) {\n//                webView.webViewClient = SnifferWebClient(params, handler)\n//            } else {\n//                webView.webViewClient = HtmlWebViewClient(params, handler)\n//            }\n//            when (params.requestMethod) {\n//                RequestMethod.POST -> webView.postUrl(params.url, params.postData)\n//                RequestMethod.GET -> webView.loadUrl(\n//                    params.url,\n//                    params.headerMap\n//                )\n//            }\n//            return webView\n//        }\n//\n//        private fun destroyWebView() {\n//            mWebView?.destroy()\n//            mWebView = null\n//        }\n//    }\n//\n//    fun load(params: AjaxParams) {\n//        if (params.sourceRegex != \"\") {\n//            mHandler.obtainMessage(MSG_SNIFF_START, params)\n//                .sendToTarget()\n//        } else {\n//            mHandler.obtainMessage(MSG_AJAX_START, params)\n//                .sendToTarget()\n//        }\n//    }\n//\n//    fun destroyWebView() {\n//        mHandler.obtainMessage(DESTROY_WEB_VIEW)\n//    }\n//\n//    class AjaxParams(val url: String) {\n//        var tag: String? = null\n//        var requestMethod = RequestMethod.GET\n//        var postData: ByteArray? = null\n//        var headerMap: Map<String, String>? = null\n//        var sourceRegex: String? = null\n//        var javaScript: String? = null\n//\n//        fun getJs(): String {\n//            javaScript?.let {\n//                if (it.isNotEmpty()) {\n//                    return it\n//                }\n//            }\n//            return JS\n//        }\n//\n//        val userAgent: String?\n//            get() = this.headerMap?.get(AppConst.UA_NAME)\n//\n//        val isSniff: Boolean\n//            get() = !TextUtils.isEmpty(sourceRegex)\n//\n//        fun setCookie(url: String) {\n//            tag?.let {\n//                val cookie = CookieManager.getInstance().getCookie(url)\n//                CookieStore.setCookie(it, cookie)\n//            }\n//        }\n//\n//        fun hasJavaScript(): Boolean {\n//            return !TextUtils.isEmpty(javaScript)\n//        }\n//\n//        fun clearJavaScript() {\n//            javaScript = null\n//        }\n//\n//    }\n//\n//    private class HtmlWebViewClient(\n//        private val params: AjaxParams,\n//        private val handler: Handler\n//    ) : WebViewClient() {\n//\n//        override fun onPageFinished(view: WebView, url: String) {\n//            params.setCookie(url)\n//            val runnable = EvalJsRunnable(view, url, params.getJs(), handler)\n//            handler.postDelayed(runnable, 1000)\n//        }\n//\n//        override fun onReceivedError(\n//            view: WebView,\n//            errorCode: Int,\n//            description: String,\n//            failingUrl: String\n//        ) {\n//            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\n//                handler.obtainMessage(MSG_ERROR, Exception(description))\n//                    .sendToTarget()\n//            }\n//        }\n//\n//        override fun onReceivedError(\n//            view: WebView,\n//            request: WebResourceRequest,\n//            error: WebResourceError\n//        ) {\n//            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n//                handler.obtainMessage(\n//                    MSG_ERROR,\n//                    Exception(error.description.toString())\n//                ).sendToTarget()\n//            }\n//        }\n//\n//        override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {\n//            handler.proceed()\n//        }\n//    }\n//\n//    private class EvalJsRunnable(\n//        webView: WebView,\n//        private val url: String,\n//        private val mJavaScript: String,\n//        private val handler: Handler\n//    ) : Runnable {\n//        var retry = 0\n//        private val mWebView: WeakReference<WebView> = WeakReference(webView)\n//        override fun run() {\n//            mWebView.get()?.evaluateJavascript(mJavaScript) {\n//                if (it.isNotEmpty() && it != \"null\") {\n//                    val content = StringEscapeUtils.unescapeJson(it)\n//                    handler.obtainMessage(MSG_SUCCESS, Res(url, content))\n//                        .sendToTarget()\n//                    handler.removeCallbacks(this)\n//                    return@evaluateJavascript\n//                }\n//                if (retry > 30) {\n//                    handler.obtainMessage(MSG_ERROR, Exception(\"time out\"))\n//                        .sendToTarget()\n//                    handler.removeCallbacks(this)\n//                    return@evaluateJavascript\n//                }\n//                retry++\n//                handler.removeCallbacks(this)\n//                handler.postDelayed(this, 1000)\n//            }\n//        }\n//    }\n//\n//    private class SnifferWebClient(\n//        private val params: AjaxParams,\n//        private val handler: Handler\n//    ) : WebViewClient() {\n//\n//        override fun onLoadResource(view: WebView, url: String) {\n//            params.sourceRegex?.let {\n//                if (url.matches(it.toRegex())) {\n//                    handler.obtainMessage(MSG_SUCCESS, Res(view.url ?: params.url, url))\n//                        .sendToTarget()\n//                }\n//            }\n//        }\n//\n//        override fun onReceivedError(\n//            view: WebView,\n//            errorCode: Int,\n//            description: String,\n//            failingUrl: String\n//        ) {\n//            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\n//                handler.obtainMessage(MSG_ERROR, Exception(description))\n//                    .sendToTarget()\n//            }\n//        }\n//\n//        override fun onReceivedError(\n//            view: WebView,\n//            request: WebResourceRequest,\n//            error: WebResourceError\n//        ) {\n//            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n//                handler.obtainMessage(\n//                    MSG_ERROR,\n//                    Exception(error.description.toString())\n//                ).sendToTarget()\n//            }\n//        }\n//\n//        override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {\n//            handler.proceed()\n//        }\n//\n//        override fun onPageFinished(view: WebView, url: String) {\n//            params.setCookie(url)\n//            if (params.hasJavaScript()) {\n//                evaluateJavascript(view, params.javaScript)\n//                params.clearJavaScript()\n//            }\n//        }\n//\n//        private fun evaluateJavascript(webView: WebView, javaScript: String?) {\n//            val runnable = LoadJsRunnable(webView, javaScript)\n//            handler.postDelayed(runnable, 1000L)\n//        }\n//    }\n//\n//    private class LoadJsRunnable(\n//        webView: WebView,\n//        private val mJavaScript: String?\n//    ) : Runnable {\n//        private val mWebView: WeakReference<WebView> = WeakReference(webView)\n//        override fun run() {\n//            mWebView.get()?.loadUrl(\"javascript:${mJavaScript ?: \"\"}\")\n//        }\n//    }\n//\n//    companion object {\n//        const val MSG_AJAX_START = 0\n//        const val MSG_SNIFF_START = 1\n//        const val MSG_SUCCESS = 2\n//        const val MSG_ERROR = 3\n//        const val DESTROY_WEB_VIEW = 4\n//        const val JS = \"document.documentElement.outerHTML\"\n//    }\n//\n//    abstract class Callback {\n//        abstract fun onResult(response: Res)\n//        abstract fun onError(error: Throwable)\n//    }\n//}"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/ByteConverter.kt",
    "content": "package io.legado.app.help.http\n\nimport okhttp3.ResponseBody\nimport retrofit2.Converter\nimport retrofit2.Retrofit\nimport java.lang.reflect.Type\n\nclass ByteConverter : Converter.Factory() {\n\n    override fun responseBodyConverter(\n        type: Type?,\n        annotations: Array<Annotation>?,\n        retrofit: Retrofit?\n    ): Converter<ResponseBody, ByteArray>? {\n        return Converter { value ->\n            value.bytes()\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/CookieStore.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage io.legado.app.help.http\n\nimport io.legado.app.utils.TextUtils\nimport io.legado.app.data.entities.Cookie\nimport io.legado.app.help.http.api.CookieManager\nimport io.legado.app.utils.NetworkUtils\n\n// TODO 处理cookie\nobject CookieStore : CookieManager {\n\n    override fun setCookie(url: String, cookie: String?) {\n        // val cookieBean = Cookie(NetworkUtils.getSubDomain(url), cookie ?: \"\")\n        // appDb.cookieDao.insert(cookieBean)\n    }\n\n    override fun replaceCookie(url: String, cookie: String) {\n        if (TextUtils.isEmpty(url) || TextUtils.isEmpty(cookie)) {\n            return\n        }\n        val oldCookie = getCookie(url)\n        if (TextUtils.isEmpty(oldCookie)) {\n            setCookie(url, cookie)\n        } else {\n            val cookieMap = cookieToMap(oldCookie)\n            cookieMap.putAll(cookieToMap(cookie))\n            val newCookie = mapToCookie(cookieMap)\n            setCookie(url, newCookie)\n        }\n    }\n\n    override fun getCookie(url: String): String {\n        // val cookieBean = appDb.cookieDao.get(NetworkUtils.getSubDomain(url))\n        // return cookieBean?.cookie ?: \"\"\n        return \"\"\n    }\n\n    override fun removeCookie(url: String) {\n        // appDb.cookieDao.delete(NetworkUtils.getSubDomain(url))\n    }\n\n    override fun cookieToMap(cookie: String): MutableMap<String, String> {\n        val cookieMap = mutableMapOf<String, String>()\n        if (cookie.isBlank()) {\n            return cookieMap\n        }\n        val pairArray = cookie.split(\";\".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()\n        for (pair in pairArray) {\n            val pairs = pair.split(\"=\".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()\n            if (pairs.size == 1) {\n                continue\n            }\n            val key = pairs[0].trim { it <= ' ' }\n            val value = pairs[1]\n            if (value.isNotBlank() || value.trim { it <= ' ' } == \"null\") {\n                cookieMap[key] = value.trim { it <= ' ' }\n            }\n        }\n        return cookieMap\n    }\n\n    override fun mapToCookie(cookieMap: Map<String, String>?): String? {\n        if (cookieMap == null || cookieMap.isEmpty()) {\n            return null\n        }\n        val builder = StringBuilder()\n        for (key in cookieMap.keys) {\n            val value = cookieMap[key]\n            if (value?.isNotBlank() == true) {\n                builder.append(key)\n                    .append(\"=\")\n                    .append(value)\n                    .append(\";\")\n            }\n        }\n        return builder.deleteCharAt(builder.lastIndexOf(\";\")).toString()\n    }\n\n    fun clear() {\n        // appDb.cookieDao.deleteOkHttp()\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt",
    "content": "package io.legado.app.help.http\n\nimport kotlinx.coroutines.CompletableDeferred\nimport kotlinx.coroutines.Deferred\nimport retrofit2.*\nimport java.lang.reflect.ParameterizedType\nimport java.lang.reflect.Type\n\nclass CoroutinesCallAdapterFactory private constructor() : CallAdapter.Factory() {\n    companion object {\n        fun create(): CoroutinesCallAdapterFactory {\n            return CoroutinesCallAdapterFactory()\n        }\n    }\n\n    override fun get(\n        returnType: Type,\n        annotations: Array<out Annotation>,\n        retrofit: Retrofit\n    ): CallAdapter<*, *>? {\n        if (Deferred::class.java != getRawType(returnType)) {\n            return null\n        }\n        check(returnType is ParameterizedType) { \"Deferred return type must be parameterized as Deferred<Foo> or Deferred<out Foo>\" }\n        val responseType = getParameterUpperBound(0, returnType)\n\n        val rawDeferredType = getRawType(responseType)\n        return if (rawDeferredType == Response::class.java) {\n            check(responseType is ParameterizedType) { \"Response must be parameterized as Response<Foo> or Response<out Foo>\" }\n            ResponseCallAdapter<Any>(\n                getParameterUpperBound(\n                    0,\n                    responseType\n                )\n            )\n        } else {\n            BodyCallAdapter<Any>(responseType)\n        }\n    }\n\n    private class BodyCallAdapter<T>(\n        private val responseType: Type\n    ) : CallAdapter<T, Deferred<T>> {\n\n        override fun responseType() = responseType\n\n        override fun adapt(call: Call<T>): Deferred<T> {\n            val deferred = CompletableDeferred<T>()\n\n            deferred.invokeOnCompletion {\n                if (deferred.isCancelled) {\n                    call.cancel()\n                }\n            }\n\n            call.enqueue(object : Callback<T> {\n                override fun onFailure(call: Call<T>, t: Throwable) {\n                    deferred.completeExceptionally(t)\n                }\n\n                override fun onResponse(call: Call<T>, response: Response<T>) {\n                    if (response.isSuccessful) {\n                        deferred.complete(response.body()!!)\n                    } else {\n                        deferred.completeExceptionally(HttpException(response))\n                    }\n                }\n            })\n\n            return deferred\n        }\n    }\n\n    private class ResponseCallAdapter<T>(\n        private val responseType: Type\n    ) : CallAdapter<T, Deferred<Response<T>>> {\n\n        override fun responseType() = responseType\n\n        override fun adapt(call: Call<T>): Deferred<Response<T>> {\n            val deferred = CompletableDeferred<Response<T>>()\n\n            deferred.invokeOnCompletion {\n                if (deferred.isCancelled) {\n                    call.cancel()\n                }\n            }\n\n            call.enqueue(object : Callback<T> {\n                override fun onFailure(call: Call<T>, t: Throwable) {\n                    deferred.completeExceptionally(t)\n                }\n\n                override fun onResponse(call: Call<T>, response: Response<T>) {\n                    deferred.complete(response)\n                }\n            })\n\n            return deferred\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/EncodeConverter.kt",
    "content": "package io.legado.app.help.http\n\nimport io.legado.app.utils.UTF8BOMFighter\nimport okhttp3.ResponseBody\nimport io.legado.app.utils.EncodingDetect\nimport retrofit2.Converter\nimport retrofit2.Retrofit\nimport java.lang.reflect.Type\nimport java.nio.charset.Charset\n\nclass EncodeConverter(private val encode: String? = null) : Converter.Factory() {\n\n    override fun responseBodyConverter(\n        type: Type?,\n        annotations: Array<Annotation>?,\n        retrofit: Retrofit?\n    ): Converter<ResponseBody, String>? {\n        return Converter { value ->\n            val responseBytes = UTF8BOMFighter.removeUTF8BOM(value.bytes())\n            encode?.let { return@Converter String(responseBytes, Charset.forName(encode)) }\n\n            var charsetName: String? = null\n            val mediaType = value.contentType()\n            //根据http头判断\n            if (mediaType != null) {\n                val charset = mediaType.charset()\n                charsetName = charset?.displayName()\n            }\n\n            if (charsetName == null) {\n                charsetName = EncodingDetect.getHtmlEncode(responseBytes)\n            }\n\n            String(responseBytes, Charset.forName(charsetName))\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/HttpHelper.kt",
    "content": "package io.legado.app.help.http\n\n// import io.legado.app.help.http.cronet.CronetInterceptor\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport okhttp3.ConnectionSpec\nimport okhttp3.Credentials\nimport okhttp3.Interceptor\nimport okhttp3.OkHttpClient\nimport okhttp3.Route\nimport okhttp3.Authenticator\nimport okhttp3.Response\nimport okhttp3.Request\nimport okhttp3.logging.HttpLoggingInterceptor\nimport java.net.InetSocketAddress\nimport java.net.Proxy\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.TimeUnit\nimport kotlin.coroutines.resume\nimport java.io.IOException\nimport io.legado.app.model.DebugLog\n\nprivate val proxyClientCache: ConcurrentHashMap<String, OkHttpClient> by lazy {\n    ConcurrentHashMap()\n}\n\nval okHttpClient: OkHttpClient by lazy {\n    val specs = arrayListOf(\n        ConnectionSpec.MODERN_TLS,\n        ConnectionSpec.COMPATIBLE_TLS,\n        ConnectionSpec.CLEARTEXT\n    )\n\n    val builder = OkHttpClient.Builder()\n        .connectTimeout(15, TimeUnit.SECONDS)\n        .writeTimeout(15, TimeUnit.SECONDS)\n        .readTimeout(15, TimeUnit.SECONDS)\n        .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory, SSLHelper.unsafeTrustManager)\n        .retryOnConnectionFailure(true)\n        .hostnameVerifier(SSLHelper.unsafeHostnameVerifier)\n        .connectionSpecs(specs)\n        .followRedirects(true)\n        .followSslRedirects(true)\n        .addInterceptor(Interceptor { chain ->\n            val request = chain.request()\n                .newBuilder()\n                .addHeader(\"Keep-Alive\", \"300\")\n                .addHeader(\"Connection\", \"Keep-Alive\")\n                .addHeader(\"Cache-Control\", \"no-cache\")\n                .build()\n            chain.proceed(request)\n        })\n    // if (AppConfig.isCronet) {\n    //     builder.addInterceptor(CronetInterceptor())\n    // }\n\n    builder.build()\n}\n\n/**\n * 缓存代理okHttp\n */\nfun getProxyClient(proxy: String? = null, debugLog: DebugLog? = null): OkHttpClient {\n    if (proxy.isNullOrBlank()) {\n        if (debugLog == null) {\n            return okHttpClient\n        }\n        val builder = okHttpClient.newBuilder()\n        val logInterceptor = HttpLoggingInterceptor(debugLog);//创建拦截对象\n        logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);//这一句一定要记得写，否则没有数据输出\n\n        builder.addNetworkInterceptor(logInterceptor)  //设置打印拦截日志\n        return builder.build()\n    }\n    if (debugLog == null) {\n        proxyClientCache[proxy]?.let {\n            return it\n        }\n    }\n    val r = Regex(\"(http|socks4|socks5)://(.*):(\\\\d{2,5})(@.*@.*)?\")\n    val ms = r.findAll(proxy)\n    val group = ms.first()\n    var username = \"\"       //代理服务器验证用户名\n    var password = \"\"       //代理服务器验证密码\n    val type = if (group.groupValues[1] == \"http\") \"http\" else \"socks\"\n    val host = group.groupValues[2]\n    val port = group.groupValues[3].toInt()\n    if (group.groupValues[4] != \"\") {\n        username = group.groupValues[4].split(\"@\")[1]\n        password = group.groupValues[4].split(\"@\")[2]\n    }\n    if (type != \"direct\" && host != \"\") {\n        val builder = okHttpClient.newBuilder()\n        if (type == \"http\") {\n            builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port)))\n        } else {\n            builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port)))\n        }\n        if (username != \"\" && password != \"\") {\n            val proxyAuthenticator = object: Authenticator {\n                @Throws(IOException::class)\n                override fun authenticate(route: Route?, response: Response): Request {\n                    //设置代理服务器账号密码\n                    val credential = Credentials.basic(username, password);\n                    return response.request.newBuilder()\n                           .header(\"Proxy-Authorization\", credential)\n                           .build();\n                }\n            }\n            builder.proxyAuthenticator(proxyAuthenticator);\n            // builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码\n            //     val credential: String = Credentials.basic(username, password)\n            //     response.request.newBuilder()\n            //         .header(\"Proxy-Authorization\", credential)\n            //         .build()\n            // }\n        }\n        if (debugLog != null) {\n            val logInterceptor = HttpLoggingInterceptor(debugLog);//创建拦截对象\n            logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);//这一句一定要记得写，否则没有数据输出\n\n            builder.addNetworkInterceptor(logInterceptor)  //设置打印拦截日志\n            return builder.build()\n        }\n        val proxyClient = builder.build()\n        proxyClientCache[proxy] = proxyClient\n        return proxyClient\n    }\n    return okHttpClient\n}\n\n// suspend fun getWebViewSrc(params: AjaxWebView.AjaxParams): StrResponse =\n//     suspendCancellableCoroutine { block ->\n//         val webView = AjaxWebView()\n//         block.invokeOnCancellation {\n//             webView.destroyWebView()\n//         }\n//         webView.callback = object : AjaxWebView.Callback() {\n//             override fun onResult(response: StrResponse) {\n\n//                 if (!block.isCompleted)\n//                     block.resume(response)\n//             }\n\n//             override fun onError(error: Throwable) {\n//                 if (!block.isCompleted)\n//                     block.cancel(error)\n//             }\n//         }\n//         webView.load(params)\n//     }"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/OkHttpUtils.kt",
    "content": "package io.legado.app.help.http\n\nimport io.legado.app.constant.AppConst\nimport io.legado.app.utils.EncodingDetect\nimport io.legado.app.utils.GSON\nimport io.legado.app.utils.UTF8BOMFighter\nimport io.legado.app.utils.Utf8BomUtils\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.Dispatchers\nimport okhttp3.*\nimport okhttp3.HttpUrl.Companion.toHttpUrl\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.RequestBody.Companion.asRequestBody\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport java.io.File\nimport java.io.IOException\nimport java.nio.charset.Charset\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException\n\nsuspend fun OkHttpClient.newCallResponse(\n    retry: Int = 0,\n    builder: Request.Builder.() -> Unit\n): Response {\n    return withContext(Dispatchers.IO) {\n        val requestBuilder = Request.Builder()\n        requestBuilder.apply(builder)\n        var response: Response? = null\n        for (i in 0..retry) {\n            response = newCall(requestBuilder.build()).await()\n            if (response.isSuccessful) {\n                return@withContext response\n            }\n        }\n        return@withContext response!!\n    }\n}\n\nsuspend fun OkHttpClient.newCallResponseBody(\n    retry: Int = 0,\n    builder: Request.Builder.() -> Unit\n): ResponseBody {\n    return newCallResponse(retry, builder).let {\n        it.body ?: throw IOException(it.message)\n    }\n}\n\nsuspend fun OkHttpClient.newCall(\n    retry: Int = 0,\n    builder: Request.Builder.() -> Unit\n): ResponseBody {\n    val requestBuilder = Request.Builder()\n    requestBuilder.apply(builder)\n    var response: Response? = null\n    for (i in 0..retry) {\n        response = this.newCall(requestBuilder.build()).await()\n        if (response.isSuccessful) {\n            return response.body!!\n        }\n    }\n    return response!!.body ?: throw IOException(response.message)\n}\n\nsuspend fun OkHttpClient.newCallStrResponse(\n    retry: Int = 0,\n    builder: Request.Builder.() -> Unit\n): StrResponse {\n    val requestBuilder = Request.Builder()\n    requestBuilder.apply(builder)\n    var response: Response? = null\n    for (i in 0..retry) {\n        response = this.newCall(requestBuilder.build()).await()\n        if (response.isSuccessful) {\n            return StrResponse(response, response.body!!.text())\n        }\n    }\n    return StrResponse(response!!, response.body?.text() ?: response.message)\n}\n\nsuspend fun Call.await(): Response = suspendCancellableCoroutine { block ->\n\n    block.invokeOnCancellation {\n        cancel()\n    }\n\n    enqueue(object : Callback {\n        override fun onFailure(call: Call, e: IOException) {\n            block.resumeWithException(e)\n        }\n\n        override fun onResponse(call: Call, response: Response) {\n            block.resume(response)\n        }\n    })\n\n}\n\nfun ResponseBody.text(encode: String? = null): String {\n    val responseBytes = Utf8BomUtils.removeUTF8BOM(bytes())\n    var charsetName: String? = encode\n\n    charsetName?.let {\n        return String(responseBytes, Charset.forName(charsetName))\n    }\n\n    //根据http头判断\n    contentType()?.charset()?.let {\n        return String(responseBytes, it)\n    }\n\n    //根据内容判断\n    charsetName = EncodingDetect.getHtmlEncode(responseBytes)\n    return String(responseBytes, Charset.forName(charsetName))\n}\n\nfun Request.Builder.addHeaders(headers: Map<String, String>) {\n    headers.forEach {\n        addHeader(it.key, it.value)\n    }\n}\n\nfun Request.Builder.get(url: String, queryMap: Map<String, String>, encoded: Boolean = false) {\n    val httpBuilder = url.toHttpUrl().newBuilder()\n    queryMap.forEach {\n        if (encoded) {\n            httpBuilder.addEncodedQueryParameter(it.key, it.value)\n        } else {\n            httpBuilder.addQueryParameter(it.key, it.value)\n        }\n    }\n    url(httpBuilder.build())\n}\n\nfun Request.Builder.postForm(form: Map<String, String>, encoded: Boolean = false) {\n    val formBody = FormBody.Builder()\n    form.forEach {\n        if (encoded) {\n            formBody.addEncoded(it.key, it.value)\n        } else {\n            formBody.add(it.key, it.value)\n        }\n    }\n    post(formBody.build())\n}\n\nfun Request.Builder.postMultipart(type: String?, form: Map<String, Any>) {\n    val multipartBody = MultipartBody.Builder()\n    type?.let {\n        multipartBody.setType(type.toMediaType())\n    }\n    form.forEach {\n        when (val value = it.value) {\n            is Map<*, *> -> {\n                val fileName = value[\"fileName\"] as String\n                val file = value[\"file\"]\n                val mediaType = (value[\"contentType\"] as? String)?.toMediaType()\n                val requestBody = when (file) {\n                    is File -> {\n                        file.asRequestBody(mediaType)\n                    }\n                    is ByteArray -> {\n                        file.toRequestBody(mediaType)\n                    }\n                    is String -> {\n                        file.toRequestBody(mediaType)\n                    }\n                    else -> {\n                        GSON.toJson(file).toRequestBody(mediaType)\n                    }\n                }\n                multipartBody.addFormDataPart(it.key, fileName, requestBody)\n            }\n            else -> multipartBody.addFormDataPart(it.key, it.value.toString())\n        }\n    }\n    post(multipartBody.build())\n}\n\nfun Request.Builder.postJson(json: String?) {\n    json?.let {\n        val requestBody = json.toRequestBody(\"application/json; charset=UTF-8\".toMediaType())\n        post(requestBody)\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/RequestMethod.kt",
    "content": "package io.legado.app.help.http\n\nenum class RequestMethod {\n    GET, POST\n}"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/Res.kt",
    "content": "package io.legado.app.help.http\n\ndata class Res(val url: String, val body: String?)"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/SSLHelper.kt",
    "content": "package io.legado.app.help.http\n\n//import android.annotation.SuppressLint\nimport java.io.IOException\nimport java.io.InputStream\nimport java.security.KeyManagementException\nimport java.security.KeyStore\nimport java.security.NoSuchAlgorithmException\nimport java.security.SecureRandom\nimport java.security.cert.CertificateException\nimport java.security.cert.CertificateFactory\nimport java.security.cert.X509Certificate\nimport javax.net.ssl.*\n\nobject SSLHelper {\n\n    val sslSocketFactory: SSLParams?\n        get() = getSslSocketFactoryBase(null, null, null)\n\n    /**\n     * 为了解决客户端不信任服务器数字证书的问题，网络上大部分的解决方案都是让客户端不对证书做任何检查，\n     * 这是一种有很大安全漏洞的办法\n     */\n    val unsafeTrustManager: X509TrustManager = object : X509TrustManager {\n//        @SuppressLint(\"TrustAllX509TrustManager\")\n        @Throws(CertificateException::class)\n        override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {\n        }\n\n//        @SuppressLint(\"TrustAllX509TrustManager\")\n        @Throws(CertificateException::class)\n        override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {\n        }\n\n        override fun getAcceptedIssuers(): Array<X509Certificate> {\n            return arrayOf()\n        }\n    }\n\n    val unsafeSSLSocketFactory: SSLSocketFactory by lazy {\n        try {\n            val sslContext = SSLContext.getInstance(\"SSL\")\n            sslContext.init(null, arrayOf(unsafeTrustManager), SecureRandom())\n            sslContext.socketFactory\n        } catch (e: Exception) {\n            throw RuntimeException(e)\n        }\n    }\n\n    /**\n     * 此类是用于主机名验证的基接口。 在握手期间，如果 URL 的主机名和服务器的标识主机名不匹配，\n     * 则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。策略可以是基于证书的或依赖于其他验证方案。\n     * 当验证 URL 主机名使用的默认规则失败时使用这些回调。如果主机名是可接受的，则返回 true\n     */\n    val unsafeHostnameVerifier: HostnameVerifier = HostnameVerifier { _, _ -> true }\n\n    class SSLParams {\n        lateinit var sSLSocketFactory: SSLSocketFactory\n        lateinit var trustManager: X509TrustManager\n    }\n\n    /**\n     * https单向认证\n     * 可以额外配置信任服务端的证书策略，否则默认是按CA证书去验证的，若不是CA可信任的证书，则无法通过验证\n     */\n    fun getSslSocketFactory(trustManager: X509TrustManager): SSLParams? {\n        return getSslSocketFactoryBase(trustManager, null, null)\n    }\n\n    /**\n     * https单向认证\n     * 用含有服务端公钥的证书校验服务端证书\n     */\n    fun getSslSocketFactory(vararg certificates: InputStream): SSLParams? {\n        return getSslSocketFactoryBase(null, null, null, *certificates)\n    }\n\n    /**\n     * https双向认证\n     * bksFile 和 password -> 客户端使用bks证书校验服务端证书\n     * certificates -> 用含有服务端公钥的证书校验服务端证书\n     */\n    fun getSslSocketFactory(bksFile: InputStream, password: String, vararg certificates: InputStream): SSLParams? {\n        return getSslSocketFactoryBase(null, bksFile, password, *certificates)\n    }\n\n    /**\n     * https双向认证\n     * bksFile 和 password -> 客户端使用bks证书校验服务端证书\n     * X509TrustManager -> 如果需要自己校验，那么可以自己实现相关校验，如果不需要自己校验，那么传null即可\n     */\n    fun getSslSocketFactory(bksFile: InputStream, password: String, trustManager: X509TrustManager): SSLParams? {\n        return getSslSocketFactoryBase(trustManager, bksFile, password)\n    }\n\n    private fun getSslSocketFactoryBase(\n        trustManager: X509TrustManager?,\n        bksFile: InputStream?,\n        password: String?,\n        vararg certificates: InputStream\n    ): SSLParams? {\n        val sslParams = SSLParams()\n        try {\n            val keyManagers = prepareKeyManager(bksFile, password)\n            val trustManagers = prepareTrustManager(*certificates)\n            val manager: X509TrustManager = trustManager ?: chooseTrustManager(trustManagers)\n            // 创建TLS类型的SSLContext对象， that uses our TrustManager\n            val sslContext = SSLContext.getInstance(\"TLS\")\n            // 用上面得到的trustManagers初始化SSLContext，这样sslContext就会信任keyStore中的证书\n            // 第一个参数是授权的密钥管理器，用来授权验证，比如授权自签名的证书验证。第二个是被授权的证书管理器，用来验证服务器端的证书\n            sslContext.init(keyManagers, arrayOf<TrustManager>(manager), null)\n            // 通过sslContext获取SSLSocketFactory对象\n            sslParams.sSLSocketFactory = sslContext.socketFactory\n            sslParams.trustManager = manager\n            return sslParams\n        } catch (e: NoSuchAlgorithmException) {\n            e.printStackTrace()\n        } catch (e: KeyManagementException) {\n            e.printStackTrace()\n        }\n        return null\n    }\n\n    private fun prepareKeyManager(bksFile: InputStream?, password: String?): Array<KeyManager>? {\n        try {\n            if (bksFile == null || password == null) return null\n            val clientKeyStore = KeyStore.getInstance(\"BKS\")\n            clientKeyStore.load(bksFile, password.toCharArray())\n            val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())\n            kmf.init(clientKeyStore, password.toCharArray())\n            return kmf.keyManagers\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n        return null\n    }\n\n    private fun prepareTrustManager(vararg certificates: InputStream): Array<TrustManager> {\n        val certificateFactory = CertificateFactory.getInstance(\"X.509\")\n        // 创建一个默认类型的KeyStore，存储我们信任的证书\n        val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())\n        keyStore.load(null)\n        for ((index, certStream) in certificates.withIndex()) {\n            val certificateAlias = Integer.toString(index)\n            // 证书工厂根据证书文件的流生成证书 cert\n            val cert = certificateFactory.generateCertificate(certStream)\n            // 将 cert 作为可信证书放入到keyStore中\n            keyStore.setCertificateEntry(certificateAlias, cert)\n            try {\n                certStream.close()\n            } catch (e: IOException) {\n                e.printStackTrace()\n            }\n        }\n        //我们创建一个默认类型的TrustManagerFactory\n        val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())\n        //用我们之前的keyStore实例初始化TrustManagerFactory，这样tmf就会信任keyStore中的证书\n        tmf.init(keyStore)\n        //通过tmf获取TrustManager数组，TrustManager也会信任keyStore中的证书\n        return tmf.trustManagers\n    }\n\n    private fun chooseTrustManager(trustManagers: Array<TrustManager>): X509TrustManager {\n        for (trustManager in trustManagers) {\n            if (trustManager is X509TrustManager) {\n                return trustManager\n            }\n        }\n        throw NullPointerException()\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/StrResponse.kt",
    "content": "package io.legado.app.help.http\n\nimport okhttp3.*\nimport okhttp3.Response.Builder\n\n/**\n * An HTTP response.\n */\n@Suppress(\"unused\", \"MemberVisibilityCanBePrivate\")\nclass StrResponse {\n    var raw: Response\n        private set\n    var body: String? = null\n        private set\n    var errorBody: ResponseBody? = null\n        private set\n\n    constructor(rawResponse: Response, body: String?) {\n        this.raw = rawResponse\n        this.body = body\n    }\n\n    constructor(url: String, body: String?) {\n        raw = Builder()\n            .code(200)\n            .message(\"OK\")\n            .protocol(Protocol.HTTP_1_1)\n            .request(Request.Builder().url(url).build())\n            .build()\n        this.body = body\n    }\n\n    constructor(rawResponse: Response, errorBody: ResponseBody?) {\n        this.raw = rawResponse\n        this.errorBody = errorBody\n    }\n\n    fun raw() = raw\n\n    fun url(): String {\n        raw.networkResponse?.let {\n            return it.request.url.toString()\n        }\n        return raw.request.url.toString()\n    }\n\n    val url: String get() = url()\n\n    fun body() = body\n\n    fun code(): Int {\n        return raw.code\n    }\n\n    fun message(): String {\n        return raw.message\n    }\n\n    fun headers(): Headers {\n        return raw.headers\n    }\n\n    fun isSuccessful(): Boolean = raw.isSuccessful\n\n    fun errorBody(): ResponseBody? {\n        return errorBody\n    }\n\n    override fun toString(): String {\n        return raw.toString()\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/help/http/api/CookieManager.kt",
    "content": "package io.legado.app.help.http.api\n\ninterface CookieManager {\n\n    /**\n     * 保存cookie\n     */\n    fun setCookie(url: String, cookie: String?)\n\n    /**\n     * 替换cookie\n     */\n    fun replaceCookie(url: String, cookie: String)\n\n    /**\n     * 获取cookie\n     */\n    fun getCookie(url: String): String\n\n    /**\n     * 移除cookie\n     */\n    fun removeCookie(url: String)\n\n    fun cookieToMap(cookie: String): MutableMap<String, String>\n\n    fun mapToCookie(cookieMap: Map<String, String>?): String?\n}"
  },
  {
    "path": "src/main/java/io/legado/app/lib/icu4j/CharsetDetector.java",
    "content": "// © 2016 and later: Unicode, Inc. and others.\n// License & terms of use: http://www.unicode.org/copyright.html\n/*\n  ******************************************************************************\n  Copyright (C) 2005-2016, International Business Machines Corporation and    *\n  others. All Rights Reserved.                                                *\n  ******************************************************************************\n */\npackage io.legado.app.lib.icu4j;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.Reader;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\n\n/**\n * <code>CharsetDetector</code> provides a facility for detecting the\n * charset or encoding of character data in an unknown format.\n * The input data can either be from an input stream or an array of bytes.\n * The result of the detection operation is a list of possibly matching\n * charsets, or, for simple use, you can just ask for a Java Reader that\n * will will work over the input data.\n * <p>\n * Character set detection is at best an imprecise operation.  The detection\n * process will attempt to identify the charset that best matches the characteristics\n * of the byte data, but the process is partly statistical in nature, and\n * the results can not be guaranteed to always be correct.\n * <p>\n * For best accuracy in charset detection, the input data should be primarily\n * in a single language, and a minimum of a few hundred bytes worth of plain text\n * in the language are needed.  The detection process will attempt to\n * ignore html or xml style markup that could otherwise obscure the content.\n * <p>\n *\n * @stable ICU 3.4\n */\n@SuppressWarnings({\"JavaDoc\", \"unused\", \"RedundantSuppression\"})\npublic class CharsetDetector {\n\n//   Question: Should we have getters corresponding to the setters for input text\n//   and declared encoding?\n\n//   A thought: If we were to create our own type of Java Reader, we could defer\n//   figuring out an actual charset for data that starts out with too much English\n//   only ASCII until the user actually read through to something that didn't look\n//   like 7 bit English.  If  nothing else ever appeared, we would never need to\n//   actually choose the \"real\" charset.  All assuming that the application just\n//   wants the data, and doesn't care about a char set name.\n\n    /**\n     * Constructor\n     *\n     * @stable ICU 3.4\n     */\n    public CharsetDetector() {\n    }\n\n    /**\n     * Set the declared encoding for charset detection.\n     * The declared encoding of an input text is an encoding obtained\n     * from an http header or xml declaration or similar source that\n     * can be provided as additional information to the charset detector.\n     * A match between a declared encoding and a possible detected encoding\n     * will raise the quality of that detected encoding by a small delta,\n     * and will also appear as a \"reason\" for the match.\n     * <p>\n     * A declared encoding that is incompatible with the input data being\n     * analyzed will not be added to the list of possible encodings.\n     *\n     * @param encoding The declared encoding\n     * @stable ICU 3.4\n     */\n    public CharsetDetector setDeclaredEncoding(String encoding) {\n        fDeclaredEncoding = encoding;\n        return this;\n    }\n\n    /**\n     * Set the input text (byte) data whose charset is to be detected.\n     *\n     * @param in the input text of unknown encoding\n     * @return This CharsetDetector\n     * @stable ICU 3.4\n     */\n    public CharsetDetector setText(byte[] in) {\n        fRawInput = in;\n        fRawLength = in.length;\n\n        return this;\n    }\n\n    private static final int kBufSize = 8000;\n\n    /**\n     * Set the input text (byte) data whose charset is to be detected.\n     * <p>\n     * The input stream that supplies the character data must have markSupported()\n     * == true; the charset detection process will read a small amount of data,\n     * then return the stream to its original position via\n     * the InputStream.reset() operation.  The exact amount that will\n     * be read depends on the characteristics of the data itself.\n     *\n     * @param in the input text of unknown encoding\n     * @return This CharsetDetector\n     * @stable ICU 3.4\n     */\n\n    public CharsetDetector setText(InputStream in) throws IOException {\n        fInputStream = in;\n        fInputStream.mark(kBufSize);\n        fRawInput = new byte[kBufSize];   // Always make a new buffer because the\n        //   previous one may have come from the caller,\n        //   in which case we can't touch it.\n        fRawLength = 0;\n        int remainingLength = kBufSize;\n        while (remainingLength > 0) {\n            // read() may give data in smallish chunks, esp. for remote sources.  Hence, this loop.\n            int bytesRead = fInputStream.read(fRawInput, fRawLength, remainingLength);\n            if (bytesRead <= 0) {\n                break;\n            }\n            fRawLength += bytesRead;\n            remainingLength -= bytesRead;\n        }\n        fInputStream.reset();\n\n        return this;\n    }\n\n\n    /**\n     * Return the charset that best matches the supplied input data.\n     * <p>\n     * Note though, that because the detection\n     * only looks at the start of the input data,\n     * there is a possibility that the returned charset will fail to handle\n     * the full set of input data.\n     * p>\n     * aise an exception if\n     * <ul>\n     *   <li>no charset appears to match the data.</li>\n     *   <li>no input text has been provided</li>\n     * </ul>\n     *\n     * @return a CharsetMatch object representing the best matching charset, or\n     * <code>null</code> if there are no matches.\n     * @stable ICU 3.4\n     */\n    public CharsetMatch detect() {\n//   TODO:  A better implementation would be to copy the detect loop from\n//          detectAll(), and cut it short as soon as a match with a high confidence\n//          is found.  This is something to be done later, after things are otherwise\n//          working.\n        CharsetMatch[] matches = detectAll();\n\n        if (matches == null || matches.length == 0) {\n            return null;\n        }\n\n        return matches[0];\n    }\n\n    /**\n     * Return an array of all charsets that appear to be plausible\n     * matches with the input data.  The array is ordered with the\n     * best quality match first.\n     * <p>\n     * aise an exception if\n     * <ul>\n     *   <li>no charsets appear to match the input data.</li>\n     *   <li>no input text has been provided</li>\n     * </ul>\n     *\n     * @return An array of CharsetMatch objects representing possibly matching charsets.\n     * @stable ICU 3.4\n     */\n    public CharsetMatch[] detectAll() {\n        ArrayList<CharsetMatch> matches = new ArrayList<>();\n\n        MungeInput();  // Strip html markup, collect byte stats.\n\n        //  Iterate over all possible charsets, remember all that\n        //    give a match quality > 0.\n        for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) {\n            CSRecognizerInfo rcinfo = ALL_CS_RECOGNIZERS.get(i);\n            boolean active = (fEnabledRecognizers != null) ? fEnabledRecognizers[i] : rcinfo.isDefaultEnabled;\n            if (active) {\n                CharsetMatch m = rcinfo.recognizer.match(this);\n                if (m != null) {\n                    matches.add(m);\n                }\n            }\n        }\n        Collections.sort(matches);      // CharsetMatch compares on confidence\n        Collections.reverse(matches);   //  Put best match first.\n        CharsetMatch[] resultArray = new CharsetMatch[matches.size()];\n        resultArray = matches.toArray(resultArray);\n        return resultArray;\n    }\n\n\n    /**\n     * Autodetect the charset of an inputStream, and return a Java Reader\n     * to access the converted input data.\n     * <p>\n     * This is a convenience method that is equivalent to\n     * <code>this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getReader();</code>\n     * <p>\n     * For the input stream that supplies the character data, markSupported()\n     * must be true; the  charset detection will read a small amount of data,\n     * then return the stream to its original position via\n     * the InputStream.reset() operation.  The exact amount that will\n     * be read depends on the characteristics of the data itself.\n     * <p>\n     * Raise an exception if no charsets appear to match the input data.\n     *\n     * @param in               The source of the byte data in the unknown charset.\n     * @param declaredEncoding A declared encoding for the data, if available,\n     *                         or null or an empty string if none is available.\n     * @stable ICU 3.4\n     */\n    public Reader getReader(InputStream in, String declaredEncoding) {\n        fDeclaredEncoding = declaredEncoding;\n\n        try {\n            setText(in);\n\n            CharsetMatch match = detect();\n\n            if (match == null) {\n                return null;\n            }\n\n            return match.getReader();\n        } catch (IOException e) {\n            return null;\n        }\n    }\n\n    /**\n     * Autodetect the charset of an inputStream, and return a String\n     * containing the converted input data.\n     * <p>\n     * This is a convenience method that is equivalent to\n     * <code>this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getString();</code>\n     * <p>\n     * Raise an exception if no charsets appear to match the input data.\n     *\n     * @param in               The source of the byte data in the unknown charset.\n     * @param declaredEncoding A declared encoding for the data, if available,\n     *                         or null or an empty string if none is available.\n     * @stable ICU 3.4\n     */\n    public String getString(byte[] in, String declaredEncoding) {\n        fDeclaredEncoding = declaredEncoding;\n\n        try {\n            setText(in);\n\n            CharsetMatch match = detect();\n\n            if (match == null) {\n                return null;\n            }\n\n            return match.getString(-1);\n        } catch (IOException e) {\n            return null;\n        }\n    }\n\n\n    /**\n     * Get the names of all charsets supported by <code>CharsetDetector</code> class.\n     * <p>\n     * <b>Note:</b> Multiple different charset encodings in a same family may use\n     * a single shared name in this implementation. For example, this method returns\n     * an array including \"ISO-8859-1\" (ISO Latin 1), but not including \"windows-1252\"\n     * (Windows Latin 1). However, actual detection result could be \"windows-1252\"\n     * when the input data matches Latin 1 code points with any points only available\n     * in \"windows-1252\".\n     *\n     * @return an array of the names of all charsets supported by\n     * <code>CharsetDetector</code> class.\n     * @stable ICU 3.4\n     */\n    public static String[] getAllDetectableCharsets() {\n        String[] allCharsetNames = new String[ALL_CS_RECOGNIZERS.size()];\n        for (int i = 0; i < allCharsetNames.length; i++) {\n            allCharsetNames[i] = ALL_CS_RECOGNIZERS.get(i).recognizer.getName();\n        }\n        return allCharsetNames;\n    }\n\n    /**\n     * Test whether or not input filtering is enabled.\n     *\n     * @return <code>true</code> if input text will be filtered.\n     * @stable ICU 3.4\n     * @see #enableInputFilter\n     */\n    public boolean inputFilterEnabled() {\n        return fStripTags;\n    }\n\n    /**\n     * Enable filtering of input text. If filtering is enabled,\n     * text within angle brackets (\"&lt;\" and \"&gt;\") will be removed\n     * before detection.\n     *\n     * @param filter <code>true</code> to enable input text filtering.\n     * @return The previous setting.\n     * @stable ICU 3.4\n     */\n    public boolean enableInputFilter(boolean filter) {\n        boolean previous = fStripTags;\n\n        fStripTags = filter;\n\n        return previous;\n    }\n\n    /*\n     *  MungeInput - after getting a set of raw input data to be analyzed, preprocess\n     *               it by removing what appears to be html markup.\n     */\n    private void MungeInput() {\n        int srci;\n        int dsti = 0;\n        byte b;\n        boolean inMarkup = false;\n        int openTags = 0;\n        int badTags = 0;\n\n        //\n        //  html / xml markup stripping.\n        //     quick and dirty, not 100% accurate, but hopefully good enough, statistically.\n        //     discard everything within < brackets >\n        //     Count how many total '<' and illegal (nested) '<' occur, so we can make some\n        //     guess as to whether the input was actually marked up at all.\n        if (fStripTags) {\n            for (srci = 0; srci < fRawLength && dsti < fInputBytes.length; srci++) {\n                b = fRawInput[srci];\n                if (b == (byte) '<') {\n                    if (inMarkup) {\n                        badTags++;\n                    }\n                    inMarkup = true;\n                    openTags++;\n                }\n\n                if (!inMarkup) {\n                    fInputBytes[dsti++] = b;\n                }\n\n                if (b == (byte) '>') {\n                    inMarkup = false;\n                }\n            }\n\n            fInputLen = dsti;\n        }\n\n        //\n        //  If it looks like this input wasn't marked up, or if it looks like it's\n        //    essentially nothing but markup abandon the markup stripping.\n        //    Detection will have to work on the unstripped input.\n        //\n        if (openTags < 5 || openTags / 5 < badTags ||\n                (fInputLen < 100 && fRawLength > 600)) {\n            int limit = fRawLength;\n\n            if (limit > kBufSize) {\n                limit = kBufSize;\n            }\n\n            for (srci = 0; srci < limit; srci++) {\n                fInputBytes[srci] = fRawInput[srci];\n            }\n            fInputLen = srci;\n        }\n\n        //\n        // Tally up the byte occurence statistics.\n        //   These are available for use by the various detectors.\n        //\n        Arrays.fill(fByteStats, (short) 0);\n        for (srci = 0; srci < fInputLen; srci++) {\n            int val = fInputBytes[srci] & 0x00ff;\n            fByteStats[val]++;\n        }\n\n        fC1Bytes = false;\n        for (int i = 0x80; i <= 0x9F; i += 1) {\n            if (fByteStats[i] != 0) {\n                fC1Bytes = true;\n                break;\n            }\n        }\n    }\n\n    /*\n     *  The following items are accessed by individual CharsetRecongizers during\n     *     the recognition process\n     *\n     */\n    byte[] fInputBytes =       // The text to be checked.  Markup will have been\n            new byte[kBufSize];  //   removed if appropriate.\n\n    int fInputLen;          // Length of the byte data in fInputBytes.\n\n    short[] fByteStats =      // byte frequency statistics for the input text.\n            new short[256];  //   Value is percent, not absolute.\n    //   Value is rounded up, so zero really means zero occurences.\n\n    boolean fC1Bytes =          // True if any bytes in the range 0x80 - 0x9F are in the input;\n            false;\n\n    String fDeclaredEncoding;\n\n\n    byte[] fRawInput;     // Original, untouched input bytes.\n    //  If user gave us a byte array, this is it.\n    //  If user gave us a stream, it's read to a\n    //  buffer here.\n    int fRawLength;    // Length of data in fRawInput array.\n\n    InputStream fInputStream;  // User's input stream, or null if the user\n    //   gave us a byte array.\n\n    //\n    //  Stuff private to CharsetDetector\n    //\n    private boolean fStripTags =   // If true, setText() will strip tags from input text.\n            false;\n\n    private boolean[] fEnabledRecognizers;   // If not null, active set of charset recognizers had\n    // been changed from the default. The array index is\n    // corresponding to ALL_RECOGNIZER. See setDetectableCharset().\n\n    private static class CSRecognizerInfo {\n        CharsetRecognizer recognizer;\n        boolean isDefaultEnabled;\n\n        CSRecognizerInfo(CharsetRecognizer recognizer, boolean isDefaultEnabled) {\n            this.recognizer = recognizer;\n            this.isDefaultEnabled = isDefaultEnabled;\n        }\n    }\n\n    /*\n     * List of recognizers for all charsets known to the implementation.\n     */\n    private static final List<CSRecognizerInfo> ALL_CS_RECOGNIZERS;\n\n    static {\n        List<CSRecognizerInfo> list = new ArrayList<>();\n\n        list.add(new CSRecognizerInfo(new CharsetRecog_UTF8(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_BE(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_LE(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_BE(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_LE(), true));\n\n        list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_sjis(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022JP(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022CN(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022KR(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_gb_18030(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_jp(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_kr(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_big5(), true));\n\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_1(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_2(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_5_ru(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_6_ar(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_7_el(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_I_he(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_he(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1251(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1256(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_KOI8_R(), true));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_9_tr(), true));\n\n        // IBM 420/424 recognizers are disabled by default\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_rtl(), false));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_ltr(), false));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_rtl(), false));\n        list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_ltr(), false));\n\n        //noinspection Java9CollectionFactory\n        ALL_CS_RECOGNIZERS = Collections.unmodifiableList(list);\n    }\n\n    /**\n     * Get the names of charsets that can be recognized by this CharsetDetector instance.\n     *\n     * @return an array of the names of charsets that can be recognized by this CharsetDetector\n     * instance.\n     * @internal\n     * @deprecated This API is ICU internal only.\n     */\n    @Deprecated\n    public String[] getDetectableCharsets() {\n        List<String> csnames = new ArrayList<>(ALL_CS_RECOGNIZERS.size());\n        for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) {\n            CSRecognizerInfo rcinfo = ALL_CS_RECOGNIZERS.get(i);\n            boolean active = (fEnabledRecognizers == null) ? rcinfo.isDefaultEnabled : fEnabledRecognizers[i];\n            if (active) {\n                csnames.add(rcinfo.recognizer.getName());\n            }\n        }\n        return csnames.toArray(new String[0]);\n    }\n\n    /**\n     * Enable or disable individual charset encoding.\n     * A name of charset encoding must be included in the names returned by\n     * {@link #getAllDetectableCharsets()}.\n     *\n     * @param encoding the name of charset encoding.\n     * @param enabled  <code>true</code> to enable, or <code>false</code> to disable the\n     *                 charset encoding.\n     * @return A reference to this <code>CharsetDetector</code>.\n     * @throws IllegalArgumentException when the name of charset encoding is\n     *                                  not supported.\n     * @internal\n     * @deprecated This API is ICU internal only.\n     */\n    @Deprecated\n    public CharsetDetector setDetectableCharset(String encoding, boolean enabled) {\n        int modIdx = -1;\n        boolean isDefaultVal = false;\n        for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) {\n            CSRecognizerInfo csrinfo = ALL_CS_RECOGNIZERS.get(i);\n            if (csrinfo.recognizer.getName().equals(encoding)) {\n                modIdx = i;\n                isDefaultVal = (csrinfo.isDefaultEnabled == enabled);\n                break;\n            }\n        }\n        if (modIdx < 0) {\n            // No matching encoding found\n            throw new IllegalArgumentException(\"Invalid encoding: \" + \"\\\"\" + encoding + \"\\\"\");\n        }\n\n        if (fEnabledRecognizers == null && !isDefaultVal) {\n            // Create an array storing the non default setting\n            fEnabledRecognizers = new boolean[ALL_CS_RECOGNIZERS.size()];\n\n            // Initialize the array with default info\n            for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) {\n                fEnabledRecognizers[i] = ALL_CS_RECOGNIZERS.get(i).isDefaultEnabled;\n            }\n        }\n\n        if (fEnabledRecognizers != null) {\n            fEnabledRecognizers[modIdx] = enabled;\n        }\n\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/lib/icu4j/CharsetMatch.java",
    "content": "// © 2016 and later: Unicode, Inc. and others.\n// License & terms of use: http://www.unicode.org/copyright.html\n/*\n * ******************************************************************************\n * Copyright (C) 2005-2016, International Business Machines Corporation and    *\n * others. All Rights Reserved.                                                *\n * ******************************************************************************\n */\npackage io.legado.app.lib.icu4j;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.Reader;\n\n\n/**\n * This class represents a charset that has been identified by a CharsetDetector\n * as a possible encoding for a set of input data.  From an instance of this\n * class, you can ask for a confidence level in the charset identification,\n * or for Java Reader or String to access the original byte data in Unicode form.\n * <p>\n * Instances of this class are created only by CharsetDetectors.\n * <p>\n * Note:  this class has a natural ordering that is inconsistent with equals.\n * The natural ordering is based on the match confidence value.\n *\n * @stable ICU 3.4\n */\n@SuppressWarnings({\"JavaDoc\", \"unused\"})\npublic class CharsetMatch implements Comparable<CharsetMatch> {\n\n\n    /**\n     * Create a java.io.Reader for reading the Unicode character data corresponding\n     * to the original byte data supplied to the Charset detect operation.\n     * <p>\n     * CAUTION:  if the source of the byte data was an InputStream, a Reader\n     * can be created for only one matching char set using this method.  If more\n     * than one charset needs to be tried, the caller will need to reset\n     * the InputStream and create InputStreamReaders itself, based on the charset name.\n     *\n     * @return the Reader for the Unicode character data.\n     * @stable ICU 3.4\n     */\n    public Reader getReader() {\n        InputStream inputStream = fInputStream;\n\n        if (inputStream == null) {\n            inputStream = new ByteArrayInputStream(fRawInput, 0, fRawLength);\n        }\n\n        try {\n            inputStream.reset();\n            return new InputStreamReader(inputStream, getName());\n        } catch (IOException e) {\n            return null;\n        }\n    }\n\n    /**\n     * Create a Java String from Unicode character data corresponding\n     * to the original byte data supplied to the Charset detect operation.\n     *\n     * @return a String created from the converted input data.\n     * @stable ICU 3.4\n     */\n    public String getString() throws java.io.IOException {\n        return getString(-1);\n\n    }\n\n    /**\n     * Create a Java String from Unicode character data corresponding\n     * to the original byte data supplied to the Charset detect operation.\n     * The length of the returned string is limited to the specified size;\n     * the string will be trunctated to this length if necessary.  A limit value of\n     * zero or less is ignored, and treated as no limit.\n     *\n     * @param maxLength The maximium length of the String to be created when the\n     *                  source of the data is an input stream, or -1 for\n     *                  unlimited length.\n     * @return a String created from the converted input data.\n     * @stable ICU 3.4\n     */\n    public String getString(int maxLength) throws java.io.IOException {\n        String result;\n        if (fInputStream != null) {\n            StringBuilder sb = new StringBuilder();\n            char[] buffer = new char[1024];\n            Reader reader = getReader();\n            int max = maxLength < 0 ? Integer.MAX_VALUE : maxLength;\n            int bytesRead;\n\n            while ((bytesRead = reader.read(buffer, 0, Math.min(max, 1024))) >= 0) {\n                sb.append(buffer, 0, bytesRead);\n                max -= bytesRead;\n            }\n\n            reader.close();\n\n            return sb.toString();\n        } else {\n            String name = getName();\n            /*\n             * getName() may return a name with a suffix 'rtl' or 'ltr'. This cannot\n             * be used to open a charset (e.g. IBM424_rtl). The ending '_rtl' or 'ltr'\n             * should be stripped off before creating the string.\n             */\n            int startSuffix = !name.contains(\"_rtl\") ? name.indexOf(\"_ltr\") : name.indexOf(\"_rtl\");\n            if (startSuffix > 0) {\n                name = name.substring(0, startSuffix);\n            }\n            result = new String(fRawInput, name);\n        }\n        return result;\n\n    }\n\n    /**\n     * Get an indication of the confidence in the charset detected.\n     * Confidence values range from 0-100, with larger numbers indicating\n     * a better match of the input data to the characteristics of the\n     * charset.\n     *\n     * @return the confidence in the charset match\n     * @stable ICU 3.4\n     */\n    public int getConfidence() {\n        return fConfidence;\n    }\n\n    /**\n     * Get the name of the detected charset.\n     * The name will be one that can be used with other APIs on the\n     * platform that accept charset names.  It is the \"Canonical name\"\n     * as defined by the class java.nio.charset.Charset; for\n     * charsets that are registered with the IANA charset registry,\n     * this is the MIME-preferred registerd name.\n     *\n     * @return The name of the charset.\n     * @stable ICU 3.4\n     * @see java.nio.charset.Charset\n     * @see java.io.InputStreamReader\n     */\n    public String getName() {\n        return fCharsetName;\n    }\n\n    /**\n     * Get the ISO code for the language of the detected charset.\n     *\n     * @return The ISO code for the language or <code>null</code> if the language cannot be determined.\n     * @stable ICU 3.4\n     */\n    public String getLanguage() {\n        return fLang;\n    }\n\n    /**\n     * Compare to other CharsetMatch objects.\n     * Comparison is based on the match confidence value, which\n     * allows CharsetDetector.detectAll() to order its results.\n     *\n     * @param other the CharsetMatch object to compare against.\n     * @return a negative integer, zero, or a positive integer as the\n     * confidence level of this CharsetMatch\n     * is less than, equal to, or greater than that of\n     * the argument.\n     * @throws ClassCastException if the argument is not a CharsetMatch.\n     * @stable ICU 4.4\n     */\n    @Override\n    public int compareTo(CharsetMatch other) {\n        int compareResult = 0;\n        if (this.fConfidence > other.fConfidence) {\n            compareResult = 1;\n        } else if (this.fConfidence < other.fConfidence) {\n            compareResult = -1;\n        }\n        return compareResult;\n    }\n\n    /*\n     *  Constructor.  Implementation internal\n     */\n    CharsetMatch(CharsetDetector det, CharsetRecognizer rec, int conf) {\n        fConfidence = conf;\n\n        // The references to the original application input data must be copied out\n        //   of the charset recognizer to here, in case the application resets the\n        //   recognizer before using this CharsetMatch.\n        if (det.fInputStream == null) {\n            // We only want the existing input byte data if it came straight from the user,\n            //   not if is just the head of a stream.\n            fRawInput = det.fRawInput;\n            fRawLength = det.fRawLength;\n        }\n        fInputStream = det.fInputStream;\n        fCharsetName = rec.getName();\n        fLang = rec.getLanguage();\n    }\n\n    /*\n     *  Constructor.  Implementation internal\n     */\n    CharsetMatch(CharsetDetector det, CharsetRecognizer rec, int conf, String csName, String lang) {\n        fConfidence = conf;\n\n        // The references to the original application input data must be copied out\n        //   of the charset recognizer to here, in case the application resets the\n        //   recognizer before using this CharsetMatch.\n        if (det.fInputStream == null) {\n            // We only want the existing input byte data if it came straight from the user,\n            //   not if is just the head of a stream.\n            fRawInput = det.fRawInput;\n            fRawLength = det.fRawLength;\n        }\n        fInputStream = det.fInputStream;\n        fCharsetName = csName;\n        fLang = lang;\n    }\n\n\n    //\n    //   Private Data\n    //\n    private final int fConfidence;\n    private byte[] fRawInput = null;     // Original, untouched input bytes.\n    //  If user gave us a byte array, this is it.\n    private int fRawLength;           // Length of data in fRawInput array.\n\n    private final InputStream fInputStream;  // User's input stream, or null if the user\n    //   gave us a byte array.\n\n    private final String fCharsetName;         // The name of the charset this CharsetMatch\n    //   represents.  Filled in by the recognizer.\n    private final String fLang;                // The language, if one was determined by\n    //   the recognizer during the detect operation.\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/lib/icu4j/CharsetRecog_2022.java",
    "content": "// © 2016 and later: Unicode, Inc. and others.\n// License & terms of use: http://www.unicode.org/copyright.html\n/*\n *******************************************************************************\n * Copyright (C) 2005 - 2012, International Business Machines Corporation and  *\n * others. All Rights Reserved.                                                *\n *******************************************************************************\n */\npackage io.legado.app.lib.icu4j;\n\n/**\n * class CharsetRecog_2022  part of the ICU charset detection imlementation.\n * This is a superclass for the individual detectors for\n * each of the detectable members of the ISO 2022 family\n * of encodings.\n * <p>\n * The separate classes are nested within this class.\n */\nabstract class CharsetRecog_2022 extends CharsetRecognizer {\n\n\n    /**\n     * Matching function shared among the 2022 detectors JP, CN and KR\n     * Counts up the number of legal an unrecognized escape sequences in\n     * the sample of text, and computes a score based on the total number &\n     * the proportion that fit the encoding.\n     *\n     * @param text            the byte buffer containing text to analyse\n     * @param textLen         the size of the text in the byte.\n     * @param escapeSequences the byte escape sequences to test for.\n     * @return match quality, in the range of 0-100.\n     */\n    int match(byte[] text, int textLen, byte[][] escapeSequences) {\n        int i, j;\n        int escN;\n        int hits = 0;\n        int misses = 0;\n        int shifts = 0;\n        int quality;\n        scanInput:\n        for (i = 0; i < textLen; i++) {\n            if (text[i] == 0x1b) {\n                checkEscapes:\n                for (escN = 0; escN < escapeSequences.length; escN++) {\n                    byte[] seq = escapeSequences[escN];\n\n                    if ((textLen - i) < seq.length) {\n                        continue;\n                    }\n\n                    for (j = 1; j < seq.length; j++) {\n                        if (seq[j] != text[i + j]) {\n                            continue checkEscapes;\n                        }\n                    }\n\n                    hits++;\n                    i += seq.length - 1;\n                    continue scanInput;\n                }\n\n                misses++;\n            }\n\n            if (text[i] == 0x0e || text[i] == 0x0f) {\n                // Shift in/out\n                shifts++;\n            }\n        }\n\n        if (hits == 0) {\n            return 0;\n        }\n\n        //\n        // Initial quality is based on relative proportion of recongized vs.\n        //   unrecognized escape sequences.\n        //   All good:  quality = 100;\n        //   half or less good: quality = 0;\n        //   linear inbetween.\n        quality = (100 * hits - 100 * misses) / (hits + misses);\n\n        // Back off quality if there were too few escape sequences seen.\n        //   Include shifts in this computation, so that KR does not get penalized\n        //   for having only a single Escape sequence, but many shifts.\n        if (hits + shifts < 5) {\n            quality -= (5 - (hits + shifts)) * 10;\n        }\n\n        if (quality < 0) {\n            quality = 0;\n        }\n        return quality;\n    }\n\n\n    static class CharsetRecog_2022JP extends CharsetRecog_2022 {\n        private final byte[][] escapeSequences = {\n                {0x1b, 0x24, 0x28, 0x43},   // KS X 1001:1992\n                {0x1b, 0x24, 0x28, 0x44},   // JIS X 212-1990\n                {0x1b, 0x24, 0x40},         // JIS C 6226-1978\n                {0x1b, 0x24, 0x41},         // GB 2312-80\n                {0x1b, 0x24, 0x42},         // JIS X 208-1983\n                {0x1b, 0x26, 0x40},         // JIS X 208 1990, 1997\n                {0x1b, 0x28, 0x42},         // ASCII\n                {0x1b, 0x28, 0x48},         // JIS-Roman\n                {0x1b, 0x28, 0x49},         // Half-width katakana\n                {0x1b, 0x28, 0x4a},         // JIS-Roman\n                {0x1b, 0x2e, 0x41},         // ISO 8859-1\n                {0x1b, 0x2e, 0x46}          // ISO 8859-7\n        };\n\n        @Override\n        String getName() {\n            return \"ISO-2022-JP\";\n        }\n\n        @Override\n        CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n    }\n\n    static class CharsetRecog_2022KR extends CharsetRecog_2022 {\n        private final byte[][] escapeSequences = {\n                {0x1b, 0x24, 0x29, 0x43}\n        };\n\n        @Override\n        String getName() {\n            return \"ISO-2022-KR\";\n        }\n\n        @Override\n        CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n    }\n\n    static class CharsetRecog_2022CN extends CharsetRecog_2022 {\n        private final byte[][] escapeSequences = {\n                {0x1b, 0x24, 0x29, 0x41},   // GB 2312-80\n                {0x1b, 0x24, 0x29, 0x47},   // CNS 11643-1992 Plane 1\n                {0x1b, 0x24, 0x2A, 0x48},   // CNS 11643-1992 Plane 2\n                {0x1b, 0x24, 0x29, 0x45},   // ISO-IR-165\n                {0x1b, 0x24, 0x2B, 0x49},   // CNS 11643-1992 Plane 3\n                {0x1b, 0x24, 0x2B, 0x4A},   // CNS 11643-1992 Plane 4\n                {0x1b, 0x24, 0x2B, 0x4B},   // CNS 11643-1992 Plane 5\n                {0x1b, 0x24, 0x2B, 0x4C},   // CNS 11643-1992 Plane 6\n                {0x1b, 0x24, 0x2B, 0x4D},   // CNS 11643-1992 Plane 7\n                {0x1b, 0x4e},               // SS2\n                {0x1b, 0x4f},               // SS3\n        };\n\n        @Override\n        String getName() {\n            return \"ISO-2022-CN\";\n        }\n\n        @Override\n        CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n    }\n\n}\n\n"
  },
  {
    "path": "src/main/java/io/legado/app/lib/icu4j/CharsetRecog_UTF8.java",
    "content": "// © 2016 and later: Unicode, Inc. and others.\n// License & terms of use: http://www.unicode.org/copyright.html\n/**\n * ******************************************************************************\n * Copyright (C) 2005 - 2014, International Business Machines Corporation and  *\n * others. All Rights Reserved.                                                *\n * ******************************************************************************\n */\npackage io.legado.app.lib.icu4j;\n\n/**\n * Charset recognizer for UTF-8\n */\nclass CharsetRecog_UTF8 extends CharsetRecognizer {\n\n    @Override\n    String getName() {\n        return \"UTF-8\";\n    }\n\n    /* (non-Javadoc)\n     * @see com.ibm.icu.text.CharsetRecognizer#match(com.ibm.icu.text.CharsetDetector)\n     */\n    @Override\n    CharsetMatch match(CharsetDetector det) {\n        boolean hasBOM = false;\n        int numValid = 0;\n        int numInvalid = 0;\n        byte[] input = det.fRawInput;\n        int i;\n        int trailBytes = 0;\n        int confidence;\n\n        if (det.fRawLength >= 3 &&\n                (input[0] & 0xFF) == 0xef && (input[1] & 0xFF) == 0xbb && (input[2] & 0xFF) == 0xbf) {\n            hasBOM = true;\n        }\n\n        // Scan for multi-byte sequences\n        for (i = 0; i < det.fRawLength; i++) {\n            int b = input[i];\n            if ((b & 0x80) == 0) {\n                continue;   // ASCII\n            }\n\n            // Hi bit on char found.  Figure out how long the sequence should be\n            if ((b & 0x0e0) == 0x0c0) {\n                trailBytes = 1;\n            } else if ((b & 0x0f0) == 0x0e0) {\n                trailBytes = 2;\n            } else if ((b & 0x0f8) == 0xf0) {\n                trailBytes = 3;\n            } else {\n                numInvalid++;\n                continue;\n            }\n\n            // Verify that we've got the right number of trail bytes in the sequence\n            for (; ; ) {\n                i++;\n                if (i >= det.fRawLength) {\n                    break;\n                }\n                b = input[i];\n                if ((b & 0xc0) != 0x080) {\n                    numInvalid++;\n                    break;\n                }\n                if (--trailBytes == 0) {\n                    numValid++;\n                    break;\n                }\n            }\n        }\n\n        // Cook up some sort of confidence score, based on presense of a BOM\n        //    and the existence of valid and/or invalid multi-byte sequences.\n        confidence = 0;\n        if (hasBOM && numInvalid == 0) {\n            confidence = 100;\n        } else if (hasBOM && numValid > numInvalid * 10) {\n            confidence = 80;\n        } else if (numValid > 3 && numInvalid == 0) {\n            confidence = 100;\n        } else if (numValid > 0 && numInvalid == 0) {\n            confidence = 80;\n        } else if (numValid == 0 && numInvalid == 0) {\n            // Plain ASCII. Confidence must be > 10, it's more likely than UTF-16, which\n            //              accepts ASCII with confidence = 10.\n            // TODO: add plain ASCII as an explicitly detected type.\n            confidence = 15;\n        } else if (numValid > numInvalid * 10) {\n            // Probably corruput utf-8 data.  Valid sequences aren't likely by chance.\n            confidence = 25;\n        }\n        return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/lib/icu4j/CharsetRecog_Unicode.java",
    "content": "// © 2016 and later: Unicode, Inc. and others.\n// License & terms of use: http://www.unicode.org/copyright.html\n/*\n *******************************************************************************\n * Copyright (C) 1996-2013, International Business Machines Corporation and    *\n * others. All Rights Reserved.                                                *\n *******************************************************************************\n *\n */\n\npackage io.legado.app.lib.icu4j;\n\n/**\n * This class matches UTF-16 and UTF-32, both big- and little-endian. The\n * BOM will be used if it is present.\n */\nabstract class CharsetRecog_Unicode extends CharsetRecognizer {\n\n    /* (non-Javadoc)\n     * @see com.ibm.icu.text.CharsetRecognizer#getName()\n     */\n    @Override\n    abstract String getName();\n\n    /* (non-Javadoc)\n     * @see com.ibm.icu.text.CharsetRecognizer#match(com.ibm.icu.text.CharsetDetector)\n     */\n    @Override\n    abstract CharsetMatch match(CharsetDetector det);\n\n    static int codeUnit16FromBytes(byte hi, byte lo) {\n        return ((hi & 0xff) << 8) | (lo & 0xff);\n    }\n\n    // UTF-16 confidence calculation. Very simple minded, but better than nothing.\n    //   Any 8 bit non-control characters bump the confidence up. These have a zero high byte,\n    //     and are very likely to be UTF-16, although they could also be part of a UTF-32 code.\n    //   NULs are a contra-indication, they will appear commonly if the actual encoding is UTF-32.\n    //   NULs should be rare in actual text.\n    static int adjustConfidence(int codeUnit, int confidence) {\n        if (codeUnit == 0) {\n            confidence -= 10;\n        } else if ((codeUnit >= 0x20 && codeUnit <= 0xff) || codeUnit == 0x0a) {\n            confidence += 10;\n        }\n        if (confidence < 0) {\n            confidence = 0;\n        } else if (confidence > 100) {\n            confidence = 100;\n        }\n        return confidence;\n    }\n\n    static class CharsetRecog_UTF_16_BE extends CharsetRecog_Unicode {\n        @Override\n        String getName() {\n            return \"UTF-16BE\";\n        }\n\n        @Override\n        CharsetMatch match(CharsetDetector det) {\n            byte[] input = det.fRawInput;\n            int confidence = 10;\n\n            int bytesToCheck = Math.min(input.length, 30);\n            for (int charIndex = 0; charIndex < bytesToCheck - 1; charIndex += 2) {\n                int codeUnit = codeUnit16FromBytes(input[charIndex], input[charIndex + 1]);\n                if (charIndex == 0 && codeUnit == 0xFEFF) {\n                    confidence = 100;\n                    break;\n                }\n                confidence = adjustConfidence(codeUnit, confidence);\n                if (confidence == 0 || confidence == 100) {\n                    break;\n                }\n            }\n            if (bytesToCheck < 4 && confidence < 100) {\n                confidence = 0;\n            }\n            if (confidence > 0) {\n                return new CharsetMatch(det, this, confidence);\n            }\n            return null;\n        }\n    }\n\n    static class CharsetRecog_UTF_16_LE extends CharsetRecog_Unicode {\n        @Override\n        String getName() {\n            return \"UTF-16LE\";\n        }\n\n        @Override\n        CharsetMatch match(CharsetDetector det) {\n            byte[] input = det.fRawInput;\n            int confidence = 10;\n\n            int bytesToCheck = Math.min(input.length, 30);\n            for (int charIndex = 0; charIndex < bytesToCheck - 1; charIndex += 2) {\n                int codeUnit = codeUnit16FromBytes(input[charIndex + 1], input[charIndex]);\n                if (charIndex == 0 && codeUnit == 0xFEFF) {\n                    confidence = 100;\n                    break;\n                }\n                confidence = adjustConfidence(codeUnit, confidence);\n                if (confidence == 0 || confidence == 100) {\n                    break;\n                }\n            }\n            if (bytesToCheck < 4 && confidence < 100) {\n                confidence = 0;\n            }\n            if (confidence > 0) {\n                return new CharsetMatch(det, this, confidence);\n            }\n            return null;\n        }\n    }\n\n    static abstract class CharsetRecog_UTF_32 extends CharsetRecog_Unicode {\n        abstract int getChar(byte[] input, int index);\n\n        @Override\n        abstract String getName();\n\n        @Override\n        CharsetMatch match(CharsetDetector det) {\n            byte[] input = det.fRawInput;\n            int limit = (det.fRawLength / 4) * 4;\n            int numValid = 0;\n            int numInvalid = 0;\n            boolean hasBOM = false;\n            int confidence = 0;\n\n            if (limit == 0) {\n                return null;\n            }\n            if (getChar(input, 0) == 0x0000FEFF) {\n                hasBOM = true;\n            }\n\n            for (int i = 0; i < limit; i += 4) {\n                int ch = getChar(input, i);\n\n                if (ch < 0 || ch >= 0x10FFFF || (ch >= 0xD800 && ch <= 0xDFFF)) {\n                    numInvalid += 1;\n                } else {\n                    numValid += 1;\n                }\n            }\n\n\n            // Cook up some sort of confidence score, based on presence of a BOM\n            //    and the existence of valid and/or invalid multi-byte sequences.\n            if (hasBOM && numInvalid == 0) {\n                confidence = 100;\n            } else if (hasBOM && numValid > numInvalid * 10) {\n                confidence = 80;\n            } else if (numValid > 3 && numInvalid == 0) {\n                confidence = 100;\n            } else if (numValid > 0 && numInvalid == 0) {\n                confidence = 80;\n            } else if (numValid > numInvalid * 10) {\n                // Probably corrupt UTF-32BE data.  Valid sequences aren't likely by chance.\n                confidence = 25;\n            }\n\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n    }\n\n    static class CharsetRecog_UTF_32_BE extends CharsetRecog_UTF_32 {\n        @Override\n        int getChar(byte[] input, int index) {\n            return (input[index + 0] & 0xFF) << 24 | (input[index + 1] & 0xFF) << 16 |\n                    (input[index + 2] & 0xFF) << 8 | (input[index + 3] & 0xFF);\n        }\n\n        @Override\n        String getName() {\n            return \"UTF-32BE\";\n        }\n    }\n\n\n    static class CharsetRecog_UTF_32_LE extends CharsetRecog_UTF_32 {\n        @Override\n        int getChar(byte[] input, int index) {\n            return (input[index + 3] & 0xFF) << 24 | (input[index + 2] & 0xFF) << 16 |\n                    (input[index + 1] & 0xFF) << 8 | (input[index + 0] & 0xFF);\n        }\n\n        @Override\n        String getName() {\n            return \"UTF-32LE\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/lib/icu4j/CharsetRecog_mbcs.java",
    "content": "// © 2016 and later: Unicode, Inc. and others.\n// License & terms of use: http://www.unicode.org/copyright.html\n/*\n ****************************************************************************\n * Copyright (C) 2005-2012, International Business Machines Corporation and *\n * others. All Rights Reserved.                                             *\n ****************************************************************************\n *\n */\npackage io.legado.app.lib.icu4j;\n\nimport java.util.Arrays;\n\n/**\n * CharsetRecognizer implemenation for Asian  - double or multi-byte - charsets.\n * Match is determined mostly by the input data adhering to the\n * encoding scheme for the charset, and, optionally,\n * frequency-of-occurence of characters.\n * <p/>\n * Instances of this class are singletons, one per encoding\n * being recognized.  They are created in the main\n * CharsetDetector class and kept in the global list of available\n * encodings to be checked.  The specific encoding being recognized\n * is determined by subclass.\n */\nabstract class CharsetRecog_mbcs extends CharsetRecognizer {\n\n    /**\n     * Get the IANA name of this charset.\n     *\n     * @return the charset name.\n     */\n    @Override\n    abstract String getName();\n\n\n    /**\n     * Test the match of this charset with the input text data\n     * which is obtained via the CharsetDetector object.\n     *\n     * @param det The CharsetDetector, which contains the input text\n     *            to be checked for being in this charset.\n     * @return Two values packed into one int  (Damn java, anyhow)\n     * <br/>\n     * bits 0-7:  the match confidence, ranging from 0-100\n     * <br/>\n     * bits 8-15: The match reason, an enum-like value.\n     */\n    int match(CharsetDetector det, int[] commonChars) {\n        @SuppressWarnings(\"unused\")\n        int singleByteCharCount = 0;  //TODO Do we really need this?\n        int doubleByteCharCount = 0;\n        int commonCharCount = 0;\n        int badCharCount = 0;\n        int totalCharCount = 0;\n        int confidence = 0;\n        iteratedChar iter = new iteratedChar();\n\n        detectBlock:\n        {\n            for (iter.reset(); nextChar(iter, det); ) {\n                totalCharCount++;\n                if (iter.error) {\n                    badCharCount++;\n                } else {\n                    long cv = iter.charValue & 0xFFFFFFFFL;\n\n                    if (cv <= 0xff) {\n                        singleByteCharCount++;\n                    } else {\n                        doubleByteCharCount++;\n                        if (commonChars != null) {\n                            // NOTE: This assumes that there are no 4-byte common chars.\n                            if (Arrays.binarySearch(commonChars, (int) cv) >= 0) {\n                                commonCharCount++;\n                            }\n                        }\n                    }\n                }\n                if (badCharCount >= 2 && badCharCount * 5 >= doubleByteCharCount) {\n                    // Bail out early if the byte data is not matching the encoding scheme.\n                    break detectBlock;\n                }\n            }\n\n            if (doubleByteCharCount <= 10 && badCharCount == 0) {\n                // Not many multi-byte chars.\n                if (doubleByteCharCount == 0 && totalCharCount < 10) {\n                    // There weren't any multibyte sequences, and there was a low density of non-ASCII single bytes.\n                    // We don't have enough data to have any confidence.\n                    // Statistical analysis of single byte non-ASCII charcters would probably help here.\n                    confidence = 0;\n                } else {\n                    //   ASCII or ISO file?  It's probably not our encoding,\n                    //   but is not incompatible with our encoding, so don't give it a zero.\n                    confidence = 10;\n                }\n\n                break detectBlock;\n            }\n\n            //\n            //  No match if there are too many characters that don't fit the encoding scheme.\n            //    (should we have zero tolerance for these?)\n            //\n            if (doubleByteCharCount < 20 * badCharCount) {\n                confidence = 0;\n                break detectBlock;\n            }\n\n            if (commonChars == null) {\n                // We have no statistics on frequently occuring characters.\n                //  Assess confidence purely on having a reasonable number of\n                //  multi-byte characters (the more the better\n                confidence = 30 + doubleByteCharCount - 20 * badCharCount;\n                if (confidence > 100) {\n                    confidence = 100;\n                }\n            } else {\n                //\n                // Frequency of occurence statistics exist.\n                //\n                double maxVal = Math.log((float) doubleByteCharCount / 4);\n                double scaleFactor = 90.0 / maxVal;\n                confidence = (int) (Math.log(commonCharCount + 1) * scaleFactor + 10);\n                confidence = Math.min(confidence, 100);\n            }\n        }   // end of detectBlock:\n\n        return confidence;\n    }\n\n    // \"Character\"  iterated character class.\n    //    Recognizers for specific mbcs encodings make their \"characters\" available\n    //    by providing a nextChar() function that fills in an instance of iteratedChar\n    //    with the next char from the input.\n    //    The returned characters are not converted to Unicode, but remain as the raw\n    //    bytes (concatenated into an int) from the codepage data.\n    //\n    //  For Asian charsets, use the raw input rather than the input that has been\n    //   stripped of markup.  Detection only considers multi-byte chars, effectively\n    //   stripping markup anyway, and double byte chars do occur in markup too.\n    //\n    static class iteratedChar {\n        int charValue = 0;             // 1-4 bytes from the raw input data\n        int nextIndex = 0;\n        boolean error = false;\n        boolean done = false;\n\n        void reset() {\n            charValue = 0;\n            nextIndex = 0;\n            error = false;\n            done = false;\n        }\n\n        int nextByte(CharsetDetector det) {\n            if (nextIndex >= det.fRawLength) {\n                done = true;\n                return -1;\n            }\n            return det.fRawInput[nextIndex++] & 0x00ff;\n        }\n    }\n\n    /**\n     * Get the next character (however many bytes it is) from the input data\n     * Subclasses for specific charset encodings must implement this function\n     * to get characters according to the rules of their encoding scheme.\n     * <p>\n     * This function is not a method of class iteratedChar only because\n     * that would require a lot of extra derived classes, which is awkward.\n     *\n     * @param it  The iteratedChar \"struct\" into which the returned char is placed.\n     * @param det The charset detector, which is needed to get at the input byte data\n     *            being iterated over.\n     * @return True if a character was returned, false at end of input.\n     */\n    abstract boolean nextChar(iteratedChar it, CharsetDetector det);\n\n\n    /**\n     * Shift-JIS charset recognizer.\n     */\n    static class CharsetRecog_sjis extends CharsetRecog_mbcs {\n        static int[] commonChars =\n                // TODO:  This set of data comes from the character frequency-\n                //        of-occurence analysis tool.  The data needs to be moved\n                //        into a resource and loaded from there.\n                {0x8140, 0x8141, 0x8142, 0x8145, 0x815b, 0x8169, 0x816a, 0x8175, 0x8176, 0x82a0,\n                        0x82a2, 0x82a4, 0x82a9, 0x82aa, 0x82ab, 0x82ad, 0x82af, 0x82b1, 0x82b3, 0x82b5,\n                        0x82b7, 0x82bd, 0x82be, 0x82c1, 0x82c4, 0x82c5, 0x82c6, 0x82c8, 0x82c9, 0x82cc,\n                        0x82cd, 0x82dc, 0x82e0, 0x82e7, 0x82e8, 0x82e9, 0x82ea, 0x82f0, 0x82f1, 0x8341,\n                        0x8343, 0x834e, 0x834f, 0x8358, 0x835e, 0x8362, 0x8367, 0x8375, 0x8376, 0x8389,\n                        0x838a, 0x838b, 0x838d, 0x8393, 0x8e96, 0x93fa, 0x95aa};\n\n        @Override\n        boolean nextChar(iteratedChar it, CharsetDetector det) {\n            it.error = false;\n            int firstByte;\n            firstByte = it.charValue = it.nextByte(det);\n            if (firstByte < 0) {\n                return false;\n            }\n\n            if (firstByte <= 0x7f || (firstByte > 0xa0 && firstByte <= 0xdf)) {\n                return true;\n            }\n\n            int secondByte = it.nextByte(det);\n            if (secondByte < 0) {\n                return false;\n            }\n            it.charValue = (firstByte << 8) | secondByte;\n            if (!((secondByte >= 0x40 && secondByte <= 0x7f) || (secondByte >= 0x80 && secondByte <= 0xff))) {\n                // Illegal second byte value.\n                it.error = true;\n            }\n            return true;\n        }\n\n        @Override\n        CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det, commonChars);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n\n        @Override\n        String getName() {\n            return \"Shift_JIS\";\n        }\n\n        @Override\n        public String getLanguage() {\n            return \"ja\";\n        }\n\n\n    }\n\n\n    /**\n     * Big5 charset recognizer.\n     */\n    static class CharsetRecog_big5 extends CharsetRecog_mbcs {\n        static int[] commonChars =\n                // TODO:  This set of data comes from the character frequency-\n                //        of-occurence analysis tool.  The data needs to be moved\n                //        into a resource and loaded from there.\n                {0xa140, 0xa141, 0xa142, 0xa143, 0xa147, 0xa149, 0xa175, 0xa176, 0xa440, 0xa446,\n                        0xa447, 0xa448, 0xa451, 0xa454, 0xa457, 0xa464, 0xa46a, 0xa46c, 0xa477, 0xa4a3,\n                        0xa4a4, 0xa4a7, 0xa4c1, 0xa4ce, 0xa4d1, 0xa4df, 0xa4e8, 0xa4fd, 0xa540, 0xa548,\n                        0xa558, 0xa569, 0xa5cd, 0xa5e7, 0xa657, 0xa661, 0xa662, 0xa668, 0xa670, 0xa6a8,\n                        0xa6b3, 0xa6b9, 0xa6d3, 0xa6db, 0xa6e6, 0xa6f2, 0xa740, 0xa751, 0xa759, 0xa7da,\n                        0xa8a3, 0xa8a5, 0xa8ad, 0xa8d1, 0xa8d3, 0xa8e4, 0xa8fc, 0xa9c0, 0xa9d2, 0xa9f3,\n                        0xaa6b, 0xaaba, 0xaabe, 0xaacc, 0xaafc, 0xac47, 0xac4f, 0xacb0, 0xacd2, 0xad59,\n                        0xaec9, 0xafe0, 0xb0ea, 0xb16f, 0xb2b3, 0xb2c4, 0xb36f, 0xb44c, 0xb44e, 0xb54c,\n                        0xb5a5, 0xb5bd, 0xb5d0, 0xb5d8, 0xb671, 0xb7ed, 0xb867, 0xb944, 0xbad8, 0xbb44,\n                        0xbba1, 0xbdd1, 0xc2c4, 0xc3b9, 0xc440, 0xc45f};\n\n        @Override\n        boolean nextChar(iteratedChar it, CharsetDetector det) {\n            it.error = false;\n            int firstByte;\n            firstByte = it.charValue = it.nextByte(det);\n            if (firstByte < 0) {\n                return false;\n            }\n\n            if (firstByte <= 0x7f || firstByte == 0xff) {\n                // single byte character.\n                return true;\n            }\n\n            int secondByte = it.nextByte(det);\n            if (secondByte < 0) {\n                return false;\n            }\n            it.charValue = (it.charValue << 8) | secondByte;\n\n            if (secondByte < 0x40 ||\n                    secondByte == 0x7f ||\n                    secondByte == 0xff) {\n                it.error = true;\n            }\n            return true;\n        }\n\n        @Override\n        CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det, commonChars);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n\n        @Override\n        String getName() {\n            return \"Big5\";\n        }\n\n\n        @Override\n        public String getLanguage() {\n            return \"zh\";\n        }\n    }\n\n\n    /**\n     * EUC charset recognizers.  One abstract class that provides the common function\n     * for getting the next character according to the EUC encoding scheme,\n     * and nested derived classes for EUC_KR, EUC_JP, EUC_CN.\n     */\n    abstract static class CharsetRecog_euc extends CharsetRecog_mbcs {\n\n        /*\n         *  (non-Javadoc)\n         *  Get the next character value for EUC based encodings.\n         *  Character \"value\" is simply the raw bytes that make up the character\n         *     packed into an int.\n         */\n        @Override\n        boolean nextChar(iteratedChar it, CharsetDetector det) {\n            it.error = false;\n            int firstByte;\n            int secondByte;\n            int thirdByte;\n            //int fourthByte = 0;\n\n            buildChar:\n            {\n                firstByte = it.charValue = it.nextByte(det);\n                if (firstByte < 0) {\n                    // Ran off the end of the input data\n                    it.done = true;\n                    break buildChar;\n                }\n                if (firstByte <= 0x8d) {\n                    // single byte char\n                    break buildChar;\n                }\n\n                secondByte = it.nextByte(det);\n                it.charValue = (it.charValue << 8) | secondByte;\n\n                if (firstByte >= 0xA1 && firstByte <= 0xfe) {\n                    // Two byte Char\n                    if (secondByte < 0xa1) {\n                        it.error = true;\n                    }\n                    break buildChar;\n                }\n                if (firstByte == 0x8e) {\n                    // Code Set 2.\n                    //   In EUC-JP, total char size is 2 bytes, only one byte of actual char value.\n                    //   In EUC-TW, total char size is 4 bytes, three bytes contribute to char value.\n                    // We don't know which we've got.\n                    // Treat it like EUC-JP.  If the data really was EUC-TW, the following two\n                    //   bytes will look like a well formed 2 byte char.\n                    if (secondByte < 0xa1) {\n                        it.error = true;\n                    }\n                    break buildChar;\n                }\n\n                if (firstByte == 0x8f) {\n                    // Code set 3.\n                    // Three byte total char size, two bytes of actual char value.\n                    thirdByte = it.nextByte(det);\n                    it.charValue = (it.charValue << 8) | thirdByte;\n                    if (thirdByte < 0xa1) {\n                        it.error = true;\n                    }\n                }\n            }\n\n            return (!it.done);\n        }\n\n        /**\n         * The charset recognize for EUC-JP.  A singleton instance of this class\n         * is created and kept by the public CharsetDetector class\n         */\n        static class CharsetRecog_euc_jp extends CharsetRecog_euc {\n            static int[] commonChars =\n                    // TODO:  This set of data comes from the character frequency-\n                    //        of-occurence analysis tool.  The data needs to be moved\n                    //        into a resource and loaded from there.\n                    {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a6, 0xa1bc, 0xa1ca, 0xa1cb, 0xa1d6, 0xa1d7, 0xa4a2,\n                            0xa4a4, 0xa4a6, 0xa4a8, 0xa4aa, 0xa4ab, 0xa4ac, 0xa4ad, 0xa4af, 0xa4b1, 0xa4b3,\n                            0xa4b5, 0xa4b7, 0xa4b9, 0xa4bb, 0xa4bd, 0xa4bf, 0xa4c0, 0xa4c1, 0xa4c3, 0xa4c4,\n                            0xa4c6, 0xa4c7, 0xa4c8, 0xa4c9, 0xa4ca, 0xa4cb, 0xa4ce, 0xa4cf, 0xa4d0, 0xa4de,\n                            0xa4df, 0xa4e1, 0xa4e2, 0xa4e4, 0xa4e8, 0xa4e9, 0xa4ea, 0xa4eb, 0xa4ec, 0xa4ef,\n                            0xa4f2, 0xa4f3, 0xa5a2, 0xa5a3, 0xa5a4, 0xa5a6, 0xa5a7, 0xa5aa, 0xa5ad, 0xa5af,\n                            0xa5b0, 0xa5b3, 0xa5b5, 0xa5b7, 0xa5b8, 0xa5b9, 0xa5bf, 0xa5c3, 0xa5c6, 0xa5c7,\n                            0xa5c8, 0xa5c9, 0xa5cb, 0xa5d0, 0xa5d5, 0xa5d6, 0xa5d7, 0xa5de, 0xa5e0, 0xa5e1,\n                            0xa5e5, 0xa5e9, 0xa5ea, 0xa5eb, 0xa5ec, 0xa5ed, 0xa5f3, 0xb8a9, 0xb9d4, 0xbaee,\n                            0xbbc8, 0xbef0, 0xbfb7, 0xc4ea, 0xc6fc, 0xc7bd, 0xcab8, 0xcaf3, 0xcbdc, 0xcdd1};\n\n            @Override\n            String getName() {\n                return \"EUC-JP\";\n            }\n\n            @Override\n            CharsetMatch match(CharsetDetector det) {\n                int confidence = match(det, commonChars);\n                return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n            }\n\n            @Override\n            public String getLanguage() {\n                return \"ja\";\n            }\n        }\n\n        /**\n         * The charset recognize for EUC-KR.  A singleton instance of this class\n         * is created and kept by the public CharsetDetector class\n         */\n        static class CharsetRecog_euc_kr extends CharsetRecog_euc {\n            static int[] commonChars =\n                    // TODO:  This set of data comes from the character frequency-\n                    //        of-occurence analysis tool.  The data needs to be moved\n                    //        into a resource and loaded from there.\n                    {0xb0a1, 0xb0b3, 0xb0c5, 0xb0cd, 0xb0d4, 0xb0e6, 0xb0ed, 0xb0f8, 0xb0fa, 0xb0fc,\n                            0xb1b8, 0xb1b9, 0xb1c7, 0xb1d7, 0xb1e2, 0xb3aa, 0xb3bb, 0xb4c2, 0xb4cf, 0xb4d9,\n                            0xb4eb, 0xb5a5, 0xb5b5, 0xb5bf, 0xb5c7, 0xb5e9, 0xb6f3, 0xb7af, 0xb7c2, 0xb7ce,\n                            0xb8a6, 0xb8ae, 0xb8b6, 0xb8b8, 0xb8bb, 0xb8e9, 0xb9ab, 0xb9ae, 0xb9cc, 0xb9ce,\n                            0xb9fd, 0xbab8, 0xbace, 0xbad0, 0xbaf1, 0xbbe7, 0xbbf3, 0xbbfd, 0xbcad, 0xbcba,\n                            0xbcd2, 0xbcf6, 0xbdba, 0xbdc0, 0xbdc3, 0xbdc5, 0xbec6, 0xbec8, 0xbedf, 0xbeee,\n                            0xbef8, 0xbefa, 0xbfa1, 0xbfa9, 0xbfc0, 0xbfe4, 0xbfeb, 0xbfec, 0xbff8, 0xc0a7,\n                            0xc0af, 0xc0b8, 0xc0ba, 0xc0bb, 0xc0bd, 0xc0c7, 0xc0cc, 0xc0ce, 0xc0cf, 0xc0d6,\n                            0xc0da, 0xc0e5, 0xc0fb, 0xc0fc, 0xc1a4, 0xc1a6, 0xc1b6, 0xc1d6, 0xc1df, 0xc1f6,\n                            0xc1f8, 0xc4a1, 0xc5cd, 0xc6ae, 0xc7cf, 0xc7d1, 0xc7d2, 0xc7d8, 0xc7e5, 0xc8ad};\n\n            @Override\n            String getName() {\n                return \"EUC-KR\";\n            }\n\n            @Override\n            CharsetMatch match(CharsetDetector det) {\n                int confidence = match(det, commonChars);\n                return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n            }\n\n            @Override\n            public String getLanguage() {\n                return \"ko\";\n            }\n        }\n    }\n\n    /**\n     * GB-18030 recognizer. Uses simplified Chinese statistics.\n     */\n    static class CharsetRecog_gb_18030 extends CharsetRecog_mbcs {\n\n        /*\n         *  (non-Javadoc)\n         *  Get the next character value for EUC based encodings.\n         *  Character \"value\" is simply the raw bytes that make up the character\n         *     packed into an int.\n         */\n        @Override\n        boolean nextChar(iteratedChar it, CharsetDetector det) {\n            it.error = false;\n            int firstByte;\n            int secondByte;\n            int thirdByte;\n            int fourthByte;\n\n            buildChar:\n            {\n                firstByte = it.charValue = it.nextByte(det);\n\n                if (firstByte < 0) {\n                    // Ran off the end of the input data\n                    it.done = true;\n                    break buildChar;\n                }\n\n                if (firstByte <= 0x80) {\n                    // single byte char\n                    break buildChar;\n                }\n\n                secondByte = it.nextByte(det);\n                it.charValue = (it.charValue << 8) | secondByte;\n\n                if (firstByte >= 0x81 && firstByte <= 0xFE) {\n                    // Two byte Char\n                    if ((secondByte >= 0x40 && secondByte <= 0x7E) || (secondByte >= 80 && secondByte <= 0xFE)) {\n                        break buildChar;\n                    }\n\n                    // Four byte char\n                    if (secondByte >= 0x30 && secondByte <= 0x39) {\n                        thirdByte = it.nextByte(det);\n\n                        if (thirdByte >= 0x81 && thirdByte <= 0xFE) {\n                            fourthByte = it.nextByte(det);\n\n                            if (fourthByte >= 0x30 && fourthByte <= 0x39) {\n                                it.charValue = (it.charValue << 16) | (thirdByte << 8) | fourthByte;\n                                break buildChar;\n                            }\n                        }\n                    }\n\n                    it.error = true;\n                }\n            }\n\n            return (!it.done);\n        }\n\n        static int[] commonChars =\n                // TODO:  This set of data comes from the character frequency-\n                //        of-occurence analysis tool.  The data needs to be moved\n                //        into a resource and loaded from there.\n                {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a4, 0xa1b0, 0xa1b1, 0xa1f1, 0xa1f3, 0xa3a1, 0xa3ac,\n                        0xa3ba, 0xb1a8, 0xb1b8, 0xb1be, 0xb2bb, 0xb3c9, 0xb3f6, 0xb4f3, 0xb5bd, 0xb5c4,\n                        0xb5e3, 0xb6af, 0xb6d4, 0xb6e0, 0xb7a2, 0xb7a8, 0xb7bd, 0xb7d6, 0xb7dd, 0xb8b4,\n                        0xb8df, 0xb8f6, 0xb9ab, 0xb9c9, 0xb9d8, 0xb9fa, 0xb9fd, 0xbacd, 0xbba7, 0xbbd6,\n                        0xbbe1, 0xbbfa, 0xbcbc, 0xbcdb, 0xbcfe, 0xbdcc, 0xbecd, 0xbedd, 0xbfb4, 0xbfc6,\n                        0xbfc9, 0xc0b4, 0xc0ed, 0xc1cb, 0xc2db, 0xc3c7, 0xc4dc, 0xc4ea, 0xc5cc, 0xc6f7,\n                        0xc7f8, 0xc8ab, 0xc8cb, 0xc8d5, 0xc8e7, 0xc9cf, 0xc9fa, 0xcab1, 0xcab5, 0xcac7,\n                        0xcad0, 0xcad6, 0xcaf5, 0xcafd, 0xccec, 0xcdf8, 0xceaa, 0xcec4, 0xced2, 0xcee5,\n                        0xcfb5, 0xcfc2, 0xcfd6, 0xd0c2, 0xd0c5, 0xd0d0, 0xd0d4, 0xd1a7, 0xd2aa, 0xd2b2,\n                        0xd2b5, 0xd2bb, 0xd2d4, 0xd3c3, 0xd3d0, 0xd3fd, 0xd4c2, 0xd4da, 0xd5e2, 0xd6d0};\n\n\n        @Override\n        String getName() {\n            return \"GB18030\";\n        }\n\n        @Override\n        CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det, commonChars);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n\n        @Override\n        public String getLanguage() {\n            return \"zh\";\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/lib/icu4j/CharsetRecog_sbcs.java",
    "content": "// © 2016 and later: Unicode, Inc. and others.\n// License & terms of use: http://www.unicode.org/copyright.html\n/*\n ****************************************************************************\n * Copyright (C) 2005-2013, International Business Machines Corporation and *\n * others. All Rights Reserved.                                             *\n ************************************************************************** *\n *\n */\n\npackage io.legado.app.lib.icu4j;\n\n/**\n * This class recognizes single-byte encodings. Because the encoding scheme is so\n * simple, language statistics are used to do the matching.\n */\nabstract class CharsetRecog_sbcs extends CharsetRecognizer {\n\n    /* (non-Javadoc)\n     * @see com.ibm.icu.text.CharsetRecognizer#getName()\n     */\n    @Override\n    abstract String getName();\n\n    static class NGramParser {\n        //        private static final int N_GRAM_SIZE = 3;\n        private static final int N_GRAM_MASK = 0xFFFFFF;\n\n        protected int byteIndex = 0;\n        private int ngram = 0;\n\n        private final int[] ngramList;\n        protected byte[] byteMap;\n\n        private int ngramCount;\n        private int hitCount;\n\n        protected byte spaceChar;\n\n        public NGramParser(int[] theNgramList, byte[] theByteMap) {\n            ngramList = theNgramList;\n            byteMap = theByteMap;\n\n            ngram = 0;\n\n            ngramCount = hitCount = 0;\n        }\n\n        /*\n         * Binary search for value in table, which must have exactly 64 entries.\n         */\n        private static int search(int[] table, int value) {\n            int index = 0;\n\n            if (table[index + 32] <= value) {\n                index += 32;\n            }\n\n            if (table[index + 16] <= value) {\n                index += 16;\n            }\n\n            if (table[index + 8] <= value) {\n                index += 8;\n            }\n\n            if (table[index + 4] <= value) {\n                index += 4;\n            }\n\n            if (table[index + 2] <= value) {\n                index += 2;\n            }\n\n            if (table[index + 1] <= value) {\n                index += 1;\n            }\n\n            if (table[index] > value) {\n                index -= 1;\n            }\n\n            if (index < 0 || table[index] != value) {\n                return -1;\n            }\n\n            return index;\n        }\n\n        private void lookup(int thisNgram) {\n            ngramCount += 1;\n\n            if (search(ngramList, thisNgram) >= 0) {\n                hitCount += 1;\n            }\n\n        }\n\n        protected void addByte(int b) {\n            ngram = ((ngram << 8) + (b & 0xFF)) & N_GRAM_MASK;\n            lookup(ngram);\n        }\n\n        private int nextByte(CharsetDetector det) {\n            if (byteIndex >= det.fInputLen) {\n                return -1;\n            }\n\n            return det.fInputBytes[byteIndex++] & 0xFF;\n        }\n\n        protected void parseCharacters(CharsetDetector det) {\n            int b;\n            boolean ignoreSpace = false;\n\n            while ((b = nextByte(det)) >= 0) {\n                byte mb = byteMap[b];\n\n                // TODO: 0x20 might not be a space in all character sets...\n                if (mb != 0) {\n                    if (!(mb == spaceChar && ignoreSpace)) {\n                        addByte(mb);\n                    }\n\n                    ignoreSpace = (mb == spaceChar);\n                }\n            }\n\n        }\n\n        public int parse(CharsetDetector det) {\n            return parse(det, (byte) 0x20);\n        }\n\n        public int parse(CharsetDetector det, byte spaceCh) {\n\n            this.spaceChar = spaceCh;\n\n            parseCharacters(det);\n\n            // TODO: Is this OK? The buffer could have ended in the middle of a word...\n            addByte(spaceChar);\n\n            double rawPercent = (double) hitCount / (double) ngramCount;\n\n//                if (rawPercent <= 2.0) {\n//                    return 0;\n//                }\n\n            // TODO - This is a bit of a hack to take care of a case\n            // were we were getting a confidence of 135...\n            if (rawPercent > 0.33) {\n                return 98;\n            }\n\n            return (int) (rawPercent * 300.0);\n        }\n    }\n\n    static class NGramParser_IBM420 extends NGramParser {\n        private byte alef = 0x00;\n\n        protected static byte[] unshapeMap = {\n/*                 -0           -1           -2           -3           -4           -5           -6           -7           -8           -9           -A           -B           -C           -D           -E           -F   */\n/* 0- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 1- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 2- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 3- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 4- */    (byte) 0x40, (byte) 0x40, (byte) 0x42, (byte) 0x42, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x47, (byte) 0x49, (byte) 0x4A, (byte) 0x4B, (byte) 0x4C, (byte) 0x4D, (byte) 0x4E, (byte) 0x4F,\n/* 5- */    (byte) 0x50, (byte) 0x49, (byte) 0x52, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x56, (byte) 0x58, (byte) 0x58, (byte) 0x5A, (byte) 0x5B, (byte) 0x5C, (byte) 0x5D, (byte) 0x5E, (byte) 0x5F,\n/* 6- */    (byte) 0x60, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x63, (byte) 0x65, (byte) 0x65, (byte) 0x67, (byte) 0x67, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n/* 7- */    (byte) 0x69, (byte) 0x71, (byte) 0x71, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x77, (byte) 0x79, (byte) 0x7A, (byte) 0x7B, (byte) 0x7C, (byte) 0x7D, (byte) 0x7E, (byte) 0x7F,\n/* 8- */    (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x80, (byte) 0x8B, (byte) 0x8B, (byte) 0x8D, (byte) 0x8D, (byte) 0x8F,\n/* 9- */    (byte) 0x90, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0x9A, (byte) 0x9A, (byte) 0x9A, (byte) 0x9E, (byte) 0x9E,\n/* A- */    (byte) 0x9E, (byte) 0xA1, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x9E, (byte) 0xAB, (byte) 0xAB, (byte) 0xAD, (byte) 0xAD, (byte) 0xAF,\n/* B- */    (byte) 0xAF, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xB1, (byte) 0xBB, (byte) 0xBB, (byte) 0xBD, (byte) 0xBD, (byte) 0xBF,\n/* C- */    (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xBF, (byte) 0xCC, (byte) 0xBF, (byte) 0xCE, (byte) 0xCF,\n/* D- */    (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDA, (byte) 0xDC, (byte) 0xDC, (byte) 0xDC, (byte) 0xDF,\n/* E- */    (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n/* F- */    (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF,\n        };\n\n\n        public NGramParser_IBM420(int[] theNgramList, byte[] theByteMap) {\n            super(theNgramList, theByteMap);\n        }\n\n        private byte isLamAlef(byte b) {\n            if (b == (byte) 0xb2 || b == (byte) 0xb3) {\n                return (byte) 0x47;\n            } else if (b == (byte) 0xb4 || b == (byte) 0xb5) {\n                return (byte) 0x49;\n            } else if (b == (byte) 0xb8 || b == (byte) 0xb9) {\n                return (byte) 0x56;\n            } else\n                return (byte) 0x00;\n        }\n\n        /*\n         * Arabic shaping needs to be done manually. Cannot call ArabicShaping class\n         * because CharsetDetector is dealing with bytes not Unicode code points. We could\n         * convert the bytes to Unicode code points but that would leave us dependent\n         * on CharsetICU which we try to avoid. IBM420 converter amongst different versions\n         * of JDK can produce different results and therefore is also avoided.\n         */\n        private int nextByte(CharsetDetector det) {\n            if (byteIndex >= det.fInputLen || det.fInputBytes[byteIndex] == 0) {\n                return -1;\n            }\n            int next;\n\n            alef = isLamAlef(det.fInputBytes[byteIndex]);\n            if (alef != (byte) 0x00)\n                next = 0xB1 & 0xFF;\n            else\n                next = unshapeMap[det.fInputBytes[byteIndex] & 0xFF] & 0xFF;\n\n            byteIndex++;\n\n            return next;\n        }\n\n        @Override\n        protected void parseCharacters(CharsetDetector det) {\n            int b;\n            boolean ignoreSpace = false;\n\n            while ((b = nextByte(det)) >= 0) {\n                byte mb = byteMap[b];\n\n                // TODO: 0x20 might not be a space in all character sets...\n                if (mb != 0) {\n                    if (!(mb == spaceChar && ignoreSpace)) {\n                        addByte(mb);\n                    }\n\n                    ignoreSpace = (mb == spaceChar);\n                }\n                if (alef != (byte) 0x00) {\n                    mb = byteMap[alef & 0xFF];\n\n                    // TODO: 0x20 might not be a space in all character sets...\n                    if (mb != 0) {\n                        if (!(mb == spaceChar && ignoreSpace)) {\n                            addByte(mb);\n                        }\n\n                        ignoreSpace = (mb == spaceChar);\n                    }\n\n                }\n            }\n        }\n    }\n\n\n    int match(CharsetDetector det, int[] ngrams, byte[] byteMap) {\n        return match(det, ngrams, byteMap, (byte) 0x20);\n    }\n\n    int match(CharsetDetector det, int[] ngrams, byte[] byteMap, byte spaceChar) {\n        NGramParser parser = new NGramParser(ngrams, byteMap);\n        return parser.parse(det, spaceChar);\n    }\n\n    @SuppressWarnings(\"SameParameterValue\")\n    int matchIBM420(CharsetDetector det, int[] ngrams, byte[] byteMap, byte spaceChar) {\n        NGramParser_IBM420 parser = new NGramParser_IBM420(ngrams, byteMap);\n        return parser.parse(det, spaceChar);\n    }\n\n    static class NGramsPlusLang {\n        int[] fNGrams;\n        String fLang;\n\n        NGramsPlusLang(String la, int[] ng) {\n            fLang = la;\n            fNGrams = ng;\n        }\n    }\n\n    static class CharsetRecog_8859_1 extends CharsetRecog_sbcs {\n        protected static byte[] byteMap = {\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xDF,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF,\n        };\n\n\n        private static final NGramsPlusLang[] ngrams_8859_1 = new NGramsPlusLang[]{\n                new NGramsPlusLang(\n                        \"da\",\n                        new int[]{\n                                0x206166, 0x206174, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207369, 0x207374, 0x207469, 0x207669, 0x616620,\n                                0x616E20, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646572, 0x646574, 0x652073, 0x656420, 0x656465, 0x656E20, 0x656E64, 0x657220, 0x657265, 0x657320,\n                                0x657420, 0x666F72, 0x676520, 0x67656E, 0x676572, 0x696765, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6572, 0x6C6967, 0x6C6C65, 0x6D6564, 0x6E6465, 0x6E6520,\n                                0x6E6720, 0x6E6765, 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722064, 0x722065, 0x722073, 0x726520, 0x737465, 0x742073, 0x746520, 0x746572, 0x74696C, 0x766572,\n                        }),\n                new NGramsPlusLang(\n                        \"de\",\n                        new int[]{\n                                0x20616E, 0x206175, 0x206265, 0x206461, 0x206465, 0x206469, 0x206569, 0x206765, 0x206861, 0x20696E, 0x206D69, 0x207363, 0x207365, 0x20756E, 0x207665, 0x20766F,\n                                0x207765, 0x207A75, 0x626572, 0x636820, 0x636865, 0x636874, 0x646173, 0x64656E, 0x646572, 0x646965, 0x652064, 0x652073, 0x65696E, 0x656974, 0x656E20, 0x657220,\n                                0x657320, 0x67656E, 0x68656E, 0x687420, 0x696368, 0x696520, 0x696E20, 0x696E65, 0x697420, 0x6C6963, 0x6C6C65, 0x6E2061, 0x6E2064, 0x6E2073, 0x6E6420, 0x6E6465,\n                                0x6E6520, 0x6E6720, 0x6E6765, 0x6E7465, 0x722064, 0x726465, 0x726569, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x756E64, 0x756E67, 0x766572,\n                        }),\n                new NGramsPlusLang(\n                        \"en\",\n                        new int[]{\n                                0x206120, 0x20616E, 0x206265, 0x20636F, 0x20666F, 0x206861, 0x206865, 0x20696E, 0x206D61, 0x206F66, 0x207072, 0x207265, 0x207361, 0x207374, 0x207468, 0x20746F,\n                                0x207768, 0x616964, 0x616C20, 0x616E20, 0x616E64, 0x617320, 0x617420, 0x617465, 0x617469, 0x642061, 0x642074, 0x652061, 0x652073, 0x652074, 0x656420, 0x656E74,\n                                0x657220, 0x657320, 0x666F72, 0x686174, 0x686520, 0x686572, 0x696420, 0x696E20, 0x696E67, 0x696F6E, 0x697320, 0x6E2061, 0x6E2074, 0x6E6420, 0x6E6720, 0x6E7420,\n                                0x6F6620, 0x6F6E20, 0x6F7220, 0x726520, 0x727320, 0x732061, 0x732074, 0x736169, 0x737420, 0x742074, 0x746572, 0x746861, 0x746865, 0x74696F, 0x746F20, 0x747320,\n                        }),\n\n                new NGramsPlusLang(\n                        \"es\",\n                        new int[]{\n                                0x206120, 0x206361, 0x20636F, 0x206465, 0x20656C, 0x20656E, 0x206573, 0x20696E, 0x206C61, 0x206C6F, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365,\n                                0x20756E, 0x207920, 0x612063, 0x612064, 0x612065, 0x61206C, 0x612070, 0x616369, 0x61646F, 0x616C20, 0x617220, 0x617320, 0x6369F3, 0x636F6E, 0x646520, 0x64656C,\n                                0x646F20, 0x652064, 0x652065, 0x65206C, 0x656C20, 0x656E20, 0x656E74, 0x657320, 0x657374, 0x69656E, 0x69F36E, 0x6C6120, 0x6C6F73, 0x6E2065, 0x6E7465, 0x6F2064,\n                                0x6F2065, 0x6F6E20, 0x6F7220, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732064, 0x732065, 0x732070, 0x736520, 0x746520, 0x746F20, 0x756520, 0xF36E20,\n                        }),\n\n                new NGramsPlusLang(\n                        \"fr\",\n                        new int[]{\n                                0x206175, 0x20636F, 0x206461, 0x206465, 0x206475, 0x20656E, 0x206574, 0x206C61, 0x206C65, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207365, 0x20736F, 0x20756E,\n                                0x20E020, 0x616E74, 0x617469, 0x636520, 0x636F6E, 0x646520, 0x646573, 0x647520, 0x652061, 0x652063, 0x652064, 0x652065, 0x65206C, 0x652070, 0x652073, 0x656E20,\n                                0x656E74, 0x657220, 0x657320, 0x657420, 0x657572, 0x696F6E, 0x697320, 0x697420, 0x6C6120, 0x6C6520, 0x6C6573, 0x6D656E, 0x6E2064, 0x6E6520, 0x6E7320, 0x6E7420,\n                                0x6F6E20, 0x6F6E74, 0x6F7572, 0x717565, 0x72206C, 0x726520, 0x732061, 0x732064, 0x732065, 0x73206C, 0x732070, 0x742064, 0x746520, 0x74696F, 0x756520, 0x757220,\n                        }),\n\n                new NGramsPlusLang(\n                        \"it\",\n                        new int[]{\n                                0x20616C, 0x206368, 0x20636F, 0x206465, 0x206469, 0x206520, 0x20696C, 0x20696E, 0x206C61, 0x207065, 0x207072, 0x20756E, 0x612063, 0x612064, 0x612070, 0x612073,\n                                0x61746F, 0x636865, 0x636F6E, 0x64656C, 0x646920, 0x652061, 0x652063, 0x652064, 0x652069, 0x65206C, 0x652070, 0x652073, 0x656C20, 0x656C6C, 0x656E74, 0x657220,\n                                0x686520, 0x692061, 0x692063, 0x692064, 0x692073, 0x696120, 0x696C20, 0x696E20, 0x696F6E, 0x6C6120, 0x6C6520, 0x6C6920, 0x6C6C61, 0x6E6520, 0x6E6920, 0x6E6F20,\n                                0x6E7465, 0x6F2061, 0x6F2064, 0x6F2069, 0x6F2073, 0x6F6E20, 0x6F6E65, 0x706572, 0x726120, 0x726520, 0x736920, 0x746120, 0x746520, 0x746920, 0x746F20, 0x7A696F,\n                        }),\n\n                new NGramsPlusLang(\n                        \"nl\",\n                        new int[]{\n                                0x20616C, 0x206265, 0x206461, 0x206465, 0x206469, 0x206565, 0x20656E, 0x206765, 0x206865, 0x20696E, 0x206D61, 0x206D65, 0x206F70, 0x207465, 0x207661, 0x207665,\n                                0x20766F, 0x207765, 0x207A69, 0x61616E, 0x616172, 0x616E20, 0x616E64, 0x617220, 0x617420, 0x636874, 0x646520, 0x64656E, 0x646572, 0x652062, 0x652076, 0x65656E,\n                                0x656572, 0x656E20, 0x657220, 0x657273, 0x657420, 0x67656E, 0x686574, 0x696520, 0x696E20, 0x696E67, 0x697320, 0x6E2062, 0x6E2064, 0x6E2065, 0x6E2068, 0x6E206F,\n                                0x6E2076, 0x6E6465, 0x6E6720, 0x6F6E64, 0x6F6F72, 0x6F7020, 0x6F7220, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x76616E, 0x766572, 0x766F6F,\n                        }),\n\n                new NGramsPlusLang(\n                        \"no\",\n                        new int[]{\n                                0x206174, 0x206176, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207365, 0x20736B, 0x20736F, 0x207374, 0x207469,\n                                0x207669, 0x20E520, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646574, 0x652073, 0x656420, 0x656E20, 0x656E65, 0x657220, 0x657265, 0x657420, 0x657474,\n                                0x666F72, 0x67656E, 0x696B6B, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6520, 0x6C6C65, 0x6D6564, 0x6D656E, 0x6E2073, 0x6E6520, 0x6E6720, 0x6E6765, 0x6E6E65,\n                                0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722073, 0x726520, 0x736F6D, 0x737465, 0x742073, 0x746520, 0x74656E, 0x746572, 0x74696C, 0x747420, 0x747465, 0x766572,\n                        }),\n\n                new NGramsPlusLang(\n                        \"pt\",\n                        new int[]{\n                                0x206120, 0x20636F, 0x206461, 0x206465, 0x20646F, 0x206520, 0x206573, 0x206D61, 0x206E6F, 0x206F20, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365,\n                                0x20756D, 0x612061, 0x612063, 0x612064, 0x612070, 0x616465, 0x61646F, 0x616C20, 0x617220, 0x617261, 0x617320, 0x636F6D, 0x636F6E, 0x646120, 0x646520, 0x646F20,\n                                0x646F73, 0x652061, 0x652064, 0x656D20, 0x656E74, 0x657320, 0x657374, 0x696120, 0x696361, 0x6D656E, 0x6E7465, 0x6E746F, 0x6F2061, 0x6F2063, 0x6F2064, 0x6F2065,\n                                0x6F2070, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732061, 0x732064, 0x732065, 0x732070, 0x737461, 0x746520, 0x746F20, 0x756520, 0xE36F20, 0xE7E36F,\n\n                        }),\n\n                new NGramsPlusLang(\n                        \"sv\",\n                        new int[]{\n                                0x206174, 0x206176, 0x206465, 0x20656E, 0x2066F6, 0x206861, 0x206920, 0x20696E, 0x206B6F, 0x206D65, 0x206F63, 0x2070E5, 0x20736B, 0x20736F, 0x207374, 0x207469,\n                                0x207661, 0x207669, 0x20E472, 0x616465, 0x616E20, 0x616E64, 0x617220, 0x617474, 0x636820, 0x646520, 0x64656E, 0x646572, 0x646574, 0x656420, 0x656E20, 0x657220,\n                                0x657420, 0x66F672, 0x67656E, 0x696C6C, 0x696E67, 0x6B6120, 0x6C6C20, 0x6D6564, 0x6E2073, 0x6E6120, 0x6E6465, 0x6E6720, 0x6E6765, 0x6E696E, 0x6F6368, 0x6F6D20,\n                                0x6F6E20, 0x70E520, 0x722061, 0x722073, 0x726120, 0x736B61, 0x736F6D, 0x742073, 0x746120, 0x746520, 0x746572, 0x74696C, 0x747420, 0x766172, 0xE47220, 0xF67220,\n                        }),\n\n        };\n\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            String name = det.fC1Bytes ? \"windows-1252\" : \"ISO-8859-1\";\n            int bestConfidenceSoFar = -1;\n            String lang = null;\n            for (NGramsPlusLang ngl : ngrams_8859_1) {\n                int confidence = match(det, ngl.fNGrams, byteMap);\n                if (confidence > bestConfidenceSoFar) {\n                    bestConfidenceSoFar = confidence;\n                    lang = ngl.fLang;\n                }\n            }\n            return bestConfidenceSoFar <= 0 ? null : new CharsetMatch(det, this, bestConfidenceSoFar, name, lang);\n        }\n\n\n        @Override\n        public String getName() {\n            return \"ISO-8859-1\";\n        }\n    }\n\n\n    static class CharsetRecog_8859_2 extends CharsetRecog_sbcs {\n        protected static byte[] byteMap = {\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0xB1, (byte) 0x20, (byte) 0xB3, (byte) 0x20, (byte) 0xB5, (byte) 0xB6, (byte) 0x20,\n                (byte) 0x20, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0x20, (byte) 0xBE, (byte) 0xBF,\n                (byte) 0x20, (byte) 0xB1, (byte) 0x20, (byte) 0xB3, (byte) 0x20, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7,\n                (byte) 0x20, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0x20, (byte) 0xBE, (byte) 0xBF,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xDF,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x20,\n        };\n\n        private static final NGramsPlusLang[] ngrams_8859_2 = new NGramsPlusLang[]{\n                new NGramsPlusLang(\n                        \"cs\",\n                        new int[]{\n                                0x206120, 0x206279, 0x20646F, 0x206A65, 0x206E61, 0x206E65, 0x206F20, 0x206F64, 0x20706F, 0x207072, 0x2070F8, 0x20726F, 0x207365, 0x20736F, 0x207374, 0x20746F,\n                                0x207620, 0x207679, 0x207A61, 0x612070, 0x636520, 0x636820, 0x652070, 0x652073, 0x652076, 0x656D20, 0x656EED, 0x686F20, 0x686F64, 0x697374, 0x6A6520, 0x6B7465,\n                                0x6C6520, 0x6C6920, 0x6E6120, 0x6EE920, 0x6EEC20, 0x6EED20, 0x6F2070, 0x6F646E, 0x6F6A69, 0x6F7374, 0x6F7520, 0x6F7661, 0x706F64, 0x706F6A, 0x70726F, 0x70F865,\n                                0x736520, 0x736F75, 0x737461, 0x737469, 0x73746E, 0x746572, 0x746EED, 0x746F20, 0x752070, 0xBE6520, 0xE16EED, 0xE9686F, 0xED2070, 0xED2073, 0xED6D20, 0xF86564,\n                        }),\n                new NGramsPlusLang(\n                        \"hu\",\n                        new int[]{\n                                0x206120, 0x20617A, 0x206265, 0x206567, 0x20656C, 0x206665, 0x206861, 0x20686F, 0x206973, 0x206B65, 0x206B69, 0x206BF6, 0x206C65, 0x206D61, 0x206D65, 0x206D69,\n                                0x206E65, 0x20737A, 0x207465, 0x20E973, 0x612061, 0x61206B, 0x61206D, 0x612073, 0x616B20, 0x616E20, 0x617A20, 0x62616E, 0x62656E, 0x656779, 0x656B20, 0x656C20,\n                                0x656C65, 0x656D20, 0x656E20, 0x657265, 0x657420, 0x657465, 0x657474, 0x677920, 0x686F67, 0x696E74, 0x697320, 0x6B2061, 0x6BF67A, 0x6D6567, 0x6D696E, 0x6E2061,\n                                0x6E616B, 0x6E656B, 0x6E656D, 0x6E7420, 0x6F6779, 0x732061, 0x737A65, 0x737A74, 0x737AE1, 0x73E967, 0x742061, 0x747420, 0x74E173, 0x7A6572, 0xE16E20, 0xE97320,\n                        }),\n                new NGramsPlusLang(\n                        \"pl\",\n                        new int[]{\n                                0x20637A, 0x20646F, 0x206920, 0x206A65, 0x206B6F, 0x206D61, 0x206D69, 0x206E61, 0x206E69, 0x206F64, 0x20706F, 0x207072, 0x207369, 0x207720, 0x207769, 0x207779,\n                                0x207A20, 0x207A61, 0x612070, 0x612077, 0x616E69, 0x636820, 0x637A65, 0x637A79, 0x646F20, 0x647A69, 0x652070, 0x652073, 0x652077, 0x65207A, 0x65676F, 0x656A20,\n                                0x656D20, 0x656E69, 0x676F20, 0x696120, 0x696520, 0x69656A, 0x6B6120, 0x6B6920, 0x6B6965, 0x6D6965, 0x6E6120, 0x6E6961, 0x6E6965, 0x6F2070, 0x6F7761, 0x6F7769,\n                                0x706F6C, 0x707261, 0x70726F, 0x70727A, 0x727A65, 0x727A79, 0x7369EA, 0x736B69, 0x737461, 0x776965, 0x796368, 0x796D20, 0x7A6520, 0x7A6965, 0x7A7920, 0xF37720,\n                        }),\n                new NGramsPlusLang(\n                        \"ro\",\n                        new int[]{\n                                0x206120, 0x206163, 0x206361, 0x206365, 0x20636F, 0x206375, 0x206465, 0x206469, 0x206C61, 0x206D61, 0x207065, 0x207072, 0x207365, 0x2073E3, 0x20756E, 0x20BA69,\n                                0x20EE6E, 0x612063, 0x612064, 0x617265, 0x617420, 0x617465, 0x617520, 0x636172, 0x636F6E, 0x637520, 0x63E320, 0x646520, 0x652061, 0x652063, 0x652064, 0x652070,\n                                0x652073, 0x656120, 0x656920, 0x656C65, 0x656E74, 0x657374, 0x692061, 0x692063, 0x692064, 0x692070, 0x696520, 0x696920, 0x696E20, 0x6C6120, 0x6C6520, 0x6C6F72,\n                                0x6C7569, 0x6E6520, 0x6E7472, 0x6F7220, 0x70656E, 0x726520, 0x726561, 0x727520, 0x73E320, 0x746520, 0x747275, 0x74E320, 0x756920, 0x756C20, 0xBA6920, 0xEE6E20,\n                        })\n        };\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            String name = det.fC1Bytes ? \"windows-1250\" : \"ISO-8859-2\";\n            int bestConfidenceSoFar = -1;\n            String lang = null;\n            for (NGramsPlusLang ngl : ngrams_8859_2) {\n                int confidence = match(det, ngl.fNGrams, byteMap);\n                if (confidence > bestConfidenceSoFar) {\n                    bestConfidenceSoFar = confidence;\n                    lang = ngl.fLang;\n                }\n            }\n            return bestConfidenceSoFar <= 0 ? null : new CharsetMatch(det, this, bestConfidenceSoFar, name, lang);\n        }\n\n        @Override\n        public String getName() {\n            return \"ISO-8859-2\";\n        }\n\n    }\n\n\n    abstract static class CharsetRecog_8859_5 extends CharsetRecog_sbcs {\n        protected static byte[] byteMap = {\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0xFE, (byte) 0xFF,\n                (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7,\n                (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7,\n                (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0x20, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0xFE, (byte) 0xFF,\n        };\n\n        @Override\n        public String getName() {\n            return \"ISO-8859-5\";\n        }\n    }\n\n    static class CharsetRecog_8859_5_ru extends CharsetRecog_8859_5 {\n        private static final int[] ngrams = {\n                0x20D220, 0x20D2DE, 0x20D4DE, 0x20D7D0, 0x20D820, 0x20DAD0, 0x20DADE, 0x20DDD0, 0x20DDD5, 0x20DED1, 0x20DFDE, 0x20DFE0, 0x20E0D0, 0x20E1DE, 0x20E1E2, 0x20E2DE,\n                0x20E7E2, 0x20EDE2, 0xD0DDD8, 0xD0E2EC, 0xD3DE20, 0xD5DBEC, 0xD5DDD8, 0xD5E1E2, 0xD5E220, 0xD820DF, 0xD8D520, 0xD8D820, 0xD8EF20, 0xDBD5DD, 0xDBD820, 0xDBECDD,\n                0xDDD020, 0xDDD520, 0xDDD8D5, 0xDDD8EF, 0xDDDE20, 0xDDDED2, 0xDE20D2, 0xDE20DF, 0xDE20E1, 0xDED220, 0xDED2D0, 0xDED3DE, 0xDED920, 0xDEDBEC, 0xDEDC20, 0xDEE1E2,\n                0xDFDEDB, 0xDFE0D5, 0xDFE0D8, 0xDFE0DE, 0xE0D0D2, 0xE0D5D4, 0xE1E2D0, 0xE1E2D2, 0xE1E2D8, 0xE1EF20, 0xE2D5DB, 0xE2DE20, 0xE2DEE0, 0xE2EC20, 0xE7E2DE, 0xEBE520,\n        };\n\n        @Override\n        public String getLanguage() {\n            return \"ru\";\n        }\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det, ngrams, byteMap);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n    }\n\n    abstract static class CharsetRecog_8859_6 extends CharsetRecog_sbcs {\n        protected static byte[] byteMap = {\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7,\n                (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF,\n                (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7,\n                (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n        };\n\n        @Override\n        public String getName() {\n            return \"ISO-8859-6\";\n        }\n    }\n\n    static class CharsetRecog_8859_6_ar extends CharsetRecog_8859_6 {\n        private static final int[] ngrams = {\n                0x20C7E4, 0x20C7E6, 0x20C8C7, 0x20D9E4, 0x20E1EA, 0x20E4E4, 0x20E5E6, 0x20E8C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E420, 0xC7E4C3, 0xC7E4C7, 0xC7E4C8,\n                0xC7E4CA, 0xC7E4CC, 0xC7E4CD, 0xC7E4CF, 0xC7E4D3, 0xC7E4D9, 0xC7E4E2, 0xC7E4E5, 0xC7E4E8, 0xC7E4EA, 0xC7E520, 0xC7E620, 0xC7E6CA, 0xC820C7, 0xC920C7, 0xC920E1,\n                0xC920E4, 0xC920E5, 0xC920E8, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xD920C7, 0xD9E4E9, 0xE1EA20, 0xE420C7, 0xE4C920, 0xE4E920, 0xE4EA20,\n                0xE520C7, 0xE5C720, 0xE5C920, 0xE5E620, 0xE620C7, 0xE720C7, 0xE7C720, 0xE8C7E4, 0xE8E620, 0xE920C7, 0xEA20C7, 0xEA20E5, 0xEA20E8, 0xEAC920, 0xEAD120, 0xEAE620,\n        };\n\n        @Override\n        public String getLanguage() {\n            return \"ar\";\n        }\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det, ngrams, byteMap);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n    }\n\n    abstract static class CharsetRecog_8859_7 extends CharsetRecog_sbcs {\n        protected static byte[] byteMap = {\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0xA1, (byte) 0xA2, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xDC, (byte) 0x20,\n                (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0x20, (byte) 0xFC, (byte) 0x20, (byte) 0xFD, (byte) 0xFE,\n                (byte) 0xC0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xF0, (byte) 0xF1, (byte) 0x20, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x20,\n        };\n\n        @Override\n        public String getName() {\n            return \"ISO-8859-7\";\n        }\n    }\n\n    static class CharsetRecog_8859_7_el extends CharsetRecog_8859_7 {\n        private static final int[] ngrams = {\n                0x20E1ED, 0x20E1F0, 0x20E3E9, 0x20E4E9, 0x20E5F0, 0x20E720, 0x20EAE1, 0x20ECE5, 0x20EDE1, 0x20EF20, 0x20F0E1, 0x20F0EF, 0x20F0F1, 0x20F3F4, 0x20F3F5, 0x20F4E7,\n                0x20F4EF, 0xDFE120, 0xE120E1, 0xE120F4, 0xE1E920, 0xE1ED20, 0xE1F0FC, 0xE1F220, 0xE3E9E1, 0xE5E920, 0xE5F220, 0xE720F4, 0xE7ED20, 0xE7F220, 0xE920F4, 0xE9E120,\n                0xE9EADE, 0xE9F220, 0xEAE1E9, 0xEAE1F4, 0xECE520, 0xED20E1, 0xED20E5, 0xED20F0, 0xEDE120, 0xEFF220, 0xEFF520, 0xF0EFF5, 0xF0F1EF, 0xF0FC20, 0xF220E1, 0xF220E5,\n                0xF220EA, 0xF220F0, 0xF220F4, 0xF3E520, 0xF3E720, 0xF3F4EF, 0xF4E120, 0xF4E1E9, 0xF4E7ED, 0xF4E7F2, 0xF4E9EA, 0xF4EF20, 0xF4EFF5, 0xF4F9ED, 0xF9ED20, 0xFEED20,\n        };\n\n        @Override\n        public String getLanguage() {\n            return \"el\";\n        }\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            String name = det.fC1Bytes ? \"windows-1253\" : \"ISO-8859-7\";\n            int confidence = match(det, ngrams, byteMap);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, \"el\");\n        }\n    }\n\n    abstract static class CharsetRecog_8859_8 extends CharsetRecog_sbcs {\n        protected static byte[] byteMap = {\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n        };\n\n        @Override\n        public String getName() {\n            return \"ISO-8859-8\";\n        }\n    }\n\n    static class CharsetRecog_8859_8_I_he extends CharsetRecog_8859_8 {\n        private static final int[] ngrams = {\n                0x20E0E5, 0x20E0E7, 0x20E0E9, 0x20E0FA, 0x20E1E9, 0x20E1EE, 0x20E4E0, 0x20E4E5, 0x20E4E9, 0x20E4EE, 0x20E4F2, 0x20E4F9, 0x20E4FA, 0x20ECE0, 0x20ECE4, 0x20EEE0,\n                0x20F2EC, 0x20F9EC, 0xE0FA20, 0xE420E0, 0xE420E1, 0xE420E4, 0xE420EC, 0xE420EE, 0xE420F9, 0xE4E5E0, 0xE5E020, 0xE5ED20, 0xE5EF20, 0xE5F820, 0xE5FA20, 0xE920E4,\n                0xE9E420, 0xE9E5FA, 0xE9E9ED, 0xE9ED20, 0xE9EF20, 0xE9F820, 0xE9FA20, 0xEC20E0, 0xEC20E4, 0xECE020, 0xECE420, 0xED20E0, 0xED20E1, 0xED20E4, 0xED20EC, 0xED20EE,\n                0xED20F9, 0xEEE420, 0xEF20E4, 0xF0E420, 0xF0E920, 0xF0E9ED, 0xF2EC20, 0xF820E4, 0xF8E9ED, 0xF9EC20, 0xFA20E0, 0xFA20E1, 0xFA20E4, 0xFA20EC, 0xFA20EE, 0xFA20F9,\n        };\n\n        @Override\n        public String getName() {\n            return \"ISO-8859-8-I\";\n        }\n\n        @Override\n        public String getLanguage() {\n            return \"he\";\n        }\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            String name = det.fC1Bytes ? \"windows-1255\" : \"ISO-8859-8-I\";\n            int confidence = match(det, ngrams, byteMap);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, \"he\");\n        }\n    }\n\n    static class CharsetRecog_8859_8_he extends CharsetRecog_8859_8 {\n        private static final int[] ngrams = {\n                0x20E0E5, 0x20E0EC, 0x20E4E9, 0x20E4EC, 0x20E4EE, 0x20E4F0, 0x20E9F0, 0x20ECF2, 0x20ECF9, 0x20EDE5, 0x20EDE9, 0x20EFE5, 0x20EFE9, 0x20F8E5, 0x20F8E9, 0x20FAE0,\n                0x20FAE5, 0x20FAE9, 0xE020E4, 0xE020EC, 0xE020ED, 0xE020FA, 0xE0E420, 0xE0E5E4, 0xE0EC20, 0xE0EE20, 0xE120E4, 0xE120ED, 0xE120FA, 0xE420E4, 0xE420E9, 0xE420EC,\n                0xE420ED, 0xE420EF, 0xE420F8, 0xE420FA, 0xE4EC20, 0xE5E020, 0xE5E420, 0xE7E020, 0xE9E020, 0xE9E120, 0xE9E420, 0xEC20E4, 0xEC20ED, 0xEC20FA, 0xECF220, 0xECF920,\n                0xEDE9E9, 0xEDE9F0, 0xEDE9F8, 0xEE20E4, 0xEE20ED, 0xEE20FA, 0xEEE120, 0xEEE420, 0xF2E420, 0xF920E4, 0xF920ED, 0xF920FA, 0xF9E420, 0xFAE020, 0xFAE420, 0xFAE5E9,\n        };\n\n        @Override\n        public String getLanguage() {\n            return \"he\";\n        }\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            String name = det.fC1Bytes ? \"windows-1255\" : \"ISO-8859-8\";\n            int confidence = match(det, ngrams, byteMap);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, \"he\");\n\n        }\n    }\n\n    abstract static class CharsetRecog_8859_9 extends CharsetRecog_sbcs {\n        protected static byte[] byteMap = {\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x69, (byte) 0xFE, (byte) 0xDF,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF,\n        };\n\n        @Override\n        public String getName() {\n            return \"ISO-8859-9\";\n        }\n    }\n\n    static class CharsetRecog_8859_9_tr extends CharsetRecog_8859_9 {\n        private static final int[] ngrams = {\n                0x206261, 0x206269, 0x206275, 0x206461, 0x206465, 0x206765, 0x206861, 0x20696C, 0x206B61, 0x206B6F, 0x206D61, 0x206F6C, 0x207361, 0x207461, 0x207665, 0x207961,\n                0x612062, 0x616B20, 0x616C61, 0x616D61, 0x616E20, 0x616EFD, 0x617220, 0x617261, 0x6172FD, 0x6173FD, 0x617961, 0x626972, 0x646120, 0x646520, 0x646920, 0x652062,\n                0x65206B, 0x656469, 0x656E20, 0x657220, 0x657269, 0x657369, 0x696C65, 0x696E20, 0x696E69, 0x697220, 0x6C616E, 0x6C6172, 0x6C6520, 0x6C6572, 0x6E2061, 0x6E2062,\n                0x6E206B, 0x6E6461, 0x6E6465, 0x6E6520, 0x6E6920, 0x6E696E, 0x6EFD20, 0x72696E, 0x72FD6E, 0x766520, 0x796120, 0x796F72, 0xFD6E20, 0xFD6E64, 0xFD6EFD, 0xFDF0FD,\n        };\n\n        @Override\n        public String getLanguage() {\n            return \"tr\";\n        }\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            String name = det.fC1Bytes ? \"windows-1254\" : \"ISO-8859-9\";\n            int confidence = match(det, ngrams, byteMap);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, \"tr\");\n        }\n    }\n\n    static class CharsetRecog_windows_1251 extends CharsetRecog_sbcs {\n        private static final int[] ngrams = {\n                0x20E220, 0x20E2EE, 0x20E4EE, 0x20E7E0, 0x20E820, 0x20EAE0, 0x20EAEE, 0x20EDE0, 0x20EDE5, 0x20EEE1, 0x20EFEE, 0x20EFF0, 0x20F0E0, 0x20F1EE, 0x20F1F2, 0x20F2EE,\n                0x20F7F2, 0x20FDF2, 0xE0EDE8, 0xE0F2FC, 0xE3EE20, 0xE5EBFC, 0xE5EDE8, 0xE5F1F2, 0xE5F220, 0xE820EF, 0xE8E520, 0xE8E820, 0xE8FF20, 0xEBE5ED, 0xEBE820, 0xEBFCED,\n                0xEDE020, 0xEDE520, 0xEDE8E5, 0xEDE8FF, 0xEDEE20, 0xEDEEE2, 0xEE20E2, 0xEE20EF, 0xEE20F1, 0xEEE220, 0xEEE2E0, 0xEEE3EE, 0xEEE920, 0xEEEBFC, 0xEEEC20, 0xEEF1F2,\n                0xEFEEEB, 0xEFF0E5, 0xEFF0E8, 0xEFF0EE, 0xF0E0E2, 0xF0E5E4, 0xF1F2E0, 0xF1F2E2, 0xF1F2E8, 0xF1FF20, 0xF2E5EB, 0xF2EE20, 0xF2EEF0, 0xF2FC20, 0xF7F2EE, 0xFBF520,\n        };\n\n        private static final byte[] byteMap = {\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x90, (byte) 0x83, (byte) 0x20, (byte) 0x83, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F,\n                (byte) 0x90, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F,\n                (byte) 0x20, (byte) 0xA2, (byte) 0xA2, (byte) 0xBC, (byte) 0x20, (byte) 0xB4, (byte) 0x20, (byte) 0x20,\n                (byte) 0xB8, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xBF,\n                (byte) 0x20, (byte) 0x20, (byte) 0xB3, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0x20, (byte) 0x20,\n                (byte) 0xB8, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0xBC, (byte) 0xBE, (byte) 0xBE, (byte) 0xBF,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7,\n                (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF,\n        };\n\n        @Override\n        public String getName() {\n            return \"windows-1251\";\n        }\n\n        @Override\n        public String getLanguage() {\n            return \"ru\";\n        }\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det, ngrams, byteMap);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n    }\n\n    static class CharsetRecog_windows_1256 extends CharsetRecog_sbcs {\n        private static final int[] ngrams = {\n                0x20C7E1, 0x20C7E4, 0x20C8C7, 0x20DAE1, 0x20DDED, 0x20E1E1, 0x20E3E4, 0x20E6C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E120, 0xC7E1C3, 0xC7E1C7, 0xC7E1C8,\n                0xC7E1CA, 0xC7E1CC, 0xC7E1CD, 0xC7E1CF, 0xC7E1D3, 0xC7E1DA, 0xC7E1DE, 0xC7E1E3, 0xC7E1E6, 0xC7E1ED, 0xC7E320, 0xC7E420, 0xC7E4CA, 0xC820C7, 0xC920C7, 0xC920DD,\n                0xC920E1, 0xC920E3, 0xC920E6, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xDA20C7, 0xDAE1EC, 0xDDED20, 0xE120C7, 0xE1C920, 0xE1EC20, 0xE1ED20,\n                0xE320C7, 0xE3C720, 0xE3C920, 0xE3E420, 0xE420C7, 0xE520C7, 0xE5C720, 0xE6C7E1, 0xE6E420, 0xEC20C7, 0xED20C7, 0xED20E3, 0xED20E6, 0xEDC920, 0xEDD120, 0xEDE420,\n        };\n\n        private static final byte[] byteMap = {\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x81, (byte) 0x20, (byte) 0x83, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x88, (byte) 0x20, (byte) 0x8A, (byte) 0x20, (byte) 0x9C, (byte) 0x8D, (byte) 0x8E, (byte) 0x8F,\n                (byte) 0x90, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x98, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x20, (byte) 0x20, (byte) 0x9F,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7,\n                (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF,\n                (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0x20,\n                (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF,\n                (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7,\n                (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xF4, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0xF9, (byte) 0x20, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0x20, (byte) 0xFF,\n        };\n\n        @Override\n        public String getName() {\n            return \"windows-1256\";\n        }\n\n        @Override\n        public String getLanguage() {\n            return \"ar\";\n        }\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det, ngrams, byteMap);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n    }\n\n    static class CharsetRecog_KOI8_R extends CharsetRecog_sbcs {\n        private static final int[] ngrams = {\n                0x20C4CF, 0x20C920, 0x20CBC1, 0x20CBCF, 0x20CEC1, 0x20CEC5, 0x20CFC2, 0x20D0CF, 0x20D0D2, 0x20D2C1, 0x20D3CF, 0x20D3D4, 0x20D4CF, 0x20D720, 0x20D7CF, 0x20DAC1,\n                0x20DCD4, 0x20DED4, 0xC1CEC9, 0xC1D4D8, 0xC5CCD8, 0xC5CEC9, 0xC5D3D4, 0xC5D420, 0xC7CF20, 0xC920D0, 0xC9C520, 0xC9C920, 0xC9D120, 0xCCC5CE, 0xCCC920, 0xCCD8CE,\n                0xCEC120, 0xCEC520, 0xCEC9C5, 0xCEC9D1, 0xCECF20, 0xCECFD7, 0xCF20D0, 0xCF20D3, 0xCF20D7, 0xCFC7CF, 0xCFCA20, 0xCFCCD8, 0xCFCD20, 0xCFD3D4, 0xCFD720, 0xCFD7C1,\n                0xD0CFCC, 0xD0D2C5, 0xD0D2C9, 0xD0D2CF, 0xD2C1D7, 0xD2C5C4, 0xD3D120, 0xD3D4C1, 0xD3D4C9, 0xD3D4D7, 0xD4C5CC, 0xD4CF20, 0xD4CFD2, 0xD4D820, 0xD9C820, 0xDED4CF,\n        };\n\n        private static final byte[] byteMap = {\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67,\n                (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F,\n                (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77,\n                (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA3, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA3, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20,\n                (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7,\n                (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF,\n                (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7,\n                (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF,\n                (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7,\n                (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF,\n                (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7,\n                (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF,\n        };\n\n        @Override\n        public String getName() {\n            return \"KOI8-R\";\n        }\n\n        @Override\n        public String getLanguage() {\n            return \"ru\";\n        }\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det, ngrams, byteMap);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n    }\n\n    abstract static class CharsetRecog_IBM424_he extends CharsetRecog_sbcs {\n        protected static byte[] byteMap = {\n/*                 -0           -1           -2           -3           -4           -5           -6           -7           -8           -9           -A           -B           -C           -D           -E           -F   */\n/* 0- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 1- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 2- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 3- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 4- */    (byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 5- */    (byte) 0x40, (byte) 0x51, (byte) 0x52, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 6- */    (byte) 0x40, (byte) 0x40, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 7- */    (byte) 0x40, (byte) 0x71, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x00, (byte) 0x40, (byte) 0x40,\n/* 8- */    (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 9- */    (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* A- */    (byte) 0xA0, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* B- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* C- */    (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* D- */    (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* E- */    (byte) 0x40, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* F- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n        };\n\n        @Override\n        public String getLanguage() {\n            return \"he\";\n        }\n    }\n\n    static class CharsetRecog_IBM424_he_rtl extends CharsetRecog_IBM424_he {\n        @Override\n        public String getName() {\n            return \"IBM424_rtl\";\n        }\n\n        private static final int[] ngrams = {\n                0x404146, 0x404148, 0x404151, 0x404171, 0x404251, 0x404256, 0x404541, 0x404546, 0x404551, 0x404556, 0x404562, 0x404569, 0x404571, 0x405441, 0x405445, 0x405641,\n                0x406254, 0x406954, 0x417140, 0x454041, 0x454042, 0x454045, 0x454054, 0x454056, 0x454069, 0x454641, 0x464140, 0x465540, 0x465740, 0x466840, 0x467140, 0x514045,\n                0x514540, 0x514671, 0x515155, 0x515540, 0x515740, 0x516840, 0x517140, 0x544041, 0x544045, 0x544140, 0x544540, 0x554041, 0x554042, 0x554045, 0x554054, 0x554056,\n                0x554069, 0x564540, 0x574045, 0x584540, 0x585140, 0x585155, 0x625440, 0x684045, 0x685155, 0x695440, 0x714041, 0x714042, 0x714045, 0x714054, 0x714056, 0x714069,\n        };\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det, ngrams, byteMap, (byte) 0x40);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n    }\n\n    static class CharsetRecog_IBM424_he_ltr extends CharsetRecog_IBM424_he {\n        @Override\n        public String getName() {\n            return \"IBM424_ltr\";\n        }\n\n        private static final int[] ngrams = {\n                0x404146, 0x404154, 0x404551, 0x404554, 0x404556, 0x404558, 0x405158, 0x405462, 0x405469, 0x405546, 0x405551, 0x405746, 0x405751, 0x406846, 0x406851, 0x407141,\n                0x407146, 0x407151, 0x414045, 0x414054, 0x414055, 0x414071, 0x414540, 0x414645, 0x415440, 0x415640, 0x424045, 0x424055, 0x424071, 0x454045, 0x454051, 0x454054,\n                0x454055, 0x454057, 0x454068, 0x454071, 0x455440, 0x464140, 0x464540, 0x484140, 0x514140, 0x514240, 0x514540, 0x544045, 0x544055, 0x544071, 0x546240, 0x546940,\n                0x555151, 0x555158, 0x555168, 0x564045, 0x564055, 0x564071, 0x564240, 0x564540, 0x624540, 0x694045, 0x694055, 0x694071, 0x694540, 0x714140, 0x714540, 0x714651\n\n        };\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            int confidence = match(det, ngrams, byteMap, (byte) 0x40);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n    }\n\n    abstract static class CharsetRecog_IBM420_ar extends CharsetRecog_sbcs {\n\n        protected static byte[] byteMap = {\n/*                 -0           -1           -2           -3           -4           -5           -6           -7           -8           -9           -A           -B           -C           -D           -E           -F   */\n/* 0- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 1- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 2- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 3- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 4- */    (byte) 0x40, (byte) 0x40, (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 5- */    (byte) 0x40, (byte) 0x51, (byte) 0x52, (byte) 0x40, (byte) 0x40, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 6- */    (byte) 0x40, (byte) 0x40, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 7- */    (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40,\n/* 8- */    (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x8B, (byte) 0x8C, (byte) 0x8D, (byte) 0x8E, (byte) 0x8F,\n/* 9- */    (byte) 0x90, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0x9B, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F,\n/* A- */    (byte) 0xA0, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xAB, (byte) 0xAC, (byte) 0xAD, (byte) 0xAE, (byte) 0xAF,\n/* B- */    (byte) 0xB0, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0x40, (byte) 0x40, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0xBD, (byte) 0xBE, (byte) 0xBF,\n/* C- */    (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0xCB, (byte) 0x40, (byte) 0xCD, (byte) 0x40, (byte) 0xCF,\n/* D- */    (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF,\n/* E- */    (byte) 0x40, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xEA, (byte) 0xEB, (byte) 0x40, (byte) 0xED, (byte) 0xEE, (byte) 0xEF,\n/* F- */    (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x40,\n        };\n\n\n        @Override\n        public String getLanguage() {\n            return \"ar\";\n        }\n\n    }\n\n    static class CharsetRecog_IBM420_ar_rtl extends CharsetRecog_IBM420_ar {\n        private static final int[] ngrams = {\n                0x4056B1, 0x4056BD, 0x405856, 0x409AB1, 0x40ABDC, 0x40B1B1, 0x40BBBD, 0x40CF56, 0x564056, 0x564640, 0x566340, 0x567540, 0x56B140, 0x56B149, 0x56B156, 0x56B158,\n                0x56B163, 0x56B167, 0x56B169, 0x56B173, 0x56B178, 0x56B19A, 0x56B1AD, 0x56B1BB, 0x56B1CF, 0x56B1DC, 0x56BB40, 0x56BD40, 0x56BD63, 0x584056, 0x624056, 0x6240AB,\n                0x6240B1, 0x6240BB, 0x6240CF, 0x634056, 0x734056, 0x736240, 0x754056, 0x756240, 0x784056, 0x9A4056, 0x9AB1DA, 0xABDC40, 0xB14056, 0xB16240, 0xB1DA40, 0xB1DC40,\n                0xBB4056, 0xBB5640, 0xBB6240, 0xBBBD40, 0xBD4056, 0xBF4056, 0xBF5640, 0xCF56B1, 0xCFBD40, 0xDA4056, 0xDC4056, 0xDC40BB, 0xDC40CF, 0xDC6240, 0xDC7540, 0xDCBD40,\n        };\n\n        @Override\n        public String getName() {\n            return \"IBM420_rtl\";\n        }\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            int confidence = matchIBM420(det, ngrams, byteMap, (byte) 0x40);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n\n    }\n\n    static class CharsetRecog_IBM420_ar_ltr extends CharsetRecog_IBM420_ar {\n        private static final int[] ngrams = {\n                0x404656, 0x4056BB, 0x4056BF, 0x406273, 0x406275, 0x4062B1, 0x4062BB, 0x4062DC, 0x406356, 0x407556, 0x4075DC, 0x40B156, 0x40BB56, 0x40BD56, 0x40BDBB, 0x40BDCF,\n                0x40BDDC, 0x40DAB1, 0x40DCAB, 0x40DCB1, 0x49B156, 0x564056, 0x564058, 0x564062, 0x564063, 0x564073, 0x564075, 0x564078, 0x56409A, 0x5640B1, 0x5640BB, 0x5640BD,\n                0x5640BF, 0x5640DA, 0x5640DC, 0x565840, 0x56B156, 0x56CF40, 0x58B156, 0x63B156, 0x63BD56, 0x67B156, 0x69B156, 0x73B156, 0x78B156, 0x9AB156, 0xAB4062, 0xADB156,\n                0xB14062, 0xB15640, 0xB156CF, 0xB19A40, 0xB1B140, 0xBB4062, 0xBB40DC, 0xBBB156, 0xBD5640, 0xBDBB40, 0xCF4062, 0xCF40DC, 0xCFB156, 0xDAB19A, 0xDCAB40, 0xDCB156\n        };\n\n        @Override\n        public String getName() {\n            return \"IBM420_ltr\";\n        }\n\n        @Override\n        public CharsetMatch match(CharsetDetector det) {\n            int confidence = matchIBM420(det, ngrams, byteMap, (byte) 0x40);\n            return confidence == 0 ? null : new CharsetMatch(det, this, confidence);\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/lib/icu4j/CharsetRecognizer.java",
    "content": "// © 2016 and later: Unicode, Inc. and others.\n// License & terms of use: http://www.unicode.org/copyright.html\n/**\n * ******************************************************************************\n * Copyright (C) 2005-2012, International Business Machines Corporation and    *\n * others. All Rights Reserved.                                                *\n * ******************************************************************************\n */\npackage io.legado.app.lib.icu4j;\n\n/**\n * Abstract class for recognizing a single charset.\n * Part of the implementation of ICU's CharsetDetector.\n * <p>\n * Each specific charset that can be recognized will have an instance\n * of some subclass of this class.  All interaction between the overall\n * CharsetDetector and the stuff specific to an individual charset happens\n * via the interface provided here.\n * <p>\n * Instances of CharsetDetector DO NOT have or maintain\n * state pertaining to a specific match or detect operation.\n * The WILL be shared by multiple instances of CharsetDetector.\n * They encapsulate const charset-specific information.\n */\nabstract class CharsetRecognizer {\n    /**\n     * Get the IANA name of this charset.\n     *\n     * @return the charset name.\n     */\n    abstract String getName();\n\n    /**\n     * Get the ISO language code for this charset.\n     *\n     * @return the language code, or <code>null</code> if the language cannot be determined.\n     */\n    public String getLanguage() {\n        return null;\n    }\n\n    /**\n     * Test the match of this charset with the input text data\n     * which is obtained via the CharsetDetector object.\n     *\n     * @param det The CharsetDetector, which contains the input text\n     *            to be checked for being in this charset.\n     * @return A CharsetMatch object containing details of match\n     * with this charset, or null if there was no match.\n     */\n    abstract CharsetMatch match(CharsetDetector det);\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/model/Debug.kt",
    "content": "package io.legado.app.model\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.model.webBook.WebBook\nimport mu.KotlinLogging\n\nprivate val logger = KotlinLogging.logger {}\n\nobject Debug : DebugLog{\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/DebugLog.kt",
    "content": "package io.legado.app.model\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport mu.KotlinLogging\nimport okhttp3.logging.HttpLoggingInterceptor\n\nprivate val logger = KotlinLogging.logger {}\n\ninterface DebugLog: HttpLoggingInterceptor.Logger {\n    fun log(\n        sourceUrl: String? = \"\",\n        msg: String? = \"\",\n        isHtml: Boolean = false\n    ) {\n        logger.info(\"sourceUrl: {}, msg: {}\", sourceUrl, msg)\n    }\n\n    override fun log(message: String) {\n        logger.debug(message)\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/Debugger.kt",
    "content": "package io.legado.app.model\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.model.webBook.WebBook\nimport io.legado.app.utils.isAbsUrl\nimport io.legado.app.utils.HtmlFormatter\nimport mu.KotlinLogging\nimport java.text.SimpleDateFormat\nimport java.util.Locale\nimport java.util.Date\n\nprivate val logger = KotlinLogging.logger {}\n\nclass Debugger(val logMsg: (String) -> Unit) : DebugLog {\n    private val debugTimeFormat = SimpleDateFormat(\"[mm:ss.SSS]\", Locale.getDefault())\n    private var startTime: Long = System.currentTimeMillis()\n\n    fun log(\n        sourceUrl: String?,\n        msg: String?\n    ) {\n        log(sourceUrl, msg, false)\n    }\n\n    override fun log(message: String) {\n        val time = debugTimeFormat.format(Date(System.currentTimeMillis() - startTime))\n        logMsg(\"$time $message\")\n    }\n\n    override fun log(\n        sourceUrl: String?,\n        msg: String?,\n        isHtml: Boolean\n    ) {\n        if (sourceUrl == null || msg == null) return\n        logger.info(\"sourceUrl: {}, msg: {}\", sourceUrl, msg)\n        var printMsg = msg\n\n        if (isHtml) {\n            printMsg = HtmlFormatter.format(msg)\n        }\n        val time = debugTimeFormat.format(Date(System.currentTimeMillis() - startTime))\n        printMsg = \"$time $printMsg\"\n        logMsg(printMsg)\n    }\n\n    suspend fun startDebug(webBook: WebBook, key: String) {\n        val bookSource = webBook.bookSource\n        webBook.debugLogger = this@Debugger\n        startTime = System.currentTimeMillis()\n        when {\n            key.isAbsUrl() -> {\n                val book = Book()\n                book.origin = bookSource.bookSourceUrl\n                book.bookUrl = key\n                log(bookSource.bookSourceUrl, \"⇒开始访问详情页:$key\")\n                infoDebug(webBook, book)\n            }\n            key.contains(\"::\") -> {\n                val url = key.substringAfter(\"::\")\n                log(bookSource.bookSourceUrl, \"⇒开始访问发现页:$url\")\n                exploreDebug(webBook, url)\n            }\n            key.startsWith(\"++\") -> {\n                val url = key.substring(2)\n                val book = Book()\n                book.origin = bookSource.bookSourceUrl\n                book.tocUrl = url\n                log(bookSource.bookSourceUrl, \"⇒开始访目录页:$url\")\n                tocDebug(webBook, book)\n            }\n            key.startsWith(\"--\") -> {\n                val url = key.substring(2)\n                val book = Book()\n                book.origin = bookSource.bookSourceUrl\n                log(bookSource.bookSourceUrl, \"⇒开始访正文页:$url\")\n                val chapter = BookChapter()\n                chapter.title = \"调试\"\n                chapter.url = url\n                contentDebug(webBook, book, chapter, null)\n            }\n            else -> {\n                log(bookSource.bookSourceUrl, \"⇒开始搜索关键字:$key\")\n                searchDebug(webBook, key)\n            }\n        }\n    }\n\n    private suspend fun exploreDebug(webBook: WebBook, url: String) {\n        webBook.debugLogger = this@Debugger\n        log(\"︾开始解析发现页\")\n        runCatching {\n            webBook.exploreBook(url, 1)\n        }.onSuccess { exploreBooks ->\n            exploreBooks.let {\n                if (exploreBooks.isNotEmpty()) {\n                    log(webBook.sourceUrl, \"︽发现页解析完成\")\n                    infoDebug(webBook, exploreBooks[0].toBook())\n                } else {\n                    log(webBook.sourceUrl, \"︽未获取到书籍\")\n                }\n            }\n        }.onFailure {\n            log(webBook.sourceUrl, \"Error: \" + it.localizedMessage)\n            throw it\n        }\n   }\n\n    private suspend fun searchDebug(webBook: WebBook, key: String) {\n        webBook.debugLogger = this@Debugger\n        log(msg = \"︾开始解析搜索页\")\n        runCatching {\n            webBook.searchBook(key, 1)\n        }.onSuccess { searchBooks ->\n            searchBooks.let {\n                if (searchBooks.isNotEmpty()) {\n                    log(webBook.sourceUrl, \"︽搜索页解析完成\")\n                    infoDebug(webBook, searchBooks[0].toBook())\n                } else {\n                    log(webBook.sourceUrl, \"︽未获取到书籍\")\n                }\n            }\n        }.onFailure {\n            log(webBook.sourceUrl, \"Error: \" + it.localizedMessage)\n            throw it\n        }\n    }\n\n    private suspend fun infoDebug(webBook: WebBook, book: Book) {\n        webBook.debugLogger = this@Debugger\n        log(msg = \"︾开始解析详情页\")\n        runCatching { webBook.getBookInfo(book.bookUrl) }\n                .onSuccess {\n                    log(webBook.sourceUrl, \"︽详情页解析完成\")\n                    tocDebug(webBook, it)\n                }\n                .onFailure {\n                    log(webBook.sourceUrl, \"Error: \" + it.localizedMessage)\n                    throw it\n                }\n    }\n\n    private suspend fun tocDebug(webBook: WebBook, book: Book) {\n        webBook.debugLogger = this@Debugger\n        log(msg = \"︾开始解析目录页\")\n        runCatching {\n            webBook.getChapterList(book)\n        }\n                .onSuccess { chapterList ->\n                    chapterList?.let {\n                        if (it.isNotEmpty()) {\n                            log(webBook.sourceUrl, \"︽目录页解析完成\")\n                            val nextChapterUrl = if (it.size > 1) it[1].url else null\n                            contentDebug(webBook, book, it[0], nextChapterUrl)\n                        } else {\n                            log(webBook.sourceUrl, \"︽目录列表为空\")\n                        }\n                    }\n                }\n                .onFailure {\n                    log(webBook.sourceUrl, \"Error: \" + it.localizedMessage)\n                    throw it\n                }\n    }\n\n    private suspend fun contentDebug(\n            webBook: WebBook,\n            book: Book,\n            bookChapter: BookChapter,\n            nextChapterUrl: String?\n    ) {\n        webBook.debugLogger = this@Debugger\n        log(webBook.sourceUrl, \"︾开始解析正文页\")\n        runCatching { webBook.getBookContent(book, bookChapter, nextChapterUrl) }\n                .onSuccess {\n                    log(webBook.sourceUrl, \"︽正文页解析完成\")\n                }\n                .onFailure {\n                    log(webBook.sourceUrl, \"Error: \" + it.localizedMessage)\n                }\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/README.md",
    "content": "# 放置一些模块类\n* analyzeRule 书源规则解析\n* localBook 本地书籍解析\n* rss 订阅规则解析\n* webBook 获取网络书籍\n\n"
  },
  {
    "path": "src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt",
    "content": "package io.legado.app.model.analyzeRule\n\nimport com.jayway.jsonpath.JsonPath\nimport com.jayway.jsonpath.ReadContext\nimport java.util.*\n\n@Suppress(\"RegExpRedundantEscape\")\nclass AnalyzeByJSonPath(json: Any) {\n\n    companion object {\n\n        fun parse(json: Any): ReadContext {\n            return when (json) {\n                is ReadContext -> json\n                is String -> JsonPath.parse(json) //JsonPath.parse<String>(json)\n                else -> JsonPath.parse(json) //JsonPath.parse<Any>(json)\n            }\n        }\n    }\n\n    private var ctx: ReadContext = parse(json)\n\n    /**\n     * 改进解析方法\n     * 解决阅读”&&“、”||“与jsonPath支持的”&&“、”||“之间的冲突\n     * 解决{$.rule}形式规则可能匹配错误的问题，旧规则用正则解析内容含‘}’的json文本时，用规则中的字段去匹配这种内容会匹配错误.现改用平衡嵌套方法解决这个问题\n     * */\n    fun getString(rule: String): String? {\n        if (rule.isEmpty()) return null\n        var result: String\n        val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡\n        val rules = ruleAnalyzes.splitRule(\"&&\", \"||\")\n\n        if (rules.size == 1) {\n\n            ruleAnalyzes.reSetPos() //将pos重置为0，复用解析器\n\n            result = ruleAnalyzes.innerRule(\"{$.\") { getString(it) } //替换所有{$.rule...}\n\n            if (result.isEmpty()) { //st为空，表明无成功替换的内嵌规则\n\n                try {\n\n                    val ob = ctx.read<Any>(rule)\n                    result = if (ob is List<*>) {\n                        ob.joinToString(\"\\n\")\n                    } else {\n                        ob.toString()\n                    }\n\n                } catch (e: Exception) {\n                    e.printStackTrace()\n                }\n\n            }\n\n            return result\n\n        } else {\n            val textList = arrayListOf<String>()\n            for (rl in rules) {\n                val temp = getString(rl)\n                if (!temp.isNullOrEmpty()) {\n                    textList.add(temp)\n                    if (ruleAnalyzes.elementsType == \"||\") {\n                        break\n                    }\n                }\n            }\n            return textList.joinToString(\"\\n\")\n        }\n    }\n\n    internal fun getStringList(rule: String): List<String> {\n        val result = ArrayList<String>()\n        if (rule.isEmpty()) return result\n        val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡\n        val rules = ruleAnalyzes.splitRule(\"&&\", \"||\", \"%%\")\n\n        if (rules.size == 1) {\n\n            ruleAnalyzes.reSetPos() //将pos重置为0，复用解析器\n\n            val st = ruleAnalyzes.innerRule(\"{$.\") { getString(it) } //替换所有{$.rule...}\n\n            if (st.isEmpty()) { //st为空，表明无成功替换的内嵌规则\n\n                try {\n\n                    val obj = ctx.read<Any>(rule) //kotlin的Any型返回值不包含null ，删除赘余 ?: return result\n\n                    if (obj is List<*>) {\n\n                        for (o in obj) result.add(o.toString())\n\n                    } else {\n                        result.add(obj.toString())\n                    }\n                } catch (e: Exception) {\n                    e.printStackTrace()\n                }\n            } else {\n                result.add(st)\n            }\n            return result\n        } else {\n            val results = ArrayList<List<String>>()\n            for (rl in rules) {\n                val temp = getStringList(rl)\n                if (temp.isNotEmpty()) {\n                    results.add(temp)\n                    if (temp.isNotEmpty() && ruleAnalyzes.elementsType == \"||\") {\n                        break\n                    }\n                }\n            }\n            if (results.size > 0) {\n                if (\"%%\" == ruleAnalyzes.elementsType) {\n                    for (i in results[0].indices) {\n                        for (temp in results) {\n                            if (i < temp.size) {\n                                result.add(temp[i])\n                            }\n                        }\n                    }\n                } else {\n                    for (temp in results) {\n                        result.addAll(temp)\n                    }\n                }\n            }\n            return result\n        }\n    }\n\n    internal fun getObject(rule: String): Any {\n        return ctx.read(rule)\n    }\n\n    internal fun getList(rule: String): ArrayList<Any>? {\n        val result = ArrayList<Any>()\n        if (rule.isEmpty()) return result\n        val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡\n        val rules = ruleAnalyzes.splitRule(\"&&\", \"||\", \"%%\")\n        if (rules.size == 1) {\n            ctx.let {\n                try {\n                    return it.read<ArrayList<Any>>(rules[0])\n                } catch (e: Exception) {\n                    e.printStackTrace()\n                }\n            }\n        } else {\n            val results = ArrayList<ArrayList<*>>()\n            for (rl in rules) {\n                val temp = getList(rl)\n                if (temp != null && temp.isNotEmpty()) {\n                    results.add(temp)\n                    if (temp.isNotEmpty() && ruleAnalyzes.elementsType == \"||\") {\n                        break\n                    }\n                }\n            }\n            if (results.size > 0) {\n                if (\"%%\" == ruleAnalyzes.elementsType) {\n                    for (i in 0 until results[0].size) {\n                        for (temp in results) {\n                            if (i < temp.size) {\n                                temp[i]?.let { result.add(it) }\n                            }\n                        }\n                    }\n                } else {\n                    for (temp in results) {\n                        result.addAll(temp)\n                    }\n                }\n            }\n        }\n        return result\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt",
    "content": "package io.legado.app.model.analyzeRule\n\nimport org.jsoup.Jsoup\nimport org.jsoup.nodes.Element\nimport org.jsoup.select.Collector\nimport org.jsoup.select.Elements\nimport org.jsoup.select.Evaluator\nimport org.seimicrawler.xpath.JXNode\nimport java.util.*\n\n/**\n * Created by GKF on 2018/1/25.\n * 书源规则解析\n */\nclass AnalyzeByJSoup(doc: Any) {\n    companion object {\n        /**\n         * \"class\", \"id\", \"tag\", \"text\", \"children\"\n         */\n        val validKeys = arrayOf(\"class\", \"id\", \"tag\", \"text\", \"children\")\n\n        fun parse(doc: Any): Element {\n            return when (doc) {\n                is Element -> doc\n                is JXNode -> if (doc.isElement) doc.asElement() else Jsoup.parse(doc.toString())\n                else -> Jsoup.parse(doc.toString())\n            }\n        }\n\n    }\n\n    private var element: Element = parse(doc)\n\n    /**\n     * 获取列表\n     */\n    internal fun getElements(rule: String) = getElements(element, rule)\n\n    /**\n     * 合并内容列表,得到内容\n     */\n    internal fun getString(ruleStr: String) =\n        if (ruleStr.isEmpty()) null\n        else getStringList(ruleStr).takeIf { it.isNotEmpty() }?.joinToString(\"\\n\")\n\n    /**\n     * 获取一个字符串\n     */\n    internal fun getString0(ruleStr: String) =\n        getStringList(ruleStr).let { if (it.isEmpty()) \"\" else it[0] }\n\n    /**\n     * 获取所有内容列表\n     */\n    internal fun getStringList(ruleStr: String): List<String> {\n\n        val textS = ArrayList<String>()\n\n        if (ruleStr.isEmpty()) return textS\n\n        //拆分规则\n        val sourceRule = SourceRule(ruleStr)\n\n        if (sourceRule.elementsRule.isEmpty()) {\n\n            textS.add(element.data() ?: \"\")\n\n        } else {\n\n            val ruleAnalyzes = RuleAnalyzer(sourceRule.elementsRule)\n            val ruleStrS = ruleAnalyzes.splitRule(\"&&\", \"||\", \"%%\")\n\n            val results = ArrayList<List<String>>()\n            for (ruleStrX in ruleStrS) {\n\n                val temp: List<String>? =\n                    if (sourceRule.isCss) {\n                        val lastIndex = ruleStrX.lastIndexOf('@')\n                        getResultLast(\n                            element.select(ruleStrX.substring(0, lastIndex)),\n                            ruleStrX.substring(lastIndex + 1)\n                        )\n                    } else {\n                        getResultList(ruleStrX)\n                    }\n\n                if (!temp.isNullOrEmpty()) {\n                    results.add(temp)\n                    if (ruleAnalyzes.elementsType == \"||\") break\n                }\n            }\n            if (results.size > 0) {\n                if (\"%%\" == ruleAnalyzes.elementsType) {\n                    for (i in results[0].indices) {\n                        for (temp in results) {\n                            if (i < temp.size) {\n                                textS.add(temp[i])\n                            }\n                        }\n                    }\n                } else {\n                    for (temp in results) {\n                        textS.addAll(temp)\n                    }\n                }\n            }\n        }\n        return textS\n    }\n\n    /**\n     * 获取Elements\n     */\n    private fun getElements(temp: Element?, rule: String): Elements {\n\n        if (temp == null || rule.isEmpty()) return Elements()\n\n        val elements = Elements()\n\n        val sourceRule = SourceRule(rule)\n        val ruleAnalyzes = RuleAnalyzer(sourceRule.elementsRule)\n        val ruleStrS = ruleAnalyzes.splitRule(\"&&\", \"||\", \"%%\")\n\n        val elementsList = ArrayList<Elements>()\n        if (sourceRule.isCss) {\n            for (ruleStr in ruleStrS) {\n                val tempS = temp.select(ruleStr)\n                elementsList.add(tempS)\n                if (tempS.size > 0 && ruleAnalyzes.elementsType == \"||\") {\n                    break\n                }\n            }\n        } else {\n            for (ruleStr in ruleStrS) {\n\n                val rsRule = RuleAnalyzer(ruleStr)\n\n                rsRule.trim()  // 修剪当前规则之前的\"@\"或者空白符\n\n                val rs = rsRule.splitRule(\"@\")\n\n                val el = if (rs.size > 1) {\n                    val el = Elements()\n                    el.add(temp)\n                    for (rl in rs) {\n                        val es = Elements()\n                        for (et in el) {\n                            es.addAll(getElements(et, rl))\n                        }\n                        el.clear()\n                        el.addAll(es)\n                    }\n                    el\n                } else ElementsSingle().getElementsSingle(temp, ruleStr)\n\n                elementsList.add(el)\n                if (el.size > 0 && ruleAnalyzes.elementsType == \"||\") {\n                    break\n                }\n            }\n        }\n        if (elementsList.size > 0) {\n            if (\"%%\" == ruleAnalyzes.elementsType) {\n                for (i in 0 until elementsList[0].size) {\n                    for (es in elementsList) {\n                        if (i < es.size) {\n                            elements.add(es[i])\n                        }\n                    }\n                }\n            } else {\n                for (es in elementsList) {\n                    elements.addAll(es)\n                }\n            }\n        }\n        return elements\n    }\n\n    /**\n     * 获取内容列表\n     */\n    private fun getResultList(ruleStr: String): List<String>? {\n\n        if (ruleStr.isEmpty()) return null\n\n        var elements = Elements()\n\n        elements.add(element)\n\n        val rule = RuleAnalyzer(ruleStr) //创建解析\n\n        rule.trim() //修建前置赘余符号\n\n        val rules = rule.splitRule(\"@\") // 切割成列表\n\n        val last = rules.size - 1\n        for (i in 0 until last) {\n            val es = Elements()\n            for (elt in elements) {\n                es.addAll(ElementsSingle().getElementsSingle(elt, rules[i]))\n            }\n            elements.clear()\n            elements = es\n        }\n        return if (elements.isEmpty()) null else getResultLast(elements, rules[last])\n    }\n\n    /**\n     * 根据最后一个规则获取内容\n     */\n    private fun getResultLast(elements: Elements, lastRule: String): List<String> {\n        val textS = ArrayList<String>()\n        when (lastRule) {\n            \"text\" -> for (element in elements) {\n                val text = element.text()\n                if (text.isNotEmpty()) {\n                    textS.add(text)\n                }\n            }\n            \"textNodes\" -> for (element in elements) {\n                val tn = arrayListOf<String>()\n                val contentEs = element.textNodes()\n                for (item in contentEs) {\n                    val text = item.text().trim { it <= ' ' }\n                    if (text.isNotEmpty()) {\n                        tn.add(text)\n                    }\n                }\n                if (tn.isNotEmpty()) {\n                    textS.add(tn.joinToString(\"\\n\"))\n                }\n            }\n            \"ownText\" -> for (element in elements) {\n                val text = element.ownText()\n                if (text.isNotEmpty()) {\n                    textS.add(text)\n                }\n            }\n            \"html\" -> {\n                elements.select(\"script\").remove()\n                elements.select(\"style\").remove()\n                val html = elements.outerHtml()\n                if (html.isNotEmpty()) {\n                    textS.add(html)\n                }\n            }\n            \"all\" -> textS.add(elements.outerHtml())\n            else -> for (element in elements) {\n\n                val url = element.attr(lastRule)\n\n                if (url.isBlank() || textS.contains(url)) continue\n\n                textS.add(url)\n            }\n        }\n        return textS\n    }\n\n    /**\n     * 1.支持阅读原有写法，':'分隔索引，!或.表示筛选方式，索引可为负数\n     * 例如 tag.div.-1:10:2 或 tag.div!0:3\n     *\n     * 2. 支持与jsonPath类似的[]索引写法\n     * 格式形如 [it,it，。。。] 或 [!it,it，。。。] 其中[!开头表示筛选方式为排除，it为单个索引或区间。\n     * 区间格式为 start:end 或 start:end:step，其中start为0可省略，end为-1可省略。\n     * 索引，区间两端及间隔都支持负数\n     * 例如 tag.div[-1, 3:-2:-10, 2]\n     * 特殊用法 tag.div[-1:0] 可在任意地方让列表反向\n     * */\n    data class ElementsSingle(\n        var split: Char = '.',\n        var beforeRule: String = \"\",\n        val indexDefault: MutableList<Int> = mutableListOf(),\n        val indexes: MutableList<Any> = mutableListOf()\n    ) {\n        /**\n         * 获取Elements按照一个规则\n         */\n        fun getElementsSingle(temp: Element, rule: String): Elements {\n\n            findIndexSet(rule) //执行索引列表处理器\n\n            /**\n             * 获取所有元素\n             * */\n            var elements =\n                if (beforeRule.isEmpty()) temp.children() //允许索引直接作为根元素，此时前置规则为空，效果与children相同\n                else {\n                    val rules = beforeRule.split(\".\")\n                    when (rules[0]) {\n                        \"children\" -> temp.children() //允许索引直接作为根元素，此时前置规则为空，效果与children相同\n                        \"class\" -> temp.getElementsByClass(rules[1])\n                        \"tag\" -> temp.getElementsByTag(rules[1])\n                        \"id\" -> Collector.collect(Evaluator.Id(rules[1]), temp)\n                        \"text\" -> temp.getElementsContainingOwnText(rules[1])\n                        else -> temp.select(beforeRule)\n                    }\n                }\n\n            val len = elements.size\n            val lastIndexes = (indexDefault.size - 1).takeIf { it != -1 } ?: indexes.size - 1\n            val indexSet = mutableSetOf<Int>()\n\n            /**\n             * 获取无重且不越界的索引集合\n             * */\n            if (indexes.isEmpty()) for (ix in lastIndexes downTo 0) { //indexes为空，表明是非[]式索引，集合是逆向遍历插入的，所以这里也逆向遍历，好还原顺序\n\n                val it = indexDefault[ix]\n                if (it in 0 until len) indexSet.add(it) //将正数不越界的索引添加到集合\n                else if (it < 0 && len >= -it) indexSet.add(it + len) //将负数不越界的索引添加到集合\n\n            } else for (ix in lastIndexes downTo 0) { //indexes不空，表明是[]式索引，集合是逆向遍历插入的，所以这里也逆向遍历，好还原顺序\n\n                if (indexes[ix] is Triple<*, *, *>) { //区间\n                    val (startX, endX, stepX) = indexes[ix] as Triple<Int?, Int?, Int> //还原储存时的类型\n\n                    val start = if (startX == null) 0 //左端省略表示0\n                    else if (startX >= 0) if (startX < len) startX else len - 1 //右端越界，设置为最大索引\n                    else if (-startX <= len) len + startX /* 将负索引转正 */ else 0 //左端越界，设置为最小索引\n\n                    val end = if (endX == null) len - 1 //右端省略表示 len - 1\n                    else if (endX >= 0) if (endX < len) endX else len - 1 //右端越界，设置为最大索引\n                    else if (-endX <= len) len + endX /* 将负索引转正 */ else 0 //左端越界，设置为最小索引\n\n                    if (start == end || stepX >= len) { //两端相同，区间里只有一个数。或间隔过大，区间实际上仅有首位\n\n                        indexSet.add(start)\n                        continue\n\n                    }\n\n                    val step =\n                        if (stepX > 0) stepX else if (-stepX < len) stepX + len else 1 //最小正数间隔为1\n\n                    //将区间展开到集合中,允许列表反向。\n                    indexSet.addAll(if (end > start) start..end step step else start downTo end step step)\n\n                } else {//单个索引\n\n                    val it = indexes[ix] as Int //还原储存时的类型\n\n                    if (it in 0 until len) indexSet.add(it) //将正数不越界的索引添加到集合\n                    else if (it < 0 && len >= -it) indexSet.add(it + len) //将负数不越界的索引添加到集合\n\n                }\n\n            }\n\n            /**\n             * 根据索引集合筛选元素\n             * */\n            if (split == '!') { //排除\n\n                for (pcInt in indexSet) elements[pcInt] = null\n\n                elements.removeAll(listOf(null)) //测试过，这样就行\n\n            } else if (split == '.') { //选择\n\n                val es = Elements()\n\n                for (pcInt in indexSet) es.add(elements[pcInt])\n\n                elements = es\n\n            }\n\n            return elements //返回筛选结果\n\n        }\n\n        private fun findIndexSet(rule: String) {\n\n            val rus = rule.trim { it <= ' ' }\n\n            var len = rus.length\n            var curInt: Int? //当前数字\n            var curMinus = false //当前数字是否为负\n            val curList = mutableListOf<Int?>() //当前数字区间\n            var l = \"\" //暂存数字字符串\n\n            val head = rus.last() == ']' //是否为常规索引写法\n\n            if (head) { //常规索引写法[index...]\n\n                len-- //跳过尾部']'\n\n                while (len-- >= 0) { //逆向遍历,可以无前置规则\n\n                    var rl = rus[len]\n                    if (rl == ' ') continue //跳过空格\n\n                    if (rl in '0'..'9') l = rl + l //将数值累接入临时字串中，遇到分界符才取出\n                    else if (rl == '-') curMinus = true\n                    else {\n\n                        curInt =\n                            if (l.isEmpty()) null else if (curMinus) -l.toInt() else l.toInt() //当前数字\n\n                        when (rl) {\n\n                            ':' -> curList.add(curInt) //区间右端或区间间隔\n\n                            else -> {\n\n                                //为保证查找顺序，区间和单个索引都添加到同一集合\n                                if (curList.isEmpty()) {\n\n                                    if (curInt == null) break //是jsoup选择器而非索引列表，跳出\n\n                                    indexes.add(curInt)\n                                } else {\n\n                                    //列表最后压入的是区间右端，若列表有两位则最先压入的是间隔\n                                    indexes.add(\n                                        Triple(\n                                            curInt,\n                                            curList.last(),\n                                            if (curList.size == 2) curList.first() else 1\n                                        )\n                                    )\n\n                                    curList.clear() //重置临时列表，避免影响到下个区间的处理\n\n                                }\n\n                                if (rl == '!') {\n                                    split = '!'\n                                    do {\n                                        rl = rus[--len]\n                                    } while (len > 0 && rl == ' ')//跳过所有空格\n                                }\n\n                                if (rl == '[') {\n                                    beforeRule = rus.substring(0, len) //遇到索引边界，返回结果\n                                    return\n                                }\n\n                                if (rl != ',') break //非索引结构，跳出\n\n                            }\n                        }\n\n                        l = \"\" //清空\n                        curMinus = false //重置\n                    }\n                }\n            } else while (len-- >= 0) { //阅读原本写法，逆向遍历,可以无前置规则\n\n                val rl = rus[len]\n                if (rl == ' ') continue //跳过空格\n\n                if (rl in '0'..'9') l = rl + l //将数值累接入临时字串中，遇到分界符才取出\n                else if (rl == '-') curMinus = true\n                else {\n\n                    if (rl == '!' || rl == '.' || rl == ':') { //分隔符或起始符\n\n                        indexDefault.add(if (curMinus) -l.toInt() else l.toInt()) // 当前数字追加到列表\n\n                        if (rl != ':') { //rl == '!'  || rl == '.'\n                            split = rl\n                            beforeRule = rus.substring(0, len)\n                            return\n                        }\n\n                    } else break //非索引结构，跳出循环\n\n                    l = \"\" //清空\n                    curMinus = false //重置\n                }\n\n            }\n\n            split = ' '\n            beforeRule = rus\n        }\n    }\n\n\n    internal inner class SourceRule(ruleStr: String) {\n        var isCss = false\n        var elementsRule: String = if (ruleStr.startsWith(\"@CSS:\", true)) {\n            isCss = true\n            ruleStr.substring(5).trim { it <= ' ' }\n        } else {\n            ruleStr\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt",
    "content": "package io.legado.app.model.analyzeRule\n\nimport java.util.*\nimport java.util.regex.Pattern\n\nobject AnalyzeByRegex {\n\n    fun getElement(res: String, regs: Array<String>, index: Int = 0): List<String>? {\n        var vIndex = index\n        val resM = Pattern.compile(regs[vIndex]).matcher(res)\n        if (!resM.find()) {\n            return null\n        }\n        // 判断索引的规则是最后一个规则\n        return if (vIndex + 1 == regs.size) {\n            // 新建容器\n            val info = arrayListOf<String>()\n            for (groupIndex in 0..resM.groupCount()) {\n                info.add(resM.group(groupIndex)!!)\n            }\n            info\n        } else {\n            val result = StringBuilder()\n            do {\n                result.append(resM.group())\n            } while (resM.find())\n            getElement(result.toString(), regs, ++vIndex)\n        }\n    }\n\n    fun getElements(res: String, regs: Array<String>, index: Int = 0): List<List<String>> {\n        var vIndex = index\n        val resM = Pattern.compile(regs[vIndex]).matcher(res)\n        if (!resM.find()) {\n            return arrayListOf()\n        }\n        // 判断索引的规则是最后一个规则\n        if (vIndex + 1 == regs.size) {\n            // 创建书息缓存数组\n            val books = ArrayList<List<String>>()\n            // 提取列表\n            do {\n                // 新建容器\n                val info = arrayListOf<String>()\n                for (groupIndex in 0..resM.groupCount()) {\n                    info.add(resM.group(groupIndex) ?: \"\")\n                }\n                books.add(info)\n            } while (resM.find())\n            return books\n        } else {\n            val result = StringBuilder()\n            do {\n                result.append(resM.group())\n            } while (resM.find())\n            return getElements(result.toString(), regs, ++vIndex)\n        }\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt",
    "content": "package io.legado.app.model.analyzeRule\n\nimport io.legado.app.utils.splitNotBlank\nimport io.legado.app.utils.TextUtils\nimport org.jsoup.nodes.Document\nimport org.jsoup.nodes.Element\nimport org.jsoup.select.Elements\nimport org.seimicrawler.xpath.JXDocument\nimport org.seimicrawler.xpath.JXNode\nimport java.util.*\n\nclass AnalyzeByXPath(doc: Any) {\n    private var jxNode: Any = parse(doc)\n\n    private fun parse(doc: Any): Any {\n        return when (doc) {\n            is JXNode -> if (doc.isElement) doc else strToJXDocument(doc.toString())\n            is Document -> JXDocument.create(doc)\n            is Element -> JXDocument.create(Elements(doc))\n            is Elements -> JXDocument.create(doc)\n            else -> strToJXDocument(doc.toString())\n        }\n    }\n\n    private fun strToJXDocument(html: String): JXDocument {\n        var html1 = html\n        if (html1.endsWith(\"</td>\")) {\n            html1 = \"<tr>${html1}</tr>\"\n        }\n        if (html1.endsWith(\"</tr>\") || html1.endsWith(\"</tbody>\")) {\n            html1 = \"<table>${html1}</table>\"\n        }\n        return JXDocument.create(html1)\n    }\n\n    private fun getResult(xPath: String): List<JXNode>? {\n        val node = jxNode\n        return if (node is JXNode) {\n            node.sel(xPath)\n        } else {\n            (node as JXDocument).selN(xPath)\n        }\n    }\n\n    internal fun getElements(xPath: String): List<JXNode>? {\n\n        if (xPath.isEmpty()) return null\n\n        val jxNodes = ArrayList<JXNode>()\n        val ruleAnalyzes = RuleAnalyzer(xPath)\n        val rules = ruleAnalyzes.splitRule(\"&&\", \"||\", \"%%\")\n\n        if (rules.size == 1) {\n            return getResult(rules[0])\n        } else {\n            val results = ArrayList<List<JXNode>>()\n            for (rl in rules) {\n                val temp = getElements(rl)\n                if (temp != null && temp.isNotEmpty()) {\n                    results.add(temp)\n                    if (temp.isNotEmpty() && ruleAnalyzes.elementsType == \"||\") {\n                        break\n                    }\n                }\n            }\n            if (results.size > 0) {\n                if (\"%%\" == ruleAnalyzes.elementsType) {\n                    for (i in results[0].indices) {\n                        for (temp in results) {\n                            if (i < temp.size) {\n                                jxNodes.add(temp[i])\n                            }\n                        }\n                    }\n                } else {\n                    for (temp in results) {\n                        jxNodes.addAll(temp)\n                    }\n                }\n            }\n        }\n        return jxNodes\n    }\n\n    internal fun getStringList(xPath: String): List<String> {\n\n        val result = ArrayList<String>()\n        val ruleAnalyzes = RuleAnalyzer(xPath)\n        val rules = ruleAnalyzes.splitRule(\"&&\", \"||\", \"%%\")\n\n        if (rules.size == 1) {\n            getResult(xPath)?.map {\n                result.add(it.asString())\n            }\n            return result\n        } else {\n            val results = ArrayList<List<String>>()\n            for (rl in rules) {\n                val temp = getStringList(rl)\n                if (temp.isNotEmpty()) {\n                    results.add(temp)\n                    if (temp.isNotEmpty() && ruleAnalyzes.elementsType == \"||\") {\n                        break\n                    }\n                }\n            }\n            if (results.size > 0) {\n                if (\"%%\" == ruleAnalyzes.elementsType) {\n                    for (i in results[0].indices) {\n                        for (temp in results) {\n                            if (i < temp.size) {\n                                result.add(temp[i])\n                            }\n                        }\n                    }\n                } else {\n                    for (temp in results) {\n                        result.addAll(temp)\n                    }\n                }\n            }\n        }\n        return result\n    }\n\n    fun getString(rule: String): String? {\n        val ruleAnalyzes = RuleAnalyzer(rule)\n        val rules = ruleAnalyzes.splitRule(\"&&\", \"||\")\n        if (rules.size == 1) {\n            getResult(rule)?.let {\n                return TextUtils.join(\"\\n\", it)\n            }\n            return null\n        } else {\n            val textList = arrayListOf<String>()\n            for (rl in rules) {\n                val temp = getString(rl)\n                if (!temp.isNullOrEmpty()) {\n                    textList.add(temp)\n                    if (ruleAnalyzes.elementsType == \"||\") {\n                        break\n                    }\n                }\n            }\n            return textList.joinToString(\"\\n\")\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt",
    "content": "package io.legado.app.model.analyzeRule\n\nimport com.script.SimpleBindings\nimport io.legado.app.constant.AppConst.SCRIPT_ENGINE\nimport io.legado.app.constant.AppPattern.JS_PATTERN\nimport io.legado.app.data.entities.BaseBook\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.BaseSource\nimport io.legado.app.help.CacheManager\nimport io.legado.app.help.JsExtensions\nimport io.legado.app.help.http.CookieStore\nimport io.legado.app.utils.*\nimport kotlinx.coroutines.runBlocking\nimport org.jsoup.nodes.Entities\nimport org.mozilla.javascript.NativeObject\nimport java.net.URL\nimport java.util.*\nimport java.util.regex.Pattern\nimport kotlin.collections.HashMap\nimport mu.KotlinLogging\nimport io.legado.app.model.analyzeRule.RuleDataInterface\nimport io.legado.app.model.webBook.WebBook\n\nprivate val logger = KotlinLogging.logger {}\n\n/**\n * 解析规则获取结果\n */\n@Suppress(\"unused\", \"RegExpRedundantEscape\")\nclass AnalyzeRule(\n    var ruleData: RuleDataInterface,\n    private val source: BaseSource? = null\n) : JsExtensions {\n\n    val book get() = ruleData as? BaseBook\n\n    var chapter: BookChapter? = null\n    var nextChapterUrl: String? = null\n    var content: Any? = null\n        private set\n    var baseUrl: String? = null\n        private set\n    var redirectUrl: URL? = null\n        private set\n    private var isJSON: Boolean = false\n    private var isRegex: Boolean = false\n\n    private var analyzeByXPath: AnalyzeByXPath? = null\n    private var analyzeByJSoup: AnalyzeByJSoup? = null\n    private var analyzeByJSonPath: AnalyzeByJSonPath? = null\n\n    private var objectChangedXP = false\n    private var objectChangedJS = false\n    private var objectChangedJP = false\n\n    @JvmOverloads\n    fun setContent(content: Any?, baseUrl: String? = null): AnalyzeRule {\n        if (content == null) throw AssertionError(\"内容不可空（Content cannot be null）\")\n        this.content = content\n        isJSON = content.toString().isJson()\n        setBaseUrl(baseUrl)\n        objectChangedXP = true\n        objectChangedJS = true\n        objectChangedJP = true\n        return this\n    }\n\n    fun setBaseUrl(baseUrl: String?): AnalyzeRule {\n        baseUrl?.let {\n            this.baseUrl = baseUrl\n        }\n        return this\n    }\n\n    fun setRedirectUrl(url: String): URL? {\n        try {\n            redirectUrl = URL(url)\n        } catch (e: Exception) {\n            log(\"URL($url) error\\n${e.localizedMessage}\")\n        }\n        return redirectUrl\n    }\n\n    /**\n     * 获取XPath解析类\n     */\n    private fun getAnalyzeByXPath(o: Any): AnalyzeByXPath {\n        return if (o != content) {\n            AnalyzeByXPath(o)\n        } else {\n            if (analyzeByXPath == null || objectChangedXP) {\n                analyzeByXPath = AnalyzeByXPath(content!!)\n                objectChangedXP = false\n            }\n            analyzeByXPath!!\n        }\n    }\n\n    /**\n     * 获取JSOUP解析类\n     */\n    private fun getAnalyzeByJSoup(o: Any): AnalyzeByJSoup {\n        return if (o != content) {\n            AnalyzeByJSoup(o)\n        } else {\n            if (analyzeByJSoup == null || objectChangedJS) {\n                analyzeByJSoup = AnalyzeByJSoup(content!!)\n                objectChangedJS = false\n            }\n            analyzeByJSoup!!\n        }\n    }\n\n    /**\n     * 获取JSON解析类\n     */\n    private fun getAnalyzeByJSonPath(o: Any): AnalyzeByJSonPath {\n        return if (o != content) {\n            AnalyzeByJSonPath(o)\n        } else {\n            if (analyzeByJSonPath == null || objectChangedJP) {\n                analyzeByJSonPath = AnalyzeByJSonPath(content!!)\n                objectChangedJP = false\n            }\n            analyzeByJSonPath!!\n        }\n    }\n\n    /**\n     * 获取文本列表\n     */\n    @JvmOverloads\n    fun getStringList(rule: String?, mContent: Any? = null, isUrl: Boolean = false): List<String>? {\n        if (rule.isNullOrEmpty()) return null\n        val ruleList = splitSourceRule(rule, false)\n        return getStringList(ruleList, mContent, isUrl)\n    }\n\n    @JvmOverloads\n    fun getStringList(\n        ruleList: List<SourceRule>,\n        mContent: Any? = null,\n        isUrl: Boolean = false\n    ): List<String>? {\n        var result: Any? = null\n        val content = mContent ?: this.content\n        if (content != null && ruleList.isNotEmpty()) {\n            result = content\n            if (content is NativeObject) {\n                result = content[ruleList[0].rule]?.toString()\n            } else {\n                for (sourceRule in ruleList) {\n                    putRule(sourceRule.putMap)\n                    sourceRule.makeUpRule(result)\n                    result?.let {\n                        if (sourceRule.rule.isNotEmpty()) {\n                            result = when (sourceRule.mode) {\n                                Mode.Js -> evalJS(sourceRule.rule, result)\n                                Mode.Json -> getAnalyzeByJSonPath(it).getStringList(sourceRule.rule)\n                                Mode.XPath -> getAnalyzeByXPath(it).getStringList(sourceRule.rule)\n                                Mode.Default -> getAnalyzeByJSoup(it).getStringList(sourceRule.rule)\n                                else -> sourceRule.rule\n                            }\n                        }\n                        if (sourceRule.replaceRegex.isNotEmpty() && result is List<*>) {\n                            val newList = ArrayList<String>()\n                            for (item in result as List<*>) {\n                                newList.add(replaceRegex(item.toString(), sourceRule))\n                            }\n                            result = newList\n                        } else if (sourceRule.replaceRegex.isNotEmpty()) {\n                            result = replaceRegex(result.toString(), sourceRule)\n                        }\n                    }\n                }\n            }\n        }\n        if (result == null) return null\n        if (result is String) {\n            result = (result as String).split(\"\\n\")\n        }\n        if (isUrl) {\n            val urlList = ArrayList<String>()\n            if (result is List<*>) {\n                for (url in result as List<*>) {\n                    val absoluteURL = NetworkUtils.getAbsoluteURL(redirectUrl, url.toString())\n                    if (absoluteURL.isNotEmpty() && !urlList.contains(absoluteURL)) {\n                        urlList.add(absoluteURL)\n                    }\n                }\n            }\n            return urlList\n        }\n        @Suppress(\"UNCHECKED_CAST\")\n        return result as? List<String>\n    }\n\n    /**\n     * 获取文本\n     */\n    @JvmOverloads\n    fun getString(ruleStr: String?, mContent: Any? = null, isUrl: Boolean = false): String {\n        if (TextUtils.isEmpty(ruleStr)) return \"\"\n        val ruleList = splitSourceRule(ruleStr)\n        return getString(ruleList, mContent, isUrl)\n    }\n\n    @JvmOverloads\n    fun getString(\n        ruleList: List<SourceRule>,\n        mContent: Any? = null,\n        isUrl: Boolean = false\n    ): String {\n        var result: Any? = null\n        val content = mContent ?: this.content\n        if (content != null && ruleList.isNotEmpty()) {\n            result = content\n            if (result is NativeObject) {\n                result = result[ruleList[0].rule]?.toString()\n            } else {\n                for (sourceRule in ruleList) {\n                    putRule(sourceRule.putMap)\n                    sourceRule.makeUpRule(result)\n                    result?.let {\n                        if (sourceRule.rule.isNotBlank() || sourceRule.replaceRegex.isEmpty()) {\n                            result = when (sourceRule.mode) {\n                                Mode.Js -> evalJS(sourceRule.rule, it)\n                                Mode.Json -> getAnalyzeByJSonPath(it).getString(sourceRule.rule)\n                                Mode.XPath -> getAnalyzeByXPath(it).getString(sourceRule.rule)\n                                Mode.Default -> if (isUrl) {\n                                    getAnalyzeByJSoup(it).getString0(sourceRule.rule)\n                                } else {\n                                    getAnalyzeByJSoup(it).getString(sourceRule.rule)\n                                }\n                                else -> sourceRule.rule\n                            }\n                        }\n                        if ((result != null) && sourceRule.replaceRegex.isNotEmpty()) {\n                            result = replaceRegex(result.toString(), sourceRule)\n                        }\n                    }\n                }\n            }\n        }\n        if (result == null) result = \"\"\n        val str = kotlin.runCatching {\n            Entities.unescape(result.toString())\n        }.onFailure {\n            log(\"Entities.unescape() error\\n${it.localizedMessage}\")\n        }.getOrElse {\n            result.toString()\n        }\n        if (isUrl) {\n            return if (str.isBlank()) {\n                baseUrl ?: \"\"\n            } else {\n                NetworkUtils.getAbsoluteURL(redirectUrl, str)\n            }\n        }\n        return str\n    }\n\n    /**\n     * 获取Element\n     */\n    fun getElement(ruleStr: String): Any? {\n        if (TextUtils.isEmpty(ruleStr)) return null\n        var result: Any? = null\n        val content = this.content\n        val ruleList = splitSourceRule(ruleStr, true)\n        if (content != null && ruleList.isNotEmpty()) {\n            result = content\n            for (sourceRule in ruleList) {\n                putRule(sourceRule.putMap)\n                sourceRule.makeUpRule(result)\n                result?.let {\n                    result = when (sourceRule.mode) {\n                        Mode.Regex -> AnalyzeByRegex.getElement(\n                            result.toString(),\n                            sourceRule.rule.splitNotBlank(\"&&\")\n                        )\n                        Mode.Js -> evalJS(sourceRule.rule, it)\n                        Mode.Json -> getAnalyzeByJSonPath(it).getObject(sourceRule.rule)\n                        Mode.XPath -> getAnalyzeByXPath(it).getElements(sourceRule.rule)\n                        else -> getAnalyzeByJSoup(it).getElements(sourceRule.rule)\n                    }\n                    if (sourceRule.replaceRegex.isNotEmpty()) {\n                        result = replaceRegex(result.toString(), sourceRule)\n                    }\n                }\n            }\n        }\n        return result\n    }\n\n    /**\n     * 获取列表\n     */\n    @Suppress(\"UNCHECKED_CAST\")\n    fun getElements(ruleStr: String): List<Any> {\n        var result: Any? = null\n        val content = this.content\n        val ruleList = splitSourceRule(ruleStr, true)\n        if (content != null && ruleList.isNotEmpty()) {\n            result = content\n            for (sourceRule in ruleList) {\n                putRule(sourceRule.putMap)\n                result?.let {\n                    result = when (sourceRule.mode) {\n                        Mode.Regex -> AnalyzeByRegex.getElements(\n                            result.toString(),\n                            sourceRule.rule.splitNotBlank(\"&&\")\n                        )\n                        Mode.Js -> evalJS(sourceRule.rule, result)\n                        Mode.Json -> getAnalyzeByJSonPath(it).getList(sourceRule.rule)\n                        Mode.XPath -> getAnalyzeByXPath(it).getElements(sourceRule.rule)\n                        else -> getAnalyzeByJSoup(it).getElements(sourceRule.rule)\n                    }\n                    if (sourceRule.replaceRegex.isNotEmpty()) {\n                        result = replaceRegex(result.toString(), sourceRule)\n                    }\n                }\n            }\n        }\n        result?.let {\n            return it as List<Any>\n        }\n        return ArrayList()\n    }\n\n    /**\n     * 保存变量\n     */\n    private fun putRule(map: Map<String, String>) {\n        for ((key, value) in map) {\n            put(key, getString(value))\n        }\n    }\n\n    /**\n     * 分离put规则\n     */\n    private fun splitPutRule(ruleStr: String, putMap: HashMap<String, String>): String {\n        var vRuleStr = ruleStr\n        val putMatcher = putPattern.matcher(vRuleStr)\n        while (putMatcher.find()) {\n            vRuleStr = vRuleStr.replace(putMatcher.group(), \"\")\n            GSON.fromJsonObject<Map<String, String>>(putMatcher.group(1))\n                .getOrNull()\n                ?.let {\n                    putMap.putAll(it)\n                }\n        }\n        return vRuleStr\n    }\n\n    /**\n     * 正则替换\n     */\n    private fun replaceRegex(result: String, rule: SourceRule): String {\n        if (rule.replaceRegex.isEmpty()) return result\n        var vResult = result\n        vResult = if (rule.replaceFirst) {\n            kotlin.runCatching {\n                val pattern = Pattern.compile(rule.replaceRegex)\n                val matcher = pattern.matcher(vResult)\n                if (matcher.find()) {\n                    matcher.group(0)!!.replaceFirst(rule.replaceRegex.toRegex(), rule.replacement)\n                } else {\n                    \"\"\n                }\n            }.getOrElse {\n                vResult.replaceFirst(rule.replaceRegex, rule.replacement)\n            }\n        } else {\n            kotlin.runCatching {\n                vResult.replace(rule.replaceRegex.toRegex(), rule.replacement)\n            }.getOrElse {\n                vResult.replace(rule.replaceRegex, rule.replacement)\n            }\n        }\n        return vResult\n    }\n\n    /**\n     * 分解规则生成规则列表\n     */\n    fun splitSourceRule(ruleStr: String?, allInOne: Boolean = false): List<SourceRule> {\n        if (ruleStr.isNullOrEmpty()) return emptyList()\n        val ruleList = ArrayList<SourceRule>()\n        var mMode: Mode = Mode.Default\n        var start = 0\n        //仅首字符为:时为AllInOne，其实:与伪类选择器冲突，建议改成?更合理\n        if (allInOne && ruleStr.startsWith(\":\")) {\n            mMode = Mode.Regex\n            isRegex = true\n            start = 1\n        } else if (isRegex) {\n            mMode = Mode.Regex\n        }\n        var tmp: String\n        val jsMatcher = JS_PATTERN.matcher(ruleStr)\n        while (jsMatcher.find()) {\n            if (jsMatcher.start() > start) {\n                tmp = ruleStr.substring(start, jsMatcher.start()).trim { it <= ' ' }\n                if (tmp.isNotEmpty()) {\n                    ruleList.add(SourceRule(tmp, mMode))\n                }\n            }\n            ruleList.add(SourceRule(jsMatcher.group(2) ?: jsMatcher.group(1), Mode.Js))\n            start = jsMatcher.end()\n        }\n\n        if (ruleStr.length > start) {\n            tmp = ruleStr.substring(start).trim { it <= ' ' }\n            if (tmp.isNotEmpty()) {\n                ruleList.add(SourceRule(tmp, mMode))\n            }\n        }\n\n        return ruleList\n    }\n\n    /**\n     * 规则类\n     */\n    inner class SourceRule internal constructor(\n        ruleStr: String,\n        internal var mode: Mode = Mode.Default\n    ) {\n        internal var rule: String\n        internal var replaceRegex = \"\"\n        internal var replacement = \"\"\n        internal var replaceFirst = false\n        internal val putMap = HashMap<String, String>()\n        private val ruleParam = ArrayList<String>()\n        private val ruleType = ArrayList<Int>()\n        private val getRuleType = -2\n        private val jsRuleType = -1\n        private val defaultRuleType = 0\n\n        init {\n            rule = when {\n                mode == Mode.Js || mode == Mode.Regex -> ruleStr\n                ruleStr.startsWith(\"@CSS:\", true) -> {\n                    mode = Mode.Default\n                    ruleStr\n                }\n                ruleStr.startsWith(\"@@\") -> {\n                    mode = Mode.Default\n                    ruleStr.substring(2)\n                }\n                ruleStr.startsWith(\"@XPath:\", true) -> {\n                    mode = Mode.XPath\n                    ruleStr.substring(7)\n                }\n                ruleStr.startsWith(\"@Json:\", true) -> {\n                    mode = Mode.Json\n                    ruleStr.substring(6)\n                }\n                isJSON || ruleStr.startsWith(\"$.\") || ruleStr.startsWith(\"$[\") -> {\n                    mode = Mode.Json\n                    ruleStr\n                }\n                ruleStr.startsWith(\"/\") -> {//XPath特征很明显,无需配置单独的识别标头\n                    mode = Mode.XPath\n                    ruleStr\n                }\n                else -> ruleStr\n            }\n            //分离put\n            rule = splitPutRule(rule, putMap)\n            //@get,{{ }}, 拆分\n            var start = 0\n            var tmp: String\n            val evalMatcher = evalPattern.matcher(rule)\n\n            if (evalMatcher.find()) {\n                tmp = rule.substring(start, evalMatcher.start())\n                if (mode != Mode.Js && mode != Mode.Regex &&\n                    (evalMatcher.start() == 0 || !tmp.contains(\"##\"))\n                ) {\n                    mode = Mode.Regex\n                }\n                do {\n                    if (evalMatcher.start() > start) {\n                        tmp = rule.substring(start, evalMatcher.start())\n                        splitRegex(tmp)\n                    }\n                    tmp = evalMatcher.group()\n                    when {\n                        tmp.startsWith(\"@get:\", true) -> {\n                            ruleType.add(getRuleType)\n                            ruleParam.add(tmp.substring(6, tmp.lastIndex))\n                        }\n                        tmp.startsWith(\"{{\") -> {\n                            ruleType.add(jsRuleType)\n                            ruleParam.add(tmp.substring(2, tmp.length - 2))\n                        }\n                        else -> {\n                            splitRegex(tmp)\n                        }\n                    }\n                    start = evalMatcher.end()\n                } while (evalMatcher.find())\n            }\n            if (rule.length > start) {\n                tmp = rule.substring(start)\n                splitRegex(tmp)\n            }\n        }\n\n        /**\n         * 拆分\\$\\d{1,2}\n         */\n        private fun splitRegex(ruleStr: String) {\n            var start = 0\n            var tmp: String\n            val ruleStrArray = ruleStr.split(\"##\")\n            val regexMatcher = regexPattern.matcher(ruleStrArray[0])\n\n            if (regexMatcher.find()) {\n                if (mode != Mode.Js && mode != Mode.Regex) {\n                    mode = Mode.Regex\n                }\n                do {\n                    if (regexMatcher.start() > start) {\n                        tmp = ruleStr.substring(start, regexMatcher.start())\n                        ruleType.add(defaultRuleType)\n                        ruleParam.add(tmp)\n                    }\n                    tmp = regexMatcher.group()\n                    ruleType.add(tmp.substring(1).toInt())\n                    ruleParam.add(tmp)\n                    start = regexMatcher.end()\n                } while (regexMatcher.find())\n            }\n            if (ruleStr.length > start) {\n                tmp = ruleStr.substring(start)\n                ruleType.add(defaultRuleType)\n                ruleParam.add(tmp)\n            }\n        }\n\n        /**\n         * 替换@get,{{ }}\n         */\n        fun makeUpRule(result: Any?) {\n            val infoVal = StringBuilder()\n            if (ruleParam.isNotEmpty()) {\n                var index = ruleParam.size\n                while (index-- > 0) {\n                    val regType = ruleType[index]\n                    when {\n                        regType > defaultRuleType -> {\n                            @Suppress(\"UNCHECKED_CAST\")\n                            (result as? List<String?>)?.run {\n                                if (this.size > regType) {\n                                    this[regType]?.let {\n                                        infoVal.insert(0, it)\n                                    }\n                                }\n                            } ?: infoVal.insert(0, ruleParam[index])\n                        }\n                        regType == jsRuleType -> {\n                            if (isRule(ruleParam[index])) {\n                                getString(arrayListOf(SourceRule(ruleParam[index]))).let {\n                                    infoVal.insert(0, it)\n                                }\n                            } else {\n                                val jsEval: Any? = evalJS(ruleParam[index], result)\n                                when {\n                                    jsEval == null -> Unit\n                                    jsEval is String -> infoVal.insert(0, jsEval)\n                                    jsEval is Double && jsEval % 1.0 == 0.0 -> infoVal.insert(\n                                        0,\n                                        String.format(\"%.0f\", jsEval)\n                                    )\n                                    else -> infoVal.insert(0, jsEval.toString())\n                                }\n                            }\n                        }\n                        regType == getRuleType -> {\n                            infoVal.insert(0, get(ruleParam[index]))\n                        }\n                        else -> infoVal.insert(0, ruleParam[index])\n                    }\n                }\n                rule = infoVal.toString()\n            }\n            //分离正则表达式\n            val ruleStrS = rule.split(\"##\")\n            rule = ruleStrS[0].trim()\n            if (ruleStrS.size > 1) {\n                replaceRegex = ruleStrS[1]\n            }\n            if (ruleStrS.size > 2) {\n                replacement = ruleStrS[2]\n            }\n            if (ruleStrS.size > 3) {\n                replaceFirst = true\n            }\n        }\n\n        private fun isRule(ruleStr: String): Boolean {\n            return ruleStr.startsWith('@') //js首个字符不可能是@，除非是装饰器，所以@开头规定为规则\n                    || ruleStr.startsWith(\"$.\")\n                    || ruleStr.startsWith(\"$[\")\n                    || ruleStr.startsWith(\"//\")\n        }\n    }\n\n    enum class Mode {\n        XPath, Json, Default, Js, Regex\n    }\n\n    fun put(key: String, value: String): String {\n        chapter?.putVariable(key, value)\n            ?: book?.putVariable(key, value)\n            ?: ruleData.putVariable(key, value)\n        return value\n    }\n\n    fun get(key: String): String {\n        when (key) {\n            \"bookName\" -> book?.let {\n                return it.name\n            }\n            \"title\" -> chapter?.let {\n                return it.title\n            }\n        }\n        return chapter?.getVariable(key)\n            ?: book?.getVariable(key)\n            ?: ruleData?.getVariable(key)\n            ?: \"\"\n    }\n\n    /**\n     * 执行JS\n     */\n    fun evalJS(jsStr: String, result: Any?): Any? {\n        val bindings = SimpleBindings()\n        bindings[\"java\"] = this\n        bindings[\"cookie\"] = CookieStore\n        bindings[\"cache\"] = CacheManager\n        bindings[\"source\"] = source\n        bindings[\"book\"] = book\n        bindings[\"result\"] = result\n        bindings[\"baseUrl\"] = baseUrl\n        bindings[\"chapter\"] = chapter\n        bindings[\"title\"] = chapter?.title\n        bindings[\"src\"] = content\n        bindings[\"nextChapterUrl\"] = nextChapterUrl\n        return SCRIPT_ENGINE.eval(jsStr, bindings)\n    }\n\n    override fun getSource(): BaseSource? {\n        return source\n    }\n\n    /**\n     * js实现跨域访问,不能删\n     */\n    override fun ajax(urlStr: String): String? {\n        return runBlocking {\n            kotlin.runCatching {\n                val analyzeUrl = AnalyzeUrl(urlStr, source = source, ruleData = book)\n                analyzeUrl.getStrResponseAwait().body\n            }.onFailure {\n                log(\"ajax(${urlStr}) error\\n${it.stackTraceToString()}\")\n                // it.printStackTrace()\n            }.getOrElse {\n                it.msg\n            }\n        }\n    }\n\n    /**\n     * 章节数转数字\n     */\n    fun toNumChapter(s: String?): String? {\n        s ?: return null\n        val matcher = titleNumPattern.matcher(s)\n        if (matcher.find()) {\n            return \"${matcher.group(1)}${StringUtils.stringToInt(matcher.group(2))}${matcher.group(3)}\"\n        }\n        return s\n    }\n\n    /**\n     * 更新BookUrl,如果搜索结果有tocUrl也会更新,有些书源bookUrl定期更新,可以在js内调用更新\n     */\n    fun refreshBookUrl() {\n        runBlocking {\n            val bookSource = source as? BookSource\n            val book = book as? Book\n            if (bookSource == null || book == null) return@runBlocking\n            val books = WebBook(bookSource).searchBook(book.name)\n            books.forEach {\n                if (it.name == book.name && it.author == book.author) {\n                    book.bookUrl = it.bookUrl\n                    if (it.tocUrl.isNotBlank()) {\n                        book.tocUrl = it.tocUrl\n                    }\n                    return@runBlocking\n                }\n            }\n        }\n    }\n\n    /**\n     * 更新tocUrl,有些书源目录url定期更新,可以在js调用更新\n     */\n    fun refreshTocUrl() {\n        runBlocking {\n            val bookSource = source as? BookSource\n            val book = book as? Book\n            if (bookSource == null || book == null) return@runBlocking\n            WebBook(bookSource).getBookInfo(book)\n        }\n    }\n\n    companion object {\n        private val putPattern = Pattern.compile(\"@put:(\\\\{[^}]+?\\\\})\", Pattern.CASE_INSENSITIVE)\n        private val evalPattern =\n            Pattern.compile(\"@get:\\\\{[^}]+?\\\\}|\\\\{\\\\{[\\\\w\\\\W]*?\\\\}\\\\}\", Pattern.CASE_INSENSITIVE)\n        private val regexPattern = Pattern.compile(\"\\\\$\\\\d{1,2}\")\n        private val titleNumPattern = Pattern.compile(\"(第)(.+?)(章)\")\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt",
    "content": "package io.legado.app.model.analyzeRule\n\nimport com.script.SimpleBindings\nimport io.legado.app.constant.AppConst\nimport io.legado.app.constant.AppConst.SCRIPT_ENGINE\nimport io.legado.app.constant.AppConst.UA_NAME\nimport io.legado.app.constant.AppPattern.JS_PATTERN\nimport io.legado.app.constant.AppPattern.dataUriRegex\nimport io.legado.app.data.entities.BaseSource\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.exception.ConcurrentException\nimport io.legado.app.help.CacheManager\nimport io.legado.app.help.JsExtensions\nimport io.legado.app.help.http.*\nimport io.legado.app.utils.*\nimport kotlinx.coroutines.runBlocking\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport okhttp3.Response\nimport java.net.URLEncoder\nimport java.util.regex.Pattern\nimport io.legado.app.model.DebugLog\n\n/**\n * Created by GKF on 2018/1/24.\n * 搜索URL规则解析\n */\nclass AnalyzeUrl(\n    val mUrl: String,\n    val key: String? = null,\n    val page: Int? = null,\n    val speakText: String? = null,\n    val speakSpeed: Int? = null,\n    var baseUrl: String = \"\",\n    private val source: BaseSource? = null,\n    private val ruleData: RuleDataInterface? = null,\n    private val chapter: BookChapter? = null,\n    headerMapF: Map<String, String>? = null,\n) : JsExtensions {\n    companion object {\n        val paramPattern: Pattern = Pattern.compile(\"\\\\s*,\\\\s*(?=\\\\{)\")\n        private val pagePattern = Pattern.compile(\"<(.*?)>\")\n        private val concurrentRecordMap = hashMapOf<String, ConcurrentRecord>()\n    }\n\n    var ruleUrl = \"\"\n        private set\n    var url: String = \"\"\n        private set\n    var body: String? = null\n        private set\n    var type: String? = null\n        private set\n    val headerMap = HashMap<String, String>()\n    private var urlNoQuery: String = \"\"\n    private var queryStr: String? = null\n    private val fieldMap = LinkedHashMap<String, String>()\n    private var charset: String? = null\n    private var method = RequestMethod.GET\n    private var proxy: String? = null\n    private var retry: Int = 0\n    private var useWebView: Boolean = false\n    private var webJs: String? = null\n\n    init {\n        if (!mUrl.isDataUrl()) {\n            val urlMatcher = paramPattern.matcher(baseUrl)\n            if (urlMatcher.find()) baseUrl = baseUrl.substring(0, urlMatcher.start())\n            (headerMapF ?: source?.getHeaderMap(true))?.let {\n                headerMap.putAll(it)\n                if (it.containsKey(\"proxy\")) {\n                    proxy = it[\"proxy\"]\n                    headerMap.remove(\"proxy\")\n                }\n            }\n            initUrl()\n        }\n    }\n\n    /**\n     * 处理url\n     */\n    fun initUrl() {\n        ruleUrl = mUrl\n        //执行@js,<js></js>\n        analyzeJs()\n        //替换参数\n        replaceKeyPageJs()\n        //处理URL\n        analyzeUrl()\n    }\n\n    /**\n     * 执行@js,<js></js>\n     */\n    private fun analyzeJs() {\n        var start = 0\n        var tmp: String\n        val jsMatcher = JS_PATTERN.matcher(ruleUrl)\n        while (jsMatcher.find()) {\n            if (jsMatcher.start() > start) {\n                tmp =\n                    ruleUrl.substring(start, jsMatcher.start()).trim { it <= ' ' }\n                if (tmp.isNotEmpty()) {\n                    ruleUrl = tmp.replace(\"@result\", ruleUrl)\n                }\n            }\n            ruleUrl = evalJS(jsMatcher.group(2) ?: jsMatcher.group(1), ruleUrl) as String\n            start = jsMatcher.end()\n        }\n        if (ruleUrl.length > start) {\n            tmp = ruleUrl.substring(start).trim { it <= ' ' }\n            if (tmp.isNotEmpty()) {\n                ruleUrl = tmp.replace(\"@result\", ruleUrl)\n            }\n        }\n    }\n\n    /**\n     * 替换关键字,页数,JS\n     */\n    private fun replaceKeyPageJs() { //先替换内嵌规则再替换页数规则，避免内嵌规则中存在大于小于号时，规则被切错\n        //js\n        if (ruleUrl.contains(\"{{\") && ruleUrl.contains(\"}}\")) {\n            val analyze = RuleAnalyzer(ruleUrl) //创建解析\n            //替换所有内嵌{{js}}\n            val url = analyze.innerRule(\"{{\", \"}}\") {\n                val jsEval = evalJS(it) ?: \"\"\n                when {\n                    jsEval is String -> jsEval\n                    jsEval is Double && jsEval % 1.0 == 0.0 -> String.format(\"%.0f\", jsEval)\n                    else -> jsEval.toString()\n                }\n            }\n            if (url.isNotEmpty()) ruleUrl = url\n        }\n        //page\n        page?.let {\n            val matcher = pagePattern.matcher(ruleUrl)\n            while (matcher.find()) {\n                val pages = matcher.group(1)!!.split(\",\")\n                ruleUrl = if (page < pages.size) { //pages[pages.size - 1]等同于pages.last()\n                    ruleUrl.replace(matcher.group(), pages[page - 1].trim { it <= ' ' })\n                } else {\n                    ruleUrl.replace(matcher.group(), pages.last().trim { it <= ' ' })\n                }\n            }\n        }\n    }\n\n    /**\n     * 解析Url\n     */\n    private fun analyzeUrl() {\n        //replaceKeyPageJs已经替换掉额外内容，此处url是基础形式，可以直接切首个‘,’之前字符串。\n        val urlMatcher = paramPattern.matcher(ruleUrl)\n        val urlNoOption =\n            if (urlMatcher.find()) ruleUrl.substring(0, urlMatcher.start()) else ruleUrl\n        url = NetworkUtils.getAbsoluteURL(baseUrl, urlNoOption)\n        NetworkUtils.getBaseUrl(url)?.let {\n            baseUrl = it\n        }\n        if (urlNoOption.length != ruleUrl.length) {\n            GSON.fromJsonObject<UrlOption>(ruleUrl.substring(urlMatcher.end())).getOrNull()\n                ?.let { option ->\n                    option.getMethod()?.let {\n                        if (it.equals(\"POST\", true)) method = RequestMethod.POST\n                    }\n                    option.getHeaderMap()?.forEach { entry ->\n                        headerMap[entry.key.toString()] = entry.value.toString()\n                    }\n                    option.getBody()?.let {\n                        body = it\n                    }\n                    type = option.getType()\n                    charset = option.getCharset()\n                    retry = option.getRetry()\n                    useWebView = option.useWebView()\n                    webJs = option.getWebJs()\n                    option.getJs()?.let { jsStr ->\n                        evalJS(jsStr, url)?.toString()?.let {\n                            url = it\n                        }\n                    }\n                }\n        }\n        headerMap[UA_NAME] ?: let {\n            headerMap[UA_NAME] = AppConst.userAgent\n        }\n        urlNoQuery = url\n        when (method) {\n            RequestMethod.GET -> {\n                val pos = url.indexOf('?')\n                if (pos != -1) {\n                    analyzeFields(url.substring(pos + 1))\n                    urlNoQuery = url.substring(0, pos)\n                }\n            }\n            RequestMethod.POST -> body?.let {\n                if (!it.isJson() && !it.isXml() && headerMap[\"Content-Type\"].isNullOrEmpty()) {\n                    analyzeFields(it)\n                }\n            }\n        }\n    }\n\n    /**\n     * 解析QueryMap\n     */\n    private fun analyzeFields(fieldsTxt: String) {\n        queryStr = fieldsTxt\n        val queryS = fieldsTxt.splitNotBlank(\"&\")\n        for (query in queryS) {\n            val queryM = query.splitNotBlank(\"=\")\n            val value = if (queryM.size > 1) queryM[1] else \"\"\n            if (charset.isNullOrEmpty()) {\n                if (NetworkUtils.hasUrlEncoded(value)) {\n                    fieldMap[queryM[0]] = value\n                } else {\n                    fieldMap[queryM[0]] = URLEncoder.encode(value, \"UTF-8\")\n                }\n            } else if (charset == \"escape\") {\n                fieldMap[queryM[0]] = EncoderUtils.escape(value)\n            } else {\n                fieldMap[queryM[0]] = URLEncoder.encode(value, charset)\n            }\n        }\n    }\n\n    /**\n     * 执行JS\n     */\n    fun evalJS(jsStr: String, result: Any? = null): Any? {\n        val bindings = SimpleBindings()\n        bindings[\"java\"] = this\n        bindings[\"baseUrl\"] = baseUrl\n        bindings[\"cookie\"] = CookieStore\n        bindings[\"cache\"] = CacheManager\n        bindings[\"page\"] = page\n        bindings[\"key\"] = key\n        bindings[\"speakText\"] = speakText\n        bindings[\"speakSpeed\"] = speakSpeed\n        bindings[\"book\"] = ruleData as? Book\n        bindings[\"source\"] = source\n        bindings[\"result\"] = result\n        return SCRIPT_ENGINE.eval(jsStr, bindings)\n    }\n\n    fun put(key: String, value: String): String {\n        chapter?.putVariable(key, value)\n            ?: ruleData?.putVariable(key, value)\n        return value\n    }\n\n    fun get(key: String): String {\n        when (key) {\n            \"bookName\" -> (ruleData as? Book)?.let {\n                return it.name\n            }\n            \"title\" -> chapter?.let {\n                return it.title\n            }\n        }\n        return chapter?.getVariable(key)\n            ?: ruleData?.getVariable(key)\n            ?: \"\"\n    }\n\n    /**\n     * 开始访问,并发判断\n     */\n    private fun fetchStart(): ConcurrentRecord? {\n        source ?: return null\n        val concurrentRate = source.concurrentRate\n        if (concurrentRate.isNullOrEmpty()) {\n            return null\n        }\n        val rateIndex = concurrentRate.indexOf(\"/\")\n        var fetchRecord = concurrentRecordMap[source.getKey()]\n        if (fetchRecord == null) {\n            fetchRecord = ConcurrentRecord(rateIndex > 0, System.currentTimeMillis(), 1)\n            concurrentRecordMap[source.getKey()] = fetchRecord\n            return fetchRecord\n        }\n        val waitTime: Int = synchronized(fetchRecord) {\n            try {\n                if (rateIndex == -1) {\n                    if (fetchRecord.frequency > 0) {\n                        return@synchronized concurrentRate.toInt()\n                    }\n                    val nextTime = fetchRecord.time + concurrentRate.toInt()\n                    if (System.currentTimeMillis() >= nextTime) {\n                        fetchRecord.time = System.currentTimeMillis()\n                        fetchRecord.frequency = 1\n                        return@synchronized 0\n                    }\n                    return@synchronized (nextTime - System.currentTimeMillis()).toInt()\n                } else {\n                    val sj = concurrentRate.substring(rateIndex + 1)\n                    val nextTime = fetchRecord.time + sj.toInt()\n                    if (System.currentTimeMillis() >= nextTime) {\n                        fetchRecord.time = System.currentTimeMillis()\n                        fetchRecord.frequency = 1\n                        return@synchronized 0\n                    }\n                    val cs = concurrentRate.substring(0, rateIndex)\n                    if (fetchRecord.frequency > cs.toInt()) {\n                        return@synchronized (nextTime - System.currentTimeMillis()).toInt()\n                    } else {\n                        fetchRecord.frequency = fetchRecord.frequency + 1\n                        return@synchronized 0\n                    }\n                }\n            } catch (e: Exception) {\n                return@synchronized 0\n            }\n        }\n        if (waitTime > 0) {\n            throw ConcurrentException(\"根据并发率还需等待${waitTime}毫秒才可以访问\", waitTime = waitTime)\n        }\n        return fetchRecord\n    }\n\n    /**\n     * 访问结束\n     */\n    private fun fetchEnd(concurrentRecord: ConcurrentRecord?) {\n        if (concurrentRecord != null && !concurrentRecord.concurrent) {\n            synchronized(concurrentRecord) {\n                concurrentRecord.frequency = concurrentRecord.frequency - 1\n            }\n        }\n    }\n\n    /**\n     * 访问网站,返回StrResponse\n     */\n    suspend fun getStrResponseAwait(\n        jsStr: String? = null,\n        sourceRegex: String? = null,\n        useWebView: Boolean = true,\n        debugLog: DebugLog? = null\n    ): StrResponse {\n        if (type != null) {\n            return StrResponse(url, StringUtils.byteToHexString(getByteArrayAwait()))\n        }\n        val concurrentRecord = fetchStart()\n        setCookie(source?.getKey())\n        val strResponse: StrResponse\n        if (this.useWebView && useWebView) {\n            throw Exception(\"不支持webview\")\n        } else {\n            strResponse = getProxyClient(proxy, debugLog).newCallStrResponse(retry) {\n                addHeaders(headerMap)\n                when (method) {\n                    RequestMethod.POST -> {\n                        url(urlNoQuery)\n                        val contentType = headerMap[\"Content-Type\"]\n                        val body = body\n                        if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {\n                            postForm(fieldMap, true)\n                        } else if (!contentType.isNullOrBlank()) {\n                            val requestBody = body.toRequestBody(contentType.toMediaType())\n                            post(requestBody)\n                        } else {\n                            postJson(body)\n                        }\n                    }\n                    else -> get(urlNoQuery, fieldMap, true)\n                }\n            }\n        }\n        fetchEnd(concurrentRecord)\n        return strResponse\n    }\n\n    @JvmOverloads\n    fun getStrResponse(\n        jsStr: String? = null,\n        sourceRegex: String? = null,\n        useWebView: Boolean = true,\n        debugLog: DebugLog? = null\n    ): StrResponse {\n        return runBlocking {\n            getStrResponseAwait(jsStr, sourceRegex, useWebView, debugLog)\n        }\n    }\n\n    /**\n     * 访问网站,返回Response\n     */\n    suspend fun getResponseAwait(): Response {\n        val concurrentRecord = fetchStart()\n        setCookie(source?.getKey())\n        @Suppress(\"BlockingMethodInNonBlockingContext\")\n        val response = getProxyClient(proxy).newCallResponse(retry) {\n            addHeaders(headerMap)\n            when (method) {\n                RequestMethod.POST -> {\n                    url(urlNoQuery)\n                    val contentType = headerMap[\"Content-Type\"]\n                    val body = body\n                    if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {\n                        postForm(fieldMap, true)\n                    } else if (!contentType.isNullOrBlank()) {\n                        val requestBody = body.toRequestBody(contentType.toMediaType())\n                        post(requestBody)\n                    } else {\n                        postJson(body)\n                    }\n                }\n                else -> get(urlNoQuery, fieldMap, true)\n            }\n        }\n        fetchEnd(concurrentRecord)\n        return response\n    }\n\n    fun getResponse(): Response {\n        return runBlocking {\n            getResponseAwait()\n        }\n    }\n\n    /**\n     * 访问网站,返回ByteArray\n     */\n    suspend fun getByteArrayAwait(): ByteArray {\n        val concurrentRecord = fetchStart()\n\n        @Suppress(\"RegExpRedundantEscape\")\n        val dataUriFindResult = dataUriRegex.find(urlNoQuery)\n        @Suppress(\"BlockingMethodInNonBlockingContext\")\n        if (dataUriFindResult != null) {\n            val dataUriBase64 = dataUriFindResult.groupValues[1]\n            val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT)\n            fetchEnd(concurrentRecord)\n            return byteArray\n        } else {\n            setCookie(source?.getKey())\n            val byteArray = getProxyClient(proxy).newCallResponseBody(retry) {\n                addHeaders(headerMap)\n                when (method) {\n                    RequestMethod.POST -> {\n                        url(urlNoQuery)\n                        val contentType = headerMap[\"Content-Type\"]\n                        val body = body\n                        if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {\n                            postForm(fieldMap, true)\n                        } else if (!contentType.isNullOrBlank()) {\n                            val requestBody = body.toRequestBody(contentType.toMediaType())\n                            post(requestBody)\n                        } else {\n                            postJson(body)\n                        }\n                    }\n                    else -> get(urlNoQuery, fieldMap, true)\n                }\n            }.bytes()\n            fetchEnd(concurrentRecord)\n            return byteArray\n        }\n    }\n\n    fun getByteArray(): ByteArray {\n        return runBlocking {\n            getByteArrayAwait()\n        }\n    }\n\n    /**\n     * 上传文件\n     */\n    suspend fun upload(fileName: String, file: Any, contentType: String): StrResponse {\n        return getProxyClient(proxy).newCallStrResponse(retry) {\n            url(urlNoQuery)\n            val bodyMap = GSON.fromJsonObject<HashMap<String, Any>>(body).getOrNull()!!\n            bodyMap.forEach { entry ->\n                if (entry.value.toString() == \"fileRequest\") {\n                    bodyMap[entry.key] = mapOf(\n                        Pair(\"fileName\", fileName),\n                        Pair(\"file\", file),\n                        Pair(\"contentType\", contentType)\n                    )\n                }\n            }\n            postMultipart(type, bodyMap)\n        }\n    }\n\n    /**\n     *设置cookie urlOption的优先级大于书源保存的cookie\n     *@param tag 书源url 缺省为传入的url\n     */\n    private fun setCookie(tag: String?) {\n        val cookie = CookieStore.getCookie(tag ?: url)\n        if (cookie.isNotEmpty()) {\n            val cookieMap = CookieStore.cookieToMap(cookie)\n            val customCookieMap = CookieStore.cookieToMap(headerMap[\"Cookie\"] ?: \"\")\n            cookieMap.putAll(customCookieMap)\n            val newCookie = CookieStore.mapToCookie(cookieMap)\n            newCookie?.let {\n                headerMap.put(\"Cookie\", it)\n            }\n        }\n    }\n\n    fun getUserAgent(): String {\n        return headerMap[UA_NAME] ?: AppConst.userAgent\n    }\n\n    fun isPost(): Boolean {\n        return method == RequestMethod.POST\n    }\n\n    override fun getSource(): BaseSource? {\n        return source\n    }\n\n    data class UrlOption(\n        private var method: String? = null,\n        private var charset: String? = null,\n        private var headers: Any? = null,\n        private var body: Any? = null,\n        private var retry: Int? = null,\n        private var type: String? = null,\n        private var webView: Any? = null,\n        private var webJs: String? = null,\n        private var js: String? = null,\n    ) {\n        fun setMethod(value: String?) {\n            method = if (value.isNullOrBlank()) null else value\n        }\n\n        fun getMethod(): String? {\n            return method\n        }\n\n        fun setCharset(value: String?) {\n            charset = if (value.isNullOrBlank()) null else value\n        }\n\n        fun getCharset(): String? {\n            return charset\n        }\n\n        fun setRetry(value: String?) {\n            retry = if (value.isNullOrEmpty()) null else value.toIntOrNull()\n        }\n\n        fun getRetry(): Int {\n            return retry ?: 0\n        }\n\n        fun setType(value: String?) {\n            type = if (value.isNullOrBlank()) null else value\n        }\n\n        fun getType(): String? {\n            return type\n        }\n\n        fun useWebView(): Boolean {\n            return when (webView) {\n                null, \"\", false, \"false\" -> false\n                else -> true\n            }\n        }\n\n        fun useWebView(boolean: Boolean) {\n            webView = if (boolean) true else null\n        }\n\n        fun setHeaders(value: String?) {\n            headers = if (value.isNullOrBlank()) {\n                null\n            } else {\n                GSON.fromJsonObject<Map<String, Any>>(value).getOrNull()\n            }\n        }\n\n        fun getHeaderMap(): Map<*, *>? {\n            return when (val value = headers) {\n                is Map<*, *> -> value\n                is String -> GSON.fromJsonObject<Map<String, Any>>(value).getOrNull()\n                else -> null\n            }\n        }\n\n        fun setBody(value: String?) {\n            body = when {\n                value.isNullOrBlank() -> null\n                value.isJsonObject() -> GSON.fromJsonObject<Map<String, Any>>(value)\n                value.isJsonArray() -> GSON.fromJsonArray<Map<String, Any>>(value)\n                else -> value\n            }\n        }\n\n        fun getBody(): String? {\n            return body?.let {\n                if (it is String) it else GSON.toJson(it)\n            }\n        }\n\n        fun setWebJs(value: String?) {\n            webJs = if (value.isNullOrBlank()) null else value\n        }\n\n        fun getWebJs(): String? {\n            return webJs\n        }\n\n        fun setJs(value: String?) {\n            js = if (value.isNullOrBlank()) null else value\n        }\n\n        fun getJs(): String? {\n            return js\n        }\n    }\n\n    data class ConcurrentRecord(\n        val concurrent: Boolean,\n        var time: Long,\n        var frequency: Int\n    )\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/model/analyzeRule/QueryTTF.java",
    "content": "package io.legado.app.model.analyzeRule;\n\nimport org.apache.commons.lang3.tuple.Pair;\nimport org.apache.commons.lang3.tuple.Triple;\n\nimport java.nio.charset.Charset;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\n\n@SuppressWarnings({\"FieldCanBeLocal\", \"StatementWithEmptyBody\", \"unused\"})\npublic class QueryTTF {\n    private static class Header {\n        public int majorVersion;\n        public int minorVersion;\n        public int numOfTables;\n        public int searchRange;\n        public int entrySelector;\n        public int rangeShift;\n    }\n\n    private static class Directory {\n        public String tag;          // table name\n        public int checkSum;       // Check sum\n        public int offset;         // Offset from beginning of file\n        public int length;         // length of the table in bytes\n    }\n\n    private static class NameLayout {\n        public int format;\n        public int count;\n        public int stringOffset;\n        public List<NameRecord> records = new LinkedList<>();\n    }\n\n    private static class NameRecord {\n        public int platformID;           // 平台标识符<0:Unicode, 1:Mac, 2:ISO, 3:Windows, 4:Custom>\n        public int encodingID;           // 编码标识符\n        public int languageID;           // 语言标识符\n        public int nameID;               // 名称标识符\n        public int length;               // 名称字符串的长度\n        public int offset;               // 名称字符串相对于stringOffset的字节偏移量\n    }\n\n    private static class HeadLayout {\n        public int majorVersion;\n        public int minorVersion;\n        public int fontRevision;\n        public int checkSumAdjustment;\n        public int magicNumber;\n        public int flags;\n        public int unitsPerEm;\n        public long created;\n        public long modified;\n        public short xMin;\n        public short yMin;\n        public short xMax;\n        public short yMax;\n        public int macStyle;\n        public int lowestRecPPEM;\n        public short fontDirectionHint;\n        public short indexToLocFormat;      // <0:loca是2字节数组, 1:loca是4字节数组>\n        public short glyphDataFormat;\n    }\n\n    private static class MaxpLayout {\n        public int majorVersion;\n        public int minorVersion;\n        public int numGlyphs;                // 字体中的字形数量\n        public int maxPoints;\n        public int maxContours;\n        public int maxCompositePoints;\n        public int maxCompositeContours;\n        public int maxZones;\n        public int maxTwilightPoints;\n        public int maxStorage;\n        public int maxFunctionDefs;\n        public int maxInstructionDefs;\n        public int maxStackElements;\n        public int maxSizeOfInstructions;\n        public int maxComponentElements;\n        public int maxComponentDepth;\n    }\n\n    private static class CmapLayout {\n        public int version;\n        public int numTables;\n        public List<CmapRecord> records = new LinkedList<>();\n        public Map<Integer, CmapFormat> tables = new HashMap<>();\n    }\n\n    private static class CmapRecord {\n        public int platformID;\n        public int encodingID;\n        public int offset;\n    }\n\n    private static class CmapFormat {\n        public int format;\n        public int length;\n        public int language;\n        public byte[] glyphIdArray;\n    }\n\n    private static class CmapFormat4 extends CmapFormat {\n        public int segCountX2;\n        public int searchRange;\n        public int entrySelector;\n        public int rangeShift;\n        public int[] endCode;\n        public int reservedPad;\n        public int[] startCode;\n        public short[] idDelta;\n        public int[] idRangeOffset;\n        public int[] glyphIdArray;\n    }\n\n    private static class CmapFormat6 extends CmapFormat {\n        public int firstCode;\n        public int entryCount;\n        public int[] glyphIdArray;\n    }\n\n    private static class CmapFormat12 extends CmapFormat {\n        public int reserved;\n        public int length;\n        public int language;\n        public int numGroups;\n        public List<Triple<Integer, Integer, Integer>> groups;\n    }\n\n    private static class GlyfLayout {\n        public short numberOfContours;      // 非负值为简单字型,负值为符合字型\n        public short xMin;\n        public short yMin;\n        public short xMax;\n        public short yMax;\n        public int[] endPtsOfContours;   // length=numberOfContours\n        public int instructionLength;\n        public byte[] instructions;         // length=instructionLength\n        public byte[] flags;\n        public short[] xCoordinates;        // length = flags.length\n        public short[] yCoordinates;        // length = flags.length\n    }\n\n    private static class ByteArrayReader {\n        public int index;\n        public byte[] buffer;\n\n        public ByteArrayReader(byte[] buffer, int index) {\n            this.buffer = buffer;\n            this.index = index;\n        }\n\n        public long ReadUIntX(long len) {\n            long result = 0;\n            for (long i = 0; i < len; ++i) {\n                result <<= 8;\n                result |= buffer[index++] & 0xFF;\n            }\n            return result;\n        }\n\n        public long ReadUInt64() {\n            return ReadUIntX(8);\n        }\n\n        public int ReadUInt32() {\n            return (int) ReadUIntX(4);\n        }\n\n        public int ReadUInt16() {\n            return (int) ReadUIntX(2);\n        }\n\n        public short ReadInt16() {\n            return (short) ReadUIntX(2);\n        }\n\n        public short ReadUInt8() {\n            return (short) ReadUIntX(1);\n        }\n\n\n        public String ReadStrings(int len, Charset charset) {\n            byte[] result = len > 0 ? new byte[len] : null;\n            for (int i = 0; i < len; ++i) result[i] = buffer[index++];\n            return new String(result, charset);\n        }\n\n        public byte GetByte() {\n            return buffer[index++];\n        }\n\n        public byte[] GetBytes(int len) {\n            byte[] result = len > 0 ? new byte[len] : null;\n            for (int i = 0; i < len; ++i) result[i] = buffer[index++];\n            return result;\n        }\n\n        public int[] GetUInt16Array(int len) {\n            int[] result = len > 0 ? new int[len] : null;\n            for (int i = 0; i < len; ++i) result[i] = ReadUInt16();\n            return result;\n        }\n\n        public short[] GetInt16Array(int len) {\n            short[] result = len > 0 ? new short[len] : null;\n            for (int i = 0; i < len; ++i) result[i] = ReadInt16();\n            return result;\n        }\n    }\n\n    private final ByteArrayReader fontReader;\n    private final Header fileHeader = new Header();\n    private final List<Directory> directorys = new LinkedList<>();\n    private final NameLayout name = new NameLayout();\n    private final HeadLayout head = new HeadLayout();\n    private final MaxpLayout maxp = new MaxpLayout();\n    private final List<Integer> loca = new LinkedList<>();\n    private final CmapLayout Cmap = new CmapLayout();\n    private final List<GlyfLayout> glyf = new LinkedList<>();\n    @SuppressWarnings(\"unchecked\")\n    private final Pair<Integer, Integer>[] pps = new Pair[]{\n            Pair.of(3, 10),\n            Pair.of(0, 4),\n            Pair.of(3, 1),\n            Pair.of(1, 0),\n            Pair.of(0, 3),\n            Pair.of(0, 1)\n    };\n\n    public final Map<Integer, String> codeToGlyph = new HashMap<>();\n    public final Map<String, Integer> glyphToCode = new HashMap<>();\n    private int limitMix = 0;\n    private int limitMax = 0;\n\n    /**\n     * 构造函数\n     *\n     * @param buffer 传入TTF字体二进制数组\n     */\n    public QueryTTF(byte[] buffer) {\n        fontReader = new ByteArrayReader(buffer, 0);\n        // 获取文件头\n        fileHeader.majorVersion = fontReader.ReadUInt16();\n        fileHeader.minorVersion = fontReader.ReadUInt16();\n        fileHeader.numOfTables = fontReader.ReadUInt16();\n        fileHeader.searchRange = fontReader.ReadUInt16();\n        fileHeader.entrySelector = fontReader.ReadUInt16();\n        fileHeader.rangeShift = fontReader.ReadUInt16();\n        // 获取目录\n        for (int i = 0; i < fileHeader.numOfTables; ++i) {\n            Directory d = new Directory();\n            d.tag = fontReader.ReadStrings(4, StandardCharsets.US_ASCII);\n            d.checkSum = fontReader.ReadUInt32();\n            d.offset = fontReader.ReadUInt32();\n            d.length = fontReader.ReadUInt32();\n            directorys.add(d);\n        }\n        // 解析表 name (字体信息,包含版权、名称、作者等...)\n        for (Directory Temp : directorys) {\n            if (Temp.tag.equals(\"name\")) {\n                fontReader.index = Temp.offset;\n                name.format = fontReader.ReadUInt16();\n                name.count = fontReader.ReadUInt16();\n                name.stringOffset = fontReader.ReadUInt16();\n                for (int i = 0; i < name.count; ++i) {\n                    NameRecord record = new NameRecord();\n                    record.platformID = fontReader.ReadUInt16();\n                    record.encodingID = fontReader.ReadUInt16();\n                    record.languageID = fontReader.ReadUInt16();\n                    record.nameID = fontReader.ReadUInt16();\n                    record.length = fontReader.ReadUInt16();\n                    record.offset = fontReader.ReadUInt16();\n                    name.records.add(record);\n                }\n            }\n        }\n        // 解析表 head (获取 head.indexToLocFormat)\n        for (Directory Temp : directorys) {\n            if (Temp.tag.equals(\"head\")) {\n                fontReader.index = Temp.offset;\n                head.majorVersion = fontReader.ReadUInt16();\n                head.minorVersion = fontReader.ReadUInt16();\n                head.fontRevision = fontReader.ReadUInt32();\n                head.checkSumAdjustment = fontReader.ReadUInt32();\n                head.magicNumber = fontReader.ReadUInt32();\n                head.flags = fontReader.ReadUInt16();\n                head.unitsPerEm = fontReader.ReadUInt16();\n                head.created = fontReader.ReadUInt64();\n                head.modified = fontReader.ReadUInt64();\n                head.xMin = fontReader.ReadInt16();\n                head.yMin = fontReader.ReadInt16();\n                head.xMax = fontReader.ReadInt16();\n                head.yMax = fontReader.ReadInt16();\n                head.macStyle = fontReader.ReadUInt16();\n                head.lowestRecPPEM = fontReader.ReadUInt16();\n                head.fontDirectionHint = fontReader.ReadInt16();\n                head.indexToLocFormat = fontReader.ReadInt16();\n                head.glyphDataFormat = fontReader.ReadInt16();\n            }\n        }\n        // 解析表 maxp (获取 maxp.numGlyphs)\n        for (Directory Temp : directorys) {\n            if (Temp.tag.equals(\"maxp\")) {\n                fontReader.index = Temp.offset;\n                maxp.majorVersion = fontReader.ReadUInt16();\n                maxp.minorVersion = fontReader.ReadUInt16();\n                maxp.numGlyphs = fontReader.ReadUInt16();\n                maxp.maxPoints = fontReader.ReadUInt16();\n                maxp.maxContours = fontReader.ReadUInt16();\n                maxp.maxCompositePoints = fontReader.ReadUInt16();\n                maxp.maxCompositeContours = fontReader.ReadUInt16();\n                maxp.maxZones = fontReader.ReadUInt16();\n                maxp.maxTwilightPoints = fontReader.ReadUInt16();\n                maxp.maxStorage = fontReader.ReadUInt16();\n                maxp.maxFunctionDefs = fontReader.ReadUInt16();\n                maxp.maxInstructionDefs = fontReader.ReadUInt16();\n                maxp.maxStackElements = fontReader.ReadUInt16();\n                maxp.maxSizeOfInstructions = fontReader.ReadUInt16();\n                maxp.maxComponentElements = fontReader.ReadUInt16();\n                maxp.maxComponentDepth = fontReader.ReadUInt16();\n            }\n        }\n        // 解析表 loca (轮廓数据偏移地址表)\n        for (Directory Temp : directorys) {\n            if (Temp.tag.equals(\"loca\")) {\n                fontReader.index = Temp.offset;\n                int offset = head.indexToLocFormat == 0 ? 2 : 4;\n                for (long i = 0; i < Temp.length; i += offset) {\n                    loca.add(offset == 2 ? fontReader.ReadUInt16() << 1 : fontReader.ReadUInt32());\n                }\n            }\n        }\n        // 解析表 cmap (Unicode编码轮廓索引对照表)\n        for (Directory Temp : directorys) {\n            if (Temp.tag.equals(\"cmap\")) {\n                fontReader.index = Temp.offset;\n                Cmap.version = fontReader.ReadUInt16();\n                Cmap.numTables = fontReader.ReadUInt16();\n\n                for (int i = 0; i < Cmap.numTables; ++i) {\n                    CmapRecord record = new CmapRecord();\n                    record.platformID = fontReader.ReadUInt16();\n                    record.encodingID = fontReader.ReadUInt16();\n                    record.offset = fontReader.ReadUInt32();\n                    Cmap.records.add(record);\n                }\n                for (int i = 0; i < Cmap.numTables; ++i) {\n                    int fmtOffset = Cmap.records.get(i).offset;\n                    fontReader.index = Temp.offset + fmtOffset;\n                    int EndIndex = fontReader.index;\n\n                    int format = fontReader.ReadUInt16();\n                    if (Cmap.tables.containsKey(fmtOffset)) continue;\n                    if (format == 0) {\n                        CmapFormat f = new CmapFormat();\n                        f.format = format;\n                        f.length = fontReader.ReadUInt16();\n                        f.language = fontReader.ReadUInt16();\n                        f.glyphIdArray = fontReader.GetBytes(f.length - 6);\n                        Cmap.tables.put(fmtOffset, f);\n                    } else if (format == 4) {\n                        CmapFormat4 f = new CmapFormat4();\n                        f.format = format;\n                        f.length = fontReader.ReadUInt16();\n                        f.language = fontReader.ReadUInt16();\n                        f.segCountX2 = fontReader.ReadUInt16();\n                        int segCount = f.segCountX2 >> 1;\n                        f.searchRange = fontReader.ReadUInt16();\n                        f.entrySelector = fontReader.ReadUInt16();\n                        f.rangeShift = fontReader.ReadUInt16();\n                        f.endCode = fontReader.GetUInt16Array(segCount);\n                        f.reservedPad = fontReader.ReadUInt16();\n                        f.startCode = fontReader.GetUInt16Array(segCount);\n                        f.idDelta = fontReader.GetInt16Array(segCount);\n                        f.idRangeOffset = fontReader.GetUInt16Array(segCount);\n                        f.glyphIdArray = fontReader.GetUInt16Array((EndIndex + f.length - fontReader.index) >> 1);\n                        Cmap.tables.put(fmtOffset, f);\n                    } else if (format == 6) {\n                        CmapFormat6 f = new CmapFormat6();\n                        f.format = format;\n                        f.length = fontReader.ReadUInt16();\n                        f.language = fontReader.ReadUInt16();\n                        f.firstCode = fontReader.ReadUInt16();\n                        f.entryCount = fontReader.ReadUInt16();\n                        f.glyphIdArray = fontReader.GetUInt16Array(f.entryCount);\n                        Cmap.tables.put(fmtOffset, f);\n                    } else if (format == 12) {\n                        CmapFormat12 f = new CmapFormat12();\n                        f.format = format;\n                        f.reserved = fontReader.ReadUInt16();\n                        f.length = fontReader.ReadUInt32();\n                        f.language = fontReader.ReadUInt32();\n                        f.numGroups = fontReader.ReadUInt32();\n                        f.groups = new ArrayList<>(f.numGroups);\n                        for (int n = 0; n < f.numGroups; ++n) {\n                            f.groups.add(Triple.of(fontReader.ReadUInt32(), fontReader.ReadUInt32(), fontReader.ReadUInt32()));\n                        }\n                        Cmap.tables.put(fmtOffset, f);\n                    }\n                }\n            }\n        }\n        // 解析表 glyf (字体轮廓数据表)\n        for (Directory Temp : directorys) {\n            if (Temp.tag.equals(\"glyf\")) {\n                fontReader.index = Temp.offset;\n                for (int i = 0; i < maxp.numGlyphs; ++i) {\n                    fontReader.index = Temp.offset + loca.get(i);\n\n                    short numberOfContours = fontReader.ReadInt16();\n                    if (numberOfContours > 0) {\n                        GlyfLayout g = new GlyfLayout();\n                        g.numberOfContours = numberOfContours;\n                        g.xMin = fontReader.ReadInt16();\n                        g.yMin = fontReader.ReadInt16();\n                        g.xMax = fontReader.ReadInt16();\n                        g.yMax = fontReader.ReadInt16();\n                        g.endPtsOfContours = fontReader.GetUInt16Array(numberOfContours);\n                        g.instructionLength = fontReader.ReadUInt16();\n                        g.instructions = fontReader.GetBytes(g.instructionLength);\n                        int flagLength = g.endPtsOfContours[g.endPtsOfContours.length - 1] + 1;\n                        // 获取轮廓点描述标志\n                        g.flags = new byte[flagLength];\n                        for (int n = 0; n < flagLength; ++n) {\n                            g.flags[n] = fontReader.GetByte();\n                            if ((g.flags[n] & 0x08) != 0x00) {\n                                for (int m = fontReader.ReadUInt8(); m > 0; --m) {\n                                    g.flags[++n] = g.flags[n - 1];\n                                }\n                            }\n                        }\n                        // 获取轮廓点描述x轴相对值\n                        g.xCoordinates = new short[flagLength];\n                        for (int n = 0; n < flagLength; ++n) {\n                            short same = (short) ((g.flags[n] & 0x10) != 0 ? 1 : -1);\n                            if ((g.flags[n] & 0x02) != 0) {\n                                g.xCoordinates[n] = (short) (same * fontReader.ReadUInt8());\n                            } else {\n                                g.xCoordinates[n] = same == 1 ? (short) 0 : fontReader.ReadInt16();\n                            }\n                        }\n                        // 获取轮廓点描述y轴相对值\n                        g.yCoordinates = new short[flagLength];\n                        for (int n = 0; n < flagLength; ++n) {\n                            short same = (short) ((g.flags[n] & 0x20) != 0 ? 1 : -1);\n                            if ((g.flags[n] & 0x04) != 0) {\n                                g.yCoordinates[n] = (short) (same * fontReader.ReadUInt8());\n                            } else {\n                                g.yCoordinates[n] = same == 1 ? (short) 0 : fontReader.ReadInt16();\n                            }\n                        }\n                        // 相对坐标转绝对坐标\n//                        for (int n = 1; n < flagLength; ++n) {\n//                            xCoordinates[n] += xCoordinates[n - 1];\n//                            yCoordinates[n] += yCoordinates[n - 1];\n//                        }\n\n                        glyf.add(g);\n                    } else {\n                        // 复合字体暂未使用\n                    }\n                }\n            }\n        }\n\n        // 建立Unicode&Glyph双向表\n        for (int key = 0; key < 130000; ++key) {\n            if (key == 0xFF) key = 0x3400;\n            int gid = getGlyfIndex(key);\n            if (gid == 0) continue;\n            StringBuilder sb = new StringBuilder();\n            // 字型数据转String，方便存HashMap\n            for (short b : glyf.get(gid).xCoordinates) sb.append(b);\n            for (short b : glyf.get(gid).yCoordinates) sb.append(b);\n            String val = sb.toString();\n            if (limitMix == 0) limitMix = key;\n            limitMax = key;\n            codeToGlyph.put(key, val);\n            if (glyphToCode.containsKey(val)) continue;\n            glyphToCode.put(val, key);\n        }\n    }\n\n    /**\n     * 获取字体信息 (1=字体名称)\n     *\n     * @param nameId 传入十进制字体信息索引\n     * @return 返回查询结果字符串\n     */\n    public String getNameById(int nameId) {\n        for (Directory Temp : directorys) {\n            if (!Temp.tag.equals(\"name\")) continue;\n            fontReader.index = Temp.offset;\n            break;\n        }\n        for (NameRecord record : name.records) {\n            if (record.nameID != nameId) continue;\n            fontReader.index += name.stringOffset + record.offset;\n            return fontReader.ReadStrings(record.length, record.platformID == 1 ? StandardCharsets.UTF_8 : StandardCharsets.UTF_16BE);\n        }\n        return \"error\";\n    }\n\n    /**\n     * 使用Unicode值查找轮廓索引\n     *\n     * @param code 传入Unicode十进制值\n     * @return 返回十进制轮廓索引\n     */\n    private int getGlyfIndex(int code) {\n        if (code == 0) return 0;\n        int fmtKey = 0;\n        for (Pair<Integer, Integer> item : pps) {\n            for (CmapRecord record : Cmap.records) {\n                if ((item.getLeft() == record.platformID) && (item.getRight() == record.encodingID)) {\n                    fmtKey = record.offset;\n                    break;\n                }\n            }\n            if (fmtKey > 0) break;\n        }\n        if (fmtKey == 0) return 0;\n\n        int glyfID = 0;\n        CmapFormat table = Cmap.tables.get(fmtKey);\n        assert table != null;\n        int fmt = table.format;\n        if (fmt == 0) {\n            if (code < table.glyphIdArray.length) glyfID = table.glyphIdArray[code] & 0xFF;\n        } else if (fmt == 4) {\n            CmapFormat4 tab = (CmapFormat4) table;\n            if (code > tab.endCode[tab.endCode.length - 1]) return 0;\n            // 二分法查找数值索引\n            int start = 0, middle, end = tab.endCode.length - 1;\n            while (start + 1 < end) {\n                middle = (start + end) / 2;\n                if (tab.endCode[middle] <= code) start = middle;\n                else end = middle;\n            }\n            if (tab.endCode[start] < code) ++start;\n            if (code < tab.startCode[start]) return 0;\n            if (tab.idRangeOffset[start] != 0) {\n                glyfID = tab.glyphIdArray[code - tab.startCode[start] + (tab.idRangeOffset[start] >> 1) - (tab.idRangeOffset.length - start)];\n            } else glyfID = code + tab.idDelta[start];\n            glyfID &= 0xFFFF;\n        } else if (fmt == 6) {\n            CmapFormat6 tab = (CmapFormat6) table;\n            int index = code - tab.firstCode;\n            if (index < 0 || index >= tab.glyphIdArray.length) glyfID = 0;\n            else glyfID = tab.glyphIdArray[index];\n        } else if (fmt == 12) {\n            CmapFormat12 tab = (CmapFormat12) table;\n            if (code > tab.groups.get(tab.numGroups - 1).getMiddle()) return 0;\n            // 二分法查找数值索引\n            int start = 0, middle, end = tab.numGroups - 1;\n            while (start + 1 < end) {\n                middle = (start + end) / 2;\n                if (tab.groups.get(middle).getLeft() <= code) start = middle;\n                else end = middle;\n            }\n            if (tab.groups.get(start).getLeft() <= code && code <= tab.groups.get(start).getMiddle()) {\n                glyfID = tab.groups.get(start).getRight() + code - tab.groups.get(start).getLeft();\n            }\n        }\n        return glyfID;\n    }\n\n    /**\n     * 判断Unicode值是否在字体范围内\n     *\n     * @param code 传入Unicode十进制值\n     * @return 返回bool查询结果\n     */\n    public boolean inLimit(char code) {\n        return (limitMix <= code) && (code < limitMax);\n    }\n\n    /**\n     * 使用Unicode值获取轮廓数据\n     *\n     * @param key 传入Unicode十进制值\n     * @return 返回轮廓数组的String值\n     */\n    public String getGlyfByCode(int key) {\n        return codeToGlyph.getOrDefault(key, \"\");\n    }\n\n    /**\n     * 使用轮廓数据获取Unicode值\n     *\n     * @param val 传入轮廓数组的String值\n     * @return 返回Unicode十进制值\n     */\n    public int getCodeByGlyf(String val) {\n        //noinspection ConstantConditions\n        return glyphToCode.getOrDefault(val, 0);\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt",
    "content": "package io.legado.app.model.analyzeRule\n\n//通用的规则切分处理\nclass RuleAnalyzer(data: String, code: Boolean = false) {\n\n    private var queue: String = data //被处理字符串\n    private var pos = 0 //当前处理到的位置\n    private var start = 0 //当前处理字段的开始\n    private var startX = 0 //当前规则的开始\n\n    private var rule = ArrayList<String>()  //分割出的规则列表\n    private var step: Int = 0 //分割字符的长度\n    var elementsType = \"\" //当前分割字符串\n    var innerType = true //是否为内嵌{{}}\n\n    fun trim() { // 修剪当前规则之前的\"@\"或者空白符\n        if (queue[pos] == '@' || queue[pos] < '!') { //在while里重复设置start和startX会拖慢执行速度，所以先来个判断是否存在需要修剪的字段，最后再一次性设置start和startX\n            pos++\n            while (queue[pos] == '@' || queue[pos] < '!') pos++\n            start = pos //开始点推移\n            startX = pos //规则起始点推移\n        }\n    }\n\n    //将pos重置为0，方便复用\n    fun reSetPos() {\n        pos = 0\n        startX = 0\n    }\n\n    /**\n     * 从剩余字串中拉出一个字符串，直到但不包括匹配序列\n     * @param seq 查找的字符串 **区分大小写**\n     * @return 是否找到相应字段。\n     */\n    fun consumeTo(seq: String): Boolean {\n        start = pos //将处理到的位置设置为规则起点\n        val offset = queue.indexOf(seq, pos)\n        return if (offset != -1) {\n            pos = offset\n            true\n        } else false\n    }\n\n    /**\n     * 从剩余字串中拉出一个字符串，直到但不包括匹配序列（匹配参数列表中一项即为匹配），或剩余字串用完。\n     * @param seq 匹配字符串序列\n     * @return 成功返回true并设置间隔，失败则直接返回fasle\n     */\n    fun consumeToAny(vararg seq: String): Boolean {\n\n        var pos = pos //声明新变量记录匹配位置，不更改类本身的位置\n\n        while (pos != queue.length) {\n\n            for (s in seq) {\n                if (queue.regionMatches(pos, s, 0, s.length)) {\n                    step = s.length //间隔数\n                    this.pos = pos //匹配成功, 同步处理位置到类\n                    return true //匹配就返回 true\n                }\n            }\n\n            pos++ //逐个试探\n        }\n        return false\n    }\n\n    /**\n     * 从剩余字串中拉出一个字符串，直到但不包括匹配序列（匹配参数列表中一项即为匹配），或剩余字串用完。\n     * @param seq 匹配字符序列\n     * @return 返回匹配位置\n     */\n    private fun findToAny(vararg seq: Char): Int {\n\n        var pos = pos //声明新变量记录匹配位置，不更改类本身的位置\n\n        while (pos != queue.length) {\n\n            for (s in seq) if (queue[pos] == s) return pos //匹配则返回位置\n\n            pos++ //逐个试探\n\n        }\n\n        return -1\n    }\n\n    /**\n     * 拉出一个非内嵌代码平衡组，存在转义文本\n     */\n    fun chompCodeBalanced(open: Char, close: Char): Boolean {\n\n        var pos = pos //声明临时变量记录匹配位置，匹配成功后才同步到类的pos\n\n        var depth = 0 //嵌套深度\n        var otherDepth = 0 //其他对称符合嵌套深度\n\n        var inSingleQuote = false //单引号\n        var inDoubleQuote = false //双引号\n\n        do {\n            if (pos == queue.length) break\n            val c = queue[pos++]\n            if (c != ESC) { //非转义字符\n                if (c == '\\'' && !inDoubleQuote) inSingleQuote = !inSingleQuote //匹配具有语法功能的单引号\n                else if (c == '\"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote //匹配具有语法功能的双引号\n\n                if (inSingleQuote || inDoubleQuote) continue //语法单元未匹配结束，直接进入下个循环\n\n                if (c == '[') depth++ //开始嵌套一层\n                else if (c == ']') depth-- //闭合一层嵌套\n                else if (depth == 0) {\n                    //处于默认嵌套中的非默认字符不需要平衡，仅depth为0时默认嵌套全部闭合，此字符才进行嵌套\n                    if (c == open) otherDepth++\n                    else if (c == close) otherDepth--\n                }\n\n            } else pos++\n\n        } while (depth > 0 || otherDepth > 0) //拉出一个平衡字串\n\n        return if (depth > 0 || otherDepth > 0) false else {\n            this.pos = pos //同步位置\n            true\n        }\n    }\n\n    /**\n     * 拉出一个规则平衡组，经过仔细测试xpath和jsoup中，引号内转义字符无效。\n     */\n    fun chompRuleBalanced(open: Char, close: Char): Boolean {\n\n        var pos = pos //声明临时变量记录匹配位置，匹配成功后才同步到类的pos\n        var depth = 0 //嵌套深度\n        var inSingleQuote = false //单引号\n        var inDoubleQuote = false //双引号\n\n        do {\n            if (pos == queue.length) break\n            val c = queue[pos++]\n            if (c == '\\'' && !inDoubleQuote) inSingleQuote = !inSingleQuote //匹配具有语法功能的单引号\n            else if (c == '\"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote //匹配具有语法功能的双引号\n\n            if (inSingleQuote || inDoubleQuote) continue //语法单元未匹配结束，直接进入下个循环\n            else if (c == '\\\\') { //不在引号中的转义字符才将下个字符转义\n                pos++\n                continue\n            }\n\n            if (c == open) depth++ //开始嵌套一层\n            else if (c == close) depth-- //闭合一层嵌套\n\n        } while (depth > 0) //拉出一个平衡字串\n\n        return if (depth > 0) false else {\n            this.pos = pos //同步位置\n            true\n        }\n    }\n\n    /**\n     * 不用正则,不到最后不切片也不用中间变量存储,只在序列中标记当前查找字段的开头结尾,到返回时才切片,高效快速准确切割规则\n     * 解决jsonPath自带的\"&&\"和\"||\"与阅读的规则冲突,以及规则正则或字符串中包含\"&&\"、\"||\"、\"%%\"、\"@\"导致的冲突\n     */\n    tailrec fun splitRule(vararg split: String): ArrayList<String> { //首段匹配,elementsType为空\n\n        if (split.size == 1) {\n            elementsType = split[0] //设置分割字串\n            return if (!consumeTo(elementsType)) {\n                rule += queue.substring(startX)\n                rule\n            } else {\n                step = elementsType.length //设置分隔符长度\n                splitRule()\n            } //递归匹配\n        } else if (!consumeToAny(* split)) { //未找到分隔符\n            rule += queue.substring(startX)\n            return rule\n        }\n\n        val end = pos //记录分隔位置\n        pos = start //重回开始，启动另一种查找\n\n        do {\n            val st = findToAny('[', '(') //查找筛选器位置\n\n            if (st == -1) {\n\n                rule = arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组\n\n                elementsType = queue.substring(end, end + step) //设置组合类型\n                pos = end + step //跳过分隔符\n\n                while (consumeTo(elementsType)) { //循环切分规则压入数组\n                    rule += queue.substring(start, pos)\n                    pos += step //跳过分隔符\n                }\n\n                rule += queue.substring(pos) //将剩余字段压入数组末尾\n\n                return rule\n            }\n\n            if (st > end) { //先匹配到st1pos，表明分隔字串不在选择器中，将选择器前分隔字串分隔的字段依次压入数组\n\n                rule = arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组\n\n                elementsType = queue.substring(end, end + step) //设置组合类型\n                pos = end + step //跳过分隔符\n\n                while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组\n                    rule += queue.substring(start, pos)\n                    pos += step //跳过分隔符\n                }\n\n                return if (pos > st) {\n                    startX = start\n                    splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配\n                } else { //执行到此，证明后面再无分隔字符\n                    rule += queue.substring(pos) //将剩余字段压入数组末尾\n                    rule\n                }\n            }\n\n            pos = st //位置推移到筛选器处\n            val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符\n\n            if (!chompBalanced(queue[pos], next)) throw Error(\n                queue.substring(0, start) + \"后未平衡\"\n            ) //拉出一个筛选器,不平衡则报错\n\n        } while (end > pos)\n\n        start = pos //设置开始查找筛选器位置的起始位置\n\n        return splitRule(* split) //递归调用首段匹配\n    }\n\n    @JvmName(\"splitRuleNext\")\n    private tailrec fun splitRule(): ArrayList<String> { //二段匹配被调用,elementsType非空(已在首段赋值),直接按elementsType查找,比首段采用的方式更快\n\n        val end = pos //记录分隔位置\n        pos = start //重回开始，启动另一种查找\n\n        do {\n            val st = findToAny('[', '(') //查找筛选器位置\n\n            if (st == -1) {\n\n                rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组\n                pos = end + step //跳过分隔符\n\n                while (consumeTo(elementsType)) { //循环切分规则压入数组\n                    rule += queue.substring(start, pos)\n                    pos += step //跳过分隔符\n                }\n\n                rule += queue.substring(pos) //将剩余字段压入数组末尾\n\n                return rule\n            }\n\n            if (st > end) { //先匹配到st1pos，表明分隔字串不在选择器中，将选择器前分隔字串分隔的字段依次压入数组\n\n                rule += arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组\n                pos = end + step //跳过分隔符\n\n                while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组\n                    rule += queue.substring(start, pos)\n                    pos += step //跳过分隔符\n                }\n\n                return if (pos > st) {\n                    startX = start\n                    splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配\n                } else { //执行到此，证明后面再无分隔字符\n                    rule += queue.substring(pos) //将剩余字段压入数组末尾\n                    rule\n                }\n            }\n\n            pos = st //位置推移到筛选器处\n            val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符\n\n            if (!chompBalanced(queue[pos], next)) throw Error(\n                queue.substring(0, start) + \"后未平衡\"\n            ) //拉出一个筛选器,不平衡则报错\n\n        } while (end > pos)\n\n        start = pos //设置开始查找筛选器位置的起始位置\n\n        return if (!consumeTo(elementsType)) {\n            rule += queue.substring(startX)\n            rule\n        } else splitRule() //递归匹配\n\n    }\n\n    /**\n     * 替换内嵌规则\n     * @param inner 起始标志,如{$.\n     * @param startStep 不属于规则部分的前置字符长度，如{$.中{不属于规则的组成部分，故startStep为1\n     * @param endStep 不属于规则部分的后置字符长度\n     * @param fr 查找到内嵌规则时，用于解析的函数\n     *\n     * */\n    fun innerRule(\n        inner: String,\n        startStep: Int = 1,\n        endStep: Int = 1,\n        fr: (String) -> String?\n    ): String {\n        val st = StringBuilder()\n\n        while (consumeTo(inner)) { //拉取成功返回true，ruleAnalyzes里的字符序列索引变量pos后移相应位置，否则返回false,且isEmpty为true\n            val posPre = pos //记录consumeTo匹配位置\n            if (chompCodeBalanced('{', '}')) {\n                val frv = fr(queue.substring(posPre + startStep, pos - endStep))\n                if (!frv.isNullOrEmpty()) {\n                    st.append(queue.substring(startX, posPre) + frv) //压入内嵌规则前的内容，及内嵌规则解析得到的字符串\n                    startX = pos //记录下次规则起点\n                    continue //获取内容成功，继续选择下个内嵌规则\n                }\n            }\n            pos += inner.length //拉出字段不平衡，inner只是个普通字串，跳到此inner后继续匹配\n        }\n\n        return if (startX == 0) \"\" else st.apply {\n            append(queue.substring(startX))\n        }.toString()\n    }\n\n    /**\n     * 替换内嵌规则\n     * @param fr 查找到内嵌规则时，用于解析的函数\n     *\n     * */\n    fun innerRule(\n        startStr: String,\n        endStr: String,\n        fr: (String) -> String?\n    ): String {\n\n        val st = StringBuilder()\n        while (consumeTo(startStr)) { //拉取成功返回true，ruleAnalyzes里的字符序列索引变量pos后移相应位置，否则返回false,且isEmpty为true\n            pos += startStr.length //跳过开始字符串\n            val posPre = pos //记录consumeTo匹配位置\n            if (consumeTo(endStr)) {\n                val frv = fr(queue.substring(posPre, pos))\n                st.append(\n                    queue.substring(\n                        startX,\n                        posPre - startStr.length\n                    ) + frv\n                ) //压入内嵌规则前的内容，及内嵌规则解析得到的字符串\n                pos += endStr.length //跳过结束字符串\n                startX = pos //记录下次规则起点\n            }\n        }\n\n        return if (startX == 0) queue else st.apply {\n            append(queue.substring(startX))\n        }.toString()\n    }\n\n    val ruleTypeList = ArrayList<String>()\n\n    //设置平衡组函数，json或JavaScript时设置成chompCodeBalanced，否则为chompRuleBalanced\n    val chompBalanced = if (code) ::chompCodeBalanced else ::chompRuleBalanced\n\n    companion object {\n\n        /**\n         * 转义字符\n         */\n        private const val ESC = '\\\\'\n\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/analyzeRule/RuleData.kt",
    "content": "package io.legado.app.model.analyzeRule\n\nimport io.legado.app.utils.GSON\n\nclass RuleData : RuleDataInterface {\n\n    override val variableMap by lazy {\n        hashMapOf<String, String>()\n    }\n\n    override fun putVariable(key: String, value: String?) {\n        if (value == null) {\n            variableMap.remove(key)\n        } else {\n            variableMap[key] = value\n        }\n    }\n\n    fun getVariable(): String? {\n        if (variableMap.isEmpty()) {\n            return null\n        }\n        return GSON.toJson(variableMap)\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/analyzeRule/RuleDataInterface.kt",
    "content": "package io.legado.app.model.analyzeRule\n\ninterface RuleDataInterface {\n\n    val variableMap: HashMap<String, String>\n\n    fun putVariable(key: String, value: String?)\n\n    fun getVariable(key: String): String? {\n        return variableMap[key]\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/localBook/CbzFile.kt",
    "content": "package io.legado.app.model.localBook\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.utils.*\nimport java.io.File\nimport java.io.InputStream\nimport java.util.*\nimport java.nio.file.Paths\nimport java.util.zip.ZipFile\nimport java.util.zip.ZipEntry\nimport java.util.zip.ZipOutputStream\nimport com.htmake.reader.utils.getFileExtetion\nimport com.htmake.reader.utils.xml2map\n\nclass CbzFile(var book: Book) {\n    var info: MutableMap<String, Any>? = null\n    var cover: InputStream? = null\n\n    companion object {\n        private var cFile: CbzFile? = null\n\n        @Synchronized\n        private fun getCbzFile(book: Book): CbzFile {\n            if (cFile == null || cFile?.book?.bookUrl != book.bookUrl) {\n                cFile = CbzFile(book)\n                return cFile!!\n            }\n            cFile?.book = book\n            return cFile!!\n        }\n\n        @Synchronized\n        fun getChapterList(book: Book): ArrayList<BookChapter> {\n            return getCbzFile(book).getChapterList()\n        }\n\n        @Synchronized\n        fun getContent(book: Book, chapter: BookChapter): String? {\n            return getCbzFile(book).getContent(chapter)\n        }\n\n        @Synchronized\n        fun upBookInfo(book: Book, onlyCover: Boolean = false) {\n            if (onlyCover) {\n                return getCbzFile(book).updateCover()\n            }\n            return getCbzFile(book).upBookInfo()\n        }\n    }\n\n    init {\n    }\n\n    private fun parseBookInfo(): Pair<MutableMap<String, Any>?, InputStream?> {\n        if (cover != null || info != null) {\n            return Pair(info, cover)\n        }\n        val zf = ZipFile(book.getLocalFile())\n        val entries = zf.entries()\n        val imageExt = listOf(\"jpg\", \"jpeg\", \"gif\", \"png\", \"bmp\", \"webp\", \"svg\")\n\n        while (entries.hasMoreElements()) {\n            val zipEntry: ZipEntry = entries.nextElement() as ZipEntry\n\n            if (!zipEntry.isDirectory) {\n                val name = zipEntry.name\n                if (name.equals(\"ComicInfo.xml\")) {\n                    // 解析书籍信息\n                    var inputStream = zf.getInputStream(zipEntry)\n                    info = xml2map(inputStream)\n                } else if (cover == null) {\n                    // 解析第一张图片\n                    val ext = getFileExtetion(name).lowercase()\n                    if (imageExt.contains(ext)) {\n                        cover = zf.getInputStream(zipEntry)\n                    }\n                }\n            }\n            if (cover != null && info != null) {\n                break;\n            }\n        }\n\n        return Pair(info, cover)\n    }\n\n    private fun upBookInfo() {\n        val result = parseBookInfo()\n        if (result.first != null) {\n            val bookInfo = result.first as Map<String, Any>\n            val info = bookInfo.get(\"ComicInfo\") as Map<String, Any>? ?: null\n            book.name = (info?.get(\"Title\") ?: book.name) as String\n            book.author = (info?.get(\"Writer\") ?: book.author) as String\n        }\n        updateCover()\n    }\n\n    private fun updateCover() {\n        val coverFile = \"${MD5Utils.md5Encode16(book.bookUrl)}.jpg\"\n        val relativeCoverUrl = Paths.get(\"assets\", book.getUserNameSpace(), \"covers\", coverFile).toString()\n        book.coverUrl = \"/\" + relativeCoverUrl\n        val coverUrl = Paths.get(book.workRoot(), \"storage\", relativeCoverUrl).toString()\n        if (!File(coverUrl).exists()) {\n            val result = parseBookInfo()\n            if (result.second != null) {\n                val coverStream = result.second as InputStream\n                FileUtils.writeInputStream(coverUrl, coverStream)\n            }\n        }\n    }\n\n    private fun getContent(chapter: BookChapter): String? {\n        return \"\"\n    }\n\n    private fun getChapterList(): ArrayList<BookChapter> {\n        val chapterList = ArrayList<BookChapter>()\n        val zf = ZipFile(book.getLocalFile())\n        val entries = zf.entries()\n        var imageFileList = arrayListOf<String>();\n        while (entries.hasMoreElements()) {\n            val zipEntry: ZipEntry = entries.nextElement() as ZipEntry\n\n            if (!zipEntry.isDirectory) {\n                val name = zipEntry.name\n                if (!name.endsWith(\".xml\")) {\n                    // 只获取图片文件\n                    imageFileList.add(name)\n                }\n            }\n        }\n        // 排序\n        imageFileList.sort()\n        for (i in 0 until imageFileList.size) {\n            val name = imageFileList.get(i)\n            val chapter = BookChapter()\n            chapter.title = name\n            chapter.index = i\n            chapter.bookUrl = book.bookUrl\n            chapter.url = name\n            chapterList.add(chapter)\n        }\n        book.latestChapterTitle = chapterList.lastOrNull()?.title\n        book.totalChapterNum = chapterList.size\n        return chapterList\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/localBook/EpubFile.kt",
    "content": "package io.legado.app.model.localBook\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.help.BookHelp\nimport io.legado.app.utils.*\nimport me.ag2s.epublib.domain.EpubBook\nimport me.ag2s.epublib.domain.Resource\nimport me.ag2s.epublib.epub.EpubReader\nimport org.jsoup.Jsoup\nimport org.jsoup.nodes.Element\nimport org.jsoup.select.Elements\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.io.IOException\nimport java.io.InputStream\nimport java.nio.charset.Charset\nimport java.nio.file.Paths\nimport java.util.*\nimport java.util.zip.ZipFile\nimport mu.KotlinLogging\nprivate val logger = KotlinLogging.logger {}\n\nclass EpubFile(var book: Book) {\n\n    companion object {\n        private var eFile: EpubFile? = null\n\n        @Synchronized\n        private fun getEFile(book: Book): EpubFile {\n            if (eFile == null || eFile?.book?.bookUrl != book.bookUrl) {\n                eFile = EpubFile(book)\n                //对于Epub文件默认不启用替换\n                // book.setUseReplaceRule(false)\n                return eFile!!\n            }\n            eFile?.book = book\n            return eFile!!\n        }\n\n        @Synchronized\n        fun getChapterList(book: Book): ArrayList<BookChapter> {\n            if (book.tocUrl.isEmpty()) {\n                book.tocUrl = \"spin+toc\"\n            }\n            val epubFile = getEFile(book)\n            return when (book.tocUrl) {\n                \"toc\" -> {\n                    logger.info(\"epubFile.getChapterList\")\n                    epubFile.getChapterList()\n                }\n                \"spin\" -> {\n                    logger.info(\"epubFile.getChapterListBySpine\")\n                    epubFile.getChapterListBySpine()\n                }\n                \"spin<toc\" -> {\n                    logger.info(\"epubFile.getChapterListBySpinAndToc true\")\n                    epubFile.getChapterListBySpinAndToc(true)\n                }\n                \"spin+toc\" -> {\n                    logger.info(\"epubFile.getChapterListBySpinAndToc\")\n                    epubFile.getChapterListBySpinAndToc()\n                }\n                \"toc+spin\" -> {\n                    logger.info(\"epubFile.getChapterListByTocAndSpin\")\n                    epubFile.getChapterListByTocAndSpin()\n                }\n                \"toc<spin\" -> {\n                    logger.info(\"epubFile.getChapterListByTocAndSpin true\")\n                    epubFile.getChapterListByTocAndSpin(true)\n                }\n                else -> {\n                    logger.info(\"epubFile.getChapterListBySpinAndToc\")\n                    epubFile.getChapterListBySpinAndToc()\n                }\n            }\n        }\n\n        @Synchronized\n        fun getContent(book: Book, chapter: BookChapter): String? {\n            return getEFile(book).getContent(chapter)\n        }\n\n        @Synchronized\n        fun getImage(\n            book: Book,\n            href: String\n        ): InputStream? {\n            return getEFile(book).getImage(href)\n        }\n\n        @Synchronized\n        fun upBookInfo(book: Book, onlyCover: Boolean = false) {\n            if (onlyCover) {\n                return getEFile(book).updateCover()\n            }\n            return getEFile(book).upBookInfo()\n        }\n    }\n\n    private var mCharset: Charset = Charset.defaultCharset()\n    private var epubBook: EpubBook? = null\n        get() {\n            if (field != null) {\n                return field\n            }\n            field = readEpub()\n            return field\n        }\n\n    init {\n        try {\n            epubBook?.let {\n                // if (book.coverUrl.isNullOrEmpty()) {\n                //     book.coverUrl = FileUtils.getPath(\n                //         appCtx.externalFiles,\n                //         \"covers\",\n                //         \"${MD5Utils.md5Encode16(book.bookUrl)}.jpg\"\n                //     )\n                // }\n                // if (!File(book.coverUrl!!).exists()) {\n                //     /*部分书籍DRM处理后，封面获取异常，待优化*/\n                //     it.coverImage?.inputStream?.use { input ->\n                //         val cover = BitmapFactory.decodeStream(input)\n                //         val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))\n                //         cover.compress(Bitmap.CompressFormat.JPEG, 90, out)\n                //         out.flush()\n                //         out.close()\n                //     }\n                // }\n            }\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n    }\n\n    /*重写epub文件解析代码，直接读出压缩包文件生成Resources给epublib，这样的好处是可以逐一修改某些文件的格式错误*/\n    private fun readEpub(): EpubBook? {\n        try {\n            val file = book.getLocalFile()\n            //通过懒加载读取epub\n            return EpubReader().readEpubLazy(ZipFile(file), \"utf-8\")\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n        return null\n    }\n\n    private fun getContent(chapter: BookChapter): String? {\n        /**\n         * <image width=\"1038\" height=\"670\" xlink:href=\"...\"/>\n         * ...titlepage.xhtml\n         */\n       if (chapter.url.contains(\"titlepage.xhtml\")) {\n           return \"<img src=\\\"cover.jpeg\\\" />\"\n       }\n       /*获取当前章节文本*/\n       epubBook?.let { epubBook ->\n           val nextUrl = chapter.getVariable(\"nextUrl\")\n           val startFragmentId = chapter.startFragmentId\n           val endFragmentId = chapter.endFragmentId\n           val elements = Elements()\n           var isChapter = false\n           /*一些书籍依靠href索引的resource会包含多个章节，需要依靠fragmentId来截取到当前章节的内容*/\n           /*注:这里较大增加了内容加载的时间，所以首次获取内容后可存储到本地cache，减少重复加载*/\n           for (res in epubBook.contents) {\n               if (chapter.url.substringBeforeLast(\"#\") == res.href) {\n                   elements.add(getBody(res, startFragmentId, endFragmentId))\n                   isChapter = true\n                  /**\n                   * fix https://github.com/gedoor/legado/issues/1927 加载全部内容的bug\n                   * content src text/000001.html（当前章节）\n-                   * content src text/000001.html#toc_id_x (下一章节）\n                    */\n                   if (res.href == nextUrl?.substringBeforeLast(\"#\")) break\n               } else if (isChapter) {\n                   // fix 最后一章存在多个html时 内容缺失\n                   if (res.href == nextUrl?.substringBeforeLast(\"#\")) {\n                       break\n                   }\n                   elements.add(getBody(res, startFragmentId, endFragmentId))\n               }\n           }\n           var html = elements.outerHtml()\n           val tag = Book.rubyTag\n           if (book.getDelTag(tag)) {\n               html = html.replace(\"<ruby>\\\\s?([\\\\u4e00-\\\\u9fa5])\\\\s?.*?</ruby>\".toRegex(), \"$1\")\n           }\n           return HtmlFormatter.formatKeepImg(html)\n       }\n       return null\n   }\n\n   private fun getBody(res: Resource, startFragmentId: String?, endFragmentId: String?): Element {\n       val body = Jsoup.parse(String(res.data, mCharset)).body()\n       if (!startFragmentId.isNullOrBlank()) {\n           body.getElementById(startFragmentId)?.previousElementSiblings()?.remove()\n       }\n       if (!endFragmentId.isNullOrBlank() && endFragmentId != startFragmentId) {\n           body.getElementById(endFragmentId)?.run {\n               nextElementSiblings().remove()\n               remove()\n           }\n       }\n       /*选择去除正文中的H标签，部分书籍标题与阅读标题重复待优化*/\n       val tag = Book.hTag\n       if (book.getDelTag(tag)) {\n           body.getElementsByTag(\"h1\").remove()\n           body.getElementsByTag(\"h2\").remove()\n           body.getElementsByTag(\"h3\").remove()\n           body.getElementsByTag(\"h4\").remove()\n           body.getElementsByTag(\"h5\").remove()\n           body.getElementsByTag(\"h6\").remove()\n           //body.getElementsMatchingOwnText(chapter.title)?.remove()\n       }\n\n       val children = body.children()\n       children.select(\"script\").remove()\n       children.select(\"style\").remove()\n       return body\n   }\n\n    private fun getImage(href: String): InputStream? {\n        val abHref = href.replace(\"../\", \"\")\n        return epubBook?.resources?.getByHref(abHref)?.inputStream\n    }\n\n    private fun upBookInfo() {\n        if (epubBook == null) {\n            eFile = null\n            book.intro = \"书籍导入异常\"\n        } else {\n            val metadata = epubBook!!.metadata\n            book.name = metadata.firstTitle\n            if (book.name.isEmpty()) {\n                book.name = book.originName.replace(\".epub\", \"\")\n            }\n\n            if (metadata.authors.size > 0) {\n                val author =\n                    metadata.authors[0].toString().replace(\"^, |, $\".toRegex(), \"\")\n                book.author = author\n            }\n            if (metadata.descriptions.size > 0) {\n                book.intro = Jsoup.parse(metadata.descriptions[0]).text()\n            }\n\n            updateCover()\n        }\n    }\n\n    fun updateCover() {\n        val coverFile = \"${MD5Utils.md5Encode16(book.bookUrl)}.jpg\"\n        val relativeCoverUrl = Paths.get(\"assets\", book.getUserNameSpace(), \"covers\", coverFile).toString()\n        book.coverUrl = \"/\" + relativeCoverUrl\n        val coverUrl = Paths.get(book.workRoot(), \"storage\", relativeCoverUrl).toString()\n        if (!File(coverUrl).exists()) {\n            FileUtils.writeBytes(coverUrl, epubBook!!.coverImage.data)\n        }\n        // 保存 cover\n        // val cover = epubBook!!.coverImage?.href\n        // if (cover != null) {\n        //     val epubRootDir = book.getEpubRootDir()\n        //     if (epubRootDir.isEmpty()) {\n        //         book.coverUrl = book.bookUrl.replace(\"storage/data/\", \"/epub/\") + \"/index/\" + cover\n        //     } else {\n        //         book.coverUrl = book.bookUrl.replace(\"storage/data/\", \"/epub/\") + \"/index/\" + epubRootDir + \"/\" + cover\n        //     }\n        // }\n    }\n\n    fun getChapterListBySpine(): ArrayList<BookChapter> {\n        val chapterList = ArrayList<BookChapter>()\n        epubBook?.spine?.spineReferences?.forEachIndexed { index, spinResource ->\n            val resource = spinResource.resource\n            var title = resource.title\n            if (title.isNullOrEmpty()) {\n                try {\n                    val doc =\n                        Jsoup.parse(String(resource.data, mCharset))\n                    val elements = doc.getElementsByTag(\"title\")\n                    if (elements.size > 0) {\n                        title = elements[0].text()\n                    }\n                } catch (e: IOException) {\n                    e.printStackTrace()\n                }\n            }\n\n            val chapter = BookChapter()\n            chapter.index = index\n            chapter.bookUrl = book.bookUrl\n            chapter.url = resource.href\n            if (index == 0 && title.isEmpty()) {\n                chapter.title = \"封面\"\n            } else {\n                chapter.title = title\n            }\n            chapterList.add(chapter)\n        }\n        book.latestChapterTitle = chapterList.lastOrNull()?.title\n        book.totalChapterNum = chapterList.size\n        return chapterList\n    }\n\n    fun getChapterList(): ArrayList<BookChapter> {\n        val chapterList = ArrayList<BookChapter>()\n        epubBook?.tableOfContents?.allUniqueResources?.forEachIndexed { index, resource ->\n            var title = resource.title\n            if (title.isNullOrEmpty()) {\n                try {\n                    val doc =\n                        Jsoup.parse(String(resource.data, mCharset))\n                    val elements = doc.getElementsByTag(\"title\")\n                    if (elements.size > 0) {\n                        title = elements[0].text()\n                    }\n                } catch (e: IOException) {\n                    e.printStackTrace()\n                }\n            }\n            val chapter = BookChapter()\n            chapter.index = index\n            chapter.bookUrl = book.bookUrl\n            chapter.url = resource.href\n            if (index == 0 && title.isEmpty()) {\n                chapter.title = \"封面\"\n            } else {\n                chapter.title = title\n            }\n            chapterList.add(chapter)\n        }\n        book.latestChapterTitle = chapterList.lastOrNull()?.title\n        book.totalChapterNum = chapterList.size\n        return chapterList\n    }\n\n    fun getChapterListBySpinAndToc(useTocTitle: Boolean = false): ArrayList<BookChapter> {\n        // 如果读取了 toc，那么 spin 就会使用 toc 的章节名\n        val tocChapterList = getChapterList()\n        val spinChapterList = getChapterListBySpine()\n\n        if (spinChapterList.size == 0) {\n            return tocChapterList;\n        }\n\n        if (tocChapterList.size == 0) {\n            return spinChapterList;\n        }\n\n        val titleMap: MutableMap<String, BookChapter> = mutableMapOf();\n\n        for (i in 0 until tocChapterList.size) {\n            titleMap.put(tocChapterList.get(i).url, tocChapterList.get(i))\n        }\n\n        for (i in 0 until spinChapterList.size) {\n            val chapter = spinChapterList.get(i)\n            val tocChapter = titleMap.get(chapter.url)\n            if (tocChapter != null && tocChapter.title.isNotEmpty()) {\n                if (useTocTitle || chapter.title.isEmpty()) {\n                    chapter.title = tocChapter.title\n                }\n            }\n        }\n\n        book.latestChapterTitle = spinChapterList.lastOrNull()?.title\n        book.totalChapterNum = spinChapterList.size\n        return spinChapterList\n    }\n\n    fun getChapterListByTocAndSpin(useSpinTitle: Boolean = false): ArrayList<BookChapter> {\n        // 如果读取了 toc，那么 spin 就会使用 toc 的章节名\n        val tocChapterList = getChapterList()\n        val spinChapterList = getChapterListBySpine()\n\n        if (tocChapterList.size == 0) {\n            return spinChapterList;\n        }\n\n        if (spinChapterList.size == 0) {\n            return tocChapterList;\n        }\n\n        val titleMap: MutableMap<String, BookChapter> = mutableMapOf();\n\n        for (i in 0 until spinChapterList.size) {\n            titleMap.put(spinChapterList.get(i).url, spinChapterList.get(i))\n        }\n\n        for (i in 0 until tocChapterList.size) {\n            val chapter = tocChapterList.get(i)\n            val tocChapter = titleMap.get(chapter.url)\n            if (tocChapter != null && tocChapter.title.isNotEmpty()) {\n                if (useSpinTitle || chapter.title.isEmpty()) {\n                    chapter.title = tocChapter.title\n                }\n            }\n        }\n\n        book.latestChapterTitle = tocChapterList.lastOrNull()?.title\n        book.totalChapterNum = tocChapterList.size\n        return tocChapterList\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/localBook/LocalBook.kt",
    "content": "package io.legado.app.model.localBook\n\nimport io.legado.app.constant.AppConst\nimport io.legado.app.constant.AppPattern\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.help.BookHelp\nimport io.legado.app.utils.*\nimport io.legado.app.exception.TocEmptyException\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.FileNotFoundException\nimport java.io.InputStream\nimport java.util.regex.Matcher\nimport java.util.regex.Pattern\nimport javax.script.SimpleBindings\n\nobject LocalBook {\n\n    private val nameAuthorPatterns = arrayOf(\n        Pattern.compile(\"(.*?)《([^《》]+)》.*?作者：(.*)\"),\n        Pattern.compile(\"(.*?)《([^《》]+)》(.*)\"),\n        Pattern.compile(\"(^)(.+) 作者：(.+)$\"),\n        Pattern.compile(\"(^)(.+) by (.+)$\")\n    )\n\n    @Throws(FileNotFoundException::class, SecurityException::class)\n    fun getBookInputStream(book: Book): InputStream {\n        val file = book.getLocalFile()\n        if (file.exists()) {\n            return FileInputStream(file)\n        }\n        throw FileNotFoundException(book.name + \" 文件不存在\")\n    }\n\n    @Throws(Exception::class)\n    fun getChapterList(book: Book): ArrayList<BookChapter> {\n        val chapters = when {\n            book.isEpub() -> {\n                EpubFile.getChapterList(book)\n            }\n            book.isUmd() -> {\n                UmdFile.getChapterList(book)\n            }\n            book.isCbz() -> {\n                CbzFile.getChapterList(book)\n            }\n            else -> {\n                TextFile.getChapterList(book)\n            }\n        }\n        if (chapters.isEmpty()) {\n            throw TocEmptyException(\"Chapterlist is empty  \" + book.getLocalFile())\n        }\n        return chapters\n    }\n\n    fun getContent(book: Book, chapter: BookChapter): String? {\n        return when {\n            book.isEpub() -> {\n                EpubFile.getContent(book, chapter)\n            }\n            book.isUmd() -> {\n                UmdFile.getContent(book, chapter)\n            }\n            book.isCbz() -> {\n                CbzFile.getContent(book, chapter)\n            }\n            else -> {\n                TextFile.getContent(book, chapter)\n            }\n        }\n    }\n\n    fun analyzeNameAuthor(fileName: String): Pair<String, String> {\n        val tempFileName = fileName.substringBeforeLast(\".\")\n        var name: String\n        var author: String\n\n        for (pattern in nameAuthorPatterns) {\n            pattern.matcher(tempFileName).takeIf { it.find() }?.run {\n                name = group(2)!!\n                val group1 = group(1) ?: \"\"\n                val group3 = group(3) ?: \"\"\n                author = BookHelp.formatBookAuthor(group1 + group3)\n                return Pair(name, author)\n            }\n        }\n\n        name = BookHelp.formatBookName(tempFileName)\n        author = BookHelp.formatBookAuthor(tempFileName.replace(name, \"\"))\n            .takeIf { it.length != tempFileName.length } ?: \"\"\n\n        return Pair(name, author)\n    }\n\n    fun deleteBook(book: Book) {\n        kotlin.runCatching {\n            var bookFile = book.getLocalFile();\n            if (book.isLocalTxt() || book.isUmd()) {\n                if (bookFile.exists()) {\n                    bookFile.delete()\n                }\n            }\n            if (book.isEpub()) {\n                bookFile = bookFile.parentFile\n                if (bookFile != null && bookFile.exists()) {\n                    FileUtils.delete(bookFile, true)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/model/localBook/TextFile.kt",
    "content": "package io.legado.app.model.localBook\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.TxtTocRule\nimport io.legado.app.help.DefaultData\nimport io.legado.app.utils.EncodingDetect\nimport io.legado.app.utils.MD5Utils\nimport io.legado.app.utils.StringUtils\nimport io.legado.app.utils.Utf8BomUtils\nimport java.io.FileNotFoundException\nimport java.nio.charset.Charset\nimport java.util.regex.Matcher\nimport java.util.regex.Pattern\nimport kotlin.math.min\nimport mu.KotlinLogging\n\nprivate val logger = KotlinLogging.logger {}\n\nclass TextFile(private val book: Book) {\n\n    companion object {\n\n        @Throws(FileNotFoundException::class)\n        fun getChapterList(book: Book): ArrayList<BookChapter> {\n            return TextFile(book).getChapterList()\n        }\n\n        @Throws(FileNotFoundException::class)\n        fun getContent(book: Book, bookChapter: BookChapter): String {\n            val count = (bookChapter.end!! - bookChapter.start!!).toInt()\n            val buffer = ByteArray(count)\n            LocalBook.getBookInputStream(book).use { bis ->\n                bis.skip(bookChapter.start!!)\n                bis.read(buffer)\n            }\n            if (book.charset == null) {\n                book.charset = EncodingDetect.getEncode(book.getLocalFile())\n            }\n            return String(buffer, book.fileCharset())\n                .substringAfter(bookChapter.title)\n                .replace(\"^[\\\\n\\\\s]+\".toRegex(), \"　　\")\n        }\n\n    }\n\n    private val blank: Byte = 0x0a\n\n    //默认从文件中获取数据的长度\n    private val bufferSize = 512000\n\n    //没有标题的时候，每个章节的最大长度\n    private val maxLengthWithNoToc = 10 * 1024\n\n    //使用正则划分目录，每个章节的最大允许长度\n    private val maxLengthWithToc = 102400\n\n    private var charset: Charset = book.fileCharset()\n\n    /**\n     * 获取目录\n     */\n    @Throws(FileNotFoundException::class)\n    fun getChapterList(): ArrayList<BookChapter> {\n        if (book.charset == null || book.tocUrl.isBlank()) {\n            LocalBook.getBookInputStream(book).use { bis ->\n                val buffer = ByteArray(bufferSize)\n                val length = bis.read(buffer)\n                if (book.charset.isNullOrBlank()) {\n                    book.charset = EncodingDetect.getEncode(buffer.copyOf(length))\n                }\n                charset = book.fileCharset()\n                if (book.tocUrl.isBlank()) {\n                    val blockContent = String(buffer, 0, length, charset)\n                    book.tocUrl = getTocRule(blockContent)?.pattern() ?: \"\"\n                }\n            }\n        }\n        val toc = analyze(book.tocUrl.toPattern(Pattern.MULTILINE))\n        toc.forEachIndexed { index, bookChapter ->\n            bookChapter.index = index\n            bookChapter.bookUrl = book.bookUrl\n            bookChapter.url = MD5Utils.md5Encode16(book.originName + index + bookChapter.title)\n        }\n        book.latestChapterTitle = toc.last().title\n        book.totalChapterNum = toc.size\n        return toc\n    }\n\n    /**\n     * 按规则解析目录\n     */\n    private fun analyze(pattern: Pattern?): ArrayList<BookChapter> {\n        if (pattern?.pattern().isNullOrEmpty()) {\n            return analyze()\n        }\n        pattern ?: return analyze()\n        val toc = arrayListOf<BookChapter>()\n        LocalBook.getBookInputStream(book).use { bis ->\n            var blockContent: String\n            //加载章节\n            var curOffset: Long = 0\n            //读取的长度\n            var length: Int\n            val buffer = ByteArray(bufferSize)\n            var bufferStart = 3\n            bis.read(buffer, 0, 3)\n            if (Utf8BomUtils.hasBom(buffer)) {\n                bufferStart = 0\n                curOffset = 3\n            }\n            //获取文件中的数据到buffer，直到没有数据为止\n            while (\n                bis.read(\n                    buffer,\n                    bufferStart,\n                    bufferSize - bufferStart\n                ).also { length = it } > 0\n            ) {\n                var end = bufferStart + length\n                if (end == bufferSize) {\n                    for (i in bufferStart + length - 1 downTo 0) {\n                        if (buffer[i] == blank) {\n                            end = i\n                            break\n                        }\n                    }\n                }\n                //将数据转换成String, 不能超过length\n                blockContent = String(buffer, 0, end, charset)\n                buffer.copyInto(buffer, 0, end, bufferStart + length)\n                bufferStart = bufferStart + length - end\n                length = end\n                //当前Block下使过的String的指针\n                var seekPos = 0\n                //进行正则匹配\n                val matcher: Matcher = pattern.matcher(blockContent)\n                //如果存在相应章节\n                while (matcher.find()) { //获取匹配到的字符在字符串中的起始位置\n                    val chapterStart = matcher.start()\n                    //获取章节内容\n                    val chapterContent = blockContent.substring(seekPos, chapterStart)\n                    val chapterLength = chapterContent.toByteArray(charset).size\n                    val lastStart = toc.lastOrNull()?.start ?: curOffset\n                    if (book.getSplitLongChapter()\n                        && curOffset + chapterLength - lastStart > maxLengthWithToc\n                    ) {\n                        toc.lastOrNull()?.let {\n                            it.end = it.start\n                        }\n                        //章节字数太多进行拆分\n                        val lastTitle = toc.lastOrNull()?.title\n                        val lastTitleLength = lastTitle?.toByteArray(charset)?.size ?: 0\n                        val chapters = analyze(\n                            lastStart + lastTitleLength,\n                            curOffset + chapterLength\n                        )\n                        lastTitle?.let {\n                            chapters.forEachIndexed { index, bookChapter ->\n                                bookChapter.title = \"$lastTitle(${index + 1})\"\n                            }\n                        }\n                        toc.addAll(chapters)\n                        //创建当前章节\n                        val curChapter = BookChapter()\n                        curChapter.title = matcher.group()\n                        curChapter.start = curOffset + chapterLength\n                        toc.add(curChapter)\n                    } else if (seekPos == 0 && chapterStart != 0) {\n                        /*\n                         * 如果 seekPos == 0 && chapterStart != 0 表示当前block处前面有一段内容\n                         * 第一种情况一定是序章 第二种情况是上一个章节的内容\n                         */\n                        if (toc.isEmpty()) { //如果当前没有章节，那么就是序章\n                            //加入简介\n                            if (StringUtils.trim(chapterContent).isNotEmpty()) {\n                                val qyChapter = BookChapter()\n                                qyChapter.title = \"前言\"\n                                qyChapter.start = curOffset\n                                qyChapter.end = chapterLength.toLong()\n                                toc.add(qyChapter)\n                            }\n                            //创建当前章节\n                            val curChapter = BookChapter()\n                            curChapter.title = matcher.group()\n                            curChapter.start = chapterLength.toLong()\n                            toc.add(curChapter)\n                        } else { //否则就block分割之后，上一个章节的剩余内容\n                            //获取上一章节\n                            val lastChapter = toc.last()\n                            lastChapter.isVolume =\n                                chapterContent.substringAfter(lastChapter.title).isBlank()\n                            //将当前段落添加上一章去\n                            lastChapter.end =\n                                lastChapter.end!! + chapterLength.toLong()\n                            //创建当前章节\n                            val curChapter = BookChapter()\n                            curChapter.title = matcher.group()\n                            curChapter.start = lastChapter.end\n                            toc.add(curChapter)\n                        }\n                    } else {\n                        if (toc.isNotEmpty()) { //获取章节内容\n                            //获取上一章节\n                            val lastChapter = toc.last()\n                            lastChapter.isVolume =\n                                chapterContent.substringAfter(lastChapter.title).isBlank()\n                            lastChapter.end =\n                                lastChapter.start!! + chapterContent.toByteArray(charset).size.toLong()\n                            //创建当前章节\n                            val curChapter = BookChapter()\n                            curChapter.title = matcher.group()\n                            curChapter.start = lastChapter.end\n                            toc.add(curChapter)\n                        } else { //如果章节不存在则创建章节\n                            val curChapter = BookChapter()\n                            curChapter.title = matcher.group()\n                            curChapter.start = curOffset\n                            curChapter.end = curOffset\n                            toc.add(curChapter)\n                        }\n                    }\n                    //设置指针偏移\n                    seekPos += chapterContent.length\n                }\n                //block的偏移点\n                curOffset += length.toLong()\n                //设置上一章的结尾\n                toc.lastOrNull()?.end = curOffset\n            }\n        }\n        System.gc()\n        System.runFinalization()\n        return toc\n    }\n\n    /**\n     * 无规则拆分目录\n     */\n    private fun analyze(\n        fileStart: Long = 0L,\n        fileEnd: Long = Long.MAX_VALUE\n    ): ArrayList<BookChapter> {\n        val toc = arrayListOf<BookChapter>()\n        LocalBook.getBookInputStream(book).use { bis ->\n            //block的个数\n            var blockPos = 0\n            //加载章节\n            var curOffset: Long = 0\n            var chapterPos = 0\n            //读取的长度\n            var length = 0\n            val buffer = ByteArray(bufferSize)\n            var bufferStart = 3\n            if (fileStart == 0L) {\n                bis.read(buffer, 0, 3)\n                if (Utf8BomUtils.hasBom(buffer)) {\n                    bufferStart = 0\n                    curOffset = 3\n                }\n            } else {\n                bis.skip(fileStart)\n                curOffset = fileStart\n                bufferStart = 0\n            }\n            //获取文件中的数据到buffer，直到没有数据为止\n            while (\n                fileEnd - curOffset - bufferStart > 0 &&\n                bis.read(\n                    buffer,\n                    bufferStart,\n                    min(\n                        (bufferSize - bufferStart).toLong(),\n                        fileEnd - curOffset - bufferStart\n                    ).toInt()\n                ).also { length = it } > 0\n            ) {\n                blockPos++\n                //章节在buffer的偏移量\n                var chapterOffset = 0\n                //当前剩余可分配的长度\n                length += bufferStart\n                var strLength = length\n                //分章的位置\n                chapterPos = 0\n                while (strLength > 0) {\n                    chapterPos++\n                    //是否长度超过一章\n                    if (strLength > maxLengthWithNoToc) { //在buffer中一章的终止点\n                        var end = length\n                        //寻找换行符作为终止点\n                        for (i in chapterOffset + maxLengthWithNoToc until length) {\n                            if (buffer[i] == blank) {\n                                end = i\n                                break\n                            }\n                        }\n                        val chapter = BookChapter()\n                        chapter.title = \"第${blockPos}章($chapterPos)\"\n                        chapter.start = toc.lastOrNull()?.end ?: curOffset\n                        chapter.end = chapter.start!! + end - chapterOffset\n                        toc.add(chapter)\n                        //减去已经被分配的长度\n                        strLength -= (end - chapterOffset)\n                        //设置偏移的位置\n                        chapterOffset = end\n                    } else {\n                        buffer.copyInto(buffer, 0, length - strLength, length)\n                        length -= strLength\n                        bufferStart = strLength\n                        strLength = 0\n                    }\n                }\n                //block的偏移点\n                curOffset += length.toLong()\n            }\n            //设置结尾章节\n            if (bufferStart > 100 || toc.isEmpty()) {\n                val chapter = BookChapter()\n                chapter.title = \"第${blockPos}章(${chapterPos})\"\n                chapter.start = toc.lastOrNull()?.end ?: curOffset\n                chapter.end = chapter.start!! + bufferStart\n                toc.add(chapter)\n            } else {\n                toc.lastOrNull()?.let {\n                    it.end = it.end!! + bufferStart\n                }\n            }\n        }\n        return toc\n    }\n\n    /**\n     * 获取所有匹配次数大于1的目录规则\n     */\n    private fun getTocRule(content: String): Pattern? {\n        val rules = getTocRules().reversed()\n        var maxCs = 1\n        var tocPattern: Pattern? = null\n        for (tocRule in rules) {\n            val pattern = tocRule.rule.toPattern(Pattern.MULTILINE)\n            val matcher = pattern.matcher(content)\n            var cs = 0\n            while (matcher.find()) {\n                cs++\n            }\n            if (cs >= maxCs) {\n                maxCs = cs\n                tocPattern = pattern\n            }\n        }\n        return tocPattern\n    }\n\n    /**\n     * 获取启用的目录规则\n     */\n    private fun getTocRules(): List<TxtTocRule> {\n        return DefaultData.txtTocRules.filter {\n            it.enable\n        }\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/localBook/UmdFile.kt",
    "content": "package io.legado.app.model.localBook\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.utils.*\nimport me.ag2s.umdlib.domain.UmdBook\nimport me.ag2s.umdlib.umd.UmdReader\nimport java.io.File\nimport java.io.InputStream\nimport java.util.*\nimport java.nio.file.Paths\n\nclass UmdFile(var book: Book) {\n    companion object {\n        private var uFile: UmdFile? = null\n\n        @Synchronized\n        private fun getUFile(book: Book): UmdFile {\n\n            if (uFile == null || uFile?.book?.bookUrl != book.bookUrl) {\n                uFile = UmdFile(book)\n                return uFile!!\n            }\n            uFile?.book = book\n            return uFile!!\n        }\n\n        @Synchronized\n        fun getChapterList(book: Book): ArrayList<BookChapter> {\n            return getUFile(book).getChapterList()\n        }\n\n        @Synchronized\n        fun getContent(book: Book, chapter: BookChapter): String? {\n            return getUFile(book).getContent(chapter)\n        }\n\n        @Synchronized\n        fun getImage(\n            book: Book,\n            href: String\n        ): InputStream? {\n            return getUFile(book).getImage(href)\n        }\n\n\n        @Synchronized\n        fun upBookInfo(book: Book, onlyCover: Boolean = false) {\n            if (onlyCover) {\n                return getUFile(book).updateCover()\n            }\n            return getUFile(book).upBookInfo()\n        }\n    }\n\n\n    private var umdBook: UmdBook? = null\n        get() {\n            if (field != null) {\n                return field\n            }\n            field = readUmd()\n            return field\n        }\n\n\n    init {\n        try {\n            umdBook?.let {\n                // if (book.coverUrl.isNullOrEmpty()) {\n                //     book.coverUrl = FileUtils.getPath(\n                //         appCtx.externalFiles,\n                //         \"covers\",\n                //         \"${MD5Utils.md5Encode16(book.bookUrl)}.jpg\"\n                //     )\n                // }\n                // if (!File(book.coverUrl!!).exists()) {\n                //     FileUtils.writeBytes(book.coverUrl!!, it.cover.coverData)\n\n                // }\n            }\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n    }\n\n    private fun readUmd(): UmdBook? {\n        val input = File(book.bookUrl).inputStream()\n        return UmdReader().read(input)\n    }\n\n    private fun upBookInfo() {\n        if (umdBook == null) {\n            uFile = null\n            book.intro = \"书籍导入异常\"\n        } else {\n            val hd = umdBook!!.header\n            book.name = hd.title\n            book.author = hd.author\n            book.kind = hd.bookType\n\n            updateCover()\n        }\n    }\n\n    private fun updateCover() {\n        if (umdBook == null) {\n            uFile = null\n            return\n        }\n        val coverFile = \"${MD5Utils.md5Encode16(book.bookUrl)}.jpg\"\n        val relativeCoverUrl = Paths.get(\"assets\", book.getUserNameSpace(), \"covers\", coverFile).toString()\n        book.coverUrl = \"/\" + relativeCoverUrl\n        val coverUrl = Paths.get(book.workRoot(), \"storage\", relativeCoverUrl).toString()\n        if (!File(coverUrl).exists()) {\n            FileUtils.writeBytes(coverUrl, umdBook!!.cover.coverData)\n        }\n    }\n\n    private fun getContent(chapter: BookChapter): String? {\n        return umdBook?.chapters?.getContentString(chapter.index)\n    }\n\n    private fun getChapterList(): ArrayList<BookChapter> {\n        val chapterList = ArrayList<BookChapter>()\n        umdBook?.chapters?.titles?.forEachIndexed { index, _ ->\n            val title = umdBook!!.chapters.getTitle(index)\n            val chapter = BookChapter()\n            chapter.title = title\n            chapter.index = index\n            chapter.bookUrl = book.bookUrl\n            chapter.url = index.toString()\n            System.out.println(\"UMD\" + chapter.url)\n            chapterList.add(chapter)\n        }\n        book.latestChapterTitle = chapterList.lastOrNull()?.title\n        book.totalChapterNum = chapterList.size\n        return chapterList\n    }\n\n    private fun getImage(@Suppress(\"UNUSED_PARAMETER\") href: String): InputStream? {\n        return null\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/rss/Rss.kt",
    "content": "package io.legado.app.model.rss\n\nimport io.legado.app.data.entities.RssArticle\nimport io.legado.app.data.entities.RssSource\nimport io.legado.app.help.coroutine.Coroutine\nimport io.legado.app.model.DebugLog\nimport io.legado.app.model.analyzeRule.AnalyzeRule\nimport io.legado.app.model.analyzeRule.AnalyzeUrl\nimport io.legado.app.model.analyzeRule.RuleData\nimport io.legado.app.utils.NetworkUtils\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlin.coroutines.CoroutineContext\n\nobject Rss {\n    suspend fun getArticles(\n        sortName: String,\n        sortUrl: String,\n        rssSource: RssSource,\n        page: Int,\n        debugLog: DebugLog?\n    ): Pair<MutableList<RssArticle>, String?> {\n        val ruleData = RuleData()\n        val analyzeUrl = AnalyzeUrl(\n            sortUrl,\n            page = page,\n            source = rssSource,\n            ruleData = ruleData,\n            headerMapF = rssSource.getHeaderMap()\n        )\n        val body = analyzeUrl.getStrResponseAwait(debugLog = debugLog).body\n        // debugLog?.log(rssSource.sourceUrl, \"┌获取链接内容:${sortUrl}\")\n        // debugLog?.log(rssSource.sourceUrl, \"└\\n${body}\")\n        return RssParserByRule.parseXML(sortName, sortUrl, body, rssSource, ruleData, debugLog)\n    }\n\n    suspend fun getContent(\n        rssArticle: RssArticle,\n        ruleContent: String,\n        rssSource: RssSource,\n        debugLog: DebugLog?\n    ): String {\n        val analyzeUrl = AnalyzeUrl(\n            rssArticle.link,\n            baseUrl = rssArticle.origin,\n            source = rssSource,\n            ruleData = rssArticle,\n            headerMapF = rssSource.getHeaderMap()\n        )\n        val body = analyzeUrl.getStrResponseAwait(debugLog = debugLog).body\n        // debugLog?.log(rssSource.sourceUrl, \"┌获取链接内容:${rssArticle.link}\")\n        // debugLog?.log(rssSource.sourceUrl, \"└\\n${body}\")\n        val analyzeRule = AnalyzeRule(rssArticle, rssSource)\n        analyzeRule.setContent(body)\n            .setBaseUrl(NetworkUtils.getAbsoluteURL(rssArticle.origin, rssArticle.link))\n        return analyzeRule.getString(ruleContent)\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/rss/RssParserByRule.kt",
    "content": "package io.legado.app.model.rss\n\nimport io.legado.app.data.entities.RssArticle\nimport io.legado.app.data.entities.RssSource\nimport io.legado.app.exception.NoStackTraceException\nimport io.legado.app.model.DebugLog\nimport io.legado.app.model.analyzeRule.AnalyzeRule\nimport io.legado.app.model.analyzeRule.RuleData\nimport io.legado.app.utils.NetworkUtils\nimport java.util.*\n\nobject RssParserByRule {\n\n    @Throws(Exception::class)\n    fun parseXML(\n        sortName: String,\n        sortUrl: String,\n        body: String?,\n        rssSource: RssSource,\n        ruleData: RuleData,\n        debugLog: DebugLog?\n    ): Pair<MutableList<RssArticle>, String?> {\n        val sourceUrl = rssSource.sourceUrl\n        var nextUrl: String? = null\n        if (body.isNullOrBlank()) {\n            throw NoStackTraceException(\n                \"error_get_web_content: \" + rssSource.sourceUrl\n            )\n        }\n        // debugLog?.log(sourceUrl, \"≡获取成功:$sourceUrl\")\n        // debugLog?.log(sourceUrl, body)\n        var ruleArticles = rssSource.ruleArticles\n        if (ruleArticles.isNullOrBlank()) {\n            debugLog?.log(sourceUrl, \"⇒列表规则为空, 使用默认规则解析\")\n            return RssParserDefault.parseXML(sortName, body, sourceUrl, debugLog)\n        } else {\n            val articleList = mutableListOf<RssArticle>()\n            val analyzeRule = AnalyzeRule(ruleData, rssSource)\n            analyzeRule.setContent(body).setBaseUrl(sortUrl)\n            analyzeRule.setRedirectUrl(sortUrl)\n            var reverse = false\n            if (ruleArticles.startsWith(\"-\")) {\n                reverse = true\n                ruleArticles = ruleArticles.substring(1)\n            }\n            debugLog?.log(sourceUrl, \"┌获取列表\")\n            val collections = analyzeRule.getElements(ruleArticles)\n            debugLog?.log(sourceUrl, \"└列表大小:${collections.size}\")\n            if (!rssSource.ruleNextPage.isNullOrEmpty()) {\n                debugLog?.log(sourceUrl, \"┌获取下一页链接\")\n                if (rssSource.ruleNextPage!!.uppercase(Locale.getDefault()) == \"PAGE\") {\n                    nextUrl = sortUrl\n                } else {\n                    nextUrl = analyzeRule.getString(rssSource.ruleNextPage)\n                    if (nextUrl.isNotEmpty()) {\n                        nextUrl = NetworkUtils.getAbsoluteURL(sortUrl, nextUrl)\n                    }\n                }\n                debugLog?.log(sourceUrl, \"└$nextUrl\")\n            }\n            val ruleTitle = analyzeRule.splitSourceRule(rssSource.ruleTitle)\n            val rulePubDate = analyzeRule.splitSourceRule(rssSource.rulePubDate)\n            val ruleDescription = analyzeRule.splitSourceRule(rssSource.ruleDescription)\n            val ruleImage = analyzeRule.splitSourceRule(rssSource.ruleImage)\n            val ruleLink = analyzeRule.splitSourceRule(rssSource.ruleLink)\n            val variable = ruleData.getVariable()\n            for ((index, item) in collections.withIndex()) {\n                getItem(\n                    sourceUrl, item, analyzeRule, variable, index == 0,\n                    ruleTitle, rulePubDate, ruleDescription, ruleImage, ruleLink, debugLog\n                )?.let {\n                    it.sort = sortName\n                    it.origin = sourceUrl\n                    articleList.add(it)\n                }\n            }\n            if (reverse) {\n                articleList.reverse()\n            }\n            return Pair(articleList, nextUrl)\n        }\n    }\n\n    private fun getItem(\n        sourceUrl: String,\n        item: Any,\n        analyzeRule: AnalyzeRule,\n        variable: String?,\n        log: Boolean,\n        ruleTitle: List<AnalyzeRule.SourceRule>,\n        rulePubDate: List<AnalyzeRule.SourceRule>,\n        ruleDescription: List<AnalyzeRule.SourceRule>,\n        ruleImage: List<AnalyzeRule.SourceRule>,\n        ruleLink: List<AnalyzeRule.SourceRule>,\n        debugLog: DebugLog?\n    ): RssArticle? {\n        val rssArticle = RssArticle(variable = variable)\n        analyzeRule.ruleData = rssArticle\n        analyzeRule.setContent(item)\n        debugLog?.log(sourceUrl, \"┌获取标题\", log)\n        rssArticle.title = analyzeRule.getString(ruleTitle)\n        debugLog?.log(sourceUrl, \"└${rssArticle.title}\", log)\n        debugLog?.log(sourceUrl, \"┌获取时间\", log)\n        rssArticle.pubDate = analyzeRule.getString(rulePubDate)\n        debugLog?.log(sourceUrl, \"└${rssArticle.pubDate}\", log)\n        debugLog?.log(sourceUrl, \"┌获取描述\", log)\n        if (ruleDescription.isNullOrEmpty()) {\n            rssArticle.description = null\n            debugLog?.log(sourceUrl, \"└描述规则为空，将会解析内容页\", log)\n        } else {\n            rssArticle.description = analyzeRule.getString(ruleDescription)\n            debugLog?.log(sourceUrl, \"└${rssArticle.description}\", log)\n        }\n        debugLog?.log(sourceUrl, \"┌获取图片url\", log)\n        rssArticle.image = analyzeRule.getString(ruleImage, isUrl = true)\n        debugLog?.log(sourceUrl, \"└${rssArticle.image}\", log)\n        debugLog?.log(sourceUrl, \"┌获取文章链接\", log)\n        rssArticle.link = NetworkUtils.getAbsoluteURL(sourceUrl, analyzeRule.getString(ruleLink))\n        debugLog?.log(sourceUrl, \"└${rssArticle.link}\", log)\n        if (rssArticle.title.isBlank()) {\n            return null\n        }\n        return rssArticle\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/rss/RssParserDefault.kt",
    "content": "package io.legado.app.model.rss\n\nimport io.legado.app.data.entities.RssArticle\nimport io.legado.app.model.DebugLog\nimport org.xmlpull.v1.XmlPullParser\nimport org.xmlpull.v1.XmlPullParserException\nimport org.xmlpull.v1.XmlPullParserFactory\nimport java.io.IOException\nimport java.io.StringReader\n\n@Suppress(\"unused\")\nobject RssParserDefault {\n\n    @Throws(XmlPullParserException::class, IOException::class)\n    fun parseXML(\n        sortName: String,\n        xml: String,\n        sourceUrl: String,\n        debugLog: DebugLog?\n    ): Pair<MutableList<RssArticle>, String?> {\n\n        val articleList = mutableListOf<RssArticle>()\n        var currentArticle = RssArticle()\n\n        // val factory = XmlPullParserFactory.newInstance()\n        val factory = XmlPullParserFactory.newInstance(\"\"\"\n        org.kxml2.io.KXmlParser\n        org.kxml2.io.KXmlSerializer\n               \"\"\", Thread.currentThread().getContextClassLoader().javaClass)\n        factory.isNamespaceAware = false\n\n        val xmlPullParser = factory.newPullParser()\n        xmlPullParser.setInput(StringReader(xml))\n\n        // A flag just to be sure of the correct parsing\n        var insideItem = false\n\n        var eventType = xmlPullParser.eventType\n\n        // Start parsing the xml\n        loop@ while (eventType != XmlPullParser.END_DOCUMENT) {\n\n            // Start parsing the item\n            if (eventType == XmlPullParser.START_TAG) {\n                when {\n                    xmlPullParser.name.equals(RSS_ITEM, true) ->\n                        insideItem = true\n                    xmlPullParser.name.equals(RSS_ITEM_TITLE, true) ->\n                        if (insideItem) currentArticle.title = xmlPullParser.nextText().trim()\n                    xmlPullParser.name.equals(RSS_ITEM_LINK, true) ->\n                        if (insideItem) currentArticle.link = xmlPullParser.nextText().trim()\n                    xmlPullParser.name.equals(RSS_ITEM_THUMBNAIL, true) ->\n                        if (insideItem) currentArticle.image =\n                            xmlPullParser.getAttributeValue(null, RSS_ITEM_URL)\n                    xmlPullParser.name.equals(RSS_ITEM_ENCLOSURE, true) ->\n                        if (insideItem) {\n                            val type =\n                                xmlPullParser.getAttributeValue(null, RSS_ITEM_TYPE)\n                            if (type != null && type.contains(\"image/\")) {\n                                currentArticle.image =\n                                    xmlPullParser.getAttributeValue(null, RSS_ITEM_URL)\n                            }\n                        }\n                    xmlPullParser.name\n                        .equals(RSS_ITEM_DESCRIPTION, true) ->\n                        if (insideItem) {\n                            val description = xmlPullParser.nextText()\n                            currentArticle.description = description.trim()\n                            if (currentArticle.image == null) {\n                                currentArticle.image = getImageUrl(description)\n                            }\n                        }\n                    xmlPullParser.name.equals(RSS_ITEM_CONTENT, true) ->\n                        if (insideItem) {\n                            val content = xmlPullParser.nextText().trim()\n                            currentArticle.content = content\n                            if (currentArticle.image == null) {\n                                currentArticle.image = getImageUrl(content)\n                            }\n                        }\n                    xmlPullParser.name\n                        .equals(RSS_ITEM_PUB_DATE, true) ->\n                        if (insideItem) {\n                            val nextTokenType = xmlPullParser.next()\n                            if (nextTokenType == XmlPullParser.TEXT) {\n                                currentArticle.pubDate = xmlPullParser.text.trim()\n                            }\n                            // Skip to be able to find date inside 'tag' tag\n                            continue@loop\n                        }\n                    xmlPullParser.name.equals(RSS_ITEM_TIME, true) ->\n                        if (insideItem) currentArticle.pubDate = xmlPullParser.nextText()\n                }\n            } else if (eventType == XmlPullParser.END_TAG\n                && xmlPullParser.name.equals(\"item\", true)\n            ) {\n                // The item is correctly parsed\n                insideItem = false\n                currentArticle.origin = sourceUrl\n                currentArticle.sort = sortName\n                articleList.add(currentArticle)\n                currentArticle = RssArticle()\n            }\n            eventType = xmlPullParser.next()\n        }\n        articleList.firstOrNull()?.let {\n            debugLog?.log(sourceUrl, \"┌获取标题\")\n            debugLog?.log(sourceUrl, \"└${it.title}\")\n            debugLog?.log(sourceUrl, \"┌获取时间\")\n            debugLog?.log(sourceUrl, \"└${it.pubDate}\")\n            debugLog?.log(sourceUrl, \"┌获取描述\")\n            debugLog?.log(sourceUrl, \"└${it.description}\")\n            debugLog?.log(sourceUrl, \"┌获取图片url\")\n            debugLog?.log(sourceUrl, \"└${it.image}\")\n            debugLog?.log(sourceUrl, \"┌获取文章链接\")\n            debugLog?.log(sourceUrl, \"└${it.link}\")\n        }\n        return Pair(articleList, null)\n    }\n\n    /**\n     * Finds the first img tag and get the src as featured image\n     *\n     * @param input The content in which to search for the tag\n     * @return The url, if there is one\n     */\n    private fun getImageUrl(input: String): String? {\n\n        var url: String? = null\n        val patternImg = \"(<img [^>]*>)\".toPattern()\n        val matcherImg = patternImg.matcher(input)\n        if (matcherImg.find()) {\n            val imgTag = matcherImg.group(1)\n            val patternLink = \"src\\\\s*=\\\\s*\\\"([^\\\"]+)\\\"\".toPattern()\n            val matcherLink = patternLink.matcher(imgTag!!)\n            if (matcherLink.find()) {\n                url = matcherLink.group(1)!!.trim()\n            }\n        }\n        return url\n    }\n\n    private const val RSS_ITEM = \"item\"\n    private const val RSS_ITEM_TITLE = \"title\"\n    private const val RSS_ITEM_LINK = \"link\"\n    private const val RSS_ITEM_CATEGORY = \"category\"\n    private const val RSS_ITEM_THUMBNAIL = \"media:thumbnail\"\n    private const val RSS_ITEM_ENCLOSURE = \"enclosure\"\n    private const val RSS_ITEM_DESCRIPTION = \"description\"\n    private const val RSS_ITEM_CONTENT = \"content:encoded\"\n    private const val RSS_ITEM_PUB_DATE = \"pubDate\"\n    private const val RSS_ITEM_TIME = \"time\"\n    private const val RSS_ITEM_URL = \"url\"\n    private const val RSS_ITEM_TYPE = \"type\"\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/webBook/BookChapterList.kt",
    "content": "package io.legado.app.model.webBook\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.rule.TocRule\nimport io.legado.app.exception.TocEmptyException\nimport io.legado.app.model.DebugLog\nimport io.legado.app.model.analyzeRule.AnalyzeRule\nimport io.legado.app.model.analyzeRule.AnalyzeUrl\nimport io.legado.app.utils.isTrue\nimport io.legado.app.utils.TextUtils\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers.IO\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.ensureActive\nimport kotlinx.coroutines.withContext\n\nobject BookChapterList {\n\n    suspend fun analyzeChapterList(\n        book: Book,\n        body: String?,\n        bookSource: BookSource,\n        baseUrl: String,\n        redirectUrl: String,\n        debugLog: DebugLog? = null\n    ): List<BookChapter> {\n        body ?: throw Exception(\n//            App.INSTANCE.getString(R.string.error_get_web_content, baseUrl)\n            //todo getString\n            \"error_get_web_content\"\n        )\n        val chapterList = arrayListOf<BookChapter>()\n        debugLog?.log(bookSource.bookSourceUrl, \"≡获取成功:${baseUrl}\")\n        // debugLog?.log(bookSource.bookSourceUrl, body)\n        val tocRule = bookSource.getTocRule()\n        val nextUrlList = arrayListOf(redirectUrl)\n        var reverse = false\n        var listRule = tocRule.chapterList ?: \"\"\n        if (listRule.startsWith(\"-\")) {\n            reverse = true\n            listRule = listRule.substring(1)\n        }\n        if (listRule.startsWith(\"+\")) {\n            listRule = listRule.substring(1)\n        }\n        var chapterData =\n            analyzeChapterList(\n                book, baseUrl, redirectUrl, body,\n                tocRule, listRule, bookSource, true, true, debugLog\n            )\n        chapterList.addAll(chapterData.first)\n        when (chapterData.second.size) {\n            0 -> Unit\n            1 -> {\n                var nextUrl = chapterData.second[0]\n                while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {\n                    nextUrlList.add(nextUrl)\n                    AnalyzeUrl(\n                        mUrl = nextUrl,\n                        source = bookSource,\n                        ruleData = book,\n                        headerMapF = bookSource.getHeaderMap()\n                    ).getStrResponseAwait(debugLog = debugLog).body?.let { nextBody ->\n                        chapterData = analyzeChapterList(\n                            book, nextUrl, nextUrl,\n                            nextBody, tocRule, listRule, bookSource, true, false, debugLog\n                        )\n                        nextUrl = chapterData.second.firstOrNull() ?: \"\"\n                        chapterList.addAll(chapterData.first)\n                    }\n                }\n                debugLog?.log(bookSource.bookSourceUrl, \"◇目录总页数:${nextUrlList.size}\")\n            }\n            else -> {\n                debugLog?.log(bookSource.bookSourceUrl, \"◇并发解析目录,总页数:${chapterData.second.size}\")\n                withContext(IO) {\n                    val asyncArray = Array(chapterData.second.size) {\n                        async(IO) {\n                            val urlStr = chapterData.second[it]\n                            val analyzeUrl = AnalyzeUrl(\n                                mUrl = urlStr,\n                                source = bookSource,\n                                ruleData = book,\n                                headerMapF = bookSource.getHeaderMap()\n                            )\n                            val res = analyzeUrl.getStrResponseAwait(debugLog = debugLog)\n                            analyzeChapterList(\n                                book, urlStr, res.url,\n                                res.body!!, tocRule, listRule, bookSource, false, false, debugLog\n                            ).first\n                        }\n                    }\n                    asyncArray.forEach { coroutine ->\n                        chapterList.addAll(coroutine.await())\n                    }\n                }\n            }\n        }\n        if (chapterList.isEmpty()) {\n            throw TocEmptyException(\"目录为空\")\n        }\n        //去重\n        if (!reverse) {\n            chapterList.reverse()\n        }\n        val lh = LinkedHashSet(chapterList)\n        val list = ArrayList(lh)\n        // if (!book.getReverseToc()) {\n        list.reverse()\n        // }\n        debugLog?.log(book.origin, \"◇目录总数:${list.size}\")\n        list.forEachIndexed { index, bookChapter ->\n            bookChapter.index = index\n        }\n        if (list.size > 0) {\n            book.latestChapterTitle = list.last().title\n        }\n//        book.durChapterTitle =\n//            list.getOrNull(book.durChapterIndex)?.title ?: book.latestChapterTitle\n        if (book.totalChapterNum < list.size) {\n            book.lastCheckCount = list.size - book.totalChapterNum\n            // book.latestChapterTime = System.currentTimeMillis()\n            // book.lastCheckTime = System.currentTimeMillis()\n        }\n        book.totalChapterNum = list.size\n        return list\n    }\n\n    private fun analyzeChapterList(\n        book: Book,\n        baseUrl: String,\n        redirectUrl: String,\n        body: String,\n        tocRule: TocRule,\n        listRule: String,\n        bookSource: BookSource,\n        getNextUrl: Boolean = true,\n        log: Boolean = false,\n        debugLog: DebugLog? = null\n    ): Pair<List<BookChapter>, List<String>>  {\n        val analyzeRule = AnalyzeRule(book, bookSource)\n        analyzeRule.setContent(body).setBaseUrl(baseUrl)\n        analyzeRule.setRedirectUrl(redirectUrl)\n        //获取目录列表\n        val chapterList = arrayListOf<BookChapter>()\n        if(log) debugLog?.log(bookSource.bookSourceUrl, \"┌获取目录列表\")\n        val elements = analyzeRule.getElements(listRule)\n        if(log) debugLog?.log(bookSource.bookSourceUrl, \"└列表大小:${elements.size}\")\n        //获取下一页链接\n        val nextUrlList = arrayListOf<String>()\n        val nextTocRule = tocRule.nextTocUrl\n        if (getNextUrl && !nextTocRule.isNullOrEmpty()) {\n            if(log) debugLog?.log(bookSource.bookSourceUrl, \"┌获取目录下一页列表\")\n            analyzeRule.getStringList(nextTocRule, isUrl = true)?.let {\n                for (item in it) {\n                    if (item != redirectUrl) {\n                        nextUrlList.add(item)\n                    }\n                }\n            }\n            if(log) debugLog?.log(bookSource.bookSourceUrl, \"└\" + TextUtils.join(\"，\\n\", nextUrlList))\n        }\n        if (elements.isNotEmpty()) {\n            if(log) debugLog?.log(bookSource.bookSourceUrl, \"┌解析目录列表\")\n            val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName)\n            val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl)\n            val vipRule = analyzeRule.splitSourceRule(tocRule.isVip)\n            val upTimeRule = analyzeRule.splitSourceRule(tocRule.updateTime)\n            val isVolumeRule = analyzeRule.splitSourceRule(tocRule.isVolume)\n            elements.forEachIndexed { index, item ->\n                analyzeRule.setContent(item)\n                val bookChapter = BookChapter(bookUrl = book.bookUrl, baseUrl = redirectUrl)\n                analyzeRule.chapter = bookChapter\n                bookChapter.title = analyzeRule.getString(nameRule)\n                bookChapter.url = analyzeRule.getString(urlRule)\n                bookChapter.tag = analyzeRule.getString(upTimeRule)\n                val isVolume = analyzeRule.getString(isVolumeRule)\n                bookChapter.isVolume = false\n                if (isVolume.isTrue()) {\n                    bookChapter.isVolume = true\n                }\n                if (bookChapter.url.isEmpty()) {\n                    if (bookChapter.isVolume) {\n                        bookChapter.url = bookChapter.title + index\n                        if(log) debugLog?.log(bookSource.bookSourceUrl, \"⇒一级目录${index}未获取到url,使用标题替代\")\n                    } else {\n                        bookChapter.url = baseUrl\n                        if(log) debugLog?.log(bookSource.bookSourceUrl, \"⇒目录${index}未获取到url,使用baseUrl替代\")\n                    }\n                }\n                if (bookChapter.title.isNotEmpty()) {\n                    var isVip = analyzeRule.getString(vipRule)\n                    if (isVip.isTrue()) {\n                        bookChapter.title = \"\\uD83D\\uDD12\" + bookChapter.title\n                    }\n                    chapterList.add(bookChapter)\n                }\n            }\n            if(log) debugLog?.log(bookSource.bookSourceUrl, \"└目录列表解析完成\")\n            if(log) debugLog?.log(bookSource.bookSourceUrl, \"≡首章信息\")\n            if(log) debugLog?.log(bookSource.bookSourceUrl, \"◇章节名称:${chapterList[0].title}\")\n            if(log) debugLog?.log(bookSource.bookSourceUrl, \"◇章节链接:${chapterList[0].url}\")\n            if(log) debugLog?.log(bookSource.bookSourceUrl, \"◇章节信息:${chapterList[0].tag}\")\n            if(log) debugLog?.log(bookSource.bookSourceUrl, \"◇是否卷名:${chapterList[0].isVolume}\")\n        }\n        return Pair(chapterList, nextUrlList)\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/webBook/BookContent.kt",
    "content": "package io.legado.app.model.webBook\n\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.rule.ContentRule\nimport io.legado.app.model.DebugLog\nimport io.legado.app.model.analyzeRule.AnalyzeRule\nimport io.legado.app.model.analyzeRule.AnalyzeUrl\nimport io.legado.app.utils.NetworkUtils\nimport io.legado.app.utils.HtmlFormatter\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers.IO\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.ensureActive\nimport kotlinx.coroutines.withContext\n\nobject BookContent {\n\n    suspend fun analyzeContent(\n            body: String?,\n            book: Book,\n            bookChapter: BookChapter,\n            bookSource: BookSource,\n            baseUrl: String,\n            redirectUrl: String,\n            nextChapterUrl: String? = null,\n            debugLog: DebugLog? = null\n    ): String {\n        body ?: throw Exception(\n                \"error_get_web_content\"\n        )\n        debugLog?.log(bookSource.bookSourceUrl, \"≡获取成功:${baseUrl}\")\n        val mNextChapterUrl = if (!nextChapterUrl.isNullOrEmpty()) {\n            nextChapterUrl\n        } else {\n            // appDb.bookChapterDao.getChapter(book.bookUrl, bookChapter.index + 1)?.url\n            null\n        }\n        val content = StringBuilder()\n        val nextUrlList = arrayListOf(redirectUrl)\n        val contentRule = bookSource.getContentRule()\n        val analyzeRule = AnalyzeRule(book, bookSource).setContent(body, baseUrl)\n        analyzeRule.setRedirectUrl(redirectUrl)\n        analyzeRule.nextChapterUrl = mNextChapterUrl\n        var contentData = analyzeContent(\n            book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl\n        )\n        content.append(contentData.first)\n        if (contentData.second.size == 1) {\n            var nextUrl = contentData.second[0]\n            while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) {\n                if (!mNextChapterUrl.isNullOrEmpty()\n                    && NetworkUtils.getAbsoluteURL(redirectUrl, nextUrl)\n                    == NetworkUtils.getAbsoluteURL(redirectUrl, mNextChapterUrl)\n                ) break\n                nextUrlList.add(nextUrl)\n                val res = AnalyzeUrl(\n                    mUrl = nextUrl,\n                    source = bookSource,\n                    ruleData = book,\n                    headerMapF = bookSource.getHeaderMap()\n                ).getStrResponseAwait(debugLog = debugLog)\n                res.body?.let { nextBody ->\n                    contentData = analyzeContent(\n                        book, nextUrl, res.url, nextBody, contentRule,\n                        bookChapter, bookSource, mNextChapterUrl, false\n                    )\n                    nextUrl =\n                        if (contentData.second.isNotEmpty()) contentData.second[0] else \"\"\n                    content.append(\"\\n\").append(contentData.first)\n                }\n            }\n            debugLog?.log(bookSource.bookSourceUrl, \"◇本章总页数:${nextUrlList.size}\")\n        } else if (contentData.second.size > 1) {\n            debugLog?.log(bookSource.bookSourceUrl, \"◇并发解析正文,总页数:${contentData.second.size}\")\n            withContext(IO) {\n                val asyncArray = Array(contentData.second.size) {\n                    async(IO) {\n                        val urlStr = contentData.second[it]\n                        val analyzeUrl = AnalyzeUrl(\n                            mUrl = urlStr,\n                            source = bookSource,\n                            ruleData = book,\n                            headerMapF = bookSource.getHeaderMap()\n                        )\n                        val res = analyzeUrl.getStrResponseAwait(debugLog = debugLog)\n                        analyzeContent(\n                            book, urlStr, res.url, res.body!!, contentRule,\n                            bookChapter, bookSource, mNextChapterUrl, false\n                        ).first\n                    }\n                }\n                asyncArray.forEach { coroutine ->\n                    content.append(\"\\n\").append(coroutine.await())\n                }\n            }\n        }\n        var contentStr = content.toString()\n        val replaceRegex = contentRule.replaceRegex\n        if (!replaceRegex.isNullOrEmpty()) {\n            contentStr = analyzeRule.getString(replaceRegex, contentStr)\n        }\n        debugLog?.log(bookSource.bookSourceUrl, \"┌获取章节名称\")\n        debugLog?.log(bookSource.bookSourceUrl, \"└${bookChapter.title}\")\n        debugLog?.log(bookSource.bookSourceUrl, \"┌获取正文内容 (长度：${contentStr.length})\")\n        if (contentStr.length > 300) {\n            debugLog?.log(bookSource.bookSourceUrl, \"└\\n${contentStr.substring(0, 50)} ... ${contentStr.substring(contentStr.length - 30, contentStr.length)}\")\n        } else {\n            debugLog?.log(bookSource.bookSourceUrl, \"└\\n${contentStr}\")\n        }\n        return contentStr\n    }\n\n    @Throws(Exception::class)\n    private fun analyzeContent(\n        book: Book,\n        baseUrl: String,\n        redirectUrl: String,\n        body: String,\n        contentRule: ContentRule,\n        chapter: BookChapter,\n        bookSource: BookSource,\n        nextChapterUrl: String?,\n        printLog: Boolean = true,\n        debugLog: DebugLog? = null\n    ): Pair<String, List<String>> {\n        val analyzeRule = AnalyzeRule(book, bookSource)\n        analyzeRule.setContent(body, baseUrl)\n        val rUrl = analyzeRule.setRedirectUrl(redirectUrl)\n        analyzeRule.nextChapterUrl = nextChapterUrl\n        val nextUrlList = arrayListOf<String>()\n        analyzeRule.chapter = chapter\n        //获取正文\n        var content = analyzeRule.getString(contentRule.content)\n        content = HtmlFormatter.formatKeepImg(content, rUrl)\n        //获取下一页链接\n        val nextUrlRule = contentRule.nextContentUrl\n        if (!nextUrlRule.isNullOrEmpty()) {\n            if(printLog) debugLog?.log(bookSource.bookSourceUrl, \"┌获取正文下一页链接\")\n            analyzeRule.getStringList(nextUrlRule, isUrl = true)?.let {\n                nextUrlList.addAll(it)\n            }\n            if(printLog) debugLog?.log(bookSource.bookSourceUrl, \"└\" + nextUrlList.joinToString(\"，\"))\n        }\n        return Pair(content, nextUrlList)\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/webBook/BookInfo.kt",
    "content": "package io.legado.app.model.webBook\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.help.BookHelp\nimport io.legado.app.model.DebugLog\nimport io.legado.app.model.analyzeRule.AnalyzeRule\nimport io.legado.app.utils.NetworkUtils\nimport io.legado.app.utils.StringUtils.wordCountFormat\nimport io.legado.app.utils.htmlFormat\n\nobject BookInfo {\n\n    @Throws(Exception::class)\n    fun analyzeBookInfo(\n        book: Book,\n        body: String?,\n        bookSource: BookSource,\n        baseUrl: String,\n        redirectUrl: String,\n        canReName: Boolean,\n        debugLog: DebugLog? = null\n    ) {\n        body ?: throw Exception(\n            \"error_get_web_content: \" + baseUrl\n        )\n        debugLog?.log(bookSource.bookSourceUrl, \"≡获取成功:${baseUrl}\")\n        val analyzeRule = AnalyzeRule(book, bookSource)\n        analyzeRule.setContent(body).setBaseUrl(baseUrl)\n        analyzeRule.setRedirectUrl(redirectUrl)\n        analyzeBookInfo(book, body, analyzeRule, bookSource, baseUrl, redirectUrl, canReName, debugLog)\n    }\n\n    @Throws(Exception::class)\n    fun analyzeBookInfo(\n        book: Book,\n        body: String?,\n        analyzeRule: AnalyzeRule,\n        bookSource: BookSource,\n        baseUrl: String,\n        redirectUrl: String,\n        canReName: Boolean,\n        debugLog: DebugLog? = null\n    ) {\n        body ?: throw Exception(\n            \"error_get_web_content: \" + baseUrl\n        )\n        val infoRule = bookSource.getBookInfoRule()\n        infoRule.init?.let {\n            if (it.isNotEmpty()) {\n                debugLog?.log(bookSource.bookSourceUrl, \"≡执行详情页初始化规则\")\n                analyzeRule.setContent(analyzeRule.getElement(it))\n            }\n        }\n        val mCanReName = canReName && !infoRule.canReName.isNullOrBlank()\n        debugLog?.log(bookSource.bookSourceUrl, \"┌获取书名\")\n        BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).let {\n            if (it.isNotEmpty() && (mCanReName || book.name.isEmpty())) {\n                book.name = it\n            }\n            debugLog?.log(bookSource.bookSourceUrl, \"└${it}\")\n        }\n        debugLog?.log(bookSource.bookSourceUrl, \"┌获取作者\")\n        BookHelp.formatBookAuthor(analyzeRule.getString(infoRule.author)).let {\n            if (it.isNotEmpty() && (mCanReName || book.author.isEmpty())) {\n                book.author = it\n            }\n            debugLog?.log(bookSource.bookSourceUrl, \"└${it}\")\n        }\n        debugLog?.log(bookSource.bookSourceUrl, \"┌获取分类\")\n        try {\n            analyzeRule.getStringList(infoRule.kind)\n                ?.joinToString(\",\")\n                ?.let {\n                    if (it.isNotEmpty()) book.kind = it\n                }\n            debugLog?.log(bookSource.bookSourceUrl, \"└${book.kind}\")\n        } catch (e: Exception) {\n            debugLog?.log(bookSource.bookSourceUrl, \"└${e.localizedMessage}\")\n        }\n        debugLog?.log(bookSource.bookSourceUrl, \"┌获取字数\")\n        try {\n            wordCountFormat(analyzeRule.getString(infoRule.wordCount)).let {\n                if (it.isNotEmpty()) book.wordCount = it\n            }\n            debugLog?.log(bookSource.bookSourceUrl, \"└${book.wordCount}\")\n        } catch (e: Exception) {\n            debugLog?.log(bookSource.bookSourceUrl, \"└${e.localizedMessage}\")\n        }\n        debugLog?.log(bookSource.bookSourceUrl, \"┌获取最新章节\")\n        try {\n            analyzeRule.getString(infoRule.lastChapter).let {\n                if (it.isNotEmpty()) book.latestChapterTitle = it\n            }\n            debugLog?.log(bookSource.bookSourceUrl, \"└${book.latestChapterTitle}\")\n        } catch (e: Exception) {\n            debugLog?.log(bookSource.bookSourceUrl, \"└${e.localizedMessage}\")\n        }\n        debugLog?.log(bookSource.bookSourceUrl, \"┌获取简介\")\n        try {\n            analyzeRule.getString(infoRule.intro).let {\n                if (it.isNotEmpty()) book.intro = it.htmlFormat()\n            }\n            debugLog?.log(bookSource.bookSourceUrl, \"└${book.intro}\")\n        } catch (e: Exception) {\n            debugLog?.log(bookSource.bookSourceUrl, \"└${e.localizedMessage}\")\n        }\n        debugLog?.log(bookSource.bookSourceUrl, \"┌获取封面链接\")\n        try {\n            analyzeRule.getString(infoRule.coverUrl).let {\n                if (it.isNotEmpty()) {\n                    book.coverUrl =\n                        NetworkUtils.getAbsoluteURL(redirectUrl, it)\n                }\n            }\n            debugLog?.log(bookSource.bookSourceUrl, \"└${book.coverUrl}\")\n        } catch (e: Exception) {\n            debugLog?.log(bookSource.bookSourceUrl, \"└${e.localizedMessage}\")\n        }\n        debugLog?.log(bookSource.bookSourceUrl, \"┌获取目录链接\")\n        book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)\n        if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl\n        if (book.tocUrl == baseUrl) {\n            book.tocHtml = body\n        }\n        debugLog?.log(bookSource.bookSourceUrl, \"└${book.tocUrl}\")\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/webBook/BookList.kt",
    "content": "package io.legado.app.model.webBook\n\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.SearchBook\nimport io.legado.app.data.entities.rule.BookListRule\nimport io.legado.app.help.BookHelp\nimport io.legado.app.model.DebugLog\nimport io.legado.app.model.analyzeRule.AnalyzeRule\nimport io.legado.app.model.analyzeRule.AnalyzeUrl\nimport io.legado.app.utils.NetworkUtils\nimport io.legado.app.utils.StringUtils.wordCountFormat\nimport io.legado.app.utils.htmlFormat\n\nobject BookList {\n\n    @Throws(Exception::class)\n    fun analyzeBookList(\n        body: String?,\n        bookSource: BookSource,\n        analyzeUrl: AnalyzeUrl,\n        baseUrl: String,\n        variableBook: SearchBook,\n        isSearch: Boolean = true,\n        debugLog: DebugLog? = null\n    ): ArrayList<SearchBook> {\n        val bookList = ArrayList<SearchBook>()\n        body ?: throw Exception(\n//            App.INSTANCE.getString(\n//                R.string.error_get_web_content,\n//                analyzeUrl.ruleUrl\n//            )\n                //todo getString\n                \"error_get_web_content\"\n        )\n        debugLog?.log(bookSource.bookSourceUrl, \"≡获取成功:${analyzeUrl.ruleUrl}\")\n        val analyzeRule = AnalyzeRule(variableBook, bookSource)\n        analyzeRule.setContent(body).setBaseUrl(baseUrl)\n        analyzeRule.setRedirectUrl(baseUrl)\n        bookSource.bookUrlPattern?.let {\n            if (baseUrl.matches(it.toRegex())) {\n                debugLog?.log(bookSource.bookSourceUrl, \"≡链接为详情页\")\n                getInfoItem(body, analyzeRule, bookSource, analyzeUrl, baseUrl,  variableBook.variable, debugLog = debugLog)?.let { searchBook ->\n                    searchBook.infoHtml = body\n                    bookList.add(searchBook)\n                }\n                return bookList\n            }\n        }\n        val collections: List<Any>\n        var reverse = false\n        val bookListRule: BookListRule = when {\n            isSearch -> bookSource.getSearchRule()\n            bookSource.getExploreRule().bookList.isNullOrBlank() -> bookSource.getSearchRule()\n            else -> bookSource.getExploreRule()\n        }\n        var ruleList: String = bookListRule.bookList ?: \"\"\n        if (ruleList.startsWith(\"-\")) {\n            reverse = true\n            ruleList = ruleList.substring(1)\n        }\n        if (ruleList.startsWith(\"+\")) {\n            ruleList = ruleList.substring(1)\n        }\n        debugLog?.log(bookSource.bookSourceUrl, \"┌获取书籍列表\")\n        collections = analyzeRule.getElements(ruleList)\n        if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) {\n            debugLog?.log(bookSource.bookSourceUrl, \"└列表为空,按详情页解析\")\n            getInfoItem(body, analyzeRule, bookSource, analyzeUrl, baseUrl, variableBook.variable, debugLog = debugLog)?.let { searchBook ->\n                searchBook.infoHtml = body\n                bookList.add(searchBook)\n            }\n        } else {\n            val ruleName = analyzeRule.splitSourceRule(bookListRule.name)\n            val ruleBookUrl = analyzeRule.splitSourceRule(bookListRule.bookUrl)\n            val ruleAuthor = analyzeRule.splitSourceRule(bookListRule.author)\n            val ruleCoverUrl = analyzeRule.splitSourceRule(bookListRule.coverUrl)\n            val ruleIntro = analyzeRule.splitSourceRule(bookListRule.intro)\n            val ruleKind = analyzeRule.splitSourceRule(bookListRule.kind)\n            val ruleLastChapter = analyzeRule.splitSourceRule(bookListRule.lastChapter)\n            val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount)\n            debugLog?.log(bookSource.bookSourceUrl, \"└列表大小:${collections.size}\")\n            for ((index, item) in collections.withIndex()) {\n                getSearchItem(\n                    item, analyzeRule, bookSource, baseUrl, variableBook.variable, index == 0,\n                    ruleName = ruleName, ruleBookUrl = ruleBookUrl, ruleAuthor = ruleAuthor,\n                    ruleCoverUrl = ruleCoverUrl, ruleIntro = ruleIntro, ruleKind = ruleKind,\n                    ruleLastChapter = ruleLastChapter, ruleWordCount = ruleWordCount,\n                    debugLog = debugLog\n                )?.let { searchBook ->\n                    if (baseUrl == searchBook.bookUrl) {\n                        searchBook.infoHtml = body\n                    }\n                    bookList.add(searchBook)\n                }\n            }\n            if (reverse) {\n                bookList.reverse()\n            }\n        }\n        return bookList\n    }\n\n    private fun getInfoItem(\n        body: String,\n        analyzeRule: AnalyzeRule,\n        bookSource: BookSource,\n        analyzeUrl: AnalyzeUrl,\n        baseUrl: String,\n        variable: String?,\n        debugLog: DebugLog? = null\n    ): SearchBook? {\n        val book = Book(variable = variable)\n        book.bookUrl = analyzeUrl.ruleUrl\n        book.origin = bookSource.bookSourceUrl\n        book.originName = bookSource.bookSourceName\n        book.originOrder = bookSource.customOrder\n        book.type = bookSource.bookSourceType\n        analyzeRule.ruleData = book\n        BookInfo.analyzeBookInfo(\n            book,\n            body,\n            analyzeRule,\n            bookSource,\n            baseUrl,\n            baseUrl,\n            false,\n            debugLog\n        )\n        if (book.name.isNotBlank()) {\n            return book.toSearchBook()\n        }\n        return null\n    }\n\n    private fun getSearchItem(\n        item: Any,\n        analyzeRule: AnalyzeRule,\n        bookSource: BookSource,\n        baseUrl: String,\n        variable: String?,\n        log: Boolean,\n        ruleName: List<AnalyzeRule.SourceRule>,\n        ruleBookUrl: List<AnalyzeRule.SourceRule>,\n        ruleAuthor: List<AnalyzeRule.SourceRule>,\n        ruleKind: List<AnalyzeRule.SourceRule>,\n        ruleCoverUrl: List<AnalyzeRule.SourceRule>,\n        ruleWordCount: List<AnalyzeRule.SourceRule>,\n        ruleIntro: List<AnalyzeRule.SourceRule>,\n        ruleLastChapter: List<AnalyzeRule.SourceRule>,\n        debugLog: DebugLog? = null\n    ): SearchBook? {\n        val searchBook = SearchBook(variable = variable)\n        searchBook.origin = bookSource.bookSourceUrl\n        searchBook.originName = bookSource.bookSourceName\n        searchBook.type = bookSource.bookSourceType\n        searchBook.originOrder = bookSource.customOrder\n        analyzeRule.ruleData = searchBook\n        analyzeRule.setContent(item)\n        if (log) debugLog?.log(bookSource.bookSourceUrl, \"┌获取书名\")\n        searchBook.name = BookHelp.formatBookName(analyzeRule.getString(ruleName))\n        if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${searchBook.name}\")\n        if (searchBook.name.isNotEmpty()) {\n            if (log) debugLog?.log(bookSource.bookSourceUrl, \"┌获取作者\")\n            searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(ruleAuthor))\n            if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${searchBook.author}\")\n            if (log) debugLog?.log(bookSource.bookSourceUrl, \"┌获取分类\")\n            try {\n                searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(\",\")\n                if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${searchBook.kind}\")\n            } catch (e: Exception) {\n                if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${e.localizedMessage}\")\n            }\n            if (log) debugLog?.log(bookSource.bookSourceUrl, \"┌获取字数\")\n            try {\n                searchBook.wordCount = wordCountFormat(analyzeRule.getString(ruleWordCount))\n                if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${searchBook.wordCount}\")\n            } catch (e: java.lang.Exception) {\n                if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${e.localizedMessage}\")\n            }\n            if (log) debugLog?.log(bookSource.bookSourceUrl, \"┌获取最新章节\")\n            try {\n                searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter)\n                if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${searchBook.latestChapterTitle}\")\n            } catch (e: java.lang.Exception) {\n                if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${e.localizedMessage}\")\n            }\n            if (log) debugLog?.log(bookSource.bookSourceUrl, \"┌获取简介\")\n            try {\n                searchBook.intro = analyzeRule.getString(ruleIntro).htmlFormat()\n                if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${searchBook.intro}\")\n            } catch (e: java.lang.Exception) {\n                if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${e.localizedMessage}\")\n            }\n            if (log) debugLog?.log(bookSource.bookSourceUrl, \"┌获取封面链接\")\n            try {\n                analyzeRule.getString(ruleCoverUrl).let {\n                    if (it.isNotEmpty()) searchBook.coverUrl =\n                        NetworkUtils.getAbsoluteURL(baseUrl, it)\n                }\n                if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${searchBook.coverUrl}\")\n            } catch (e: java.lang.Exception) {\n                if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${e.localizedMessage}\")\n            }\n            if (log) debugLog?.log(bookSource.bookSourceUrl, \"┌获取详情页链接\")\n            searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, isUrl = true)\n            if (searchBook.bookUrl.isEmpty()) {\n                searchBook.bookUrl = baseUrl\n            }\n            if (log) debugLog?.log(bookSource.bookSourceUrl, \"└${searchBook.bookUrl}\")\n            return searchBook\n        }\n        return null\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/model/webBook/WebBook.kt",
    "content": "package io.legado.app.model.webBook\n\nimport io.legado.app.data.entities.Book\nimport io.legado.app.data.entities.BookChapter\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.SearchBook\nimport io.legado.app.help.http.StrResponse\nimport io.legado.app.model.analyzeRule.AnalyzeUrl\nimport io.legado.app.model.webBook.BookChapterList\nimport io.legado.app.model.webBook.BookContent\nimport io.legado.app.model.webBook.BookInfo\nimport io.legado.app.model.webBook.BookList\nimport io.legado.app.model.Debug\nimport io.legado.app.model.DebugLog\nimport mu.KotlinLogging\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nprivate val logger = KotlinLogging.logger {}\n\nclass WebBook(val bookSource: BookSource, val debugLog: Boolean = true, var debugLogger: DebugLog? = null) {\n\n    constructor(bookSourceString: String, debugLog: Boolean = true) : this(BookSource.fromJson(bookSourceString).getOrNull() ?: BookSource(), debugLog)\n\n    val sourceUrl: String\n        get() = bookSource.bookSourceUrl\n\n    val debugger: DebugLog?\n        get() {\n            if (debugLogger != null) {\n                return debugLogger\n            }\n            if (debugLog) {\n                return Debug\n            }\n            return null\n        }\n\n    /**\n     * 搜索\n     */\n    suspend fun searchBook(\n        key: String,\n        page: Int? = 1\n    ): List<SearchBook> {\n        val variableBook = SearchBook()\n        return bookSource.searchUrl?.let { searchUrl ->\n            val analyzeUrl = AnalyzeUrl(\n                mUrl = searchUrl,\n                key = key,\n                page = page,\n                baseUrl = bookSource.bookSourceUrl,\n                source = bookSource,\n                ruleData = variableBook,\n                headerMapF = bookSource.getHeaderMap(true),\n            )\n            var res = analyzeUrl.getStrResponseAwait(debugLog = debugger)\n            //检测书源是否已登录\n            bookSource.loginCheckJs?.let { checkJs ->\n                if (checkJs.isNotBlank()) {\n                    res = analyzeUrl.evalJS(checkJs, res) as StrResponse\n                }\n            }\n            BookList.analyzeBookList(\n                res.body,\n                bookSource,\n                analyzeUrl,\n                res.url,\n                variableBook,\n                true,\n                debugLog = debugger\n            ).map {\n                it.tocHtml = \"\"\n                it.infoHtml = \"\"\n                it\n            }\n        } ?: arrayListOf()\n\n    }\n\n    /**\n     * 发现\n     */\n    suspend fun exploreBook(\n        url: String,\n        page: Int? = 1\n    ): List<SearchBook> {\n        val variableBook = SearchBook()\n        val analyzeUrl = AnalyzeUrl(\n            mUrl = url,\n            page = page,\n            baseUrl = bookSource.bookSourceUrl,\n            source = bookSource,\n            ruleData = variableBook,\n            headerMapF = bookSource.getHeaderMap(true)\n        )\n        var res = analyzeUrl.getStrResponseAwait(debugLog = debugger)\n        //检测书源是否已登录\n        bookSource.loginCheckJs?.let { checkJs ->\n            if (checkJs.isNotBlank()) {\n                res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse\n            }\n        }\n        return BookList.analyzeBookList(\n            res.body,\n            bookSource,\n            analyzeUrl,\n            res.url,\n            variableBook,\n            false,\n            debugLog = debugger\n        )\n    }\n\n    /**\n     * 书籍信息\n     */\n    suspend fun getBookInfo(book: Book, canReName: Boolean = true): Book {\n        book.type = bookSource.bookSourceType\n        if (!book.infoHtml.isNullOrEmpty()) {\n            BookInfo.analyzeBookInfo(\n                book,\n                book.infoHtml,\n                bookSource,\n                book.bookUrl,\n                book.bookUrl,\n                canReName\n            )\n            return book\n        } else {\n            return getBookInfo(book.bookUrl, canReName)\n        }\n    }\n\n    /**\n     * 书籍信息\n     */\n    suspend fun getBookInfo(bookUrl: String, canReName: Boolean = true): Book {\n        val book = Book()\n        book.bookUrl = bookUrl\n        book.origin = bookSource.bookSourceUrl\n        book.originName = bookSource.bookSourceName\n        book.originOrder = bookSource.customOrder\n        book.type = bookSource.bookSourceType\n        val analyzeUrl = AnalyzeUrl(\n            mUrl = book.bookUrl,\n            baseUrl = bookSource.bookSourceUrl,\n            source = bookSource,\n            ruleData = book,\n            headerMapF = bookSource.getHeaderMap(true)\n        )\n        var res = analyzeUrl.getStrResponseAwait(debugLog = debugger)\n        //检测书源是否已登录\n        bookSource.loginCheckJs?.let { checkJs ->\n            if (checkJs.isNotBlank()) {\n                res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse\n            }\n        }\n\n        BookInfo.analyzeBookInfo(book, res.body, bookSource, book.bookUrl, res.url, canReName, debugLog = debugger)\n        book.tocHtml = null\n        return book\n    }\n\n    /**\n     * 目录\n     */\n    suspend fun getChapterList(\n        book: Book\n    ): List<BookChapter> {\n        book.type = bookSource.bookSourceType\n        return if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {\n            BookChapterList.analyzeChapterList(\n                book,\n                book.tocHtml,\n                bookSource,\n                book.tocUrl,\n                book.tocUrl\n            )\n        } else {\n            val analyzeUrl = AnalyzeUrl(\n                mUrl = book.tocUrl,\n                baseUrl = book.bookUrl,\n                source = bookSource,\n                ruleData = book,\n                headerMapF = bookSource.getHeaderMap(true)\n            )\n            var res = analyzeUrl.getStrResponseAwait(debugLog = debugger)\n            //检测书源是否已登录\n            bookSource.loginCheckJs?.let { checkJs ->\n                if (checkJs.isNotBlank()) {\n                    res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse\n                }\n            }\n            return BookChapterList.analyzeChapterList(book, res.body, bookSource, book.tocUrl, res.url, debugLog = debugger)\n        }\n    }\n\n    /**\n     * 章节内容\n     */\n    suspend fun getBookContent(\n       book: Book,\n       bookChapter: BookChapter,\n        // bookChapterUrl:String,\n        nextChapterUrl: String? = null\n    ): String {\n       if (bookSource.getContentRule().content.isNullOrEmpty()) {\n            debugger?.log(bookSource.bookSourceUrl, \"⇒正文规则为空,使用章节链接: ${bookChapter.url}\")\n            return bookChapter.url\n       }\n       if (bookChapter.isVolume && bookChapter.url.startsWith(bookChapter.title)) {\n            debugger?.log(bookSource.bookSourceUrl, \"⇒一级目录正文不解析规则\")\n            return bookChapter.tag ?: \"\"\n        }\n//        val body = if (book != null && bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {\n//            book.tocHtml\n//        } else {\n        logger.info(\"bookChapterUrl: {}\", bookChapter.url, bookChapter.getAbsoluteURL())\n        val analyzeUrl = AnalyzeUrl(\n            mUrl = bookChapter.getAbsoluteURL(),\n            baseUrl = book.tocUrl,\n            source = bookSource,\n            ruleData = book,\n            chapter = bookChapter,\n            headerMapF = bookSource.getHeaderMap(true)\n        )\n        var res = analyzeUrl.getStrResponseAwait(\n            jsStr = bookSource.getContentRule().webJs,\n            sourceRegex = bookSource.getContentRule().sourceRegex,\n            debugLog = debugger\n        )\n        return BookContent.analyzeContent(\n            res.body,\n            book,\n            bookChapter,\n            bookSource,\n            bookChapter.url,\n            res.url,\n            nextChapterUrl,\n            debugLog = debugger\n        )\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/utils/ACache.kt",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage io.legado.app.utils\n\nimport com.htmake.reader.init.appCtx\nimport java.io.*\nimport java.util.*\nimport java.util.concurrent.atomic.AtomicInteger\nimport java.util.concurrent.atomic.AtomicLong\nimport kotlin.math.min\nimport mu.KotlinLogging\n\nprivate val logger = KotlinLogging.logger {}\n\n\n/**\n * 本地缓存\n */\n@Suppress(\"unused\", \"MemberVisibilityCanBePrivate\")\nclass ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) {\n\n    companion object {\n        const val TIME_HOUR = 60 * 60\n        const val TIME_DAY = TIME_HOUR * 24\n        private const val MAX_SIZE = 1000 * 1000 * 50 // 50 mb\n        private const val MAX_COUNT = Integer.MAX_VALUE // 不限制存放数据的数量\n        private val mInstanceMap = HashMap<String, ACache>()\n\n        @JvmOverloads\n        fun get(\n            cacheName: String = \"ACache\",\n            maxSize: Long = MAX_SIZE.toLong(),\n            maxCount: Int = MAX_COUNT\n        ): ACache {\n            val f = File(appCtx.cacheDir, cacheName)\n            return get(f, maxSize, maxCount)\n        }\n\n        @JvmOverloads\n        fun get(\n            cacheDir: File,\n            maxSize: Long = MAX_SIZE.toLong(),\n            maxCount: Int = MAX_COUNT\n        ): ACache {\n            synchronized(this) {\n                var manager = mInstanceMap[cacheDir.absoluteFile.toString()]\n                if (manager == null) {\n                    manager = ACache(cacheDir, maxSize, maxCount)\n                    mInstanceMap[cacheDir.absolutePath] = manager\n                }\n                return manager\n            }\n        }\n    }\n\n    private var mCache: ACacheManager? = null\n\n    init {\n        try {\n            if (!cacheDir.exists() && !cacheDir.mkdirs()) {\n                logger.info(\"ACache can't make dirs in %s\" + cacheDir.absolutePath)\n            }\n            mCache = ACacheManager(cacheDir, max_size, max_count)\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n\n    }\n\n    // =======================================\n    // ============ String数据 读写 ==============\n    // =======================================\n\n    /**\n     * 保存 String数据 到 缓存中\n     *\n     * @param key   保存的key\n     * @param value 保存的String数据\n     */\n    fun put(key: String, value: String) {\n        mCache?.let { mCache ->\n            try {\n                val file = mCache.newFile(key)\n                file.writeText(value)\n                mCache.put(file)\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n        }\n    }\n\n    /**\n     * 保存 String数据 到 缓存中\n     *\n     * @param key      保存的key\n     * @param value    保存的String数据\n     * @param saveTime 保存的时间，单位：秒\n     */\n    fun put(key: String, value: String, saveTime: Int) {\n        put(key, Utils.newStringWithDateInfo(saveTime, value))\n    }\n\n    /**\n     * 读取 String数据\n     *\n     * @return String 数据\n     */\n    fun getAsString(key: String): String? {\n        mCache?.let { mCache ->\n            val file = mCache[key]\n            if (!file.exists())\n                return null\n            var removeFile = false\n            try {\n                val text = file.readText()\n                if (!Utils.isDue(text)) {\n                    return Utils.clearDateInfo(text)\n                } else {\n                    removeFile = true\n                }\n            } catch (e: IOException) {\n                e.printStackTrace()\n            } finally {\n                if (removeFile)\n                    remove(key)\n            }\n        }\n        return null\n    }\n\n    fun getByHashCode(hashCode: String): String? {\n        mCache?.let { mCache ->\n            val file =  mCache.newFileFromHashCode(hashCode)\n\n            if (!file.exists())\n                return null\n            var removeFile = false\n            try {\n                val text = file.readText()\n                if (!Utils.isDue(text)) {\n                    return Utils.clearDateInfo(text)\n                } else {\n                    removeFile = true\n                }\n            } catch (e: IOException) {\n                e.printStackTrace()\n            } finally {\n                if (removeFile)\n                    file.delete()\n            }\n        }\n        return null\n    }\n\n    // =======================================\n    // ========== JSONObject 数据 读写 =========\n    // =======================================\n\n    // /**\n    //  * 保存 JSONObject数据 到 缓存中\n    //  *\n    //  * @param key   保存的key\n    //  * @param value 保存的JSON数据\n    //  */\n    // fun put(key: String, value: JSONObject) {\n    //     put(key, value.toString())\n    // }\n\n    // /**\n    //  * 保存 JSONObject数据 到 缓存中\n    //  *\n    //  * @param key      保存的key\n    //  * @param value    保存的JSONObject数据\n    //  * @param saveTime 保存的时间，单位：秒\n    //  */\n    // fun put(key: String, value: JSONObject, saveTime: Int) {\n    //     put(key, value.toString(), saveTime)\n    // }\n\n    // /**\n    //  * 读取JSONObject数据\n    //  *\n    //  * @return JSONObject数据\n    //  */\n    // fun getAsJSONObject(key: String): JSONObject? {\n    //     val json = getAsString(key) ?: return null\n    //     return try {\n    //         JSONObject(json)\n    //     } catch (e: Exception) {\n    //         null\n    //     }\n    // }\n\n    // // =======================================\n    // // ============ JSONArray 数据 读写 =============\n    // // =======================================\n\n    // /**\n    //  * 保存 JSONArray数据 到 缓存中\n    //  *\n    //  * @param key   保存的key\n    //  * @param value 保存的JSONArray数据\n    //  */\n    // fun put(key: String, value: JSONArray) {\n    //     put(key, value.toString())\n    // }\n\n    // /**\n    //  * 保存 JSONArray数据 到 缓存中\n    //  *\n    //  * @param key      保存的key\n    //  * @param value    保存的JSONArray数据\n    //  * @param saveTime 保存的时间，单位：秒\n    //  */\n    // fun put(key: String, value: JSONArray, saveTime: Int) {\n    //     put(key, value.toString(), saveTime)\n    // }\n\n    // /**\n    //  * 读取JSONArray数据\n    //  *\n    //  * @return JSONArray数据\n    //  */\n    // fun getAsJSONArray(key: String): JSONArray? {\n    //     val json = getAsString(key)\n    //     return try {\n    //         JSONArray(json)\n    //     } catch (e: Exception) {\n    //         null\n    //     }\n\n    // }\n\n    // =======================================\n    // ============== byte 数据 读写 =============\n    // =======================================\n\n    /**\n     * 保存 byte数据 到 缓存中\n     *\n     * @param key   保存的key\n     * @param value 保存的数据\n     */\n    fun put(key: String, value: ByteArray) {\n        mCache?.let { mCache ->\n            val file = mCache.newFile(key)\n            file.writeBytes(value)\n            mCache.put(file)\n        }\n    }\n\n    /**\n     * 保存 byte数据 到 缓存中\n     *\n     * @param key      保存的key\n     * @param value    保存的数据\n     * @param saveTime 保存的时间，单位：秒\n     */\n    fun put(key: String, value: ByteArray, saveTime: Int) {\n        put(key, Utils.newByteArrayWithDateInfo(saveTime, value))\n    }\n\n    /**\n     * 获取 byte 数据\n     *\n     * @return byte 数据\n     */\n    fun getAsBinary(key: String): ByteArray? {\n        mCache?.let { mCache ->\n            var removeFile = false\n            try {\n                val file = mCache[key]\n                if (!file.exists())\n                    return null\n\n                val byteArray = file.readBytes()\n                return if (!Utils.isDue(byteArray)) {\n                    Utils.clearDateInfo(byteArray)\n                } else {\n                    removeFile = true\n                    null\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n            } finally {\n                if (removeFile)\n                    remove(key)\n            }\n        }\n        return null\n    }\n\n    /**\n     * 保存 Serializable数据到 缓存中\n     *\n     * @param key      保存的key\n     * @param value    保存的value\n     * @param saveTime 保存的时间，单位：秒\n     */\n    @JvmOverloads\n    fun put(key: String, value: Serializable, saveTime: Int = -1) {\n        try {\n            val byteArrayOutputStream = ByteArrayOutputStream()\n            ObjectOutputStream(byteArrayOutputStream).use { oos ->\n                oos.writeObject(value)\n                val data = byteArrayOutputStream.toByteArray()\n                if (saveTime != -1) {\n                    put(key, data, saveTime)\n                } else {\n                    put(key, data)\n                }\n            }\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n    }\n\n    /**\n     * 读取 Serializable数据\n     *\n     * @return Serializable 数据\n     */\n    fun getAsObject(key: String): Any? {\n        val data = getAsBinary(key)\n        if (data != null) {\n            var bis: ByteArrayInputStream? = null\n            var ois: ObjectInputStream? = null\n            try {\n                bis = ByteArrayInputStream(data)\n                ois = ObjectInputStream(bis)\n                return ois.readObject()\n            } catch (e: Exception) {\n                e.printStackTrace()\n            } finally {\n                try {\n                    bis?.close()\n                } catch (e: IOException) {\n                    e.printStackTrace()\n                }\n\n                try {\n                    ois?.close()\n                } catch (e: IOException) {\n                    e.printStackTrace()\n                }\n\n            }\n        }\n        return null\n\n    }\n\n    // =======================================\n    // ============== bitmap 数据 读写 =============\n    // =======================================\n\n    // /**\n    //  * 保存 bitmap 到 缓存中\n    //  *\n    //  * @param key   保存的key\n    //  * @param value 保存的bitmap数据\n    //  */\n    // fun put(key: String, value: Bitmap) {\n    //     put(key, Utils.bitmap2Bytes(value))\n    // }\n\n    // /**\n    //  * 保存 bitmap 到 缓存中\n    //  *\n    //  * @param key      保存的key\n    //  * @param value    保存的 bitmap 数据\n    //  * @param saveTime 保存的时间，单位：秒\n    //  */\n    // fun put(key: String, value: Bitmap, saveTime: Int) {\n    //     put(key, Utils.bitmap2Bytes(value), saveTime)\n    // }\n\n    /**\n     * 读取 bitmap 数据\n     *\n     * @return bitmap 数据\n     */\n    // fun getAsBitmap(key: String): Bitmap? {\n    //     return if (getAsBinary(key) == null) {\n    //         null\n    //     } else Utils.bytes2Bitmap(getAsBinary(key)!!)\n    // }\n\n    // =======================================\n    // ============= drawable 数据 读写 =============\n    // =======================================\n\n    // /**\n    //  * 保存 drawable 到 缓存中\n    //  *\n    //  * @param key   保存的key\n    //  * @param value 保存的drawable数据\n    //  */\n    // fun put(key: String, value: Drawable) {\n    //     put(key, Utils.drawable2Bitmap(value))\n    // }\n\n    // /**\n    //  * 保存 drawable 到 缓存中\n    //  *\n    //  * @param key      保存的key\n    //  * @param value    保存的 drawable 数据\n    //  * @param saveTime 保存的时间，单位：秒\n    //  */\n    // fun put(key: String, value: Drawable, saveTime: Int) {\n    //     put(key, Utils.drawable2Bitmap(value), saveTime)\n    // }\n\n    /**\n     * 读取 Drawable 数据\n     *\n     * @return Drawable 数据\n     */\n    // fun getAsDrawable(key: String): Drawable? {\n    //     return if (getAsBinary(key) == null) {\n    //         null\n    //     } else Utils.bitmap2Drawable(\n    //         Utils.bytes2Bitmap(\n    //             getAsBinary(key)!!\n    //         )\n    //     )\n    // }\n\n    /**\n     * 获取缓存文件\n     *\n     * @return value 缓存的文件\n     */\n    fun file(key: String): File? {\n        mCache?.let { mCache ->\n            try {\n                val f = mCache.newFile(key)\n                if (f.exists()) {\n                    return f\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n        }\n        return null\n    }\n\n    /**\n     * 移除某个key\n     *\n     * @return 是否移除成功\n     */\n    fun remove(key: String): Boolean {\n        return mCache?.remove(key) == true\n    }\n\n    /**\n     * 清除所有数据\n     */\n    fun clear() {\n        mCache?.clear()\n    }\n\n    /**\n     * @author 杨福海（michael） www.yangfuhai.com\n     * @version 1.0\n     * title 时间计算工具类\n     */\n    private object Utils {\n\n        private const val mSeparator = ' '\n\n        /**\n         * 判断缓存的String数据是否到期\n         *\n         * @return true：到期了 false：还没有到期\n         */\n        fun isDue(str: String): Boolean {\n            return isDue(str.toByteArray())\n        }\n\n        /**\n         * 判断缓存的byte数据是否到期\n         *\n         * @return true：到期了 false：还没有到期\n         */\n        fun isDue(data: ByteArray): Boolean {\n            try {\n                val text = getDateInfoFromDate(data)\n                if (text != null && text.size == 2) {\n                    var saveTimeStr = text[0]\n                    while (saveTimeStr.startsWith(\"0\")) {\n                        saveTimeStr = saveTimeStr\n                            .substring(1)\n                    }\n                    val saveTime = java.lang.Long.valueOf(saveTimeStr)\n                    val deleteAfter = java.lang.Long.valueOf(text[1])\n                    if (System.currentTimeMillis() > saveTime + deleteAfter * 1000) {\n                        return true\n                    }\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n\n            return false\n        }\n\n        fun newStringWithDateInfo(second: Int, strInfo: String): String {\n            return createDateInfo(second) + strInfo\n        }\n\n        fun newByteArrayWithDateInfo(second: Int, data2: ByteArray): ByteArray {\n            val data1 = createDateInfo(second).toByteArray()\n            val retData = ByteArray(data1.size + data2.size)\n            System.arraycopy(data1, 0, retData, 0, data1.size)\n            System.arraycopy(data2, 0, retData, data1.size, data2.size)\n            return retData\n        }\n\n        fun clearDateInfo(strInfo: String?): String? {\n            strInfo?.let {\n                if (hasDateInfo(strInfo.toByteArray())) {\n                    return strInfo.substring(strInfo.indexOf(mSeparator) + 1)\n                }\n            }\n            return strInfo\n        }\n\n        fun clearDateInfo(data: ByteArray): ByteArray {\n            return if (hasDateInfo(data)) {\n                copyOfRange(\n                    data, indexOf(data, mSeparator) + 1,\n                    data.size\n                )\n            } else data\n        }\n\n        fun hasDateInfo(data: ByteArray?): Boolean {\n            return (data != null && data.size > 15 && data[13] == '-'.code.toByte()\n                    && indexOf(data, mSeparator) > 14)\n        }\n\n        fun getDateInfoFromDate(data: ByteArray): Array<String>? {\n            if (hasDateInfo(data)) {\n                val saveDate = String(copyOfRange(data, 0, 13))\n                val deleteAfter = String(\n                    copyOfRange(\n                        data, 14,\n                        indexOf(data, mSeparator)\n                    )\n                )\n                return arrayOf(saveDate, deleteAfter)\n            }\n            return null\n        }\n\n        @Suppress(\"SameParameterValue\")\n        private fun indexOf(data: ByteArray, c: Char): Int {\n            for (i in data.indices) {\n                if (data[i] == c.code.toByte()) {\n                    return i\n                }\n            }\n            return -1\n        }\n\n        private fun copyOfRange(original: ByteArray, from: Int, to: Int): ByteArray {\n            val newLength = to - from\n            require(newLength >= 0) { \"$from > $to\" }\n            val copy = ByteArray(newLength)\n            System.arraycopy(\n                original, from, copy, 0,\n                min(original.size - from, newLength)\n            )\n            return copy\n        }\n\n        private fun createDateInfo(second: Int): String {\n            val currentTime = StringBuilder(System.currentTimeMillis().toString() + \"\")\n            while (currentTime.length < 13) {\n                currentTime.insert(0, \"0\")\n            }\n            return \"$currentTime-$second$mSeparator\"\n        }\n\n        // /*\n        //  * Bitmap → byte[]\n        //  */\n        // fun bitmap2Bytes(bm: Bitmap): ByteArray {\n        //     val byteArrayOutputStream = ByteArrayOutputStream()\n        //     bm.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)\n        //     return byteArrayOutputStream.toByteArray()\n        // }\n\n        // /*\n        //  * byte[] → Bitmap\n        //  */\n        // fun bytes2Bitmap(b: ByteArray): Bitmap? {\n        //     return if (b.isEmpty()) {\n        //         null\n        //     } else BitmapFactory.decodeByteArray(b, 0, b.size)\n        // }\n\n        // /*\n        //  * Drawable → Bitmap\n        //  */\n        // fun drawable2Bitmap(drawable: Drawable): Bitmap {\n        //     // 取 drawable 的长宽\n        //     val w = drawable.intrinsicWidth\n        //     val h = drawable.intrinsicHeight\n        //     // 取 drawable 的颜色格式\n        //     @Suppress(\"DEPRECATION\")\n        //     val config = if (drawable.opacity != PixelFormat.OPAQUE)\n        //         Bitmap.Config.ARGB_8888\n        //     else\n        //         Bitmap.Config.RGB_565\n        //     // 建立对应 bitmap\n        //     val bitmap = Bitmap.createBitmap(w, h, config)\n        //     // 建立对应 bitmap 的画布\n        //     val canvas = Canvas(bitmap)\n        //     drawable.setBounds(0, 0, w, h)\n        //     // 把 drawable 内容画到画布中\n        //     drawable.draw(canvas)\n        //     return bitmap\n        // }\n\n        // /*\n        //  * Bitmap → Drawable\n        //  */\n        // fun bitmap2Drawable(bm: Bitmap?): Drawable? {\n        //     return if (bm == null) {\n        //         null\n        //     } else BitmapDrawable(appCtx.resources, bm)\n        // }\n    }\n\n    /**\n     * @author 杨福海（michael） www.yangfuhai.com\n     * @version 1.0\n     * title 缓存管理器\n     */\n    open inner class ACacheManager(\n        private var cacheDir: File,\n        private val sizeLimit: Long,\n        private val countLimit: Int\n    ) {\n        private val cacheSize: AtomicLong = AtomicLong()\n        private val cacheCount: AtomicInteger = AtomicInteger()\n        private val lastUsageDates = Collections\n            .synchronizedMap(HashMap<File, Long>())\n\n        init {\n            calculateCacheSizeAndCacheCount()\n        }\n\n        /**\n         * 计算 cacheSize和cacheCount\n         */\n        private fun calculateCacheSizeAndCacheCount() {\n            Thread {\n\n                try {\n                    var size = 0\n                    var count = 0\n                    val cachedFiles = cacheDir.listFiles()\n                    if (cachedFiles != null) {\n                        for (cachedFile in cachedFiles) {\n                            size += calculateSize(cachedFile).toInt()\n                            count += 1\n                            lastUsageDates[cachedFile] = cachedFile.lastModified()\n                        }\n                        cacheSize.set(size.toLong())\n                        cacheCount.set(count)\n                    }\n                } catch (e: Exception) {\n                    e.printStackTrace()\n                }\n\n\n            }.start()\n        }\n\n        fun put(file: File) {\n\n            try {\n                var curCacheCount = cacheCount.get()\n                while (curCacheCount + 1 > countLimit) {\n                    val freedSize = removeNext()\n                    cacheSize.addAndGet(-freedSize)\n\n                    curCacheCount = cacheCount.addAndGet(-1)\n                }\n                cacheCount.addAndGet(1)\n\n                val valueSize = calculateSize(file)\n                var curCacheSize = cacheSize.get()\n                while (curCacheSize + valueSize > sizeLimit) {\n                    val freedSize = removeNext()\n                    curCacheSize = cacheSize.addAndGet(-freedSize)\n                }\n                cacheSize.addAndGet(valueSize)\n\n                val currentTime = System.currentTimeMillis()\n                file.setLastModified(currentTime)\n                lastUsageDates[file] = currentTime\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n\n        }\n\n        operator fun get(key: String): File {\n            val file = newFile(key)\n            val currentTime = System.currentTimeMillis()\n            file.setLastModified(currentTime)\n            lastUsageDates[file] = currentTime\n\n            return file\n        }\n\n        fun newFile(key: String): File {\n            return File(cacheDir, key.hashCode().toString() + \"\")\n        }\n\n        fun newFileFromHashCode(hashCode: String): File {\n            return File(cacheDir, hashCode)\n        }\n\n        fun remove(key: String): Boolean {\n            val image = get(key)\n            return image.delete()\n        }\n\n        fun clear() {\n            try {\n                lastUsageDates.clear()\n                cacheSize.set(0)\n                val files = cacheDir.listFiles()\n                if (files != null) {\n                    for (f in files) {\n                        f.delete()\n                    }\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n\n        }\n\n        /**\n         * 移除旧的文件\n         */\n        private fun removeNext(): Long {\n            try {\n                if (lastUsageDates.isEmpty()) {\n                    return 0\n                }\n\n                var oldestUsage: Long? = null\n                var mostLongUsedFile: File? = null\n                val entries = lastUsageDates.entries\n                synchronized(lastUsageDates) {\n                    for ((key, lastValueUsage) in entries) {\n                        if (mostLongUsedFile == null) {\n                            mostLongUsedFile = key\n                            oldestUsage = lastValueUsage\n                        } else {\n                            if (lastValueUsage < oldestUsage!!) {\n                                oldestUsage = lastValueUsage\n                                mostLongUsedFile = key\n                            }\n                        }\n                    }\n                }\n\n                var fileSize: Long = 0\n                if (mostLongUsedFile != null) {\n                    fileSize = calculateSize(mostLongUsedFile!!)\n                    if (mostLongUsedFile!!.delete()) {\n                        lastUsageDates.remove(mostLongUsedFile)\n                    }\n                }\n                return fileSize\n            } catch (e: Exception) {\n                e.printStackTrace()\n                return 0\n            }\n\n        }\n\n        private fun calculateSize(file: File): Long {\n            return file.length()\n        }\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/utils/AnkoHelps.kt",
    "content": "package io.legado.app.utils\n\ninline fun <T> attempt(f: () -> T): AttemptResult<T> {\n    var value: T? = null\n    var error: Throwable? = null\n    try {\n        value = f()\n    } catch(t: Throwable) {\n        error = t\n    }\n    return AttemptResult(value, error)\n}\n\ndata class AttemptResult<out T> @PublishedApi internal constructor(val value: T?, val error: Throwable?) {\n    inline fun <R> then(f: (T) -> R): AttemptResult<R> {\n        if (isError) {\n            @Suppress(\"UNCHECKED_CAST\")\n            return this as AttemptResult<R>\n        }\n\n        return attempt { f(value as T) }\n    }\n\n    inline val isError: Boolean\n        get() = error != null\n\n    inline val hasValue: Boolean\n        get() = error == null\n}"
  },
  {
    "path": "src/main/java/io/legado/app/utils/Base64.java",
    "content": "package io.legado.app.utils;\n\nimport java.io.UnsupportedEncodingException;\n\npublic class Base64 {\n    /**\n     * Default values for encoder/decoder flags.\n     */\n    public static final int DEFAULT = 0;\n    /**\n     * Encoder flag bit to omit the padding '=' characters at the end\n     * of the output (if any).\n     */\n    public static final int NO_PADDING = 1;\n    /**\n     * Encoder flag bit to omit all line terminators (i.e., the output\n     * will be on one long line).\n     */\n    public static final int NO_WRAP = 2;\n    /**\n     * Encoder flag bit to indicate lines should be terminated with a\n     * CRLF pair instead of just an LF.  Has no effect if {@code\n     * NO_WRAP} is specified as well.\n     */\n    public static final int CRLF = 4;\n    /**\n     * Encoder/decoder flag bit to indicate using the \"URL and\n     * filename safe\" variant of Base64 (see RFC 3548 section 4) where\n     * {@code -} and {@code _} are used in place of {@code +} and\n     * {@code /}.\n     */\n    public static final int URL_SAFE = 8;\n    /**\n     * Flag to pass to {@link Base64OutputStream} to indicate that it\n     * should not close the output stream it is wrapping when it\n     * itself is closed.\n     */\n    public static final int NO_CLOSE = 16;\n\n    //  --------------------------------------------------------\n    //  shared code\n    //  --------------------------------------------------------\n    /* package */ static abstract class Coder {\n        public byte[] output;\n        public int op;\n\n        /**\n         * Encode/decode another block of input data.  this.output is\n         * provided by the caller, and must be big enough to hold all\n         * the coded data.  On exit, this.opwill be set to the length\n         * of the coded data.\n         *\n         * @param finish true if this is the final call to process for\n         *               this object.  Will finalize the coder state and\n         *               include any final bytes in the output.\n         * @return true if the input so far is good; false if some\n         * error has been detected in the input stream..\n         */\n        public abstract boolean process(byte[] input, int offset, int len, boolean finish);\n\n        /**\n         * @return the maximum number of bytes a call to process()\n         * could produce for the given number of input bytes.  This may\n         * be an overestimate.\n         */\n        public abstract int maxOutputSize(int len);\n    }\n    //  --------------------------------------------------------\n    //  decoding\n    //  --------------------------------------------------------\n\n    /**\n     * Decode the Base64-encoded data in input and return the data in\n     * a new byte array.\n     *\n     * <p>The padding '=' characters at the end are considered optional, but\n     * if any are present, there must be the correct number of them.\n     *\n     * @param str   the input String to decode, which is converted to\n     *              bytes using the default charset\n     * @param flags controls certain features of the decoded output.\n     *              Pass {@code DEFAULT} to decode standard Base64.\n     * @throws IllegalArgumentException if the input contains\n     *                                  incorrect padding\n     */\n    public static byte[] decode(String str, int flags) {\n        return decode(str.getBytes(), flags);\n    }\n\n    /**\n     * Decode the Base64-encoded data in input and return the data in\n     * a new byte array.\n     *\n     * <p>The padding '=' characters at the end are considered optional, but\n     * if any are present, there must be the correct number of them.\n     *\n     * @param input the input array to decode\n     * @param flags controls certain features of the decoded output.\n     *              Pass {@code DEFAULT} to decode standard Base64.\n     * @throws IllegalArgumentException if the input contains\n     *                                  incorrect padding\n     */\n    public static byte[] decode(byte[] input, int flags) {\n        return decode(input, 0, input.length, flags);\n    }\n\n    /**\n     * Decode the Base64-encoded data in input and return the data in\n     * a new byte array.\n     *\n     * <p>The padding '=' characters at the end are considered optional, but\n     * if any are present, there must be the correct number of them.\n     *\n     * @param input  the data to decode\n     * @param offset the position within the input array at which to start\n     * @param len    the number of bytes of input to decode\n     * @param flags  controls certain features of the decoded output.\n     *               Pass {@code DEFAULT} to decode standard Base64.\n     * @throws IllegalArgumentException if the input contains\n     *                                  incorrect padding\n     */\n    public static byte[] decode(byte[] input, int offset, int len, int flags) {\n        // Allocate space for the most data the input could represent.\n        // (It could contain less if it contains whitespace, etc.)\n        Decoder decoder = new Decoder(flags, new byte[len * 3 / 4]);\n        if (!decoder.process(input, offset, len, true)) {\n            throw new IllegalArgumentException(\"bad base-64\");\n        }\n        // Maybe we got lucky and allocated exactly enough output space.\n        if (decoder.op == decoder.output.length) {\n            return decoder.output;\n        }\n        // Need to shorten the array, so allocate a new one of the\n        // right size and copy.\n        byte[] temp = new byte[decoder.op];\n        System.arraycopy(decoder.output, 0, temp, 0, decoder.op);\n        return temp;\n    }\n\n    /* package */ static class Decoder extends Coder {\n        /**\n         * Lookup table for turning bytes into their position in the\n         * Base64 alphabet.\n         */\n        private static final int DECODE[] = {\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,\n                52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,\n                -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,\n                15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,\n                -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,\n                41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n        };\n        /**\n         * Decode lookup table for the \"web safe\" variant (RFC 3548\n         * sec. 4) where - and _ replace + and /.\n         */\n        private static final int DECODE_WEBSAFE[] = {\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,\n                52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,\n                -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,\n                15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,\n                -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,\n                41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n                -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,\n        };\n        /**\n         * Non-data values in the DECODE arrays.\n         */\n        private static final int SKIP = -1;\n        private static final int EQUALS = -2;\n        /**\n         * States 0-3 are reading through the next input tuple.\n         * State 4 is having read one '=' and expecting exactly\n         * one more.\n         * State 5 is expecting no more data or padding characters\n         * in the input.\n         * State 6 is the error state; an error has been detected\n         * in the input and no future input can \"fix\" it.\n         */\n        private int state;   // state number (0 to 6)\n        private int value;\n        final private int[] alphabet;\n\n        public Decoder(int flags, byte[] output) {\n            this.output = output;\n            alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;\n            state = 0;\n            value = 0;\n        }\n\n        /**\n         * @return an overestimate for the number of bytes {@code\n         * len} bytes could decode to.\n         */\n        public int maxOutputSize(int len) {\n            return len * 3 / 4 + 10;\n        }\n\n        /**\n         * Decode another block of input data.\n         *\n         * @return true if the state machine is still healthy.  false if\n         * bad base-64 data has been detected in the input stream.\n         */\n        public boolean process(byte[] input, int offset, int len, boolean finish) {\n            if (this.state == 6) return false;\n            int p = offset;\n            len += offset;\n            // Using local variables makes the decoder about 12%\n            // faster than if we manipulate the member variables in\n            // the loop.  (Even alphabet makes a measurable\n            // difference, which is somewhat surprising to me since\n            // the member variable is final.)\n            int state = this.state;\n            int value = this.value;\n            int op = 0;\n            final byte[] output = this.output;\n            final int[] alphabet = this.alphabet;\n            while (p < len) {\n                // Try the fast path:  we're starting a new tuple and the\n                // next four bytes of the input stream are all data\n                // bytes.  This corresponds to going through states\n                // 0-1-2-3-0.  We expect to use this method for most of\n                // the data.\n                //\n                // If any of the next four bytes of input are non-data\n                // (whitespace, etc.), value will end up negative.  (All\n                // the non-data values in decode are small negative\n                // numbers, so shifting any of them up and or'ing them\n                // together will result in a value with its top bit set.)\n                //\n                // You can remove this whole block and the output should\n                // be the same, just slower.\n                if (state == 0) {\n                    while (p + 4 <= len &&\n                            (value = ((alphabet[input[p] & 0xff] << 18) |\n                                    (alphabet[input[p + 1] & 0xff] << 12) |\n                                    (alphabet[input[p + 2] & 0xff] << 6) |\n                                    (alphabet[input[p + 3] & 0xff]))) >= 0) {\n                        output[op + 2] = (byte) value;\n                        output[op + 1] = (byte) (value >> 8);\n                        output[op] = (byte) (value >> 16);\n                        op += 3;\n                        p += 4;\n                    }\n                    if (p >= len) break;\n                }\n                // The fast path isn't available -- either we've read a\n                // partial tuple, or the next four input bytes aren't all\n                // data, or whatever.  Fall back to the slower state\n                // machine implementation.\n                int d = alphabet[input[p++] & 0xff];\n                switch (state) {\n                    case 0:\n                        if (d >= 0) {\n                            value = d;\n                            ++state;\n                        } else if (d != SKIP) {\n                            this.state = 6;\n                            return false;\n                        }\n                        break;\n                    case 1:\n                        if (d >= 0) {\n                            value = (value << 6) | d;\n                            ++state;\n                        } else if (d != SKIP) {\n                            this.state = 6;\n                            return false;\n                        }\n                        break;\n                    case 2:\n                        if (d >= 0) {\n                            value = (value << 6) | d;\n                            ++state;\n                        } else if (d == EQUALS) {\n                            // Emit the last (partial) output tuple;\n                            // expect exactly one more padding character.\n                            output[op++] = (byte) (value >> 4);\n                            state = 4;\n                        } else if (d != SKIP) {\n                            this.state = 6;\n                            return false;\n                        }\n                        break;\n                    case 3:\n                        if (d >= 0) {\n                            // Emit the output triple and return to state 0.\n                            value = (value << 6) | d;\n                            output[op + 2] = (byte) value;\n                            output[op + 1] = (byte) (value >> 8);\n                            output[op] = (byte) (value >> 16);\n                            op += 3;\n                            state = 0;\n                        } else if (d == EQUALS) {\n                            // Emit the last (partial) output tuple;\n                            // expect no further data or padding characters.\n                            output[op + 1] = (byte) (value >> 2);\n                            output[op] = (byte) (value >> 10);\n                            op += 2;\n                            state = 5;\n                        } else if (d != SKIP) {\n                            this.state = 6;\n                            return false;\n                        }\n                        break;\n                    case 4:\n                        if (d == EQUALS) {\n                            ++state;\n                        } else if (d != SKIP) {\n                            this.state = 6;\n                            return false;\n                        }\n                        break;\n                    case 5:\n                        if (d != SKIP) {\n                            this.state = 6;\n                            return false;\n                        }\n                        break;\n                }\n            }\n            if (!finish) {\n                // We're out of input, but a future call could provide\n                // more.\n                this.state = state;\n                this.value = value;\n                this.op = op;\n                return true;\n            }\n            // Done reading input.  Now figure out where we are left in\n            // the state machine and finish up.\n            switch (state) {\n                case 0:\n                    // Output length is a multiple of three.  Fine.\n                    break;\n                case 1:\n                    // Read one extra input byte, which isn't enough to\n                    // make another output byte.  Illegal.\n                    this.state = 6;\n                    return false;\n                case 2:\n                    // Read two extra input bytes, enough to emit 1 more\n                    // output byte.  Fine.\n                    output[op++] = (byte) (value >> 4);\n                    break;\n                case 3:\n                    // Read three extra input bytes, enough to emit 2 more\n                    // output bytes.  Fine.\n                    output[op++] = (byte) (value >> 10);\n                    output[op++] = (byte) (value >> 2);\n                    break;\n                case 4:\n                    // Read one padding '=' when we expected 2.  Illegal.\n                    this.state = 6;\n                    return false;\n                case 5:\n                    // Read all the padding '='s we expected and no more.\n                    // Fine.\n                    break;\n            }\n            this.state = state;\n            this.op = op;\n            return true;\n        }\n    }\n    //  --------------------------------------------------------\n    //  encoding\n    //  --------------------------------------------------------\n\n    /**\n     * Base64-encode the given data and return a newly allocated\n     * String with the result.\n     *\n     * @param input the data to encode\n     * @param flags controls certain features of the encoded output.\n     *              Passing {@code DEFAULT} results in output that\n     *              adheres to RFC 2045.\n     */\n    public static String encodeToString(byte[] input, int flags) {\n        try {\n            return new String(encode(input, flags), \"US-ASCII\");\n        } catch (UnsupportedEncodingException e) {\n            // US-ASCII is guaranteed to be available.\n            throw new AssertionError(e);\n        }\n    }\n\n    /**\n     * Base64-encode the given data and return a newly allocated\n     * String with the result.\n     *\n     * @param input  the data to encode\n     * @param offset the position within the input array at which to\n     *               start\n     * @param len    the number of bytes of input to encode\n     * @param flags  controls certain features of the encoded output.\n     *               Passing {@code DEFAULT} results in output that\n     *               adheres to RFC 2045.\n     */\n    public static String encodeToString(byte[] input, int offset, int len, int flags) {\n        try {\n            return new String(encode(input, offset, len, flags), \"US-ASCII\");\n        } catch (UnsupportedEncodingException e) {\n            // US-ASCII is guaranteed to be available.\n            throw new AssertionError(e);\n        }\n    }\n\n    /**\n     * Base64-encode the given data and return a newly allocated\n     * byte[] with the result.\n     *\n     * @param input the data to encode\n     * @param flags controls certain features of the encoded output.\n     *              Passing {@code DEFAULT} results in output that\n     *              adheres to RFC 2045.\n     */\n    public static byte[] encode(byte[] input, int flags) {\n        return encode(input, 0, input.length, flags);\n    }\n\n    /**\n     * Base64-encode the given data and return a newly allocated\n     * byte[] with the result.\n     *\n     * @param input  the data to encode\n     * @param offset the position within the input array at which to\n     *               start\n     * @param len    the number of bytes of input to encode\n     * @param flags  controls certain features of the encoded output.\n     *               Passing {@code DEFAULT} results in output that\n     *               adheres to RFC 2045.\n     */\n    public static byte[] encode(byte[] input, int offset, int len, int flags) {\n        Encoder encoder = new Encoder(flags, null);\n        // Compute the exact length of the array we will produce.\n        int output_len = len / 3 * 4;\n        // Account for the tail of the data and the padding bytes, if any.\n        if (encoder.do_padding) {\n            if (len % 3 > 0) {\n                output_len += 4;\n            }\n        } else {\n            switch (len % 3) {\n                case 0:\n                    break;\n                case 1:\n                    output_len += 2;\n                    break;\n                case 2:\n                    output_len += 3;\n                    break;\n            }\n        }\n        // Account for the newlines, if any.\n        if (encoder.do_newline && len > 0) {\n            output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) *\n                    (encoder.do_cr ? 2 : 1);\n        }\n        encoder.output = new byte[output_len];\n        encoder.process(input, offset, len, true);\n        assert encoder.op == output_len;\n        return encoder.output;\n    }\n\n    /* package */ static class Encoder extends Coder {\n        /**\n         * Emit a new line every this many output tuples.  Corresponds to\n         * a 76-character line length (the maximum allowable according to\n         * <a href=\"http://www.ietf.org/rfc/rfc2045.txt\">RFC 2045</a>).\n         */\n        public static final int LINE_GROUPS = 19;\n        /**\n         * Lookup table for turning Base64 alphabet positions (6 bits)\n         * into output bytes.\n         */\n        private static final byte ENCODE[] = {\n                'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',\n                'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',\n                'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',\n                'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',\n        };\n        /**\n         * Lookup table for turning Base64 alphabet positions (6 bits)\n         * into output bytes.\n         */\n        private static final byte ENCODE_WEBSAFE[] = {\n                'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',\n                'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',\n                'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',\n                'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',\n        };\n        final private byte[] tail;\n        /* package */ int tailLen;\n        private int count;\n        final public boolean do_padding;\n        final public boolean do_newline;\n        final public boolean do_cr;\n        final private byte[] alphabet;\n\n        public Encoder(int flags, byte[] output) {\n            this.output = output;\n            do_padding = (flags & NO_PADDING) == 0;\n            do_newline = (flags & NO_WRAP) == 0;\n            do_cr = (flags & CRLF) != 0;\n            alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;\n            tail = new byte[2];\n            tailLen = 0;\n            count = do_newline ? LINE_GROUPS : -1;\n        }\n\n        /**\n         * @return an overestimate for the number of bytes {@code\n         * len} bytes could encode to.\n         */\n        public int maxOutputSize(int len) {\n            return len * 8 / 5 + 10;\n        }\n\n        public boolean process(byte[] input, int offset, int len, boolean finish) {\n            // Using local variables makes the encoder about 9% faster.\n            final byte[] alphabet = this.alphabet;\n            final byte[] output = this.output;\n            int op = 0;\n            int count = this.count;\n            int p = offset;\n            len += offset;\n            int v = -1;\n            // First we need to concatenate the tail of the previous call\n            // with any input bytes available now and see if we can empty\n            // the tail.\n            switch (tailLen) {\n                case 0:\n                    // There was no tail.\n                    break;\n                case 1:\n                    if (p + 2 <= len) {\n                        // A 1-byte tail with at least 2 bytes of\n                        // input available now.\n                        v = ((tail[0] & 0xff) << 16) |\n                                ((input[p++] & 0xff) << 8) |\n                                (input[p++] & 0xff);\n                        tailLen = 0;\n                    }\n                    ;\n                    break;\n                case 2:\n                    if (p + 1 <= len) {\n                        // A 2-byte tail with at least 1 byte of input.\n                        v = ((tail[0] & 0xff) << 16) |\n                                ((tail[1] & 0xff) << 8) |\n                                (input[p++] & 0xff);\n                        tailLen = 0;\n                    }\n                    break;\n            }\n            if (v != -1) {\n                output[op++] = alphabet[(v >> 18) & 0x3f];\n                output[op++] = alphabet[(v >> 12) & 0x3f];\n                output[op++] = alphabet[(v >> 6) & 0x3f];\n                output[op++] = alphabet[v & 0x3f];\n                if (--count == 0) {\n                    if (do_cr) output[op++] = '\\r';\n                    output[op++] = '\\n';\n                    count = LINE_GROUPS;\n                }\n            }\n            // At this point either there is no tail, or there are fewer\n            // than 3 bytes of input available.\n            // The main loop, turning 3 input bytes into 4 output bytes on\n            // each iteration.\n            while (p + 3 <= len) {\n                v = ((input[p] & 0xff) << 16) |\n                        ((input[p + 1] & 0xff) << 8) |\n                        (input[p + 2] & 0xff);\n                output[op] = alphabet[(v >> 18) & 0x3f];\n                output[op + 1] = alphabet[(v >> 12) & 0x3f];\n                output[op + 2] = alphabet[(v >> 6) & 0x3f];\n                output[op + 3] = alphabet[v & 0x3f];\n                p += 3;\n                op += 4;\n                if (--count == 0) {\n                    if (do_cr) output[op++] = '\\r';\n                    output[op++] = '\\n';\n                    count = LINE_GROUPS;\n                }\n            }\n            if (finish) {\n                // Finish up the tail of the input.  Note that we need to\n                // consume any bytes in tail before any bytes\n                // remaining in input; there should be at most two bytes\n                // total.\n                if (p - tailLen == len - 1) {\n                    int t = 0;\n                    v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;\n                    tailLen -= t;\n                    output[op++] = alphabet[(v >> 6) & 0x3f];\n                    output[op++] = alphabet[v & 0x3f];\n                    if (do_padding) {\n                        output[op++] = '=';\n                        output[op++] = '=';\n                    }\n                    if (do_newline) {\n                        if (do_cr) output[op++] = '\\r';\n                        output[op++] = '\\n';\n                    }\n                } else if (p - tailLen == len - 2) {\n                    int t = 0;\n                    v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |\n                            (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);\n                    tailLen -= t;\n                    output[op++] = alphabet[(v >> 12) & 0x3f];\n                    output[op++] = alphabet[(v >> 6) & 0x3f];\n                    output[op++] = alphabet[v & 0x3f];\n                    if (do_padding) {\n                        output[op++] = '=';\n                    }\n                    if (do_newline) {\n                        if (do_cr) output[op++] = '\\r';\n                        output[op++] = '\\n';\n                    }\n                } else if (do_newline && op > 0 && count != LINE_GROUPS) {\n                    if (do_cr) output[op++] = '\\r';\n                    output[op++] = '\\n';\n                }\n                assert tailLen == 0;\n                assert p == len;\n            } else {\n                // Save the leftovers in tail to be consumed on the next\n                // call to encodeInternal.\n                if (p == len - 1) {\n                    tail[tailLen++] = input[p];\n                } else if (p == len - 2) {\n                    tail[tailLen++] = input[p];\n                    tail[tailLen++] = input[p + 1];\n                }\n            }\n            this.op = op;\n            this.count = count;\n            return true;\n        }\n    }\n\n    private Base64() {\n    }   // don't instantiate\n}"
  },
  {
    "path": "src/main/java/io/legado/app/utils/EncoderUtils.kt",
    "content": "package io.legado.app.utils\n\nimport io.legado.app.utils.Base64\nimport java.security.spec.AlgorithmParameterSpec\nimport javax.crypto.Cipher\nimport javax.crypto.spec.IvParameterSpec\nimport javax.crypto.spec.SecretKeySpec\n\n@Suppress(\"unused\")\nobject EncoderUtils {\n\n    fun escape(src: String): String {\n        val tmp = StringBuilder()\n        for (char in src) {\n            val charCode = char.code\n            if (charCode in 48..57 || charCode in 65..90 || charCode in 97..122) {\n                tmp.append(char)\n                continue\n            }\n\n            val prefix = when {\n                charCode < 16 -> \"%0\"\n                charCode < 256 -> \"%\"\n                else -> \"%u\"\n            }\n            tmp.append(prefix).append(charCode.toString(16))\n        }\n        return tmp.toString()\n    }\n\n    @JvmOverloads\n    fun base64Decode(str: String, flags: Int = Base64.DEFAULT): String {\n        val bytes = Base64.decode(str, flags)\n        return String(bytes)\n    }\n\n    @JvmOverloads\n    fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? {\n        return Base64.encodeToString(str.toByteArray(), flags)\n    }\n\n    //////////AES Start\n\n    /**\n     * Return the Base64-encode bytes of AES encryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the Base64-encode bytes of AES encryption\n     */\n    @Throws(Exception::class)\n    fun encryptAES2Base64(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String? = \"DES/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return Base64.encode(encryptAES(data, key, transformation, iv), Base64.NO_WRAP)\n    }\n\n    /**\n     * Return the bytes of AES encryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES encryption\n     */\n    @Throws(Exception::class)\n    fun encryptAES(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String? = \"DES/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return symmetricTemplate(data, key, \"AES\", transformation!!, iv, true)\n    }\n\n\n    /**\n     * Return the bytes of AES decryption for Base64-encode bytes.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES decryption for Base64-encode bytes\n     */\n    @Throws(Exception::class)\n    fun decryptBase64AES(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String = \"DES/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return decryptAES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)\n    }\n\n    /**\n     * Return the bytes of AES decryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES decryption\n     */\n    @Throws(Exception::class)\n    fun decryptAES(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String = \"DES/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return symmetricTemplate(data, key, \"AES\", transformation, iv, false)\n    }\n\n\n    /**\n     * Return the bytes of symmetric encryption or decryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param algorithm      The name of algorithm.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, <i>DES/CBC/PKCS5Padding</i>.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @param isEncrypt      True to encrypt, false otherwise.\n     * @return the bytes of symmetric encryption or decryption\n     */\n    @Suppress(\"SameParameterValue\")\n    @Throws(Exception::class)\n    private fun symmetricTemplate(\n        data: ByteArray?,\n        key: ByteArray?,\n        algorithm: String,\n        transformation: String,\n        iv: ByteArray?,\n        isEncrypt: Boolean\n    ): ByteArray? {\n        return if (data == null || data.isEmpty() || key == null || key.isEmpty()) null\n        else {\n            val keySpec = SecretKeySpec(key, algorithm)\n            val cipher = Cipher.getInstance(transformation)\n            val mode = if (isEncrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE\n            if (iv == null || iv.isEmpty()) {\n                cipher.init(mode, keySpec)\n            } else {\n                val params: AlgorithmParameterSpec = IvParameterSpec(iv)\n                cipher.init(mode, keySpec, params)\n            }\n            cipher.doFinal(data)\n        }\n    }\n    //////////DES Start\n\n    /**\n     * Return the Base64-encode bytes of DES encryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the Base64-encode bytes of AES encryption\n     */\n    @Throws(Exception::class)\n    fun encryptDES2Base64(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String? = \"DES/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return Base64.encode(encryptDES(data, key, transformation, iv), Base64.NO_WRAP)\n    }\n\n    /**\n     * Return the bytes of DES encryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES encryption\n     */\n    @Throws(Exception::class)\n    fun encryptDES(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String? = \"DES/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return symmetricTemplate(data, key, \"DES\", transformation!!, iv, true)\n    }\n\n\n    /**\n     * Return the bytes of DES decryption for Base64-encode bytes.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES decryption for Base64-encode bytes\n     */\n    @Throws(Exception::class)\n    fun decryptBase64DES(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String = \"DES/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return decryptDES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)\n    }\n\n    /**\n     * Return the bytes of DES decryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES decryption\n     */\n    @Throws(Exception::class)\n    fun decryptDES(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String = \"DES/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return symmetricTemplate(data, key, \"DES\", transformation, iv, false)\n    }\n\n    //////////DESede Start\n\n    /**\n     * Return the Base64-encode bytes of DESede encryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the Base64-encode bytes of AES encryption\n     */\n    @Throws(Exception::class)\n    fun encryptDESede2Base64(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String? = \"DESede/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return Base64.encode(encryptDESede(data, key, transformation, iv), Base64.NO_WRAP)\n    }\n\n    /**\n     * Return the bytes of DESede encryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES encryption\n     */\n    @Throws(Exception::class)\n    fun encryptDESede(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String? = \"DESede/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return symmetricTemplate(data, key, \"DESede\", transformation!!, iv, true)\n    }\n\n\n    /**\n     * Return the bytes of DESede decryption for Base64-encode bytes.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES decryption for Base64-encode bytes\n     */\n    @Throws(Exception::class)\n    fun decryptBase64DESede(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String = \"DESede/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return decryptDESede(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)\n    }\n\n    /**\n     * Return the bytes of DESede decryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation,\n     * 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES decryption\n     */\n    @Throws(Exception::class)\n    fun decryptDESede(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String = \"DESede/ECB/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return symmetricTemplate(data, key, \"DESede\", transformation, iv, false)\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/utils/EncodingDetect.kt",
    "content": "package io.legado.app.utils\n\nimport io.legado.app.lib.icu4j.CharsetDetector\nimport org.jsoup.Jsoup\nimport java.io.File\nimport java.io.FileInputStream\nimport java.nio.charset.StandardCharsets\nimport java.util.*\n\n/**\n * 自动获取文件的编码\n * */\n@Suppress(\"MemberVisibilityCanBePrivate\", \"unused\")\nobject EncodingDetect {\n\n    fun getHtmlEncode(bytes: ByteArray): String? {\n        try {\n            val doc = Jsoup.parse(String(bytes, StandardCharsets.UTF_8))\n            val metaTags = doc.getElementsByTag(\"meta\")\n            var charsetStr: String\n            for (metaTag in metaTags) {\n                charsetStr = metaTag.attr(\"charset\")\n                if (!charsetStr.isEmpty()) {\n                    return charsetStr\n                }\n                val content = metaTag.attr(\"content\")\n                val httpEquiv = metaTag.attr(\"http-equiv\")\n                if (httpEquiv.lowercase(Locale.getDefault()) == \"content-type\") {\n                    charsetStr = if (content.lowercase(Locale.getDefault()).contains(\"charset\")) {\n                        content.substring(\n                            content.lowercase(Locale.getDefault())\n                                .indexOf(\"charset\") + \"charset=\".length\n                        )\n                    } else {\n                        content.substring(content.lowercase(Locale.getDefault()).indexOf(\";\") + 1)\n                    }\n                    if (!charsetStr.isEmpty()) {\n                        return charsetStr\n                    }\n                }\n            }\n        } catch (ignored: Exception) {\n        }\n        return getEncode(bytes)\n    }\n\n    fun getEncode(bytes: ByteArray): String {\n        val match = CharsetDetector().setText(bytes).detect()\n        return match?.name ?: \"UTF-8\"\n    }\n\n    /**\n     * 得到文件的编码\n     */\n    fun getEncode(filePath: String): String {\n        return getEncode(File(filePath))\n    }\n\n    /**\n     * 得到文件的编码\n     */\n    fun getEncode(file: File): String {\n        val tempByte = getFileBytes(file)\n        return getEncode(tempByte)\n    }\n\n    private fun getFileBytes(file: File?): ByteArray {\n        val byteArray = ByteArray(8000)\n        try {\n            FileInputStream(file).use {\n                it.read(byteArray)\n            }\n        } catch (e: Exception) {\n            System.err.println(\"Error: $e\")\n        }\n        return byteArray\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/utils/FileExtensions.kt",
    "content": "package io.legado.app.utils\n\nimport java.io.File\n\nfun File.getFile(vararg subDirFiles: String): File {\n    val path = FileUtils.getPath(this, *subDirFiles)\n    return File(path)\n}\n\nfun File.exists(vararg subDirFiles: String): Boolean {\n    return getFile(*subDirFiles).exists()\n}\n\n"
  },
  {
    "path": "src/main/java/io/legado/app/utils/FilesUtil.kt",
    "content": "package io.legado.app.utils\n\nimport java.io.*\nimport java.nio.charset.Charset\nimport java.text.SimpleDateFormat\nimport java.util.*\nimport java.util.regex.Pattern\nimport java.text.DecimalFormat\n\nobject FileUtils {\n    const val GB: Long = 1073741824\n    const val MB: Long = 1048576\n    const val KB: Long = 1024\n\n    fun exists(root: File, vararg subDirFiles: String): Boolean {\n        return getFile(root, *subDirFiles).exists()\n    }\n\n    fun createFileIfNotExist(root: File, vararg subDirFiles: String): File {\n        val filePath = getPath(root, *subDirFiles)\n        return createFileIfNotExist(filePath)\n    }\n\n    fun createFolderIfNotExist(root: File, vararg subDirs: String): File {\n        val filePath = getPath(root, *subDirs)\n        return createFolderIfNotExist(filePath)\n    }\n\n    fun createFolderIfNotExist(filePath: String): File {\n        val file = File(filePath)\n        //如果文件夹不存在，就创建它\n        if (!file.exists()) {\n            file.mkdirs()\n        }\n        return file\n    }\n\n    @Synchronized\n    fun createFileIfNotExist(filePath: String): File {\n        val file = File(filePath)\n        try {\n            if (!file.exists()) {\n                //创建父类文件夹\n                file.parent?.let {\n                    createFolderIfNotExist(it)\n                }\n                //创建文件\n                file.createNewFile()\n            }\n        } catch (e: IOException) {\n            e.printStackTrace()\n        }\n        return file\n    }\n\n    fun createFileWithReplace(filePath: String): File {\n        val file = File(filePath)\n        if (!file.exists()) {\n            //创建父类文件夹\n            file.parent?.let {\n                createFolderIfNotExist(it)\n            }\n            //创建文件\n            file.createNewFile()\n        } else {\n            file.delete()\n            file.createNewFile()\n        }\n        return file\n    }\n\n    fun getFile(root: File, vararg subDirFiles: String): File {\n        val filePath = getPath(root, *subDirFiles)\n        return File(filePath)\n    }\n\n    fun getPath(root: File, vararg subDirFiles: String): String {\n        val path = StringBuilder(root.absolutePath)\n        subDirFiles.forEach {\n            if (it.isNotEmpty()) {\n                path.append(File.separator).append(it)\n            }\n        }\n        return path.toString()\n    }\n\n    //递归删除文件夹下的数据\n    @Synchronized\n    fun deleteFile(filePath: String) {\n        val file = File(filePath)\n        if (!file.exists()) return\n\n        if (file.isDirectory) {\n            val files = file.listFiles()\n            files?.forEach { subFile ->\n                val path = subFile.path\n                deleteFile(path)\n            }\n        }\n        //删除文件\n        file.delete()\n    }\n\n    fun getCachePath(): String {\n        // TODO\n        throw Exception(\"Not implemented\")\n        return \"\"\n    }\n\n    const val BY_NAME_ASC = 0\n    const val BY_NAME_DESC = 1\n    const val BY_TIME_ASC = 2\n    const val BY_TIME_DESC = 3\n    const val BY_SIZE_ASC = 4\n    const val BY_SIZE_DESC = 5\n    const val BY_EXTENSION_ASC = 6\n    const val BY_EXTENSION_DESC = 7\n\n    @kotlin.annotation.Retention(AnnotationRetention.SOURCE)\n    annotation class SortType\n\n    /**\n     * 将目录分隔符统一为平台默认的分隔符，并为目录结尾添加分隔符\n     */\n    fun separator(path: String): String {\n        var path1 = path\n        val separator = File.separator\n        path1 = path1.replace(\"\\\\\", separator)\n        if (!path1.endsWith(separator)) {\n            path1 += separator\n        }\n        return path1\n    }\n\n    fun closeSilently(c: Closeable?) {\n        if (c == null) {\n            return\n        }\n        try {\n            c.close()\n        } catch (ignored: IOException) {\n        }\n\n    }\n\n    /**\n     * 列出指定目录下的所有子目录\n     */\n    @JvmOverloads\n    fun listDirs(\n        startDirPath: String,\n        excludeDirs: Array<String>? = null, @SortType sortType: Int = BY_NAME_ASC\n    ): Array<File> {\n        var excludeDirs1 = excludeDirs\n        val dirList = ArrayList<File>()\n        val startDir = File(startDirPath)\n        if (!startDir.isDirectory) {\n            return arrayOf()\n        }\n        val dirs = startDir.listFiles(FileFilter { f ->\n            if (f == null) {\n                return@FileFilter false\n            }\n            f.isDirectory\n        }) ?: return arrayOf()\n        if (excludeDirs1 == null) {\n            excludeDirs1 = arrayOf()\n        }\n        for (dir in dirs) {\n            val file = dir.absoluteFile\n            if (!excludeDirs1.contentDeepToString().contains(file.name)) {\n                dirList.add(file)\n            }\n        }\n        when (sortType) {\n            BY_NAME_ASC -> Collections.sort(dirList, SortByName())\n            BY_NAME_DESC -> {\n                Collections.sort(dirList, SortByName())\n                dirList.reverse()\n            }\n            BY_TIME_ASC -> Collections.sort(dirList, SortByTime())\n            BY_TIME_DESC -> {\n                Collections.sort(dirList, SortByTime())\n                dirList.reverse()\n            }\n            BY_SIZE_ASC -> Collections.sort(dirList, SortBySize())\n            BY_SIZE_DESC -> {\n                Collections.sort(dirList, SortBySize())\n                dirList.reverse()\n            }\n            BY_EXTENSION_ASC -> Collections.sort(dirList, SortByExtension())\n            BY_EXTENSION_DESC -> {\n                Collections.sort(dirList, SortByExtension())\n                dirList.reverse()\n            }\n        }\n        return dirList.toTypedArray()\n    }\n\n    /**\n     * 列出指定目录下的所有子目录及所有文件\n     */\n    @JvmOverloads\n    fun listDirsAndFiles(\n        startDirPath: String,\n        allowExtensions: Array<String>? = null\n    ): Array<File>? {\n        val dirs: Array<File>?\n        val files: Array<File>? = if (allowExtensions == null) {\n            listFiles(startDirPath)\n        } else {\n            listFiles(startDirPath, allowExtensions)\n        }\n        dirs = listDirs(startDirPath)\n        if (files == null) {\n            return null\n        }\n        return dirs + files\n    }\n\n    /**\n     * 列出指定目录下的所有文件\n     */\n    @JvmOverloads\n    fun listFiles(\n        startDirPath: String,\n        filterPattern: Pattern? = null, @SortType sortType: Int = BY_NAME_ASC\n    ): Array<File> {\n        val fileList = ArrayList<File>()\n        val f = File(startDirPath)\n        if (!f.isDirectory) {\n            return arrayOf()\n        }\n        val files = f.listFiles(FileFilter { file ->\n            if (file == null) {\n                return@FileFilter false\n            }\n            if (file.isDirectory) {\n                return@FileFilter false\n            }\n\n            filterPattern?.matcher(file.name)?.find() ?: true\n        })\n            ?: return arrayOf()\n        for (file in files) {\n            fileList.add(file.absoluteFile)\n        }\n        when (sortType) {\n            BY_NAME_ASC -> Collections.sort(fileList, SortByName())\n            BY_NAME_DESC -> {\n                Collections.sort(fileList, SortByName())\n                fileList.reverse()\n            }\n            BY_TIME_ASC -> Collections.sort(fileList, SortByTime())\n            BY_TIME_DESC -> {\n                Collections.sort(fileList, SortByTime())\n                fileList.reverse()\n            }\n            BY_SIZE_ASC -> Collections.sort(fileList, SortBySize())\n            BY_SIZE_DESC -> {\n                Collections.sort(fileList, SortBySize())\n                fileList.reverse()\n            }\n            BY_EXTENSION_ASC -> Collections.sort(fileList, SortByExtension())\n            BY_EXTENSION_DESC -> {\n                Collections.sort(fileList, SortByExtension())\n                fileList.reverse()\n            }\n        }\n        return fileList.toTypedArray()\n    }\n\n    /**\n     * 列出指定目录下的所有文件\n     */\n    fun listFiles(startDirPath: String, allowExtensions: Array<String>?): Array<File>? {\n        val file = File(startDirPath)\n        return file.listFiles { _, name ->\n            //返回当前目录所有以某些扩展名结尾的文件\n            val extension = getExtension(name)\n            allowExtensions?.contentDeepToString()?.contains(extension) == true\n                    || allowExtensions == null\n        }\n    }\n\n    /**\n     * 列出指定目录下的所有文件\n     */\n    fun listFiles(startDirPath: String, allowExtension: String?): Array<File>? {\n        return if (allowExtension == null)\n            listFiles(startDirPath, allowExtension = null)\n        else\n            listFiles(startDirPath, arrayOf(allowExtension))\n    }\n\n    /**\n     * 判断文件或目录是否存在\n     */\n    fun exist(path: String): Boolean {\n        val file = File(path)\n        return file.exists()\n    }\n\n    /**\n     * 删除文件或目录\n     */\n    @JvmOverloads\n    fun delete(file: File, deleteRootDir: Boolean = false): Boolean {\n        var result = false\n        if (file.isFile) {\n            //是文件\n            result = deleteResolveEBUSY(file)\n        } else {\n            //是目录\n            val files = file.listFiles() ?: return false\n            if (files.isEmpty()) {\n                result = deleteRootDir && deleteResolveEBUSY(file)\n            } else {\n                for (f in files) {\n                    delete(f, deleteRootDir)\n                    result = deleteResolveEBUSY(f)\n                }\n            }\n            if (deleteRootDir) {\n                result = deleteResolveEBUSY(file)\n            }\n        }\n        return result\n    }\n\n    /**\n     * bug: open failed: EBUSY (Device or resource busy)\n     * fix: http://stackoverflow.com/questions/11539657/open-failed-ebusy-device-or-resource-busy\n     */\n    private fun deleteResolveEBUSY(file: File): Boolean {\n        // Before you delete a Directory or File: rename it!\n        val to = File(file.absolutePath + System.currentTimeMillis())\n\n        file.renameTo(to)\n        return to.delete()\n    }\n\n    /**\n     * 删除文件或目录\n     */\n    @JvmOverloads\n    fun delete(path: String, deleteRootDir: Boolean = false): Boolean {\n        val file = File(path)\n\n        return if (file.exists()) {\n            delete(file, deleteRootDir)\n        } else false\n    }\n\n    /**\n     * 复制文件为另一个文件，或复制某目录下的所有文件及目录到另一个目录下\n     */\n    fun copy(src: String, tar: String): Boolean {\n        val srcFile = File(src)\n        return srcFile.exists() && copy(srcFile, File(tar))\n    }\n\n    /**\n     * 复制文件或目录\n     */\n    fun copy(src: File, tar: File): Boolean {\n        try {\n            if (src.isFile) {\n                val `is` = FileInputStream(src)\n                val op = FileOutputStream(tar)\n                val bis = BufferedInputStream(`is`)\n                val bos = BufferedOutputStream(op)\n                val bt = ByteArray(1024 * 8)\n                while (true) {\n                    val len = bis.read(bt)\n                    if (len == -1) {\n                        break\n                    } else {\n                        bos.write(bt, 0, len)\n                    }\n                }\n                bis.close()\n                bos.close()\n            } else if (src.isDirectory) {\n                tar.mkdirs()\n                src.listFiles()?.forEach { file ->\n                    copy(file.absoluteFile, File(tar.absoluteFile, file.name))\n                }\n            }\n            return true\n        } catch (e: Exception) {\n            return false\n        }\n\n    }\n\n    /**\n     * 移动文件或目录\n     */\n    fun move(src: String, tar: String): Boolean {\n        return move(File(src), File(tar))\n    }\n\n    /**\n     * 移动文件或目录\n     */\n    fun move(src: File, tar: File): Boolean {\n        return rename(src, tar)\n    }\n\n    /**\n     * 文件重命名\n     */\n    fun rename(oldPath: String, newPath: String): Boolean {\n        return rename(File(oldPath), File(newPath))\n    }\n\n    /**\n     * 文件重命名\n     */\n    fun rename(src: File, tar: File): Boolean {\n        return src.renameTo(tar)\n    }\n\n    /**\n     * 读取文本文件, 失败将返回空串\n     */\n    @JvmOverloads\n    fun readText(filepath: String, charset: String = \"utf-8\"): String {\n        try {\n            val data = readBytes(filepath)\n            if (data != null) {\n                return String(data, Charset.forName(charset)).trim { it <= ' ' }\n            }\n        } catch (ignored: UnsupportedEncodingException) {\n        }\n\n        return \"\"\n    }\n\n    /**\n     * 读取文件内容, 失败将返回空串\n     */\n    fun readBytes(filepath: String): ByteArray? {\n        var fis: FileInputStream? = null\n        try {\n            fis = FileInputStream(filepath)\n            val baos = ByteArrayOutputStream()\n            val buffer = ByteArray(1024)\n            while (true) {\n                val len = fis.read(buffer, 0, buffer.size)\n                if (len == -1) {\n                    break\n                } else {\n                    baos.write(buffer, 0, len)\n                }\n            }\n            val data = baos.toByteArray()\n            baos.close()\n            return data\n        } catch (e: IOException) {\n            return null\n        } finally {\n            closeSilently(fis)\n        }\n    }\n\n    /**\n     * 保存文本内容\n     */\n    @JvmOverloads\n    fun writeText(filepath: String, content: String, charset: String = \"utf-8\"): Boolean {\n        return try {\n            writeBytes(filepath, content.toByteArray(charset(charset)))\n        } catch (e: UnsupportedEncodingException) {\n            false\n        }\n\n    }\n\n    /**\n     * 保存文件内容\n     */\n    fun writeBytes(filepath: String, data: ByteArray): Boolean {\n        val file = File(filepath)\n        var fos: FileOutputStream? = null\n        return try {\n            if (!file.exists()) {\n                file.parentFile?.mkdirs()\n                file.createNewFile()\n            }\n            fos = FileOutputStream(filepath)\n            fos.write(data)\n            true\n        } catch (e: IOException) {\n            false\n        } finally {\n            closeSilently(fos)\n        }\n    }\n\n    /**\n     * 保存文件内容\n     */\n    fun writeInputStream(filepath: String, data: InputStream): Boolean {\n        val file = File(filepath)\n        return writeInputStream(file, data)\n    }\n\n    /**\n     * 保存文件内容\n     */\n    fun writeInputStream(file: File, data: InputStream): Boolean {\n        var fos: FileOutputStream? = null\n        return try {\n            if (!file.exists()) {\n                file.parentFile?.mkdirs()\n                file.createNewFile()\n            }\n            val buffer = ByteArray(1024 * 4)\n            fos = FileOutputStream(file)\n            while (true) {\n                val len = data.read(buffer, 0, buffer.size)\n                if (len == -1) {\n                    break\n                } else {\n                    fos.write(buffer, 0, len)\n                }\n            }\n            data.close()\n            fos.flush()\n\n            true\n        } catch (e: IOException) {\n            false\n        } finally {\n            closeSilently(fos)\n        }\n    }\n\n    /**\n     * 追加文本内容\n     */\n    fun appendText(path: String, content: String): Boolean {\n        val file = File(path)\n        var writer: FileWriter? = null\n        return try {\n            if (!file.exists()) {\n\n                file.createNewFile()\n            }\n            writer = FileWriter(file, true)\n            writer.write(content)\n            true\n        } catch (e: IOException) {\n            false\n        } finally {\n            closeSilently(writer)\n        }\n    }\n\n    /**\n     * 获取文件大小\n     */\n    fun getLength(path: String): Long {\n        val file = File(path)\n        return if (!file.isFile || !file.exists()) {\n            0\n        } else file.length()\n    }\n\n    /**\n     * 获取文件或网址的名称（包括后缀）\n     */\n    fun getName(pathOrUrl: String?): String {\n        if (pathOrUrl == null) {\n            return \"\"\n        }\n        val pos = pathOrUrl.lastIndexOf('/')\n        return if (0 <= pos) {\n            pathOrUrl.substring(pos + 1)\n        } else {\n            System.currentTimeMillis().toString() + \".\" + getExtension(pathOrUrl)\n        }\n    }\n\n    /**\n     * 获取文件名（不包括扩展名）\n     */\n    fun getNameExcludeExtension(path: String): String {\n        return try {\n            var fileName = File(path).name\n            val lastIndexOf = fileName.lastIndexOf(\".\")\n            if (lastIndexOf != -1) {\n                fileName = fileName.substring(0, lastIndexOf)\n            }\n            fileName\n        } catch (e: Exception) {\n            \"\"\n        }\n\n    }\n\n    /**\n     * 获取格式化后的文件大小\n     */\n    fun getSize(path: String): String {\n        val fileSize = getLength(path)\n        return toFileSizeString(fileSize)\n    }\n\n    fun toFileSizeString(fileSize: Long): String {\n        val df = DecimalFormat(\"0.00\")\n        val fileSizeString: String\n        fileSizeString = when {\n            fileSize < KB -> fileSize.toString() + \"B\"\n            fileSize < MB -> df.format(fileSize.toDouble() / KB) + \"K\"\n            fileSize < GB -> df.format(fileSize.toDouble() / MB) + \"M\"\n            else -> df.format(fileSize.toDouble() / GB) + \"G\"\n        }\n        return fileSizeString\n    }\n\n    /**\n     * 获取文件后缀,不包括“.”\n     */\n    fun getExtension(pathOrUrl: String): String {\n        val dotPos = pathOrUrl.lastIndexOf('.')\n        return if (0 <= dotPos) {\n            pathOrUrl.substring(dotPos + 1)\n        } else {\n            \"ext\"\n        }\n    }\n\n    /**\n     * 获取文件的MIME类型\n     */\n    fun getMimeType(pathOrUrl: String): String {\n        // val ext = getExtension(pathOrUrl)\n        // val map = MimeTypeMap.getSingleton()\n        // return map.getMimeTypeFromExtension(ext) ?: \"*/*\"\n        //\n        throw Exception(\"Not implemented\")\n    }\n\n    /**\n     * 获取格式化后的文件/目录创建或最后修改时间\n     */\n    @JvmOverloads\n    fun getDateTime(path: String, format: String = \"yyyy年MM月dd日HH:mm\"): String {\n        val file = File(path)\n        return getDateTime(file, format)\n    }\n\n    /**\n     * 获取格式化后的文件/目录创建或最后修改时间\n     */\n    fun getDateTime(file: File, format: String): String {\n        val cal = Calendar.getInstance()\n        cal.timeInMillis = file.lastModified()\n        return SimpleDateFormat(format, Locale.PRC).format(cal.time)\n    }\n\n    /**\n     * 比较两个文件的最后修改时间\n     */\n    fun compareLastModified(path1: String, path2: String): Int {\n        val stamp1 = File(path1).lastModified()\n        val stamp2 = File(path2).lastModified()\n        return when {\n            stamp1 > stamp2 -> {\n                1\n            }\n            stamp1 < stamp2 -> {\n                -1\n            }\n            else -> {\n                0\n            }\n        }\n    }\n\n    /**\n     * 创建多级别的目录\n     */\n    fun makeDirs(path: String): Boolean {\n        return makeDirs(File(path))\n    }\n\n    /**\n     * 创建多级别的目录\n     */\n    fun makeDirs(file: File): Boolean {\n        return file.mkdirs()\n    }\n\n    class SortByExtension : Comparator<File> {\n\n        override fun compare(f1: File?, f2: File?): Int {\n            return if (f1 == null || f2 == null) {\n                if (f1 == null) {\n                    -1\n                } else {\n                    1\n                }\n            } else {\n                if (f1.isDirectory && f2.isFile) {\n                    -1\n                } else if (f1.isFile && f2.isDirectory) {\n                    1\n                } else {\n                    f1.name.compareTo(f2.name, ignoreCase = true)\n                }\n            }\n        }\n\n    }\n\n    class SortByName : Comparator<File> {\n        private var caseSensitive: Boolean = false\n\n        constructor(caseSensitive: Boolean) {\n            this.caseSensitive = caseSensitive\n        }\n\n        constructor() {\n            this.caseSensitive = false\n        }\n\n        override fun compare(f1: File?, f2: File?): Int {\n            if (f1 == null || f2 == null) {\n                return if (f1 == null) {\n                    -1\n                } else {\n                    1\n                }\n            } else {\n                return if (f1.isDirectory && f2.isFile) {\n                    -1\n                } else if (f1.isFile && f2.isDirectory) {\n                    1\n                } else {\n                    val s1 = f1.name\n                    val s2 = f2.name\n                    if (caseSensitive) {\n                        s1.compareTo(s2, ignoreCase = false)\n                    } else {\n                        s1.compareTo(s2, ignoreCase = true)\n                    }\n                }\n            }\n        }\n\n    }\n\n    class SortBySize : Comparator<File> {\n\n        override fun compare(f1: File?, f2: File?): Int {\n            return if (f1 == null || f2 == null) {\n                if (f1 == null) {\n                    -1\n                } else {\n                    1\n                }\n            } else {\n                if (f1.isDirectory && f2.isFile) {\n                    -1\n                } else if (f1.isFile && f2.isDirectory) {\n                    1\n                } else {\n                    if (f1.length() < f2.length()) {\n                        -1\n                    } else {\n                        1\n                    }\n                }\n            }\n        }\n\n    }\n\n    class SortByTime : Comparator<File> {\n\n        override fun compare(f1: File?, f2: File?): Int {\n            return if (f1 == null || f2 == null) {\n                if (f1 == null) {\n                    -1\n                } else {\n                    1\n                }\n            } else {\n                if (f1.isDirectory && f2.isFile) {\n                    -1\n                } else if (f1.isFile && f2.isDirectory) {\n                    1\n                } else {\n                    if (f1.lastModified() > f2.lastModified()) {\n                        -1\n                    } else {\n                        1\n                    }\n                }\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/utils/GsonExtensions.kt",
    "content": "package io.legado.app.utils\n\nimport com.google.gson.*\nimport com.google.gson.internal.LinkedTreeMap\nimport com.google.gson.reflect.TypeToken\nimport com.google.gson.stream.JsonWriter\nimport java.io.InputStream\nimport java.io.InputStreamReader\nimport java.io.OutputStream\nimport java.io.OutputStreamWriter\nimport java.lang.reflect.ParameterizedType\nimport java.lang.reflect.Type\nimport kotlin.math.ceil\n\n\nval GSON: Gson by lazy {\n    GsonBuilder()\n        .registerTypeAdapter(\n            object : TypeToken<Map<String?, Any?>?>() {}.type,\n            MapDeserializerDoubleAsIntFix()\n        )\n        .registerTypeAdapter(Int::class.java, IntJsonDeserializer())\n        .disableHtmlEscaping()\n        .setPrettyPrinting()\n        .create()\n}\n\ninline fun <reified T> genericType(): Type = object : TypeToken<T>() {}.type\n\ninline fun <reified T> Gson.fromJsonObject(json: String?): Result<T?> {\n    return kotlin.runCatching {\n        fromJson(json, genericType<T>()) as? T\n    }\n}\n\ninline fun <reified T> Gson.fromJsonArray(json: String?): Result<List<T>?> {\n    return kotlin.runCatching {\n        fromJson(json, ParameterizedTypeImpl(T::class.java)) as? List<T>\n    }\n}\n\ninline fun <reified T> Gson.fromJsonObject(inputStream: InputStream?): Result<T?> {\n    return kotlin.runCatching {\n        val reader = InputStreamReader(inputStream)\n        fromJson(reader, genericType<T>()) as? T\n    }\n}\n\ninline fun <reified T> Gson.fromJsonArray(inputStream: InputStream?): Result<List<T>?> {\n    return kotlin.runCatching {\n        val reader = InputStreamReader(inputStream)\n        fromJson(reader, ParameterizedTypeImpl(T::class.java)) as? List<T>\n    }\n}\n\nfun Gson.writeToOutputStream(out: OutputStream, any: Any) {\n    val writer = JsonWriter(OutputStreamWriter(out, \"UTF-8\"))\n    writer.setIndent(\"  \")\n    if (any is List<*>) {\n        writer.beginArray()\n        any.forEach {\n            it?.let {\n                toJson(it, it::class.java, writer)\n            }\n        }\n        writer.endArray()\n    } else {\n        toJson(any, any::class.java, writer)\n    }\n    writer.close()\n}\n\nclass ParameterizedTypeImpl(private val clazz: Class<*>) : ParameterizedType {\n    override fun getRawType(): Type = List::class.java\n\n    override fun getOwnerType(): Type? = null\n\n    override fun getActualTypeArguments(): Array<Type> = arrayOf(clazz)\n}\n\n/**\n * int类型转化失败时跳过\n */\nclass IntJsonDeserializer : JsonDeserializer<Int?> {\n\n    override fun deserialize(\n        json: JsonElement,\n        typeOfT: Type?,\n        context: JsonDeserializationContext?\n    ): Int? {\n        return when {\n            json.isJsonPrimitive -> {\n                val prim = json.asJsonPrimitive\n                if (prim.isNumber) {\n                    prim.asNumber.toInt()\n                } else {\n                    null\n                }\n            }\n            else -> null\n        }\n    }\n\n}\n\n\n/**\n * 修复Int变为Double的问题\n */\nclass MapDeserializerDoubleAsIntFix :\n    JsonDeserializer<Map<String, Any?>?> {\n\n    @Throws(JsonParseException::class)\n    override fun deserialize(\n        jsonElement: JsonElement,\n        type: Type,\n        jsonDeserializationContext: JsonDeserializationContext\n    ): Map<String, Any?>? {\n        @Suppress(\"unchecked_cast\")\n        return read(jsonElement) as? Map<String, Any?>\n    }\n\n    fun read(json: JsonElement): Any? {\n        when {\n            json.isJsonArray -> {\n                val list: MutableList<Any?> = ArrayList()\n                val arr = json.asJsonArray\n                for (anArr in arr) {\n                    list.add(read(anArr))\n                }\n                return list\n            }\n            json.isJsonObject -> {\n                val map: MutableMap<String, Any?> =\n                    LinkedTreeMap()\n                val obj = json.asJsonObject\n                val entitySet =\n                    obj.entrySet()\n                for ((key, value) in entitySet) {\n                    map[key] = read(value)\n                }\n                return map\n            }\n            json.isJsonPrimitive -> {\n                val prim = json.asJsonPrimitive\n                when {\n                    prim.isBoolean -> {\n                        return prim.asBoolean\n                    }\n                    prim.isString -> {\n                        return prim.asString\n                    }\n                    prim.isNumber -> {\n                        val num: Number = prim.asNumber\n                        // here you can handle double int/long values\n                        // and return any type you want\n                        // this solution will transform 3.0 float to long values\n                        return if (ceil(num.toDouble()) == num.toLong().toDouble()) {\n                            num.toLong()\n                        } else {\n                            num.toDouble()\n                        }\n                    }\n                }\n            }\n        }\n        return null\n    }\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/utils/HtmlFormatter.kt",
    "content": "package io.legado.app.utils\n\nimport io.legado.app.model.analyzeRule.AnalyzeUrl\nimport java.net.URL\nimport java.util.regex.Pattern\n\nobject HtmlFormatter {\n    private val wrapHtmlRegex = \"</?(?:div|p|br|hr|h\\\\d|article|dd|dl)[^>]*>\".toRegex()\n    private val commentRegex = \"<!--[^>]*-->\".toRegex() //注释\n    private val notImgHtmlRegex = \"</?(?!img)[a-zA-Z]+(?=[ >])[^<>]*>\".toRegex()\n    private val otherHtmlRegex = \"</?[a-zA-Z]+(?=[ >])[^<>]*>\".toRegex()\n    private val formatImagePattern = Pattern.compile(\n        \"<img[^>]*src *= *\\\"([^\\\"{]*\\\\{(?:[^{}]|\\\\{[^}]+\\\\})+\\\\})\\\"[^>]*>|<img[^>]*data-[^=]*= *\\\"([^\\\"]*)\\\"[^>]*>|<img[^>]*src *= *\\\"([^\\\"]*)\\\"[^>]*>\",\n        Pattern.CASE_INSENSITIVE\n    )\n\n    fun format(html: String?, otherRegex: Regex = otherHtmlRegex): String {\n        html ?: return \"\"\n        return html.replace(wrapHtmlRegex, \"\\n\")\n            .replace(commentRegex, \"\")\n            .replace(otherRegex, \"\")\n            .replace(\"\\\\s*\\\\n+\\\\s*\".toRegex(), \"\\n　　\")\n            .replace(\"^[\\\\n\\\\s]+\".toRegex(), \"　　\")\n            .replace(\"[\\\\n\\\\s]+$\".toRegex(), \"\")\n    }\n\n    fun formatKeepImg(html: String?, redirectUrl: URL? = null): String {\n        html ?: return \"\"\n        val keepImgHtml = format(html, notImgHtmlRegex)\n\n        //正则的“|”处于顶端而不处于（）中时，具有类似||的熔断效果，故以此机制简化原来的代码\n        val matcher = formatImagePattern.matcher(keepImgHtml)\n        var appendPos = 0\n        val sb = StringBuffer()\n        while (matcher.find()) {\n            var param = \"\"\n            sb.append(\n                keepImgHtml.substring(appendPos, matcher.start()), \"<img src=\\\"${\n                    NetworkUtils.getAbsoluteURL(\n                        redirectUrl,\n                        matcher.group(1)?.let {\n                            val urlMatcher = AnalyzeUrl.paramPattern.matcher(it)\n                            if (urlMatcher.find()) {\n                                param = ',' + it.substring(urlMatcher.end())\n                                it.substring(0, urlMatcher.start())\n                            } else it\n                        } ?: matcher.group(2) ?: matcher.group(3)!!\n                    ) + param\n                }\\\">\"\n            )\n            appendPos = matcher.end()\n        }\n        if (appendPos < keepImgHtml.length) sb.append(\n            keepImgHtml.substring(\n                appendPos,\n                keepImgHtml.length\n            )\n        )\n        return sb.toString()\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/utils/JsonExtensions.kt",
    "content": "package io.legado.app.utils\n\nimport com.jayway.jsonpath.*\n\nval jsonPath: ParseContext by lazy {\n    JsonPath.using(\n        Configuration.builder()\n            .options(Option.SUPPRESS_EXCEPTIONS)\n            .build()\n    )\n}\n\nfun ReadContext.readString(path: String): String? = this.read(path, String::class.java)\n\nfun ReadContext.readBool(path: String): Boolean? = this.read(path, Boolean::class.java)\n\nfun ReadContext.readInt(path: String): Int? = this.read(path, Int::class.java)\n\nfun ReadContext.readLong(path: String): Long? = this.read(path, Long::class.java)\n\n"
  },
  {
    "path": "src/main/java/io/legado/app/utils/JsoupExtensions.kt",
    "content": "package io.legado.app.utils\n\nimport org.jsoup.internal.StringUtil\nimport org.jsoup.nodes.CDataNode\nimport org.jsoup.nodes.Element\nimport org.jsoup.nodes.Node\nimport org.jsoup.nodes.TextNode\nimport org.jsoup.select.NodeTraversor\nimport org.jsoup.select.NodeVisitor\n\n\nfun Element.textArray(): Array<String> {\n    val sb = StringUtil.borrowBuilder()\n    NodeTraversor.traverse(object : NodeVisitor {\n        override fun head(node: Node, depth: Int) {\n            if (node is TextNode) {\n                appendNormalisedText(sb, node)\n            } else if (node is Element) {\n                if (sb.isNotEmpty() &&\n                    (node.isBlock || node.tag().name == \"br\") &&\n                    !lastCharIsWhitespace(sb)\n                ) sb.append(\"\\n\")\n            }\n        }\n\n        override fun tail(node: Node, depth: Int) {\n            if (node is Element) {\n                if (node.isBlock && node.nextSibling() is TextNode\n                    && !lastCharIsWhitespace(sb)\n                ) {\n                    sb.append(\"\\n\")\n                }\n            }\n        }\n    }, this)\n    val text = StringUtil.releaseBuilder(sb).trim { it <= ' ' }\n    return text.splitNotBlank(\"\\n\")\n}\n\nprivate fun appendNormalisedText(sb: StringBuilder, textNode: TextNode) {\n    val text = textNode.wholeText\n    if (preserveWhitespace(textNode.parentNode()) || textNode is CDataNode)\n        sb.append(text)\n    else StringUtil.appendNormalisedWhitespace(sb, text, lastCharIsWhitespace(sb))\n}\n\nprivate fun preserveWhitespace(node: Node?): Boolean {\n    if (node is Element) {\n        var el = node as Element?\n        var i = 0\n        do {\n            if (el!!.tag().preserveWhitespace()) return true\n            el = el.parent()\n            i++\n        } while (i < 6 && el != null)\n    }\n    return false\n}\n\nprivate fun lastCharIsWhitespace(sb: java.lang.StringBuilder): Boolean {\n    return sb.isNotEmpty() && sb[sb.length - 1] == ' '\n}\n\n"
  },
  {
    "path": "src/main/java/io/legado/app/utils/LogUtils.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage io.legado.app.utils\n\nfun Throwable.printOnDebug() {\n    printStackTrace()\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/utils/MD5Utils.kt",
    "content": "package io.legado.app.utils\n\nimport java.security.MessageDigest\nimport java.security.NoSuchAlgorithmException\n\n/**\n * 将字符串转化为MD5\n */\n\nobject MD5Utils {\n\n    fun md5Encode(str: String?): String {\n        if (str == null) return \"\"\n        var reStr = \"\"\n        try {\n            val md5:MessageDigest = MessageDigest.getInstance(\"MD5\")\n            val bytes:ByteArray = md5.digest(str.toByteArray())\n            val stringBuffer:StringBuilder = StringBuilder()\n            for (b in bytes) {\n                val bt:Int = b.toInt() and 0xff\n                if (bt < 16) {\n                    stringBuffer.append(0)\n                }\n                stringBuffer.append(Integer.toHexString(bt))\n            }\n            reStr = stringBuffer.toString()\n        } catch (e: NoSuchAlgorithmException) {\n            e.printStackTrace()\n        }\n\n        return reStr\n    }\n\n    fun md5Encode16(str: String): String {\n        var reStr = md5Encode(str)\n        reStr = reStr.substring(8, 24)\n        return reStr\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/utils/NetworkUtils.kt",
    "content": "package io.legado.app.utils\n\nimport retrofit2.Response\nimport java.net.InetAddress\nimport java.net.NetworkInterface\nimport java.net.SocketException\nimport java.net.URL\nimport java.util.*\nimport java.util.regex.Pattern\n\nobject NetworkUtils {\n    fun getUrl(response: Response<*>): String {\n        val networkResponse = response.raw().networkResponse\n        return networkResponse?.request?.url?.toString()\n            ?: response.raw().request.url.toString()\n    }\n\n    private val notNeedEncoding: BitSet by lazy {\n        val bitSet = BitSet(256)\n        for (i in 'a'.code..'z'.code) {\n            bitSet.set(i)\n        }\n        for (i in 'A'.code..'Z'.code) {\n            bitSet.set(i)\n        }\n        for (i in '0'.code..'9'.code) {\n            bitSet.set(i)\n        }\n        for (char in \"+-_.$:()!*@&#,[]\") {\n            bitSet.set(char.code)\n        }\n        return@lazy bitSet\n    }\n\n    /**\n     * 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+'\n     * 0-9a-zA-Z保留 <br></br>\n     * ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留\n     * 其他字符转成%XX的格式，X是16进制的大写字符，范围是[0-9A-F]\n     */\n    fun hasUrlEncoded(str: String): Boolean {\n        var needEncode = false\n        var i = 0\n        while (i < str.length) {\n            val c = str[i]\n            if (notNeedEncoding.get(c.code)) {\n                i++\n                continue\n            }\n            if (c == '%' && i + 2 < str.length) {\n                // 判断是否符合urlEncode规范\n                val c1 = str[++i]\n                val c2 = str[++i]\n                if (isDigit16Char(c1) && isDigit16Char(c2)) {\n                    i++\n                    continue\n                }\n            }\n            // 其他字符，肯定需要urlEncode\n            needEncode = true\n            break\n        }\n\n        return !needEncode\n    }\n\n    /**\n     * 判断c是否是16进制的字符\n     */\n    private fun isDigit16Char(c: Char): Boolean {\n        return c in '0'..'9' || c in 'A'..'F' || c in 'a'..'f'\n    }\n\n    /**\n     * 获取绝对地址\n     */\n    fun getAbsoluteURL(baseURL: String?, relativePath: String): String {\n        if (baseURL.isNullOrEmpty()) return relativePath\n        if (relativePath.isNullOrEmpty()) return baseURL\n        var relativeUrl = relativePath\n        try {\n            val absoluteUrl = URL(baseURL.substringBefore(\",\"))\n            val parseUrl = URL(absoluteUrl, relativePath)\n            relativeUrl = parseUrl.toString()\n            return relativeUrl\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n        return relativeUrl\n    }\n\n\n    /**\n     * 获取绝对地址\n     */\n    fun getAbsoluteURL(baseURL: URL?, relativePath: String): String {\n        if (baseURL == null) return relativePath\n        var relativeUrl = relativePath\n        try {\n            val parseUrl = URL(baseURL, relativePath)\n            relativeUrl = parseUrl.toString()\n            return relativeUrl\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n        return relativeUrl\n    }\n\n    fun getBaseUrl(url: String?): String? {\n        if (url == null || !url.startsWith(\"http\")) return null\n        val index = url.indexOf(\"/\", 9)\n        return if (index == -1) {\n            url\n        } else url.substring(0, index)\n    }\n\n    fun getSubDomain(url: String?): String {\n        val baseUrl = getBaseUrl(url) ?: return \"\"\n        return if (baseUrl.indexOf(\".\") == baseUrl.lastIndexOf(\".\")) {\n            baseUrl.substring(baseUrl.lastIndexOf(\"/\") + 1)\n        } else baseUrl.substring(baseUrl.indexOf(\".\") + 1)\n    }\n\n    /**\n     * Get local Ip address.\n     */\n    fun getLocalIPAddress(): InetAddress? {\n        var enumeration: Enumeration<NetworkInterface>? = null\n        try {\n            enumeration = NetworkInterface.getNetworkInterfaces()\n        } catch (e: SocketException) {\n            e.printStackTrace()\n        }\n\n        if (enumeration != null) {\n            while (enumeration.hasMoreElements()) {\n                val nif = enumeration.nextElement()\n                val addresses = nif.inetAddresses\n                if (addresses != null) {\n                    while (addresses.hasMoreElements()) {\n                        val address = addresses.nextElement()\n                        if (!address.isLoopbackAddress && isIPv4Address(address.hostAddress)) {\n                            return address\n                        }\n                    }\n                }\n            }\n        }\n        return null\n    }\n\n    /**\n     * Check if valid IPV4 address.\n     *\n     * @param input the address string to check for validity.\n     * @return True if the input parameter is a valid IPv4 address.\n     */\n    fun isIPv4Address(input: String): Boolean {\n        return IPV4_PATTERN.matcher(input).matches()\n    }\n\n    /**\n     * Ipv4 address check.\n     */\n    private val IPV4_PATTERN = Pattern.compile(\n        \"^(\" + \"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\\\.){3}\" +\n                \"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$\"\n    )\n\n}"
  },
  {
    "path": "src/main/java/io/legado/app/utils/SourceAnalyzer.kt",
    "content": "package io.legado.app.help\n\nimport com.jayway.jsonpath.JsonPath\nimport io.legado.app.constant.AppConst\nimport io.legado.app.constant.BookType\nimport io.legado.app.data.entities.BookSource\nimport io.legado.app.data.entities.rule.*\nimport io.legado.app.exception.NoStackTraceException\nimport io.legado.app.utils.*\nimport io.legado.app.model.Debug\nimport java.io.InputStream\n\nimport java.util.regex.Pattern\n\n@Suppress(\"RegExpRedundantEscape\")\nobject SourceAnalyzer {\n    private val headerPattern = Pattern.compile(\"@Header:\\\\{.+?\\\\}\", Pattern.CASE_INSENSITIVE)\n    private val jsPattern = Pattern.compile(\"\\\\{\\\\{.+?\\\\}\\\\}\", Pattern.CASE_INSENSITIVE)\n\n    fun jsonToBookSources(json: String): Result<MutableList<BookSource>> {\n        return kotlin.runCatching {\n            val bookSources = mutableListOf<BookSource>()\n            when {\n                json.isJsonArray() -> {\n                    val items: List<Map<String, Any>> = jsonPath.parse(json).read(\"$\")\n                    for (item in items) {\n                        val jsonItem = jsonPath.parse(item)\n                        jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {\n                            bookSources.add(it)\n                        }\n                    }\n                }\n                json.isJsonObject() -> {\n                    jsonToBookSource(json).getOrThrow().let {\n                        bookSources.add(it)\n                    }\n                }\n                else -> {\n                    throw NoStackTraceException(\"格式不对\")\n                }\n            }\n            bookSources\n        }\n    }\n\n    fun jsonToBookSources(inputStream: InputStream): Result<MutableList<BookSource>> {\n        return kotlin.runCatching {\n            val bookSources = mutableListOf<BookSource>()\n            kotlin.runCatching {\n                val items: List<Map<String, Any>> = jsonPath.parse(inputStream).read(\"$\")\n                for (item in items) {\n                    val jsonItem = jsonPath.parse(item)\n                    jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {\n                        bookSources.add(it)\n                    }\n                }\n            }.onFailure {\n                val item: Map<String, Any> = jsonPath.parse(inputStream).read(\"$\")\n                val jsonItem = jsonPath.parse(item)\n                jsonToBookSource(jsonItem.jsonString()).getOrThrow().let {\n                    bookSources.add(it)\n                }\n            }\n            bookSources\n        }\n    }\n\n    fun jsonToBookSource(json: String): Result<BookSource> {\n        val source = BookSource()\n        val sourceAny = GSON.fromJsonObject<BookSourceAny>(json.trim())\n            .onFailure {\n                Debug.log(\"转化书源出错\", it.localizedMessage)\n            }.getOrNull()\n        return kotlin.runCatching {\n            if (sourceAny?.ruleToc == null) {\n                source.apply {\n                    val jsonItem = jsonPath.parse(json.trim())\n                    bookSourceUrl = jsonItem.readString(\"bookSourceUrl\")\n                        ?: throw NoStackTraceException(\"格式不对\")\n                    bookSourceName = jsonItem.readString(\"bookSourceName\") ?: \"\"\n                    bookSourceGroup = jsonItem.readString(\"bookSourceGroup\")\n                    // loginUrl = jsonItem.readString(\"loginUrl\")\n                    // loginUi = jsonItem.readString(\"loginUi\")\n                    // loginCheckJs = jsonItem.readString(\"loginCheckJs\")\n                    bookSourceComment = jsonItem.readString(\"bookSourceComment\") ?: \"\"\n                    bookUrlPattern = jsonItem.readString(\"ruleBookUrlPattern\")\n                    customOrder = jsonItem.readInt(\"serialNumber\") ?: 0\n                    header = uaToHeader(jsonItem.readString(\"httpUserAgent\"))\n                    searchUrl = toNewUrl(jsonItem.readString(\"ruleSearchUrl\"))\n                    exploreUrl = toNewUrls(jsonItem.readString(\"ruleFindUrl\"))\n                    val sourceType = jsonItem.readString(\"bookSourceType\")\n                    bookSourceType = when(sourceType) {\n                        \"AUDIO\" -> BookType.audio\n                        \"audio\" -> BookType.audio\n                        \"1\" -> BookType.audio\n                        \"IMAGE\" -> BookType.image\n                        \"image\" -> BookType.image\n                        \"2\" -> BookType.image\n                        \"FILE\" -> BookType.file\n                        \"file\" -> BookType.file\n                        \"3\" -> BookType.file\n                        else -> BookType.default\n                    }\n                    enabled = jsonItem.readBool(\"enable\") ?: true\n                    if (exploreUrl.isNullOrBlank()) {\n                        enabledExplore = false\n                    }\n                    ruleSearch = SearchRule(\n                        bookList = toNewRule(jsonItem.readString(\"ruleSearchList\")),\n                        name = toNewRule(jsonItem.readString(\"ruleSearchName\")),\n                        author = toNewRule(jsonItem.readString(\"ruleSearchAuthor\")),\n                        intro = toNewRule(jsonItem.readString(\"ruleSearchIntroduce\")),\n                        kind = toNewRule(jsonItem.readString(\"ruleSearchKind\")),\n                        bookUrl = toNewRule(jsonItem.readString(\"ruleSearchNoteUrl\")),\n                        coverUrl = toNewRule(jsonItem.readString(\"ruleSearchCoverUrl\")),\n                        lastChapter = toNewRule(jsonItem.readString(\"ruleSearchLastChapter\"))\n                    )\n                    ruleExplore = ExploreRule(\n                        bookList = toNewRule(jsonItem.readString(\"ruleFindList\")),\n                        name = toNewRule(jsonItem.readString(\"ruleFindName\")),\n                        author = toNewRule(jsonItem.readString(\"ruleFindAuthor\")),\n                        intro = toNewRule(jsonItem.readString(\"ruleFindIntroduce\")),\n                        kind = toNewRule(jsonItem.readString(\"ruleFindKind\")),\n                        bookUrl = toNewRule(jsonItem.readString(\"ruleFindNoteUrl\")),\n                        coverUrl = toNewRule(jsonItem.readString(\"ruleFindCoverUrl\")),\n                        lastChapter = toNewRule(jsonItem.readString(\"ruleFindLastChapter\"))\n                    )\n                    ruleBookInfo = BookInfoRule(\n                        init = toNewRule(jsonItem.readString(\"ruleBookInfoInit\")),\n                        name = toNewRule(jsonItem.readString(\"ruleBookName\")),\n                        author = toNewRule(jsonItem.readString(\"ruleBookAuthor\")),\n                        intro = toNewRule(jsonItem.readString(\"ruleIntroduce\")),\n                        kind = toNewRule(jsonItem.readString(\"ruleBookKind\")),\n                        coverUrl = toNewRule(jsonItem.readString(\"ruleCoverUrl\")),\n                        lastChapter = toNewRule(jsonItem.readString(\"ruleBookLastChapter\")),\n                        tocUrl = toNewRule(jsonItem.readString(\"ruleChapterUrl\"))\n                    )\n                    ruleToc = TocRule(\n                        chapterList = toNewRule(jsonItem.readString(\"ruleChapterList\")),\n                        chapterName = toNewRule(jsonItem.readString(\"ruleChapterName\")),\n                        chapterUrl = toNewRule(jsonItem.readString(\"ruleContentUrl\")),\n                        nextTocUrl = toNewRule(jsonItem.readString(\"ruleChapterUrlNext\"))\n                    )\n                    var content = toNewRule(jsonItem.readString(\"ruleBookContent\")) ?: \"\"\n                    if (content.startsWith(\"$\") && !content.startsWith(\"$.\")) {\n                        content = content.substring(1)\n                    }\n                    ruleContent = ContentRule(\n                        content = content,\n                        replaceRegex = toNewRule(jsonItem.readString(\"ruleBookContentReplace\")),\n                        nextContentUrl = toNewRule(jsonItem.readString(\"ruleContentUrlNext\"))\n                    )\n                }\n            } else {\n                source.bookSourceUrl = sourceAny.bookSourceUrl\n                source.bookSourceName = sourceAny.bookSourceName\n                source.bookSourceGroup = sourceAny.bookSourceGroup\n                source.bookSourceType = sourceAny.bookSourceType\n                source.bookUrlPattern = sourceAny.bookUrlPattern\n                source.customOrder = sourceAny.customOrder\n                source.enabled = sourceAny.enabled\n                source.enabledExplore = sourceAny.enabledExplore\n                source.concurrentRate = sourceAny.concurrentRate\n                source.header = sourceAny.header\n                source.loginUrl = when (sourceAny.loginUrl) {\n                    null -> null\n                    is String -> sourceAny.loginUrl.toString()\n                    else -> JsonPath.parse(sourceAny.loginUrl).readString(\"url\")\n                }\n                // source.loginUi = if (sourceAny.loginUi is List<*>) {\n                //     GSON.toJson(sourceAny.loginUi)\n                // } else {\n                //     sourceAny.loginUi?.toString()\n                // }\n                source.loginCheckJs = sourceAny.loginCheckJs\n                source.bookSourceComment = sourceAny.bookSourceComment\n                source.lastUpdateTime = sourceAny.lastUpdateTime\n                source.respondTime = sourceAny.respondTime\n                source.weight = sourceAny.weight\n                source.exploreUrl = sourceAny.exploreUrl\n                source.ruleExplore = if (sourceAny.ruleExplore is String) {\n                    GSON.fromJsonObject<ExploreRule>(sourceAny.ruleExplore.toString())\n                        .getOrNull()\n                } else {\n                    GSON.fromJsonObject<ExploreRule>(GSON.toJson(sourceAny.ruleExplore))\n                        .getOrNull()\n                }\n                source.searchUrl = sourceAny.searchUrl\n                source.ruleSearch = if (sourceAny.ruleSearch is String) {\n                    GSON.fromJsonObject<SearchRule>(sourceAny.ruleSearch.toString())\n                        .getOrNull()\n                } else {\n                    GSON.fromJsonObject<SearchRule>(GSON.toJson(sourceAny.ruleSearch))\n                        .getOrNull()\n                }\n                source.ruleBookInfo = if (sourceAny.ruleBookInfo is String) {\n                    GSON.fromJsonObject<BookInfoRule>(sourceAny.ruleBookInfo.toString())\n                        .getOrNull()\n                } else {\n                    GSON.fromJsonObject<BookInfoRule>(GSON.toJson(sourceAny.ruleBookInfo))\n                        .getOrNull()\n                }\n                source.ruleToc = if (sourceAny.ruleToc is String) {\n                    GSON.fromJsonObject<TocRule>(sourceAny.ruleToc.toString())\n                        .getOrNull()\n                } else {\n                    GSON.fromJsonObject<TocRule>(GSON.toJson(sourceAny.ruleToc))\n                        .getOrNull()\n                }\n                source.ruleContent = if (sourceAny.ruleContent is String) {\n                    GSON.fromJsonObject<ContentRule>(sourceAny.ruleContent.toString())\n                        .getOrNull()\n                } else {\n                    GSON.fromJsonObject<ContentRule>(GSON.toJson(sourceAny.ruleContent))\n                        .getOrNull()\n                }\n            }\n            source\n        }\n    }\n\n    data class BookSourceAny(\n        var bookSourceName: String = \"\",                // 名称\n        var bookSourceGroup: String? = null,            // 分组\n        var bookSourceUrl: String = \"\",                 // 地址，包括 http/https\n        var bookSourceType: Int = BookType.default,     // 类型，0 文本，1 音频\n        var bookUrlPattern: String? = null,             // 详情页url正则\n        var customOrder: Int = 0,                       // 手动排序编号\n        var enabled: Boolean = true,                    // 是否启用\n        var enabledExplore: Boolean = true,             // 启用发现\n        var concurrentRate: String? = null,             // 并发率\n        var header: String? = null,                     // 请求头\n        var loginUrl: Any? = null,                      // 登录规则\n        var loginUi: Any? = null,                       // 登录UI\n        var loginCheckJs: String? = null,               //登录检测js\n        var bookSourceComment: String? = \"\",            //书源注释\n        var lastUpdateTime: Long = 0,                   // 最后更新时间，用于排序\n        var respondTime: Long = 180000L,                // 响应时间，用于排序\n        var weight: Int = 0,                            // 智能排序的权重\n        var exploreUrl: String? = null,                 // 发现url\n        var ruleExplore: Any? = null,                   // 发现规则\n        var searchUrl: String? = null,                  // 搜索url\n        var ruleSearch: Any? = null,                    // 搜索规则\n        var ruleBookInfo: Any? = null,                  // 书籍信息页规则\n        var ruleToc: Any? = null,                       // 目录页规则\n        var ruleContent: Any? = null                    // 正文页规则\n    )\n\n    // default规则适配\n    // #正则#替换内容 替换成 ##正则##替换内容\n    // | 替换成 ||\n    // & 替换成 &&\n    private fun toNewRule(oldRule: String?): String? {\n        if (oldRule.isNullOrBlank()) return null\n        var newRule = oldRule\n        var reverse = false\n        var allinone = false\n        if (oldRule.startsWith(\"-\")) {\n            reverse = true\n            newRule = oldRule.substring(1)\n        }\n        if (newRule.startsWith(\"+\")) {\n            allinone = true\n            newRule = newRule.substring(1)\n        }\n        if (!newRule.startsWith(\"@CSS:\", true) &&\n            !newRule.startsWith(\"@XPath:\", true) &&\n            !newRule.startsWith(\"//\") &&\n            !newRule.startsWith(\"##\") &&\n            !newRule.startsWith(\":\") &&\n            !newRule.contains(\"@js:\", true) &&\n            !newRule.contains(\"<js>\", true)\n        ) {\n            if (newRule.contains(\"#\") && !newRule.contains(\"##\")) {\n                newRule = oldRule.replace(\"#\", \"##\")\n            }\n            if (newRule.contains(\"|\") && !newRule.contains(\"||\")) {\n                if (newRule.contains(\"##\")) {\n                    val list = newRule.split(\"##\")\n                    if (list[0].contains(\"|\")) {\n                        newRule = list[0].replace(\"|\", \"||\")\n                        for (i in 1 until list.size) {\n                            newRule += \"##\" + list[i]\n                        }\n                    }\n                } else {\n                    newRule = newRule.replace(\"|\", \"||\")\n                }\n            }\n            if (newRule.contains(\"&\")\n                && !newRule.contains(\"&&\")\n                && !newRule.contains(\"http\")\n                && !newRule.startsWith(\"/\")\n            ) {\n                newRule = newRule.replace(\"&\", \"&&\")\n            }\n        }\n        if (allinone) {\n            newRule = \"+$newRule\"\n        }\n        if (reverse) {\n            newRule = \"-$newRule\"\n        }\n        return newRule\n    }\n\n    private fun toNewUrls(oldUrls: String?): String? {\n        if (oldUrls.isNullOrBlank()) return null\n        if (oldUrls.startsWith(\"@js:\") || oldUrls.startsWith(\"<js>\")) {\n            return oldUrls\n        }\n        if (!oldUrls.contains(\"\\n\") && !oldUrls.contains(\"&&\")) {\n            return toNewUrl(oldUrls)\n        }\n        val urls = oldUrls.split(\"(&&|\\r?\\n)+\".toRegex())\n        return urls.map {\n            toNewUrl(it)?.replace(\"\\n\\\\s*\".toRegex(), \"\")\n        }.joinToString(\"\\n\")\n    }\n\n    private fun toNewUrl(oldUrl: String?): String? {\n        if (oldUrl.isNullOrBlank()) return null\n        var url: String = oldUrl\n        if (oldUrl.startsWith(\"<js>\", true)) {\n            url = url.replace(\"=searchKey\", \"={{key}}\")\n                .replace(\"=searchPage\", \"={{page}}\")\n            return url\n        }\n        val map = HashMap<String, String>()\n        var mather = headerPattern.matcher(url)\n        if (mather.find()) {\n            val header = mather.group()\n            url = url.replace(header, \"\")\n            map[\"headers\"] = header.substring(8)\n        }\n        var urlList = url.split(\"|\")\n        url = urlList[0]\n        if (urlList.size > 1) {\n            map[\"charset\"] = urlList[1].split(\"=\")[1]\n        }\n        mather = jsPattern.matcher(url)\n        val jsList = arrayListOf<String>()\n        while (mather.find()) {\n            jsList.add(mather.group())\n            url = url.replace(jsList.last(), \"$${jsList.size - 1}\")\n        }\n        url = url.replace(\"{\", \"<\").replace(\"}\", \">\")\n        url = url.replace(\"searchKey\", \"{{key}}\")\n        url = url.replace(\"<searchPage([-+]1)>\".toRegex(), \"{{page$1}}\")\n            .replace(\"searchPage([-+]1)\".toRegex(), \"{{page$1}}\")\n            .replace(\"searchPage\", \"{{page}}\")\n        for ((index, item) in jsList.withIndex()) {\n            url = url.replace(\n                \"$$index\",\n                item.replace(\"searchKey\", \"key\").replace(\"searchPage\", \"page\")\n            )\n        }\n        urlList = url.split(\"@\")\n        url = urlList[0]\n        if (urlList.size > 1) {\n            map[\"method\"] = \"POST\"\n            map[\"body\"] = urlList[1]\n        }\n        if (map.size > 0) {\n            url += \",\" + GSON.toJson(map)\n        }\n        return url\n    }\n\n    private fun uaToHeader(ua: String?): String? {\n        if (ua.isNullOrEmpty()) return null\n        val map = mapOf(Pair(AppConst.UA_NAME, ua))\n        return GSON.toJson(map)\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/utils/StringExtensions.kt",
    "content": "package io.legado.app.utils\n\n// import org.apache.commons.text.StringEscapeUtils\nimport io.legado.app.constant.AppPattern.dataUriRegex\n\nfun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()\n\nfun String?.isAbsUrl() = if (this.isNullOrBlank()) false else this.startsWith(\"http://\", true)\n        || this.startsWith(\"https://\", true)\n\nfun String?.isDataUrl() =\n    this?.let {\n        dataUriRegex.matches(it)\n    } ?: false\n\nfun String?.isJson(): Boolean = this?.run {\n    val str = this.trim()\n    when {\n        str.startsWith(\"{\") && str.endsWith(\"}\") -> true\n        str.startsWith(\"[\") && str.endsWith(\"]\") -> true\n        else -> false\n    }\n} ?: false\n\nfun String?.isJsonObject(): Boolean = this?.run {\n    val str = this.trim()\n    str.startsWith(\"{\") && str.endsWith(\"}\")\n} ?: false\n\nfun String?.isJsonArray(): Boolean = this?.run {\n    val str = this.trim()\n    str.startsWith(\"[\") && str.endsWith(\"]\")\n} ?: false\n\nfun String?.isXml(): Boolean =\n    this?.run {\n        val str = this.trim()\n        str.startsWith(\"<\") && str.endsWith(\">\")\n    } ?: false\n\nfun String?.isTrue(nullIsTrue: Boolean = false): Boolean {\n    if (this.isNullOrBlank() || this == \"null\") {\n        return nullIsTrue\n    }\n    return !this.matches(\"\\\\s*(?i)(false|no|not|0)\\\\s*\".toRegex())\n}\n\nfun String?.htmlFormat(): String = if (this.isNullOrBlank()) \"\" else\n    this.replace(\"(?i)<(br[\\\\s/]*|/*p\\\\b.*?|/*div\\\\b.*?)>\".toRegex(), \"\\n\")// 替换特定标签为换行符\n        .replace(\"<[script>]*.*?>|&nbsp;\".toRegex(), \"\")// 删除script标签对和空格转义符\n        .replace(\"\\\\s*\\\\n+\\\\s*\".toRegex(), \"\\n　　\")// 移除空行,并增加段前缩进2个汉字\n        .replace(\"^[\\\\n\\\\s]+\".toRegex(), \"　　\")//移除开头空行,并增加段前缩进2个汉字\n        .replace(\"[\\\\n\\\\s]+$\".toRegex(), \"\") //移除尾部空行\n\nfun String.splitNotBlank(vararg delimiter: String): Array<String> = run {\n    this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray()\n}\n\nfun String.splitNotBlank(regex: Regex, limit: Int = 0): Array<String> = run {\n    this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray()\n}\n\nfun String.startWithIgnoreCase(start: String): Boolean {\n    return if (this.isBlank()) false else startsWith(start, true)\n}\n\nfun String.cnCompare(other: String): Int {\n    // return java.text.Collator.getInstance(Locale.CHINA).compare(this, other)\n    return this.compareTo(other)\n}\n\n/**\n * 将字符串拆分为单个字符,包含emoji\n */\nfun String.toStringArray(): Array<String> {\n    var codePointIndex = 0\n    return try {\n        Array(codePointCount(0, length)) {\n            val start = codePointIndex\n            codePointIndex = offsetByCodePoints(start, 1)\n            substring(start, codePointIndex)\n        }\n    } catch (e: Exception) {\n        split(\"\").toTypedArray()\n    }\n}\n\n"
  },
  {
    "path": "src/main/java/io/legado/app/utils/StringUtils.kt",
    "content": "package io.legado.app.utils\n\nimport io.legado.app.utils.TextUtils\nimport java.text.DecimalFormat\nimport java.text.ParseException\nimport java.text.SimpleDateFormat\nimport java.util.*\nimport java.util.regex.Matcher\nimport java.util.regex.Pattern\nimport kotlin.math.abs\nimport kotlin.math.log10\nimport kotlin.math.pow\n\nobject StringUtils {\n    private val TAG = \"StringUtils\"\n    private const val HOUR_OF_DAY = 24\n    private const val DAY_OF_YESTERDAY = 2\n    private const val TIME_UNIT = 60\n    private val ChnMap = chnMap\n\n    private val chnMap: HashMap<Char, Int>\n        get() {\n            val map = HashMap<Char, Int>()\n            var cnStr = \"零一二三四五六七八九十\"\n            var c = cnStr.toCharArray()\n            for (i in 0..10) {\n                map[c[i]] = i\n            }\n            cnStr = \"〇壹贰叁肆伍陆柒捌玖拾\"\n            c = cnStr.toCharArray()\n            for (i in 0..10) {\n                map[c[i]] = i\n            }\n            map['两'] = 2\n            map['百'] = 100\n            map['佰'] = 100\n            map['千'] = 1000\n            map['仟'] = 1000\n            map['万'] = 10000\n            map['亿'] = 100000000\n            return map\n        }\n\n    //将时间转换成日期\n    fun dateConvert(time: Long, pattern: String): String {\n        val date = Date(time)\n        val format = SimpleDateFormat(pattern)\n        return format.format(date)\n    }\n\n    //将日期转换成昨天、今天、明天\n    fun dateConvert(source: String, pattern: String): String {\n        val format = SimpleDateFormat(pattern)\n        val calendar = Calendar.getInstance()\n        try {\n            val date = format.parse(source)\n            val curTime = calendar.timeInMillis\n            calendar.time = date\n            //将MISC 转换成 sec\n            val difSec = Math.abs((curTime - date.time) / 1000)\n            val difMin = difSec / 60\n            val difHour = difMin / 60\n            val difDate = difHour / 60\n            val oldHour = calendar.get(Calendar.HOUR)\n            //如果没有时间\n            if (oldHour == 0) {\n                //比日期:昨天今天和明天\n                if (difDate == 0L) {\n                    return \"今天\"\n                } else if (difDate < DAY_OF_YESTERDAY) {\n                    return \"昨天\"\n                } else {\n                    val convertFormat = SimpleDateFormat(\"yyyy-MM-dd\")\n                    return convertFormat.format(date)\n                }\n            }\n\n            return when {\n                difSec < TIME_UNIT -> difSec.toString() + \"秒前\"\n                difMin < TIME_UNIT -> difMin.toString() + \"分钟前\"\n                difHour < HOUR_OF_DAY -> difHour.toString() + \"小时前\"\n                difDate < DAY_OF_YESTERDAY -> \"昨天\"\n                else -> {\n                    val convertFormat = SimpleDateFormat(\"yyyy-MM-dd\")\n                    convertFormat.format(date)\n                }\n            }\n        } catch (e: ParseException) {\n            e.printStackTrace()\n        }\n\n        return \"\"\n    }\n\n    /**\n     * 单位转换\n     */\n    fun toSize(length: Long): String {\n        if (length <= 0) return \"0\"\n        val units = arrayOf(\"b\", \"kb\", \"M\", \"G\", \"T\")\n        //计算单位的，原理是利用lg,公式是 lg(1024^n) = nlg(1024)，最后 nlg(1024)/lg(1024) = n。\n        val digitGroups =\n            (log10(length.toDouble()) / log10(1024.0)).toInt()\n        //计算原理是，size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2\n        return DecimalFormat(\"#,##0.##\")\n            .format(length / 1024.0.pow(digitGroups.toDouble())) + \" \" + units[digitGroups]\n    }\n\n    fun toFirstCapital(str: String): String {\n        return str.substring(0, 1).uppercase(Locale.getDefault()) + str.substring(1)\n    }\n\n    /**\n     * 将文本中的半角字符，转换成全角字符\n     */\n    fun halfToFull(input: String): String {\n        val c = input.toCharArray()\n        for (i in c.indices) {\n            if (c[i].code == 32)\n            //半角空格\n            {\n                c[i] = 12288.toChar()\n                continue\n            }\n            //根据实际情况，过滤不需要转换的符号\n            //if (c[i] == 46) //半角点号，不转换\n            // continue;\n\n            if (c[i].code in 33..126)\n            //其他符号都转换为全角\n                c[i] = (c[i].code + 65248).toChar()\n        }\n        return String(c)\n    }\n\n    /**\n     * 字符串全角转换为半角\n     */\n    fun fullToHalf(input: String): String {\n        val c = input.toCharArray()\n        for (i in c.indices) {\n            if (c[i].code == 12288)\n            //全角空格\n            {\n                c[i] = 32.toChar()\n                continue\n            }\n\n            if (c[i].code in 65281..65374)\n                c[i] = (c[i].code - 65248).toChar()\n        }\n        return String(c)\n    }\n\n    /**\n     * 中文大写数字转数字\n     */\n    fun chineseNumToInt(chNum: String): Int {\n        var result = 0\n        var tmp = 0\n        var billion = 0\n        val cn = chNum.toCharArray()\n\n        // \"一零二五\" 形式\n        if (cn.size > 1 && chNum.matches(\"^[〇零一二三四五六七八九壹贰叁肆伍陆柒捌玖]$\".toRegex())) {\n            for (i in cn.indices) {\n                cn[i] = (48 + ChnMap[cn[i]]!!).toChar()\n            }\n            return Integer.parseInt(String(cn))\n        }\n\n        // \"一千零二十五\", \"一千二\" 形式\n        return kotlin.runCatching {\n            for (i in cn.indices) {\n                val tmpNum = ChnMap[cn[i]]!!\n                when {\n                    tmpNum == 100000000 -> {\n                        result += tmp\n                        result *= tmpNum\n                        billion = billion * 100000000 + result\n                        result = 0\n                        tmp = 0\n                    }\n                    tmpNum == 10000 -> {\n                        result += tmp\n                        result *= tmpNum\n                        tmp = 0\n                    }\n                    tmpNum >= 10 -> {\n                        if (tmp == 0)\n                            tmp = 1\n                        result += tmpNum * tmp\n                        tmp = 0\n                    }\n                    else -> {\n                        tmp = if (i >= 2 && i == cn.size - 1 && ChnMap[cn[i - 1]]!! > 10)\n                            tmpNum * ChnMap[cn[i - 1]]!! / 10\n                        else\n                            tmp * 10 + tmpNum\n                    }\n                }\n            }\n            result += tmp + billion\n            result\n        }.getOrDefault(-1)\n    }\n\n    /**\n     * 字符串转数字\n     */\n    fun stringToInt(str: String?): Int {\n        if (str != null) {\n            val num = fullToHalf(str).replace(\"\\\\s+\".toRegex(), \"\")\n            return kotlin.runCatching {\n                Integer.parseInt(num)\n            }.getOrElse {\n                chineseNumToInt(num)\n            }\n        }\n        return -1\n    }\n\n    /**\n     * 是否包含数字\n     */\n    fun isContainNumber(company: String): Boolean {\n        val p = Pattern.compile(\"[0-9]+\")\n        val m = p.matcher(company)\n        return m.find()\n    }\n\n    /**\n     * 是否数字\n     */\n    fun isNumeric(str: String): Boolean {\n        val pattern = Pattern.compile(\"-?[0-9]+\")\n        val isNum = pattern.matcher(str)\n        return isNum.matches()\n    }\n\n    fun wordCountFormat(wc: String?): String {\n        if (wc == null) return \"\"\n        var wordsS = \"\"\n        if (isNumeric(wc)) {\n            val words: Int = wc.toInt()\n            if (words > 0) {\n                wordsS = words.toString() + \"字\"\n                if (words > 10000) {\n                    val df = DecimalFormat(\"#.#\")\n                    wordsS = df.format(words * 1.0f / 10000f.toDouble()) + \"万字\"\n                }\n            }\n        } else {\n            wordsS = wc\n        }\n        return wordsS\n    }\n\n    /**\n     * 移除字符串首尾空字符的高效方法(利用ASCII值判断,包括全角空格)\n     */\n    fun trim(s: String): String {\n        if (s.isEmpty()) return \"\"\n        var start = 0\n        val len = s.length\n        var end = len - 1\n        while (start < end && (s[start].code <= 0x20 || s[start] == '　')) {\n            ++start\n        }\n        while (start < end && (s[end].code <= 0x20 || s[end] == '　')) {\n            --end\n        }\n        if (end < len) ++end\n        return if (start > 0 || end < len) s.substring(start, end) else s\n    }\n\n    /**\n     * 重复字符串\n     */\n    fun repeat(str: String, n: Int): String {\n        val stringBuilder = StringBuilder()\n        for (i in 0 until n) {\n            stringBuilder.append(str)\n        }\n        return stringBuilder.toString()\n    }\n\n    /**\n     * 移除UTF头\n     */\n    fun removeUTFCharacters(data: String?): String? {\n        if (data == null) return null\n        val p = Pattern.compile(\"\\\\\\\\u(\\\\p{XDigit}{4})\")\n        val m = p.matcher(data)\n        val buf = StringBuffer(data.length)\n        while (m.find()) {\n            val ch = Integer.parseInt(m.group(1)!!, 16).toChar().toString()\n            m.appendReplacement(buf, Matcher.quoteReplacement(ch))\n        }\n        m.appendTail(buf)\n        return buf.toString()\n    }\n\n    fun formatHtml(html: String): String {\n        return if (TextUtils.isEmpty(html)) \"\" else html.replace(\"(?i)<(br[\\\\s/]*|/*p.*?|/*div.*?)>\".toRegex(), \"\\n\")// 替换特定标签为换行符\n                .replace(\"<[script>]*.*?>|&nbsp;\".toRegex(), \"\")// 删除script标签对和空格转义符\n                .replace(\"\\\\s*\\\\n+\\\\s*\".toRegex(), \"\\n　　\")// 移除空行,并增加段前缩进2个汉字\n                .replace(\"^[\\\\n\\\\s]+\".toRegex(), \"　　\")//移除开头空行,并增加段前缩进2个汉字\n                .replace(\"[\\\\n\\\\s]+$\".toRegex(), \"\") //移除尾部空行\n    }\n\n    fun byteToHexString(bytes: ByteArray?): String {\n        if (bytes == null) return \"\"\n        val sb = StringBuilder(bytes.size * 2)\n        for (b in bytes) {\n            val hex = 0xff and b.toInt()\n            if (hex < 16) {\n                sb.append('0')\n            }\n            sb.append(Integer.toHexString(hex))\n        }\n        return sb.toString()\n    }\n\n    fun hexStringToByte(hexString: String): ByteArray {\n        val hexStr = hexString.replace(\" \", \"\")\n        val len = hexStr.length\n        val bytes = ByteArray(len / 2)\n        var i = 0\n        while (i < len) {\n            // 两位一组，表示一个字节,把这样表示的16进制字符串，还原成一个字节\n            bytes[i / 2] = ((Character.digit(hexString[i], 16) shl 4) +\n                    Character.digit(hexString[i + 1], 16)).toByte()\n            i += 2\n        }\n        return bytes\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/utils/TextUtils.java",
    "content": "package io.legado.app.utils;\n\nimport java.util.Iterator;\n\npublic class TextUtils {\n\n    public static boolean isEmpty(CharSequence str) {\n        return str == null || str.length() == 0;\n    }\n\n\n    /**\n     * Returns a string containing the tokens joined by delimiters.\n     *\n     * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string\n     *     \"null\" will be used as the delimiter.\n     * @param tokens an array objects to be joined. Strings will be formed from the objects by\n     *     calling object.toString(). If tokens is null, a NullPointerException will be thrown. If\n     *     tokens is an empty array, an empty string will be returned.\n     */\n    public static String join(CharSequence delimiter, Object[] tokens) {\n        final int length = tokens.length;\n        if (length == 0) {\n            return \"\";\n        }\n        final StringBuilder sb = new StringBuilder();\n        sb.append(tokens[0]);\n        for (int i = 1; i < length; i++) {\n            sb.append(delimiter);\n            sb.append(tokens[i]);\n        }\n        return sb.toString();\n    }\n\n\n    /**\n     * Returns a string containing the tokens joined by delimiters.\n     *\n     * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string\n     *     \"null\" will be used as the delimiter.\n     * @param tokens an array objects to be joined. Strings will be formed from the objects by\n     *     calling object.toString(). If tokens is null, a NullPointerException will be thrown. If\n     *     tokens is empty, an empty string will be returned.\n     */\n    public static String join(CharSequence delimiter, Iterable tokens) {\n        final Iterator<?> it = tokens.iterator();\n        if (!it.hasNext()) {\n            return \"\";\n        }\n        final StringBuilder sb = new StringBuilder();\n        sb.append(it.next());\n        while (it.hasNext()) {\n            sb.append(delimiter);\n            sb.append(it.next());\n        }\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "src/main/java/io/legado/app/utils/ThrowableExtensions.kt",
    "content": "package io.legado.app.utils\n\nval Throwable.msg: String\n    get() {\n        val stackTrace = stackTraceToString()\n        val lMsg = this.localizedMessage ?: \"noErrorMsg\"\n        return when {\n            stackTrace.isNotEmpty() -> stackTrace\n            else -> lMsg\n        }\n    }"
  },
  {
    "path": "src/main/java/io/legado/app/utils/UTF8BOMFighter.kt",
    "content": "package io.legado.app.utils\n\nobject UTF8BOMFighter {\n    private val UTF8_BOM_BYTES = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte())\n\n    fun removeUTF8BOM(xmlText: String): String {\n        val bytes = xmlText.toByteArray()\n        val containsBOM = (bytes.size > 3\n                && bytes[0] == UTF8_BOM_BYTES[0]\n                && bytes[1] == UTF8_BOM_BYTES[1]\n                && bytes[2] == UTF8_BOM_BYTES[2])\n        if (containsBOM) {\n            return String(bytes, 3, bytes.size - 3)\n        }\n        return xmlText\n    }\n\n    fun removeUTF8BOM(bytes: ByteArray): ByteArray {\n        val containsBOM = (bytes.size > 3\n                && bytes[0] == UTF8_BOM_BYTES[0]\n                && bytes[1] == UTF8_BOM_BYTES[1]\n                && bytes[2] == UTF8_BOM_BYTES[2])\n        if (containsBOM) {\n            val copy = ByteArray(bytes.size - 3)\n            System.arraycopy(bytes, 3, copy, 0, bytes.size - 3)\n            return copy\n        }\n        return bytes\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/utils/Utf8BomUtils.kt",
    "content": "package io.legado.app.utils\n\n@Suppress(\"unused\")\nobject Utf8BomUtils {\n    private val UTF8_BOM_BYTES = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte())\n\n    fun removeUTF8BOM(xmlText: String): String {\n        val bytes = xmlText.toByteArray()\n        val containsBOM = (bytes.size > 3\n                && bytes[0] == UTF8_BOM_BYTES[0]\n                && bytes[1] == UTF8_BOM_BYTES[1]\n                && bytes[2] == UTF8_BOM_BYTES[2])\n        if (containsBOM) {\n            return String(bytes, 3, bytes.size - 3)\n        }\n        return xmlText\n    }\n\n    fun removeUTF8BOM(bytes: ByteArray): ByteArray {\n        val containsBOM = (bytes.size > 3\n                && bytes[0] == UTF8_BOM_BYTES[0]\n                && bytes[1] == UTF8_BOM_BYTES[1]\n                && bytes[2] == UTF8_BOM_BYTES[2])\n        if (containsBOM) {\n            val copy = ByteArray(bytes.size - 3)\n            System.arraycopy(bytes, 3, copy, 0, bytes.size - 3)\n            return copy\n        }\n        return bytes\n    }\n\n    fun hasBom(bytes: ByteArray): Boolean {\n        return (bytes.size > 3\n                && bytes[0] == UTF8_BOM_BYTES[0]\n                && bytes[1] == UTF8_BOM_BYTES[1]\n                && bytes[2] == UTF8_BOM_BYTES[2])\n    }\n}"
  },
  {
    "path": "src/main/java/io/legado/app/utils/ZipUtils.kt",
    "content": "package io.legado.app.utils\n\nimport kotlinx.coroutines.Dispatchers.IO\nimport kotlinx.coroutines.withContext\nimport java.io.*\nimport java.util.*\nimport java.util.zip.ZipEntry\nimport java.util.zip.ZipFile\nimport java.util.zip.ZipOutputStream\nimport mu.KotlinLogging\n\nprivate val logger = KotlinLogging.logger {}\n\n@Suppress(\"unused\", \"BlockingMethodInNonBlockingContext\", \"MemberVisibilityCanBePrivate\")\nobject ZipUtils {\n\n    /**\n     * Zip the files.\n     *\n     * @param srcFiles    The source of files.\n     * @param zipFilePath The path of ZIP file.\n     * @return `true`: success<br></br>`false`: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    suspend fun zipFiles(\n        srcFiles: Collection<String>,\n        zipFilePath: String\n    ): Boolean {\n        return zipFiles(srcFiles, zipFilePath, null)\n    }\n\n    /**\n     * Zip the files.\n     *\n     * @param srcFilePaths The paths of source files.\n     * @param zipFilePath  The path of ZIP file.\n     * @param comment      The comment.\n     * @return `true`: success<br></br>`false`: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    suspend fun zipFiles(\n        srcFilePaths: Collection<String>?,\n        zipFilePath: String?,\n        comment: String?\n    ): Boolean = withContext(IO) {\n        if (srcFilePaths == null || zipFilePath == null) return@withContext false\n        ZipOutputStream(FileOutputStream(zipFilePath)).use {\n            for (srcFile in srcFilePaths) {\n                if (!zipFile(getFileByPath(srcFile)!!, \"\", it, comment))\n                    return@withContext false\n            }\n            return@withContext true\n        }\n    }\n\n    /**\n     * Zip the files.\n     *\n     * @param srcFiles The source of files.\n     * @param zipFile  The ZIP file.\n     * @param comment  The comment.\n     * @return `true`: success<br></br>`false`: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    @Throws(IOException::class)\n    @JvmOverloads\n    fun zipFiles(\n        srcFiles: Collection<File>?,\n        zipFile: File?,\n        comment: String? = null\n    ): Boolean {\n        if (srcFiles == null || zipFile == null) return false\n        ZipOutputStream(FileOutputStream(zipFile)).use {\n            for (srcFile in srcFiles) {\n                if (!zipFile(srcFile, \"\", it, comment)) return false\n            }\n            return true\n        }\n    }\n\n    /**\n     * Zip the file.\n     *\n     * @param srcFilePath The path of source file.\n     * @param zipFilePath The path of ZIP file.\n     * @return `true`: success<br></br>`false`: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    @Throws(IOException::class)\n    fun zipFile(\n        srcFilePath: String,\n        zipFilePath: String\n    ): Boolean {\n        return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), null)\n    }\n\n    /**\n     * Zip the file.\n     *\n     * @param srcFilePath The path of source file.\n     * @param zipFilePath The path of ZIP file.\n     * @param comment     The comment.\n     * @return `true`: success<br></br>`false`: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    @Throws(IOException::class)\n    fun zipFile(\n        srcFilePath: String,\n        zipFilePath: String,\n        comment: String\n    ): Boolean {\n        return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), comment)\n    }\n\n    /**\n     * Zip the file.\n     *\n     * @param srcFile The source of file.\n     * @param zipFile The ZIP file.\n     * @param comment The comment.\n     * @return `true`: success<br></br>`false`: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    @Throws(IOException::class)\n    @JvmOverloads\n    fun zipFile(\n        srcFile: File?,\n        zipFile: File?,\n        comment: String? = null\n    ): Boolean {\n        if (srcFile == null || zipFile == null) return false\n        ZipOutputStream(FileOutputStream(zipFile)).use { zos ->\n            return zipFile(srcFile, \"\", zos, comment)\n        }\n    }\n\n    @Throws(IOException::class)\n    private fun zipFile(\n        srcFile: File,\n        rootPath: String,\n        zos: ZipOutputStream,\n        comment: String?\n    ): Boolean {\n        var rootPath1 = rootPath\n        if (!srcFile.exists()) return true\n        rootPath1 = rootPath1 + (if (isSpace(rootPath1)) \"\" else File.separator) + srcFile.name\n        if (srcFile.isDirectory) {\n            val fileList = srcFile.listFiles()\n            if (fileList == null || fileList.isEmpty()) {\n                val entry = ZipEntry(\"$rootPath1/\")\n                entry.comment = comment\n                zos.putNextEntry(entry)\n                zos.closeEntry()\n            } else {\n                for (file in fileList) {\n                    if (!zipFile(file, rootPath1, zos, comment)) return false\n                }\n            }\n        } else {\n            BufferedInputStream(FileInputStream(srcFile)).use { `is` ->\n                val entry = ZipEntry(rootPath1)\n                entry.comment = comment\n                zos.putNextEntry(entry)\n                zos.write(`is`.readBytes())\n                zos.closeEntry()\n            }\n        }\n        return true\n    }\n\n    /**\n     * Unzip the file.\n     *\n     * @param zipFilePath The path of ZIP file.\n     * @param destDirPath The path of destination directory.\n     * @return the unzipped files\n     * @throws IOException if unzip unsuccessfully\n     */\n    @Throws(IOException::class)\n    fun unzipFile(zipFilePath: String, destDirPath: String): List<File>? {\n        return unzipFileByKeyword(zipFilePath, destDirPath, null)\n    }\n\n    /**\n     * Unzip the file.\n     *\n     * @param zipFile The ZIP file.\n     * @param destDir The destination directory.\n     * @return the unzipped files\n     * @throws IOException if unzip unsuccessfully\n     */\n    @Throws(IOException::class)\n    fun unzipFile(\n        zipFile: File,\n        destDir: File\n    ): List<File>? {\n        return unzipFileByKeyword(zipFile, destDir, null)\n    }\n\n    /**\n     * Unzip the file by keyword.\n     *\n     * @param zipFilePath The path of ZIP file.\n     * @param destDirPath The path of destination directory.\n     * @param keyword     The keyboard.\n     * @return the unzipped files\n     * @throws IOException if unzip unsuccessfully\n     */\n    @Throws(IOException::class)\n    fun unzipFileByKeyword(\n        zipFilePath: String,\n        destDirPath: String,\n        keyword: String?\n    ): List<File>? {\n        return unzipFileByKeyword(\n            getFileByPath(zipFilePath),\n            getFileByPath(destDirPath),\n            keyword\n        )\n    }\n\n    /**\n     * Unzip the file by keyword.\n     *\n     * @param zipFile The ZIP file.\n     * @param destDir The destination directory.\n     * @param keyword The keyboard.\n     * @return the unzipped files\n     * @throws IOException if unzip unsuccessfully\n     */\n    @Throws(IOException::class)\n    fun unzipFileByKeyword(\n        zipFile: File?,\n        destDir: File?,\n        keyword: String?\n    ): List<File>? {\n        if (zipFile == null || destDir == null) return null\n        val files = ArrayList<File>()\n        val zip = ZipFile(zipFile)\n        val entries = zip.entries()\n        zip.use {\n            if (isSpace(keyword)) {\n                while (entries.hasMoreElements()) {\n                    val entry = entries.nextElement() as ZipEntry\n                    val entryName = entry.name\n                    if (entryName.contains(\"../\")) {\n                        logger.error(\"ZipUtils \" + \"entryName: $entryName is dangerous!\")\n                        continue\n                    }\n                    if (!unzipChildFile(destDir, files, zip, entry, entryName)) return files\n                }\n            } else {\n                while (entries.hasMoreElements()) {\n                    val entry = entries.nextElement() as ZipEntry\n                    val entryName = entry.name\n                    if (entryName.contains(\"../\")) {\n                        logger.error(\"ZipUtils \" + \"entryName: $entryName is dangerous!\")\n                        continue\n                    }\n                    if (entryName.contains(keyword!!)) {\n                        if (!unzipChildFile(destDir, files, zip, entry, entryName)) return files\n                    }\n                }\n            }\n        }\n        return files\n    }\n\n    @Throws(IOException::class)\n    private fun unzipChildFile(\n        destDir: File,\n        files: MutableList<File>,\n        zip: ZipFile,\n        entry: ZipEntry,\n        name: String\n    ): Boolean {\n        val file = File(destDir, name)\n        files.add(file)\n        if (entry.isDirectory) {\n            return createOrExistsDir(file)\n        } else {\n            if (!createOrExistsFile(file)) return false\n            BufferedInputStream(zip.getInputStream(entry)).use { `in` ->\n                BufferedOutputStream(FileOutputStream(file)).use { out ->\n                    out.write(`in`.readBytes())\n                }\n            }\n        }\n        return true\n    }\n\n    /**\n     * Return the files' path in ZIP file.\n     *\n     * @param zipFilePath The path of ZIP file.\n     * @return the files' path in ZIP file\n     * @throws IOException if an I/O error has occurred\n     */\n    @Throws(IOException::class)\n    fun getFilesPath(zipFilePath: String): List<String>? {\n        return getFilesPath(getFileByPath(zipFilePath))\n    }\n\n    /**\n     * Return the files' path in ZIP file.\n     *\n     * @param zipFile The ZIP file.\n     * @return the files' path in ZIP file\n     * @throws IOException if an I/O error has occurred\n     */\n    @Throws(IOException::class)\n    fun getFilesPath(zipFile: File?): List<String>? {\n        if (zipFile == null) return null\n        val paths = ArrayList<String>()\n        val zip = ZipFile(zipFile)\n        val entries = zip.entries()\n        while (entries.hasMoreElements()) {\n            val entryName = (entries.nextElement() as ZipEntry).name\n            if (entryName.contains(\"../\")) {\n                logger.error(\"ZipUtils \" + \"entryName: $entryName is dangerous!\")\n                paths.add(entryName)\n            } else {\n                paths.add(entryName)\n            }\n        }\n        zip.close()\n        return paths\n    }\n\n    /**\n     * Return the files' comment in ZIP file.\n     *\n     * @param zipFilePath The path of ZIP file.\n     * @return the files' comment in ZIP file\n     * @throws IOException if an I/O error has occurred\n     */\n    @Throws(IOException::class)\n    fun getComments(zipFilePath: String): List<String>? {\n        return getComments(getFileByPath(zipFilePath))\n    }\n\n    /**\n     * Return the files' comment in ZIP file.\n     *\n     * @param zipFile The ZIP file.\n     * @return the files' comment in ZIP file\n     * @throws IOException if an I/O error has occurred\n     */\n    @Throws(IOException::class)\n    fun getComments(zipFile: File?): List<String>? {\n        if (zipFile == null) return null\n        val comments = ArrayList<String>()\n        val zip = ZipFile(zipFile)\n        val entries = zip.entries()\n        while (entries.hasMoreElements()) {\n            val entry = entries.nextElement() as ZipEntry\n            comments.add(entry.comment)\n        }\n        zip.close()\n        return comments\n    }\n\n    private fun createOrExistsDir(file: File?): Boolean {\n        return file != null && if (file.exists()) file.isDirectory else file.mkdirs()\n    }\n\n    private fun createOrExistsFile(file: File?): Boolean {\n        if (file == null) return false\n        if (file.exists()) return file.isFile\n        if (!createOrExistsDir(file.parentFile)) return false\n        return try {\n            file.createNewFile()\n        } catch (e: IOException) {\n            e.printStackTrace()\n            false\n        }\n    }\n\n    private fun getFileByPath(filePath: String): File? {\n        return if (isSpace(filePath)) null else File(filePath)\n    }\n\n    private fun isSpace(s: String?): Boolean {\n        if (s == null) return true\n        var i = 0\n        val len = s.length\n        while (i < len) {\n            if (!Character.isWhitespace(s[i])) {\n                return false\n            }\n            ++i\n        }\n        return true\n    }\n}"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/Constants.java",
    "content": "package me.ag2s.epublib;\n\n\npublic interface Constants {\n\n  String CHARACTER_ENCODING = \"UTF-8\";\n  String DOCTYPE_XHTML = \"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD XHTML 1.1//EN\\\" \\\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\\\">\";\n  String NAMESPACE_XHTML = \"http://www.w3.org/1999/xhtml\";\n  String EPUB_GENERATOR_NAME = \"Ag2S EpubLib\";\n  String EPUB_DUOKAN_NAME = \"DK-SONGTI\";\n  char FRAGMENT_SEPARATOR_CHAR = '#';\n  String DEFAULT_TOC_ID = \"toc\";\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java",
    "content": "package me.ag2s.epublib.browsersupport;\n\nimport java.util.EventObject;\n\nimport me.ag2s.epublib.domain.EpubBook;\nimport me.ag2s.epublib.domain.Resource;\nimport me.ag2s.epublib.util.StringUtil;\n\n/**\n * Used to tell NavigationEventListener just what kind of navigation action\n * the user just did.\n *\n * @author paul\n *\n */\n@SuppressWarnings(\"unused\")\npublic class NavigationEvent extends EventObject {\n\n  private static final long serialVersionUID = -6346750144308952762L;\n\n  private Resource oldResource;\n  private int oldSpinePos;\n    private Navigator navigator;\n    private EpubBook oldBook;\n    private int oldSectionPos;\n  private String oldFragmentId;\n\n  public NavigationEvent(Object source) {\n    super(source);\n  }\n\n  public NavigationEvent(Object source, Navigator navigator) {\n    super(source);\n    this.navigator = navigator;\n    this.oldBook = navigator.getBook();\n    this.oldFragmentId = navigator.getCurrentFragmentId();\n    this.oldSectionPos = navigator.getCurrentSectionPos();\n    this.oldResource = navigator.getCurrentResource();\n    this.oldSpinePos = navigator.getCurrentSpinePos();\n  }\n\n  /**\n   * The previous position within the section.\n   *\n   * @return The previous position within the section.\n   */\n  public int getOldSectionPos() {\n    return oldSectionPos;\n  }\n\n  public Navigator getNavigator() {\n    return navigator;\n  }\n\n  public String getOldFragmentId() {\n    return oldFragmentId;\n  }\n\n  // package\n  void setOldFragmentId(String oldFragmentId) {\n    this.oldFragmentId = oldFragmentId;\n  }\n\n    public EpubBook getOldBook() {\n        return oldBook;\n    }\n\n  // package\n  void setOldPagePos(int oldPagePos) {\n    this.oldSectionPos = oldPagePos;\n  }\n\n  public int getCurrentSectionPos() {\n    return navigator.getCurrentSectionPos();\n  }\n\n  public int getOldSpinePos() {\n    return oldSpinePos;\n  }\n\n  public int getCurrentSpinePos() {\n    return navigator.getCurrentSpinePos();\n  }\n\n  public String getCurrentFragmentId() {\n    return navigator.getCurrentFragmentId();\n  }\n\n  public boolean isBookChanged() {\n    if (oldBook == null) {\n      return true;\n    }\n    return oldBook != navigator.getBook();\n  }\n\n  public boolean isSpinePosChanged() {\n    return getOldSpinePos() != getCurrentSpinePos();\n  }\n\n  public boolean isFragmentChanged() {\n    return StringUtil.equals(getOldFragmentId(), getCurrentFragmentId());\n  }\n\n  public Resource getOldResource() {\n    return oldResource;\n  }\n\n  public Resource getCurrentResource() {\n    return navigator.getCurrentResource();\n  }\n\n  public void setOldResource(Resource oldResource) {\n    this.oldResource = oldResource;\n  }\n\n\n  public void setOldSpinePos(int oldSpinePos) {\n    this.oldSpinePos = oldSpinePos;\n  }\n\n\n  public void setNavigator(Navigator navigator) {\n    this.navigator = navigator;\n  }\n\n\n    public void setOldBook(EpubBook oldBook) {\n        this.oldBook = oldBook;\n    }\n\n    public EpubBook getCurrentBook() {\n        return getNavigator().getBook();\n    }\n\n  public boolean isResourceChanged() {\n    return oldResource != getCurrentResource();\n  }\n\n  @SuppressWarnings(\"NullableProblems\")\n  public String toString() {\n    return StringUtil.toString(\n        \"oldSectionPos\", oldSectionPos,\n        \"oldResource\", oldResource,\n        \"oldBook\", oldBook,\n        \"oldFragmentId\", oldFragmentId,\n        \"oldSpinePos\", oldSpinePos,\n        \"currentPagePos\", getCurrentSectionPos(),\n        \"currentResource\", getCurrentResource(),\n        \"currentBook\", getCurrentBook(),\n        \"currentFragmentId\", getCurrentFragmentId(),\n        \"currentSpinePos\", getCurrentSpinePos()\n    );\n  }\n\n  public boolean isSectionPosChanged() {\n    return oldSectionPos != getCurrentSectionPos();\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/browsersupport/NavigationEventListener.java",
    "content": "package me.ag2s.epublib.browsersupport;\n\n/**\n * Implemented by classes that want to be notified if the user moves to\n * another location in the book.\n *\n * @author paul\n *\n */\npublic interface NavigationEventListener {\n\n  /**\n   * Called whenever the user navigates to another position in the book.\n   *\n   * @param navigationEvent f\n   */\n  void navigationPerformed(NavigationEvent navigationEvent);\n}"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java",
    "content": "package me.ag2s.epublib.browsersupport;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport me.ag2s.epublib.domain.EpubBook;\nimport me.ag2s.epublib.domain.Resource;\n\n/**\n * A history of the user's locations with the epub.\n *\n * @author paul.siegmann\n */\npublic class NavigationHistory implements NavigationEventListener {\n\n  public static final int DEFAULT_MAX_HISTORY_SIZE = 1000;\n  private static final long DEFAULT_HISTORY_WAIT_TIME = 1000;\n\n  private static class Location {\n\n    private String href;\n\n    public Location(String href) {\n      super();\n      this.href = href;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public void setHref(String href) {\n      this.href = href;\n    }\n\n    public String getHref() {\n      return href;\n    }\n  }\n\n  private long lastUpdateTime = 0;\n  private List<Location> locations = new ArrayList<>();\n  private final Navigator navigator;\n  private int currentPos = -1;\n  private int currentSize = 0;\n  private int maxHistorySize = DEFAULT_MAX_HISTORY_SIZE;\n  private long historyWaitTime = DEFAULT_HISTORY_WAIT_TIME;\n\n  public NavigationHistory(Navigator navigator) {\n    this.navigator = navigator;\n    navigator.addNavigationEventListener(this);\n    initBook(navigator.getBook());\n  }\n\n  public int getCurrentPos() {\n    return currentPos;\n  }\n\n\n  public int getCurrentSize() {\n    return currentSize;\n  }\n\n  public void initBook(EpubBook book) {\n    if (book == null) {\n      return;\n    }\n    locations = new ArrayList<>();\n    currentPos = -1;\n    currentSize = 0;\n    if (navigator.getCurrentResource() != null) {\n      addLocation(navigator.getCurrentResource().getHref());\n    }\n  }\n\n  /**\n   * If the time between a navigation event is less than the historyWaitTime\n   * then the new location is not added to the history.\n   *\n   * When a user is rapidly viewing many pages using the slider we do not\n   * want all of them to be added to the history.\n   *\n   * @return the time we wait before adding the page to the history\n   */\n  public long getHistoryWaitTime() {\n    return historyWaitTime;\n  }\n\n  public void setHistoryWaitTime(long historyWaitTime) {\n    this.historyWaitTime = historyWaitTime;\n  }\n\n  public void addLocation(Resource resource) {\n    if (resource == null) {\n      return;\n    }\n    addLocation(resource.getHref());\n  }\n\n  /**\n   * Adds the location after the current position.\n   * If the currentposition is not the end of the list then the elements\n   * between the current element and the end of the list will be discarded.\n   *\n   * Does nothing if the new location matches the current location.\n   * <br/>\n   * If this nr of locations becomes larger then the historySize then the\n   * first item(s) will be removed.\n   *v\n   * @param location  d\n   */\n  public void addLocation(Location location) {\n    // do nothing if the new location matches the current location\n    if (!(locations.isEmpty()) &&\n        location.getHref().equals(locations.get(currentPos).getHref())) {\n      return;\n    }\n    currentPos++;\n    if (currentPos != currentSize) {\n      locations.set(currentPos, location);\n    } else {\n      locations.add(location);\n      checkHistorySize();\n    }\n    currentSize = currentPos + 1;\n  }\n\n  /**\n   * Removes all elements that are too much for the maxHistorySize\n   * out of the history.\n   */\n  private void checkHistorySize() {\n    while (locations.size() > maxHistorySize) {\n      locations.remove(0);\n      currentSize--;\n      currentPos--;\n    }\n  }\n\n  public void addLocation(String href) {\n    addLocation(new Location(href));\n  }\n\n  private String getLocationHref(int pos) {\n    if (pos < 0 || pos >= locations.size()) {\n      return null;\n    }\n    return locations.get(currentPos).getHref();\n  }\n\n  /**\n   * Moves the current positions delta positions.\n   *\n   * move(-1) to go one position back in history.<br/>\n   * move(1) to go one position forward.<br/>发\n   *\n   * @param delta f\n   *\n   * @return Whether we actually moved. If the requested value is illegal\n   * it will return false, true otherwise.\n   */\n  public boolean move(int delta) {\n    if (((currentPos + delta) < 0)\n        || ((currentPos + delta) >= currentSize)) {\n      return false;\n    }\n    currentPos += delta;\n    navigator.gotoResource(getLocationHref(currentPos), this);\n    return true;\n  }\n\n\n  /**\n   * If this is not the source of the navigationEvent then the addLocation\n   * will be called with the href of the currentResource in the navigationEvent.\n   */\n  @Override\n  public void navigationPerformed(NavigationEvent navigationEvent) {\n    if (this == navigationEvent.getSource()) {\n      return;\n    }\n    if (navigationEvent.getCurrentResource() == null) {\n      return;\n    }\n\n    if ((System.currentTimeMillis() - this.lastUpdateTime) > historyWaitTime) {\n      // if the user scrolled rapidly through the pages then the last page\n      // will not be added to the history. We fix that here:\n      addLocation(navigationEvent.getOldResource());\n\n      addLocation(navigationEvent.getCurrentResource().getHref());\n    }\n    lastUpdateTime = System.currentTimeMillis();\n  }\n\n  public String getCurrentHref() {\n    if (currentPos < 0 || currentPos >= locations.size()) {\n      return null;\n    }\n    return locations.get(currentPos).getHref();\n  }\n\n  public void setMaxHistorySize(int maxHistorySize) {\n    this.maxHistorySize = maxHistorySize;\n  }\n\n  public int getMaxHistorySize() {\n    return maxHistorySize;\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/browsersupport/Navigator.java",
    "content": "package me.ag2s.epublib.browsersupport;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport me.ag2s.epublib.domain.EpubBook;\nimport me.ag2s.epublib.domain.Resource;\n\n/**\n * A helper class for epub browser applications.\n * <p>\n * It helps moving from one resource to the other, from one resource\n * to the other and keeping other elements of the application up-to-date\n * by calling the NavigationEventListeners.\n *\n * @author paul\n */\npublic class Navigator implements Serializable {\n\n  private static final long serialVersionUID = 1076126986424925474L;\n  private EpubBook book;\n  private int currentSpinePos;\n  private Resource currentResource;\n  private int currentPagePos;\n  private String currentFragmentId;\n\n  private final List<NavigationEventListener> eventListeners = new ArrayList<>();\n\n  public Navigator() {\n    this(null);\n  }\n\n  public Navigator(EpubBook book) {\n    this.book = book;\n    this.currentSpinePos = 0;\n    if (book != null) {\n      this.currentResource = book.getCoverPage();\n    }\n    this.currentPagePos = 0;\n  }\n\n  private synchronized void handleEventListeners(\n      NavigationEvent navigationEvent) {\n    for (int i = 0; i < eventListeners.size(); i++) {\n      NavigationEventListener navigationEventListener = eventListeners.get(i);\n      navigationEventListener.navigationPerformed(navigationEvent);\n    }\n  }\n\n  public boolean addNavigationEventListener(\n      NavigationEventListener navigationEventListener) {\n    return this.eventListeners.add(navigationEventListener);\n  }\n\n  public boolean removeNavigationEventListener(\n      NavigationEventListener navigationEventListener) {\n    return this.eventListeners.remove(navigationEventListener);\n  }\n\n  public int gotoFirstSpineSection(Object source) {\n    return gotoSpineSection(0, source);\n  }\n\n  public int gotoPreviousSpineSection(Object source) {\n    return gotoPreviousSpineSection(0, source);\n  }\n\n  public int gotoPreviousSpineSection(int pagePos, Object source) {\n    if (currentSpinePos < 0) {\n      return gotoSpineSection(0, pagePos, source);\n    } else {\n      return gotoSpineSection(currentSpinePos - 1, pagePos, source);\n    }\n  }\n\n  public boolean hasNextSpineSection() {\n    return (currentSpinePos < (book.getSpine().size() - 1));\n  }\n\n  public boolean hasPreviousSpineSection() {\n    return (currentSpinePos > 0);\n  }\n\n  public int gotoNextSpineSection(Object source) {\n    if (currentSpinePos < 0) {\n      return gotoSpineSection(0, source);\n    } else {\n      return gotoSpineSection(currentSpinePos + 1, source);\n    }\n  }\n\n  public int gotoResource(String resourceHref, Object source) {\n    Resource resource = book.getResources().getByHref(resourceHref);\n    return gotoResource(resource, source);\n  }\n\n\n  public int gotoResource(Resource resource, Object source) {\n    return gotoResource(resource, 0, null, source);\n  }\n\n  public int gotoResource(Resource resource, String fragmentId, Object source) {\n    return gotoResource(resource, 0, fragmentId, source);\n  }\n\n  public int gotoResource(Resource resource, int pagePos, Object source) {\n    return gotoResource(resource, pagePos, null, source);\n  }\n\n  public int gotoResource(Resource resource, int pagePos, String fragmentId,\n      Object source) {\n    if (resource == null) {\n      return -1;\n    }\n    NavigationEvent navigationEvent = new NavigationEvent(source, this);\n    this.currentResource = resource;\n    this.currentSpinePos = book.getSpine().getResourceIndex(currentResource);\n    this.currentPagePos = pagePos;\n    this.currentFragmentId = fragmentId;\n    handleEventListeners(navigationEvent);\n\n    return currentSpinePos;\n  }\n\n  public int gotoResourceId(String resourceId, Object source) {\n    return gotoSpineSection(book.getSpine().findFirstResourceById(resourceId),\n        source);\n  }\n\n  public int gotoSpineSection(int newSpinePos, Object source) {\n    return gotoSpineSection(newSpinePos, 0, source);\n  }\n\n  /**\n   * Go to a specific section.\n   * Illegal spine positions are silently ignored.\n   *\n   * @param newSpinePos f\n   * @param source f\n   * @return The current position within the spine\n   */\n  public int gotoSpineSection(int newSpinePos, int newPagePos, Object source) {\n    if (newSpinePos == currentSpinePos) {\n      return currentSpinePos;\n    }\n    if (newSpinePos < 0 || newSpinePos >= book.getSpine().size()) {\n      return currentSpinePos;\n    }\n    NavigationEvent navigationEvent = new NavigationEvent(source, this);\n    currentSpinePos = newSpinePos;\n    currentPagePos = newPagePos;\n    currentResource = book.getSpine().getResource(currentSpinePos);\n    handleEventListeners(navigationEvent);\n    return currentSpinePos;\n  }\n\n  public int gotoLastSpineSection(Object source) {\n    return gotoSpineSection(book.getSpine().size() - 1, source);\n  }\n\n  public void gotoBook(EpubBook book, Object source) {\n    NavigationEvent navigationEvent = new NavigationEvent(source, this);\n    this.book = book;\n    this.currentFragmentId = null;\n    this.currentPagePos = 0;\n    this.currentResource = null;\n    this.currentSpinePos = book.getSpine().getResourceIndex(currentResource);\n    handleEventListeners(navigationEvent);\n  }\n\n  /**\n   * The current position within the spine.\n   *\n   * @return something &lt; 0 if the current position is not within the spine.\n   */\n  public int getCurrentSpinePos() {\n    return currentSpinePos;\n  }\n\n  public Resource getCurrentResource() {\n    return currentResource;\n  }\n\n  /**\n   * Sets the current index and resource without calling the eventlisteners.\n   *\n   * If you want the eventListeners called use gotoSection(index);\n   *\n   * @param currentIndex f\n   */\n  public void setCurrentSpinePos(int currentIndex) {\n    this.currentSpinePos = currentIndex;\n    this.currentResource = book.getSpine().getResource(currentIndex);\n  }\n\n  public EpubBook getBook() {\n    return book;\n  }\n\n  /**\n   * Sets the current index and resource without calling the eventlisteners.\n   *\n   * If you want the eventListeners called use gotoSection(index);\n   *\n   */\n  public int setCurrentResource(Resource currentResource) {\n    this.currentSpinePos = book.getSpine().getResourceIndex(currentResource);\n    this.currentResource = currentResource;\n    return currentSpinePos;\n  }\n\n  public String getCurrentFragmentId() {\n    return currentFragmentId;\n  }\n\n  public int getCurrentSectionPos() {\n    return currentPagePos;\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/browsersupport/package-info.java",
    "content": "/**\n * Provides classes that help make an epub reader application.\n *\n * These classes have no dependencies on graphic toolkits, they're purely\n * to help with the browsing/navigation logic.\n */\npackage me.ag2s.epublib.browsersupport;\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/Author.java",
    "content": "package me.ag2s.epublib.domain;\n\n\n\nimport me.ag2s.epublib.util.StringUtil;\n\nimport java.io.Serializable;\n\n/**\n * Represents one of the authors of the book\n *\n * @author paul\n */\npublic class Author implements Serializable {\n\n    private static final long serialVersionUID = 6663408501416574200L;\n\n    private String firstname;\n    private String lastname;\n    private Relator relator = Relator.AUTHOR;\n\n    public Author(String singleName) {\n        this(\"\", singleName);\n    }\n\n    public Author(String firstname, String lastname) {\n        this.firstname = firstname;\n        this.lastname = lastname;\n    }\n\n    public String getFirstname() {\n        return firstname;\n    }\n\n    public void setFirstname(String firstname) {\n        this.firstname = firstname;\n    }\n\n    public String getLastname() {\n        return lastname;\n    }\n\n    public void setLastname(String lastname) {\n        this.lastname = lastname;\n    }\n\n\n    @Override\n    @SuppressWarnings(\"NullableProblems\")\n    public String toString() {\n        return this.lastname + \", \" + this.firstname;\n    }\n\n    public int hashCode() {\n        return StringUtil.hashCode(firstname, lastname);\n    }\n\n    public boolean equals(Object authorObject) {\n        if (!(authorObject instanceof Author)) {\n            return false;\n        }\n        Author other = (Author) authorObject;\n        return StringUtil.equals(firstname, other.firstname)\n                && StringUtil.equals(lastname, other.lastname);\n    }\n\n    /**\n     * 设置贡献者的角色\n     *\n     * @param code 角色编号\n     */\n\n    public void setRole(String code) {\n        Relator result = Relator.byCode(code);\n        if (result == null) {\n            result = Relator.AUTHOR;\n        }\n        this.relator = result;\n    }\n\n    public Relator getRelator() {\n        return relator;\n    }\n\n\n    public void setRelator(Relator relator) {\n        this.relator = relator;\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/Date.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.Serializable;\nimport java.text.SimpleDateFormat;\nimport java.util.Locale;\n\nimport me.ag2s.epublib.epub.PackageDocumentBase;\n\n/**\n * A Date used by the book's metadata.\n * <p>\n * Examples: creation-date, modification-date, etc\n *\n * @author paul\n */\npublic class Date implements Serializable {\n\n    private static final long serialVersionUID = 7533866830395120136L;\n\n    public enum Event {\n        PUBLICATION(\"publication\"),\n        MODIFICATION(\"modification\"),\n        CREATION(\"creation\");\n\n        private final String value;\n\n        Event(String v) {\n            value = v;\n        }\n\n        public static Event fromValue(String v) {\n            for (Event c : Event.values()) {\n                if (c.value.equals(v)) {\n                    return c;\n                }\n            }\n            return null;\n        }\n\n        @Override\n        @SuppressWarnings(\"NullableProblems\")\n        public String toString() {\n            return value;\n        }\n    }\n\n\n    private Event event;\n    private String dateString;\n\n    public Date() {\n        this(new java.util.Date(), Event.CREATION);\n    }\n\n    public Date(java.util.Date date) {\n        this(date, (Event) null);\n    }\n\n    public Date(String dateString) {\n        this(dateString, (Event) null);\n    }\n\n    public Date(java.util.Date date, Event event) {\n        this((new SimpleDateFormat(PackageDocumentBase.dateFormat, Locale.US)).format(date),\n                event);\n    }\n\n    public Date(String dateString, Event event) {\n        this.dateString = dateString;\n        this.event = event;\n    }\n\n    public Date(java.util.Date date, String event) {\n        this((new SimpleDateFormat(PackageDocumentBase.dateFormat, Locale.US)).format(date),\n                event);\n    }\n\n    public Date(String dateString, String event) {\n        this(checkDate(dateString), Event.fromValue(event));\n        this.dateString = dateString;\n    }\n\n    private static String checkDate(String dateString) {\n        if (dateString == null) {\n            throw new IllegalArgumentException(\n                    \"Cannot create a date from a blank string\");\n        }\n        return dateString;\n    }\n\n    public String getValue() {\n        return dateString;\n    }\n\n    public Event getEvent() {\n        return event;\n    }\n\n    public void setEvent(Event event) {\n        this.event = event;\n    }\n\n    @Override\n    @SuppressWarnings(\"NullableProblems\")\n    public String toString() {\n        if (event == null) {\n            return dateString;\n        }\n        return \"\" + event + \":\" + dateString;\n    }\n}\n\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/EpubBook.java",
    "content": "package me.ag2s.epublib.domain;\n\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Representation of a Book.\n * <p>\n * All resources of a Book (html, css, xml, fonts, images) are represented\n * as Resources. See getResources() for access to these.<br/>\n * A Book as 3 indexes into these Resources, as per the epub specification.<br/>\n * <dl>\n * <dt>Spine</dt>\n * <dd>these are the Resources to be shown when a user reads the book from\n * start to finish.</dd>\n * <dt>Table of Contents<dt>\n * <dd>The table of contents. Table of Contents references may be in a\n * different order and contain different Resources than the spine, and often do.\n * <dt>Guide</dt>\n * <dd>The Guide has references to a set of special Resources like the\n * cover page, the Glossary, the copyright page, etc.\n * </dl>\n * <p/>\n * The complication is that these 3 indexes may and usually do point to\n * different pages.\n * A chapter may be split up in 2 pieces to fit it in to memory. Then the\n * spine will contain both pieces, but the Table of Contents only the first.\n * <p>\n * The Content page may be in the Table of Contents, the Guide, but not\n * in the Spine.\n * Etc.\n * <p/>\n * <p>\n * Please see the illustration at: doc/schema.svg\n *\n * @author paul\n * @author jake\n */\npublic class EpubBook implements Serializable {\n\n    private static final long serialVersionUID = 2068355170895770100L;\n\n    private Resources resources = new Resources();\n    private Metadata metadata = new Metadata();\n    private Spine spine = new Spine();\n    private TableOfContents tableOfContents = new TableOfContents();\n    private final Guide guide = new Guide();\n    private Resource opfResource;\n    private Resource ncxResource;\n    private Resource coverImage;\n\n\n    private String version = \"2.0\";\n\n    public String getVersion() {\n        return version;\n    }\n\n    public void setVersion(String version) {\n        this.version = version;\n    }\n\n    public boolean isEpub3() {\n        return this.version.startsWith(\"3.\");\n    }\n\n    @SuppressWarnings(\"UnusedReturnValue\")\n    public TOCReference addSection(\n            TOCReference parentSection, String sectionTitle, Resource resource) {\n        return addSection(parentSection, sectionTitle, resource, null);\n    }\n\n    /**\n     * Adds the resource to the table of contents of the book as a child\n     * section of the given parentSection\n     *\n     * @param parentSection parentSection\n     * @param sectionTitle  sectionTitle\n     * @param resource      resource\n     * @param fragmentId    fragmentId\n     * @return The table of contents\n     */\n    public TOCReference addSection(\n            TOCReference parentSection, String sectionTitle, Resource resource,\n            String fragmentId) {\n        getResources().add(resource);\n        if (spine.findFirstResourceById(resource.getId()) < 0) {\n            spine.addSpineReference(new SpineReference(resource));\n        }\n        return parentSection.addChildSection(\n                new TOCReference(sectionTitle, resource, fragmentId));\n    }\n\n    public TOCReference addSection(String title, Resource resource) {\n        return addSection(title, resource, null);\n    }\n\n    /**\n     * Adds a resource to the book's set of resources, table of contents and\n     * if there is no resource with the id in the spine also adds it to the spine.\n     *\n     * @param title      title\n     * @param resource   resource\n     * @param fragmentId fragmentId\n     * @return The table of contents\n     */\n    public TOCReference addSection(\n            String title, Resource resource, String fragmentId) {\n        getResources().add(resource);\n        TOCReference tocReference = tableOfContents\n                .addTOCReference(new TOCReference(title, resource, fragmentId));\n        if (spine.findFirstResourceById(resource.getId()) < 0) {\n            spine.addSpineReference(new SpineReference(resource));\n        }\n        return tocReference;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public void generateSpineFromTableOfContents() {\n        Spine spine = new Spine(tableOfContents);\n\n        // in case the tocResource was already found and assigned\n        spine.setTocResource(this.spine.getTocResource());\n\n        this.spine = spine;\n    }\n\n    /**\n     * The Book's metadata (titles, authors, etc)\n     *\n     * @return The Book's metadata (titles, authors, etc)\n     */\n    public Metadata getMetadata() {\n        return metadata;\n    }\n\n    public void setMetadata(Metadata metadata) {\n        this.metadata = metadata;\n    }\n\n\n    public void setResources(Resources resources) {\n        this.resources = resources;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public Resource addResource(Resource resource) {\n        return resources.add(resource);\n    }\n\n    /**\n     * The collection of all images, chapters, sections, xhtml files,\n     * stylesheets, etc that make up the book.\n     *\n     * @return The collection of all images, chapters, sections, xhtml files,\n     * stylesheets, etc that make up the book.\n     */\n    public Resources getResources() {\n        return resources;\n    }\n\n\n    /**\n     * The sections of the book that should be shown if a user reads the book\n     * from start to finish.\n     *\n     * @return The Spine\n     */\n    public Spine getSpine() {\n        return spine;\n    }\n\n\n    public void setSpine(Spine spine) {\n        this.spine = spine;\n    }\n\n\n    /**\n     * The Table of Contents of the book.\n     *\n     * @return The Table of Contents of the book.\n     */\n    public TableOfContents getTableOfContents() {\n        return tableOfContents;\n    }\n\n\n    public void setTableOfContents(TableOfContents tableOfContents) {\n        this.tableOfContents = tableOfContents;\n    }\n\n    /**\n     * The book's cover page as a Resource.\n     * An XHTML document containing a link to the cover image.\n     *\n     * @return The book's cover page as a Resource\n     */\n    public Resource getCoverPage() {\n        Resource coverPage = guide.getCoverPage();\n        if (coverPage == null) {\n            coverPage = spine.getResource(0);\n        }\n        return coverPage;\n    }\n\n\n    public void setCoverPage(Resource coverPage) {\n        if (coverPage == null) {\n            return;\n        }\n        if (resources.notContainsByHref(coverPage.getHref())) {\n            resources.add(coverPage);\n        }\n        guide.setCoverPage(coverPage);\n    }\n\n    /**\n     * Gets the first non-blank title from the book's metadata.\n     *\n     * @return the first non-blank title from the book's metadata.\n     */\n    public String getTitle() {\n        return getMetadata().getFirstTitle();\n    }\n\n\n    /**\n     * The book's cover image.\n     *\n     * @return The book's cover image.\n     */\n    public Resource getCoverImage() {\n        return coverImage;\n    }\n\n    public void setCoverImage(Resource coverImage) {\n        if (coverImage == null) {\n            return;\n        }\n        if (resources.notContainsByHref(coverImage.getHref())) {\n            resources.add(coverImage);\n        }\n        this.coverImage = coverImage;\n    }\n\n    /**\n     * The guide; contains references to special sections of the book like\n     * colophon, glossary, etc.\n     *\n     * @return The guide; contains references to special sections of the book\n     * like colophon, glossary, etc.\n     */\n    public Guide getGuide() {\n        return guide;\n    }\n\n    /**\n     * All Resources of the Book that can be reached via the Spine, the\n     * TableOfContents or the Guide.\n     * <p/>\n     * Consists of a list of \"reachable\" resources:\n     * <ul>\n     * <li>The coverpage</li>\n     * <li>The resources of the Spine that are not already in the result</li>\n     * <li>The resources of the Table of Contents that are not already in the\n     * result</li>\n     * <li>The resources of the Guide that are not already in the result</li>\n     * </ul>\n     * To get all html files that make up the epub file use\n     * {@link #getResources()}\n     *\n     * @return All Resources of the Book that can be reached via the Spine,\n     * the TableOfContents or the Guide.\n     */\n    public List<Resource> getContents() {\n        Map<String, Resource> result = new LinkedHashMap<>();\n        addToContentsResult(getCoverPage(), result);\n\n        for (SpineReference spineReference : getSpine().getSpineReferences()) {\n            addToContentsResult(spineReference.getResource(), result);\n        }\n\n        for (Resource resource : getTableOfContents().getAllUniqueResources()) {\n            addToContentsResult(resource, result);\n        }\n\n        for (GuideReference guideReference : getGuide().getReferences()) {\n            addToContentsResult(guideReference.getResource(), result);\n        }\n\n        return new ArrayList<>(result.values());\n    }\n\n    private static void addToContentsResult(Resource resource,\n                                            Map<String, Resource> allReachableResources) {\n        if (resource != null && (!allReachableResources\n                .containsKey(resource.getHref()))) {\n            allReachableResources.put(resource.getHref(), resource);\n        }\n    }\n\n    public Resource getOpfResource() {\n        return opfResource;\n    }\n\n    public void setOpfResource(Resource opfResource) {\n        this.opfResource = opfResource;\n    }\n\n    public void setNcxResource(Resource ncxResource) {\n        this.ncxResource = ncxResource;\n    }\n\n    public Resource getNcxResource() {\n        return ncxResource;\n    }\n}\n\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/EpubResourceProvider.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\n\n/**\n * @author jake\n */\npublic class EpubResourceProvider implements LazyResourceProvider {\n\n  private final String epubFilename;\n\n  /**\n   * @param epubFilename the file name for the epub we're created from.\n   */\n  public EpubResourceProvider(String epubFilename) {\n    this.epubFilename = epubFilename;\n  }\n\n  @Override\n  public InputStream getResourceStream(String href) throws IOException {\n    ZipFile zipFile = new ZipFile(epubFilename);\n    ZipEntry zipEntry = zipFile.getEntry(href);\n    if (zipEntry == null) {\n      zipFile.close();\n      throw new IllegalStateException(\n          \"Cannot find entry \" + href + \" in epub file \" + epubFilename);\n    }\n    return new ResourceInputStream(zipFile.getInputStream(zipEntry), zipFile);\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/FileResourceProvider.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * 用于创建epub，添加大文件（如大量图片）时容易OOM，使用LazyResource，避免OOM.\n *\n */\n\npublic class FileResourceProvider implements LazyResourceProvider {\n    //需要导入资源的父目录\n    String dir;\n\n    /**\n     * 创建一个文件夹里面文件夹的LazyResourceProvider，用于LazyResource。\n     * @param parentDir 文件的目录\n     */\n    public FileResourceProvider(String parentDir) {\n        this.dir = parentDir;\n    }\n\n    /**\n     * 创建一个文件夹里面文件夹的LazyResourceProvider，用于LazyResource。\n     * @param parentFile 文件夹\n     */\n    @SuppressWarnings(\"unused\")\n    public FileResourceProvider(File parentFile) {\n        this.dir = parentFile.getPath();\n    }\n\n    /**\n     * 根据子文件名href,再父目录下读取文件获取FileInputStream\n     * @param href 子文件名href\n     * @return 对应href的FileInputStream\n     * @throws IOException 抛出IOException\n     */\n    @Override\n    public InputStream getResourceStream(String href) throws IOException {\n        return new FileInputStream(new File(dir, href));\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/Guide.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * The guide is a selection of special pages of the book.\n * Examples of these are the cover, list of illustrations, etc.\n *\n * It is an optional part of an epub, and support for the various types\n * of references varies by reader.\n *\n * The only part of this that is heavily used is the cover page.\n *\n * @author paul\n *\n */\npublic class Guide implements Serializable {\n\n  /**\n   *\n   */\n  private static final long serialVersionUID = -6256645339915751189L;\n\n  public static final String DEFAULT_COVER_TITLE = GuideReference.COVER;\n\n  private List<GuideReference> references = new ArrayList<>();\n  private static final int COVERPAGE_NOT_FOUND = -1;\n  private static final int COVERPAGE_UNITIALIZED = -2;\n\n  private int coverPageIndex = -1;\n\n  public List<GuideReference> getReferences() {\n    return references;\n  }\n\n  public void setReferences(List<GuideReference> references) {\n    this.references = references;\n    uncheckCoverPage();\n  }\n\n  private void uncheckCoverPage() {\n    coverPageIndex = COVERPAGE_UNITIALIZED;\n  }\n\n  public GuideReference getCoverReference() {\n    checkCoverPage();\n    if (coverPageIndex >= 0) {\n      return references.get(coverPageIndex);\n    }\n    return null;\n  }\n  @SuppressWarnings(\"UnusedReturnValue\")\n  public int setCoverReference(GuideReference guideReference) {\n    if (coverPageIndex >= 0) {\n      references.set(coverPageIndex, guideReference);\n    } else {\n      references.add(0, guideReference);\n      coverPageIndex = 0;\n    }\n    return coverPageIndex;\n  }\n\n  private void checkCoverPage() {\n    if (coverPageIndex == COVERPAGE_UNITIALIZED) {\n      initCoverPage();\n    }\n  }\n\n\n  private void initCoverPage() {\n    int result = COVERPAGE_NOT_FOUND;\n    for (int i = 0; i < references.size(); i++) {\n      GuideReference guideReference = references.get(i);\n      if (guideReference.getType().equals(GuideReference.COVER)) {\n        result = i;\n        break;\n      }\n    }\n    coverPageIndex = result;\n  }\n\n  /**\n   * The coverpage of the book.\n   *\n   * @return The coverpage of the book.\n   */\n  public Resource getCoverPage() {\n    GuideReference guideReference = getCoverReference();\n    if (guideReference == null) {\n      return null;\n    }\n    return guideReference.getResource();\n  }\n\n  public void setCoverPage(Resource coverPage) {\n    GuideReference coverpageGuideReference = new GuideReference(coverPage,\n        GuideReference.COVER, DEFAULT_COVER_TITLE);\n    setCoverReference(coverpageGuideReference);\n  }\n\n  @SuppressWarnings(\"UnusedReturnValue\")\n  public ResourceReference addReference(GuideReference reference) {\n    this.references.add(reference);\n    uncheckCoverPage();\n    return reference;\n  }\n\n  /**\n   * A list of all GuideReferences that have the given\n   * referenceTypeName (ignoring case).\n   *\n   * @param referenceTypeName referenceTypeName\n   * @return A list of all GuideReferences that have the given\n   *    referenceTypeName (ignoring case).\n   */\n  public List<GuideReference> getGuideReferencesByType(\n      String referenceTypeName) {\n    List<GuideReference> result = new ArrayList<>();\n    for (GuideReference guideReference : references) {\n      if (referenceTypeName.equalsIgnoreCase(guideReference.getType())) {\n        result.add(guideReference);\n      }\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/GuideReference.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport me.ag2s.epublib.util.StringUtil;\nimport java.io.Serializable;\n\n\n/**\n * These are references to elements of the book's guide.\n *\n * @see Guide\n *\n * @author paul\n *\n */\npublic class GuideReference extends TitledResourceReference\n    implements Serializable {\n\n  private static final long serialVersionUID = -316179702440631834L;\n\n  /**\n   * the book cover(s), jacket information, etc.\n   */\n  public static final String COVER = \"cover\";\n\n  /**\n   * human-readable page with title, author, publisher, and other metadata\n   */\n  public static String TITLE_PAGE = \"title-page\";\n\n  /**\n   * Human-readable table of contents.\n   * Not to be confused the epub file table of contents\n   *\n   */\n  public static String TOC = \"toc\";\n\n  /**\n   * back-of-book style index\n   */\n  public static String INDEX = \"index\";\n  public static String GLOSSARY = \"glossary\";\n  public static String ACKNOWLEDGEMENTS = \"acknowledgements\";\n  public static String BIBLIOGRAPHY = \"bibliography\";\n  public static String COLOPHON = \"colophon\";\n  public static String COPYRIGHT_PAGE = \"copyright-page\";\n  public static String DEDICATION = \"dedication\";\n\n  /**\n   * an epigraph is a phrase, quotation, or poem that is set at the\n   * beginning of a document or component.\n   *\n   * source: http://en.wikipedia.org/wiki/Epigraph_%28literature%29\n   */\n  public static String EPIGRAPH = \"epigraph\";\n\n  public static String FOREWORD = \"foreword\";\n\n  /**\n   * list of illustrations\n   */\n  public static String LOI = \"loi\";\n\n  /**\n   * list of tables\n   */\n  public static String LOT = \"lot\";\n  public static String NOTES = \"notes\";\n  public static String PREFACE = \"preface\";\n\n  /**\n   * A page of content (e.g. \"Chapter 1\")\n   */\n  public static String TEXT = \"text\";\n\n  private String type;\n\n  public GuideReference(Resource resource) {\n    this(resource, null);\n  }\n\n  public GuideReference(Resource resource, String title) {\n    super(resource, title);\n  }\n\n  public GuideReference(Resource resource, String type, String title) {\n    this(resource, type, title, null);\n  }\n\n  public GuideReference(Resource resource, String type, String title,\n      String fragmentId) {\n    super(resource, title, fragmentId);\n    this.type = StringUtil.isNotBlank(type) ? type.toLowerCase() : null;\n  }\n\n  public String getType() {\n    return type;\n  }\n\n  public void setType(String type) {\n    this.type = type;\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/Identifier.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport me.ag2s.epublib.util.StringUtil;\nimport java.io.Serializable;\nimport java.util.List;\nimport java.util.UUID;\n\n/**\n * A Book's identifier.\n *\n * Defaults to a random UUID and scheme \"UUID\"\n *\n * @author paul\n */\npublic class Identifier implements Serializable {\n\n  private static final long serialVersionUID = 955949951416391810L;\n  @SuppressWarnings(\"unused\")\n  public interface Scheme {\n\n    String UUID = \"UUID\";\n    String ISBN = \"ISBN\";\n    String URL = \"URL\";\n    String URI = \"URI\";\n  }\n\n  private boolean bookId = false;\n  private String scheme;\n  private String value;\n\n  /**\n   * Creates an Identifier with as value a random UUID and scheme \"UUID\"\n   */\n  public Identifier() {\n    this(Scheme.UUID, UUID.randomUUID().toString());\n  }\n\n\n  public Identifier(String scheme, String value) {\n    this.scheme = scheme;\n    this.value = value;\n  }\n\n  /**\n   * The first identifier for which the bookId is true is made the\n   * bookId identifier.\n   *\n   * If no identifier has bookId == true then the first bookId identifier\n   * is written as the primary.\n   *\n   * @param identifiers i\n   * @return The first identifier for which the bookId is true is made\n   * \t\tthe bookId identifier.\n   */\n  public static Identifier getBookIdIdentifier(List<Identifier> identifiers) {\n    if (identifiers == null || identifiers.isEmpty()) {\n      return null;\n    }\n\n    Identifier result = null;\n    for (Identifier identifier : identifiers) {\n      if (identifier.isBookId()) {\n        result = identifier;\n        break;\n      }\n    }\n\n    if (result == null) {\n      result = identifiers.get(0);\n    }\n\n    return result;\n  }\n\n  public String getScheme() {\n    return scheme;\n  }\n\n  public void setScheme(String scheme) {\n    this.scheme = scheme;\n  }\n\n  public String getValue() {\n    return value;\n  }\n\n  public void setValue(String value) {\n    this.value = value;\n  }\n\n\n  public void setBookId(boolean bookId) {\n    this.bookId = bookId;\n  }\n\n\n  /**\n   * This bookId property allows the book creator to add multiple ids and\n   * tell the epubwriter which one to write out as the bookId.\n   *\n   * The Dublin Core metadata spec allows multiple identifiers for a Book.\n   * The epub spec requires exactly one identifier to be marked as the book id.\n   *\n   * @return whether this is the unique book id.\n   */\n  public boolean isBookId() {\n    return bookId;\n  }\n\n  public int hashCode() {\n    return StringUtil.defaultIfNull(scheme).hashCode() ^ StringUtil\n        .defaultIfNull(value).hashCode();\n  }\n\n  public boolean equals(Object otherIdentifier) {\n    if (!(otherIdentifier instanceof Identifier)) {\n      return false;\n    }\n    return StringUtil.equals(scheme, ((Identifier) otherIdentifier).scheme)\n        && StringUtil.equals(value, ((Identifier) otherIdentifier).value);\n  }\n  @SuppressWarnings(\"NullableProblems\")\n  @Override\n  public String toString() {\n    if (StringUtil.isBlank(scheme)) {\n      return \"\" + value;\n    }\n    return \"\" + scheme + \":\" + value;\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/LazyResource.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport me.ag2s.epublib.util.IOUtil;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * A Resource that loads its data only on-demand from a EPUB book file.\n * This way larger books can fit into memory and can be opened faster.\n */\npublic class LazyResource extends Resource {\n\n  private static final long serialVersionUID = 5089400472352002866L;\n  private  final String TAG= getClass().getName();\n\n  private final LazyResourceProvider resourceProvider;\n  private final long cachedSize;\n\n  /**\n   * Creates a lazy resource, when the size is unknown.\n   *\n   * @param resourceProvider The resource provider loads data on demand.\n   * @param href The resource's href within the epub.\n   */\n  public LazyResource(LazyResourceProvider resourceProvider, String href) {\n    this(resourceProvider, -1, href);\n  }\n  public LazyResource(LazyResourceProvider resourceProvider, String href, String originalHref) {\n    this(resourceProvider, -1, href, originalHref);\n  }\n\n  /**\n   * Creates a Lazy resource, by not actually loading the data for this entry.\n   *\n   * The data will be loaded on the first call to getData()\n   *\n   * @param resourceProvider The resource provider loads data on demand.\n   * @param size The size of this resource.\n   * @param href The resource's href within the epub.\n   */\n  public LazyResource(\n          LazyResourceProvider resourceProvider, long size, String href) {\n    super(null, null, href, MediaTypes.determineMediaType(href));\n    this.resourceProvider = resourceProvider;\n    this.cachedSize = size;\n  }\n  public LazyResource(\n      LazyResourceProvider resourceProvider, long size, String href, String originalHref) {\n    super(null, null, href, originalHref, MediaTypes.determineMediaType(href));\n    this.resourceProvider = resourceProvider;\n    this.cachedSize = size;\n  }\n\n  /**\n   * Gets the contents of the Resource as an InputStream.\n   *\n   * @return The contents of the Resource.\n   *\n   * @throws IOException IOException\n   */\n  public InputStream getInputStream() throws IOException {\n    if (isInitialized()) {\n      return new ByteArrayInputStream(getData());\n    } else {\n      return resourceProvider.getResourceStream(this.originalHref);\n    }\n  }\n\n  /**\n   * Initializes the resource by loading its data into memory.\n   *\n   * @throws IOException IOException\n   */\n  public void initialize() throws IOException {\n    getData();\n  }\n\n  /**\n   * The contents of the resource as a byte[]\n   *\n   * If this resource was lazy-loaded and the data was not yet loaded,\n   * it will be loaded into memory at this point.\n   *  This included opening the zip file, so expect a first load to be slow.\n   *\n   * @return The contents of the resource\n   */\n  public byte[] getData() throws IOException {\n\n    if (data == null) {\n\n      // Log.d(TAG, \"Initializing lazy resource: \" + this.getHref());\n\n      InputStream in = resourceProvider.getResourceStream(this.originalHref);\n      byte[] readData = IOUtil.toByteArray(in, (int) this.cachedSize);\n      if (readData == null) {\n        throw new IOException(\n            \"Could not load the contents of resource: \" + this.getHref());\n      } else {\n        this.data = readData;\n      }\n\n      in.close();\n    }\n\n    return data;\n  }\n\n  /**\n   * Tells this resource to release its cached data.\n   *\n   * If this resource was not lazy-loaded, this is a no-op.\n   */\n  public void close() {\n    if (this.resourceProvider != null) {\n      this.data = null;\n    }\n  }\n\n  /**\n   * Returns if the data for this resource has been loaded into memory.\n   *\n   * @return true if data was loaded.\n   */\n  public boolean isInitialized() {\n    return data != null;\n  }\n\n  /**\n   * Returns the size of this resource in bytes.\n   *\n   * @return the size.\n   */\n  public long getSize() {\n    if (data != null) {\n      return data.length;\n    }\n\n    return cachedSize;\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/LazyResourceProvider.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * @author jake\n */\npublic interface LazyResourceProvider {\n\n  InputStream getResourceStream(String href) throws IOException;\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/ManifestItemProperties.java",
    "content": "package me.ag2s.epublib.domain;\n@SuppressWarnings(\"unused\")\npublic enum ManifestItemProperties implements ManifestProperties {\n  COVER_IMAGE(\"cover-image\"),\n  MATHML(\"mathml\"),\n  NAV(\"nav\"),\n  REMOTE_RESOURCES(\"remote-resources\"),\n  SCRIPTED(\"scripted\"),\n  SVG(\"svg\"),\n  SWITCH(\"switch\");\n\n  private final String name;\n\n  ManifestItemProperties(String name) {\n    this.name = name;\n  }\n\n  public String getName() {\n    return name;\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/ManifestItemRefProperties.java",
    "content": "package me.ag2s.epublib.domain;\n@SuppressWarnings(\"unused\")\npublic enum ManifestItemRefProperties implements ManifestProperties {\n\tPAGE_SPREAD_LEFT(\"page-spread-left\"),\n\tPAGE_SPREAD_RIGHT(\"page-spread-right\");\n\t\n\tprivate final String name;\n\t\n\tManifestItemRefProperties(String name) {\n\t\tthis.name = name;\n\t}\n\t\n\tpublic String getName() {\n\t\treturn name;\n\t}\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/ManifestProperties.java",
    "content": "package me.ag2s.epublib.domain;\n\npublic interface ManifestProperties {\n\n  String getName();\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/MediaType.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.Serializable;\nimport java.util.Arrays;\nimport java.util.Collection;\n\n/**\n * MediaType is used to tell the type of content a resource is.\n *\n * Examples of mediatypes are image/gif, text/css and application/xhtml+xml\n *\n * All allowed mediaTypes are maintained bye the MediaTypeService.\n *\n * @see MediaTypes\n *\n * @author paul\n */\npublic class MediaType implements Serializable {\n\n  private static final long serialVersionUID = -7256091153727506788L;\n  private final String name;\n  private final String defaultExtension;\n  private final Collection<String> extensions;\n\n  public MediaType(String name, String defaultExtension) {\n    this(name, defaultExtension, new String[]{defaultExtension});\n  }\n\n  public MediaType(String name, String defaultExtension,\n      String[] extensions) {\n    this(name, defaultExtension, Arrays.asList(extensions));\n  }\n\n  public int hashCode() {\n    if (name == null) {\n      return 0;\n    }\n    return name.hashCode();\n  }\n\n  public MediaType(String name, String defaultExtension,\n      Collection<String> mextensions) {\n    super();\n    this.name = name;\n    this.defaultExtension = defaultExtension;\n    this.extensions = mextensions;\n  }\n\n  public String getName() {\n    return name;\n  }\n\n\n  public String getDefaultExtension() {\n    return defaultExtension;\n  }\n\n\n  public Collection<String> getExtensions() {\n    return extensions;\n  }\n\n  public boolean equals(Object otherMediaType) {\n    if (!(otherMediaType instanceof MediaType)) {\n      return false;\n    }\n    return name.equals(((MediaType) otherMediaType).getName());\n  }\n  @SuppressWarnings(\"NullableProblems\")\n  public String toString() {\n    return name;\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/MediaTypes.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport me.ag2s.epublib.util.StringUtil;\nimport java.util.HashMap;\nimport java.util.Map;\n\n\n/**\n * Manages mediatypes that are used by epubs\n *\n * @author paul\n */\npublic class MediaTypes {\n\n  public static final MediaType XHTML = new MediaType(\"application/xhtml+xml\",\n      \".xhtml\", new String[]{\".htm\", \".html\", \".xhtml\"});\n  public static final MediaType EPUB = new MediaType(\"application/epub+zip\",\n      \".epub\");\n  public static final MediaType NCX = new MediaType(\"application/x-dtbncx+xml\",\n      \".ncx\");\n\n  public static final MediaType JAVASCRIPT = new MediaType(\"text/javascript\",\n      \".js\");\n  public static final MediaType CSS = new MediaType(\"text/css\", \".css\");\n\n  // images\n  public static final MediaType JPG = new MediaType(\"image/jpeg\", \".jpg\",\n      new String[]{\".jpg\", \".jpeg\"});\n  public static final MediaType PNG = new MediaType(\"image/png\", \".png\");\n  public static final MediaType GIF = new MediaType(\"image/gif\", \".gif\");\n\n  public static final MediaType SVG = new MediaType(\"image/svg+xml\", \".svg\");\n\n  // fonts\n  public static final MediaType TTF = new MediaType(\n      \"application/x-truetype-font\", \".ttf\");\n  public static final MediaType OPENTYPE = new MediaType(\n      \"application/vnd.ms-opentype\", \".otf\");\n  public static final MediaType WOFF = new MediaType(\"application/font-woff\",\n      \".woff\");\n\n  // audio\n  public static final MediaType MP3 = new MediaType(\"audio/mpeg\", \".mp3\");\n  public static final MediaType OGG = new MediaType(\"audio/ogg\", \".ogg\");\n\n  // video\n  public static final MediaType MP4 = new MediaType(\"video/mp4\", \".mp4\");\n\n  public static final MediaType SMIL = new MediaType(\"application/smil+xml\",\n      \".smil\");\n  public static final MediaType XPGT = new MediaType(\n      \"application/adobe-page-template+xml\", \".xpgt\");\n  public static final MediaType PLS = new MediaType(\"application/pls+xml\",\n      \".pls\");\n\n  public static final MediaType[] mediaTypes = new MediaType[]{\n      XHTML, EPUB, JPG, PNG, GIF, CSS, SVG, TTF, NCX, XPGT, OPENTYPE, WOFF,\n      SMIL, PLS, JAVASCRIPT, MP3, MP4, OGG\n  };\n\n  public static final Map<String, MediaType> mediaTypesByName = new HashMap<>();\n\n  static {\n    for (MediaType mediaType : mediaTypes) {\n      mediaTypesByName.put(mediaType.getName(), mediaType);\n    }\n  }\n\n  public static boolean isBitmapImage(MediaType mediaType) {\n    return mediaType == JPG || mediaType == PNG || mediaType == GIF;\n  }\n\n  /**\n   * Gets the MediaType based on the file extension.\n   * Null of no matching extension found.\n   *\n   * @param filename filename\n   * @return the MediaType based on the file extension.\n   */\n  public static MediaType determineMediaType(String filename) {\n    for (MediaType mediaType : mediaTypesByName.values()) {\n      for (String extension : mediaType.getExtensions()) {\n        if (StringUtil.endsWithIgnoreCase(filename, extension)) {\n          return mediaType;\n        }\n      }\n    }\n    return null;\n  }\n\n  public static MediaType getMediaTypeByName(String mediaTypeName) {\n    return mediaTypesByName.get(mediaTypeName);\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/Metadata.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport me.ag2s.epublib.util.StringUtil;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport javax.xml.namespace.QName;\n\n/**\n * A Book's collection of Metadata.\n * In the future it should contain all Dublin Core attributes, for now\n * it contains a set of often-used ones.\n *\n * @author paul\n */\npublic class Metadata implements Serializable {\n\n    private static final long serialVersionUID = -2437262888962149444L;\n\n    public static final String DEFAULT_LANGUAGE = \"en\";\n\n    private boolean autoGeneratedId;//true;\n    private List<Author> authors = new ArrayList<>();\n    private List<Author> contributors = new ArrayList<>();\n    private List<Date> dates = new ArrayList<>();\n    private String language = DEFAULT_LANGUAGE;\n    private Map<QName, String> otherProperties = new HashMap<>();\n    private List<String> rights = new ArrayList<>();\n    private List<String> titles = new ArrayList<>();\n    private List<Identifier> identifiers = new ArrayList<>();\n    private List<String> subjects = new ArrayList<>();\n    private String format = MediaTypes.EPUB.getName();\n    private List<String> types = new ArrayList<>();\n    private List<String> descriptions = new ArrayList<>();\n    private List<String> publishers = new ArrayList<>();\n    private Map<String, String> metaAttributes = new HashMap<>();\n\n    public Metadata() {\n        identifiers.add(new Identifier());\n        autoGeneratedId = true;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public boolean isAutoGeneratedId() {\n        return autoGeneratedId;\n    }\n\n    /**\n     * Metadata properties not hard-coded like the author, title, etc.\n     *\n     * @return Metadata properties not hard-coded like the author, title, etc.\n     */\n    public Map<QName, String> getOtherProperties() {\n        return otherProperties;\n    }\n\n    public void setOtherProperties(Map<QName, String> otherProperties) {\n        this.otherProperties = otherProperties;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public Date addDate(Date date) {\n        this.dates.add(date);\n        return date;\n    }\n\n    public List<Date> getDates() {\n        return dates;\n    }\n\n    public void setDates(List<Date> dates) {\n        this.dates = dates;\n    }\n\n    @SuppressWarnings(\"UnusedReturnValue\")\n    public Author addAuthor(Author author) {\n        authors.add(author);\n        return author;\n    }\n\n    public List<Author> getAuthors() {\n        return authors;\n    }\n\n    public void setAuthors(List<Author> authors) {\n        this.authors = authors;\n    }\n\n    @SuppressWarnings(\"UnusedReturnValue\")\n    public Author addContributor(Author contributor) {\n        contributors.add(contributor);\n        return contributor;\n    }\n\n    public List<Author> getContributors() {\n        return contributors;\n    }\n\n    public void setContributors(List<Author> contributors) {\n        this.contributors = contributors;\n    }\n\n    public String getLanguage() {\n        return language;\n    }\n\n    public void setLanguage(String language) {\n        this.language = language;\n    }\n\n    public List<String> getSubjects() {\n        return subjects;\n    }\n\n    public void setSubjects(List<String> subjects) {\n        this.subjects = subjects;\n    }\n\n    public void setRights(List<String> rights) {\n        this.rights = rights;\n    }\n\n    public List<String> getRights() {\n        return rights;\n    }\n\n\n    /**\n     * Gets the first non-blank title of the book.\n     * Will return \"\" if no title found.\n     *\n     * @return the first non-blank title of the book.\n     */\n    public String getFirstTitle() {\n        if (titles == null || titles.isEmpty()) {\n            return \"\";\n        }\n        for (String title : titles) {\n            if (StringUtil.isNotBlank(title)) {\n                return title;\n            }\n        }\n        return \"\";\n    }\n\n    public String addTitle(String title) {\n        this.titles.add(title);\n        return title;\n    }\n\n    public void setTitles(List<String> titles) {\n        this.titles = titles;\n    }\n\n    public List<String> getTitles() {\n        return titles;\n    }\n\n    @SuppressWarnings(\"UnusedReturnValue\")\n    public String addPublisher(String publisher) {\n        this.publishers.add(publisher);\n        return publisher;\n    }\n\n    public void setPublishers(List<String> publishers) {\n        this.publishers = publishers;\n    }\n\n    public List<String> getPublishers() {\n        return publishers;\n    }\n\n    @SuppressWarnings(\"UnusedReturnValue\")\n    public String addDescription(String description) {\n        this.descriptions.add(description);\n        return description;\n    }\n\n    public void setDescriptions(List<String> descriptions) {\n        this.descriptions = descriptions;\n    }\n\n    public List<String> getDescriptions() {\n        return descriptions;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public Identifier addIdentifier(Identifier identifier) {\n        if (autoGeneratedId && (!(identifiers.isEmpty()))) {\n            identifiers.set(0, identifier);\n        } else {\n            identifiers.add(identifier);\n        }\n        autoGeneratedId = false;\n        return identifier;\n    }\n\n    public void setIdentifiers(List<Identifier> identifiers) {\n        this.identifiers = identifiers;\n        autoGeneratedId = false;\n    }\n\n    public List<Identifier> getIdentifiers() {\n        return identifiers;\n    }\n\n    public void setFormat(String format) {\n        this.format = format;\n    }\n\n    public String getFormat() {\n        return format;\n    }\n\n    @SuppressWarnings(\"UnusedReturnValue\")\n    public String addType(String type) {\n        this.types.add(type);\n        return type;\n    }\n\n    public List<String> getTypes() {\n        return types;\n    }\n\n    public void setTypes(List<String> types) {\n        this.types = types;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public String getMetaAttribute(String name) {\n        return metaAttributes.get(name);\n    }\n\n    public void setMetaAttributes(Map<String, String> metaAttributes) {\n        this.metaAttributes = metaAttributes;\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/Relator.java",
    "content": "package me.ag2s.epublib.domain;\n\n\n/**\n * A relator denotes which role a certain individual had in the creation/modification of the ebook.\n *\n * Examples are 'creator', 'blurb writer', etc.\n *\n * This is contains the complete Library of Concress relator list.\n *\n * @see <a href=\"http://www.loc.gov/marc/relators/relaterm.html\">MARC Code List for Relators</a>\n *\n * @author paul\n */\npublic enum Relator {\n\n  /**\n   * Use for a person or organization who principally exhibits acting skills in a musical or dramatic presentation or entertainment.\n   */\n  ACTOR(\"act\", \"Actor\"),\n\n  /**\n   * Use for a person or organization who 1) reworks a musical composition, usually for a different medium, or 2) rewrites novels or stories for motion pictures or other audiovisual medium.\n   */\n  ADAPTER(\"adp\", \"Adapter\"),\n\n  /**\n   * Use for a person or organization that reviews, examines and interprets data or information in a specific area.\n   */\n  ANALYST(\"anl\", \"Analyst\"),\n\n  /**\n   * Use for a person or organization who draws the two-dimensional figures, manipulates the three dimensional objects and/or also programs the computer to move objects and images for the purpose of animated film processing. Animation cameras, stands, celluloid screens, transparencies and inks are some of the tools of the animator.\n   */\n  ANIMATOR(\"anm\", \"Animator\"),\n\n  /**\n   * Use for a person who writes manuscript annotations on a printed item.\n   */\n  ANNOTATOR(\"ann\", \"Annotator\"),\n\n  /**\n   * Use for a person or organization responsible for the submission of an application or who is named as eligible for the results of the processing of the application (e.g., bestowing of rights, reward, title, position).\n   */\n  APPLICANT(\"app\", \"Applicant\"),\n\n  /**\n   * Use for a person or organization who designs structures or oversees their construction.\n   */\n  ARCHITECT(\"arc\", \"Architect\"),\n\n  /**\n   * Use for a person or organization who transcribes a musical composition, usually for a different medium from that of the original; in an arrangement the musical substance remains essentially unchanged.\n   */\n  ARRANGER(\"arr\", \"Arranger\"),\n\n  /**\n   * Use for a person (e.g., a painter or sculptor) who makes copies of works of visual art.\n   */\n  ART_COPYIST(\"acp\", \"Art copyist\"),\n\n  /**\n   * Use for a person (e.g., a painter) or organization who conceives, and perhaps also implements, an original graphic design or work of art, if specific codes (e.g., [egr], [etr]) are not desired. For book illustrators, prefer Illustrator [ill].\n   */\n  ARTIST(\"art\", \"Artist\"),\n\n  /**\n   * Use for a person responsible for controlling the development of the artistic style of an entire production, including the choice of works to be presented and selection of senior production staff.\n   */\n  ARTISTIC_DIRECTOR(\"ard\", \"Artistic director\"),\n\n  /**\n   * Use for a person or organization to whom a license for printing or publishing has been transferred.\n   */\n  ASSIGNEE(\"asg\", \"Assignee\"),\n\n  /**\n   * Use for a person or organization associated with or found in an item or collection, which cannot be determined to be that of a Former owner [fmo] or other designated relator indicative of provenance.\n   */\n  ASSOCIATED_NAME(\"asn\", \"Associated name\"),\n\n  /**\n   * Use for an author, artist, etc., relating him/her to a work for which there is or once was substantial authority for designating that person as author, creator, etc. of the work.\n   */\n  ATTRIBUTED_NAME(\"att\", \"Attributed name\"),\n\n  /**\n   * Use for a person or organization in charge of the estimation and public auctioning of goods, particularly books, artistic works, etc.\n   */\n  AUCTIONEER(\"auc\", \"Auctioneer\"),\n\n  /**\n   * Use for a person or organization chiefly responsible for the intellectual or artistic content of a work, usually printed text. This term may also be used when more than one person or body bears such responsibility.\n   */\n  AUTHOR(\"aut\", \"Author\"),\n\n  /**\n   * Use for a person or organization whose work is largely quoted or extracted in works to which he or she did not contribute directly. Such quotations are found particularly in exhibition catalogs, collections of photographs, etc.\n   */\n  AUTHOR_IN_QUOTATIONS_OR_TEXT_EXTRACTS(\"aqt\",\n      \"Author in quotations or text extracts\"),\n\n  /**\n   * Use for a person or organization responsible for an afterword, postface, colophon, etc. but who is not the chief author of a work.\n   */\n  AUTHOR_OF_AFTERWORD_COLOPHON_ETC(\"aft\",\n      \"Author of afterword, colophon, etc.\"),\n\n  /**\n   * Use for a person or organization responsible for the dialog or spoken commentary for a screenplay or sound recording.\n   */\n  AUTHOR_OF_DIALOG(\"aud\", \"Author of dialog\"),\n\n  /**\n   * Use for a person or organization responsible for an introduction, preface, foreword, or other critical introductory matter, but who is not the chief author.\n   */\n  AUTHOR_OF_INTRODUCTION_ETC(\"aui\", \"Author of introduction, etc.\"),\n\n  /**\n   * Use for a person or organization responsible for a motion picture screenplay, dialog, spoken commentary, etc.\n   */\n  AUTHOR_OF_SCREENPLAY_ETC(\"aus\", \"Author of screenplay, etc.\"),\n\n  /**\n   * Use for a person or organization responsible for a work upon which the work represented by the catalog record is based. This may be appropriate for adaptations, sequels, continuations, indexes, etc.\n   */\n  BIBLIOGRAPHIC_ANTECEDENT(\"ant\", \"Bibliographic antecedent\"),\n\n  /**\n   * Use for a person or organization responsible for the binding of printed or manuscript materials.\n   */\n  BINDER(\"bnd\", \"Binder\"),\n\n  /**\n   * Use for a person or organization responsible for the binding design of a book, including the type of binding, the type of materials used, and any decorative aspects of the binding.\n   */\n  BINDING_DESIGNER(\"bdd\", \"Binding designer\"),\n\n  /**\n   * Use for the named entity responsible for writing a commendation or testimonial for a work, which appears on or within the publication itself, frequently on the back or dust jacket of print publications or on advertising material for all media.\n   */\n  BLURB_WRITER(\"blw\", \"Blurb writer\"),\n\n  /**\n   * Use for a person or organization responsible for the entire graphic design of a book, including arrangement of type and illustration, choice of materials, and process used.\n   */\n  BOOK_DESIGNER(\"bkd\", \"Book designer\"),\n\n  /**\n   * Use for a person or organization responsible for the production of books and other print media, if specific codes (e.g., [bkd], [egr], [tyd], [prt]) are not desired.\n   */\n  BOOK_PRODUCER(\"bkp\", \"Book producer\"),\n\n  /**\n   * Use for a person or organization responsible for the design of flexible covers designed for or published with a book, including the type of materials used, and any decorative aspects of the bookjacket.\n   */\n  BOOKJACKET_DESIGNER(\"bjd\", \"Bookjacket designer\"),\n\n  /**\n   * Use for a person or organization responsible for the design of a book owner's identification label that is most commonly pasted to the inside front cover of a book.\n   */\n  BOOKPLATE_DESIGNER(\"bpd\", \"Bookplate designer\"),\n\n  /**\n   * Use for a person or organization who makes books and other bibliographic materials available for purchase. Interest in the materials is primarily lucrative.\n   */\n  BOOKSELLER(\"bsl\", \"Bookseller\"),\n\n  /**\n   * Use for a person or organization who writes in an artistic hand, usually as a copyist and or engrosser.\n   */\n  CALLIGRAPHER(\"cll\", \"Calligrapher\"),\n\n  /**\n   * Use for a person or organization responsible for the creation of maps and other cartographic materials.\n   */\n  CARTOGRAPHER(\"ctg\", \"Cartographer\"),\n\n  /**\n   * Use for a censor, bowdlerizer, expurgator, etc., official or private.\n   */\n  CENSOR(\"cns\", \"Censor\"),\n\n  /**\n   * Use for a person or organization who composes or arranges dances or other movements (e.g., \"master of swords\") for a musical or dramatic presentation or entertainment.\n   */\n  CHOREOGRAPHER(\"chr\", \"Choreographer\"),\n\n  /**\n   * Use for a person or organization who is in charge of the images captured for a motion picture film. The cinematographer works under the supervision of a director, and may also be referred to as director of photography. Do not confuse with videographer.\n   */\n  CINEMATOGRAPHER(\"cng\", \"Cinematographer\"),\n\n  /**\n   * Use for a person or organization for whom another person or organization is acting.\n   */\n  CLIENT(\"cli\", \"Client\"),\n\n  /**\n   * Use for a person or organization that takes a limited part in the elaboration of a work of another person or organization that brings complements (e.g., appendices, notes) to the work.\n   */\n  COLLABORATOR(\"clb\", \"Collaborator\"),\n\n  /**\n   * Use for a person or organization who has brought together material from various sources that has been arranged, described, and cataloged as a collection. A collector is neither the creator of the material nor a person to whom manuscripts in the collection may have been addressed.\n   */\n  COLLECTOR(\"col\", \"Collector\"),\n\n  /**\n   * Use for a person or organization responsible for the production of photographic prints from film or other colloid that has ink-receptive and ink-repellent surfaces.\n   */\n  COLLOTYPER(\"clt\", \"Collotyper\"),\n\n  /**\n   * Use for the named entity responsible for applying color to drawings, prints, photographs, maps, moving images, etc.\n   */\n  COLORIST(\"clr\", \"Colorist\"),\n\n  /**\n   * Use for a person or organization who provides interpretation, analysis, or a discussion of the subject matter on a recording, motion picture, or other audiovisual medium.\n   */\n  COMMENTATOR(\"cmm\", \"Commentator\"),\n\n  /**\n   * Use for a person or organization responsible for the commentary or explanatory notes about a text. For the writer of manuscript annotations in a printed book, use Annotator [ann].\n   */\n  COMMENTATOR_FOR_WRITTEN_TEXT(\"cwt\", \"Commentator for written text\"),\n\n  /**\n   * Use for a person or organization who produces a work or publication by selecting and putting together material from the works of various persons or bodies.\n   */\n  COMPILER(\"com\", \"Compiler\"),\n\n  /**\n   * Use for the party who applies to the courts for redress, usually in an equity proceeding.\n   */\n  COMPLAINANT(\"cpl\", \"Complainant\"),\n\n  /**\n   * Use for a complainant who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding.\n   */\n  COMPLAINANT_APPELLANT(\"cpt\", \"Complainant-appellant\"),\n\n  /**\n   * Use for a complainant against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding.\n   */\n  COMPLAINANT_APPELLEE(\"cpe\", \"Complainant-appellee\"),\n\n  /**\n   * Use for a person or organization who creates a musical work, usually a piece of music in manuscript or printed form.\n   */\n  COMPOSER(\"cmp\", \"Composer\"),\n\n  /**\n   * Use for a person or organization responsible for the creation of metal slug, or molds made of other materials, used to produce the text and images in printed matter.\n   */\n  COMPOSITOR(\"cmt\", \"Compositor\"),\n\n  /**\n   * Use for a person or organization responsible for the original idea on which a work is based, this includes the scientific author of an audio-visual item and the conceptor of an advertisement.\n   */\n  CONCEPTOR(\"ccp\", \"Conceptor\"),\n\n  /**\n   * Use for a person who directs a performing group (orchestra, chorus, opera, etc.) in a musical or dramatic presentation or entertainment.\n   */\n  CONDUCTOR(\"cnd\", \"Conductor\"),\n\n  /**\n   * Use for the named entity responsible for documenting, preserving, or treating printed or manuscript material, works of art, artifacts, or other media.\n   */\n  CONSERVATOR(\"con\", \"Conservator\"),\n\n  /**\n   * Use for a person or organization relevant to a resource, who is called upon for professional advice or services in a specialized field of knowledge or training.\n   */\n  CONSULTANT(\"csl\", \"Consultant\"),\n\n  /**\n   * Use for a person or organization relevant to a resource, who is engaged specifically to provide an intellectual overview of a strategic or operational task and by analysis, specification, or instruction, to create or propose a cost-effective course of action or solution.\n   */\n  CONSULTANT_TO_A_PROJECT(\"csp\", \"Consultant to a project\"),\n\n  /**\n   * Use for the party who opposes, resists, or disputes, in a court of law, a claim, decision, result, etc.\n   */\n  CONTESTANT(\"cos\", \"Contestant\"),\n\n  /**\n   * Use for a contestant who takes an appeal from one court of law or jurisdiction to another to reverse the judgment.\n   */\n  CONTESTANT_APPELLANT(\"cot\", \"Contestant-appellant\"),\n\n  /**\n   * Use for a contestant against whom an appeal is taken from one court of law or jurisdiction to another to reverse the judgment.\n   */\n  CONTESTANT_APPELLEE(\"coe\", \"Contestant-appellee\"),\n\n  /**\n   * Use for the party defending a claim, decision, result, etc. being opposed, resisted, or disputed in a court of law.\n   */\n  CONTESTEE(\"cts\", \"Contestee\"),\n\n  /**\n   * Use for a contestee who takes an appeal from one court or jurisdiction to another to reverse the judgment.\n   */\n  CONTESTEE_APPELLANT(\"ctt\", \"Contestee-appellant\"),\n\n  /**\n   * Use for a contestee against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment.\n   */\n  CONTESTEE_APPELLEE(\"cte\", \"Contestee-appellee\"),\n\n  /**\n   * Use for a person or organization relevant to a resource, who enters into a contract with another person or organization to perform a specific task.\n   */\n  CONTRACTOR(\"ctr\", \"Contractor\"),\n\n  /**\n   * Use for a person or organization one whose work has been contributed to a larger work, such as an anthology, serial publication, or other compilation of individual works. Do not use if the sole function in relation to a work is as author, editor, compiler or translator.\n   */\n  CONTRIBUTOR(\"ctb\", \"Contributor\"),\n\n  /**\n   * Use for a person or organization listed as a copyright owner at the time of registration. Copyright can be granted or later transferred to another person or organization, at which time the claimant becomes the copyright holder.\n   */\n  COPYRIGHT_CLAIMANT(\"cpc\", \"Copyright claimant\"),\n\n  /**\n   * Use for a person or organization to whom copy and legal rights have been granted or transferred for the intellectual content of a work. The copyright holder, although not necessarily the creator of the work, usually has the exclusive right to benefit financially from the sale and use of the work to which the associated copyright protection applies.\n   */\n  COPYRIGHT_HOLDER(\"cph\", \"Copyright holder\"),\n\n  /**\n   * Use for a person or organization who is a corrector of manuscripts, such as the scriptorium official who corrected the work of a scribe. For printed matter, use Proofreader.\n   */\n  CORRECTOR(\"crr\", \"Corrector\"),\n\n  /**\n   * Use for a person or organization who was either the writer or recipient of a letter or other communication.\n   */\n  CORRESPONDENT(\"crp\", \"Correspondent\"),\n\n  /**\n   * Use for a person or organization who designs or makes costumes, fixes hair, etc., for a musical or dramatic presentation or entertainment.\n   */\n  COSTUME_DESIGNER(\"cst\", \"Costume designer\"),\n\n  /**\n   * Use for a person or organization responsible for the graphic design of a book cover, album cover, slipcase, box, container, etc. For a person or organization responsible for the graphic design of an entire book, use Book designer; for book jackets, use Bookjacket designer.\n   */\n  COVER_DESIGNER(\"cov\", \"Cover designer\"),\n\n  /**\n   * Use for a person or organization responsible for the intellectual or artistic content of a work.\n   */\n  CREATOR(\"cre\", \"Creator\"),\n\n  /**\n   * Use for a person or organization responsible for conceiving and organizing an exhibition.\n   */\n  CURATOR_OF_AN_EXHIBITION(\"cur\", \"Curator of an exhibition\"),\n\n  /**\n   * Use for a person or organization who principally exhibits dancing skills in a musical or dramatic presentation or entertainment.\n   */\n  DANCER(\"dnc\", \"Dancer\"),\n\n  /**\n   * Use for a person or organization that submits data for inclusion in a database or other collection of data.\n   */\n  DATA_CONTRIBUTOR(\"dtc\", \"Data contributor\"),\n\n  /**\n   * Use for a person or organization responsible for managing databases or other data sources.\n   */\n  DATA_MANAGER(\"dtm\", \"Data manager\"),\n\n  /**\n   * Use for a person or organization to whom a book, manuscript, etc., is dedicated (not the recipient of a gift).\n   */\n  DEDICATEE(\"dte\", \"Dedicatee\"),\n\n  /**\n   * Use for the author of a dedication, which may be a formal statement or in epistolary or verse form.\n   */\n  DEDICATOR(\"dto\", \"Dedicator\"),\n\n  /**\n   * Use for the party defending or denying allegations made in a suit and against whom relief or recovery is sought in the courts, usually in a legal action.\n   */\n  DEFENDANT(\"dfd\", \"Defendant\"),\n\n  /**\n   * Use for a defendant who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in a legal action.\n   */\n  DEFENDANT_APPELLANT(\"dft\", \"Defendant-appellant\"),\n\n  /**\n   * Use for a defendant against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in a legal action.\n   */\n  DEFENDANT_APPELLEE(\"dfe\", \"Defendant-appellee\"),\n\n  /**\n   * Use for the organization granting a degree for which the thesis or dissertation described was presented.\n   */\n  DEGREE_GRANTOR(\"dgg\", \"Degree grantor\"),\n\n  /**\n   * Use for a person or organization executing technical drawings from others' designs.\n   */\n  DELINEATOR(\"dln\", \"Delineator\"),\n\n  /**\n   * Use for an entity depicted or portrayed in a work, particularly in a work of art.\n   */\n  DEPICTED(\"dpc\", \"Depicted\"),\n\n  /**\n   * Use for a person or organization placing material in the physical custody of a library or repository without transferring the legal title.\n   */\n  DEPOSITOR(\"dpt\", \"Depositor\"),\n\n  /**\n   * Use for a person or organization responsible for the design if more specific codes (e.g., [bkd], [tyd]) are not desired.\n   */\n  DESIGNER(\"dsr\", \"Designer\"),\n\n  /**\n   * Use for a person or organization who is responsible for the general management of a work or who supervises the production of a performance for stage, screen, or sound recording.\n   */\n  DIRECTOR(\"drt\", \"Director\"),\n\n  /**\n   * Use for a person who presents a thesis for a university or higher-level educational degree.\n   */\n  DISSERTANT(\"dis\", \"Dissertant\"),\n\n  /**\n   * Use for the name of a place from which a resource, e.g., a serial, is distributed.\n   */\n  DISTRIBUTION_PLACE(\"dbp\", \"Distribution place\"),\n\n  /**\n   * Use for a person or organization that has exclusive or shared marketing rights for an item.\n   */\n  DISTRIBUTOR(\"dst\", \"Distributor\"),\n\n  /**\n   * Use for a person or organization who is the donor of a book, manuscript, etc., to its present owner. Donors to previous owners are designated as Former owner [fmo] or Inscriber [ins].\n   */\n  DONOR(\"dnr\", \"Donor\"),\n\n  /**\n   * Use for a person or organization who prepares artistic or technical drawings.\n   */\n  DRAFTSMAN(\"drm\", \"Draftsman\"),\n\n  /**\n   * Use for a person or organization to which authorship has been dubiously or incorrectly ascribed.\n   */\n  DUBIOUS_AUTHOR(\"dub\", \"Dubious author\"),\n\n  /**\n   * Use for a person or organization who prepares for publication a work not primarily his/her own, such as by elucidating text, adding introductory or other critical matter, or technically directing an editorial staff.\n   */\n  EDITOR(\"edt\", \"Editor\"),\n\n  /**\n   * Use for a person responsible for setting up a lighting rig and focusing the lights for a production, and running the lighting at a performance.\n   */\n  ELECTRICIAN(\"elg\", \"Electrician\"),\n\n  /**\n   * Use for a person or organization who creates a duplicate printing surface by pressure molding and electrodepositing of metal that is then backed up with lead for printing.\n   */\n  ELECTROTYPER(\"elt\", \"Electrotyper\"),\n\n  /**\n   * Use for a person or organization that is responsible for technical planning and design, particularly with construction.\n   */\n  ENGINEER(\"eng\", \"Engineer\"),\n\n  /**\n   * Use for a person or organization who cuts letters, figures, etc. on a surface, such as a wooden or metal plate, for printing.\n   */\n  ENGRAVER(\"egr\", \"Engraver\"),\n\n  /**\n   * Use for a person or organization who produces text or images for printing by subjecting metal, glass, or some other surface to acid or the corrosive action of some other substance.\n   */\n  ETCHER(\"etr\", \"Etcher\"),\n\n  /**\n   * Use for the name of the place where an event such as a conference or a concert took place.\n   */\n  EVENT_PLACE(\"evp\", \"Event place\"),\n\n  /**\n   * Use for a person or organization in charge of the description and appraisal of the value of goods, particularly rare items, works of art, etc.\n   */\n  EXPERT(\"exp\", \"Expert\"),\n\n  /**\n   * Use for a person or organization that executed the facsimile.\n   */\n  FACSIMILIST(\"fac\", \"Facsimilist\"),\n\n  /**\n   * Use for a person or organization that manages or supervises the work done to collect raw data or do research in an actual setting or environment (typically applies to the natural and social sciences).\n   */\n  FIELD_DIRECTOR(\"fld\", \"Field director\"),\n\n  /**\n   * Use for a person or organization who is an editor of a motion picture film. This term is used regardless of the medium upon which the motion picture is produced or manufactured (e.g., acetate film, video tape).\n   */\n  FILM_EDITOR(\"flm\", \"Film editor\"),\n\n  /**\n   * Use for a person or organization who is identified as the only party or the party of the first part. In the case of transfer of right, this is the assignor, transferor, licensor, grantor, etc. Multiple parties can be named jointly as the first party\n   */\n  FIRST_PARTY(\"fpy\", \"First party\"),\n\n  /**\n   * Use for a person or organization who makes or imitates something of value or importance, especially with the intent to defraud.\n   */\n  FORGER(\"frg\", \"Forger\"),\n\n  /**\n   * Use for a person or organization who owned an item at any time in the past. Includes those to whom the material was once presented. A person or organization giving the item to the present owner is designated as Donor [dnr]\n   */\n  FORMER_OWNER(\"fmo\", \"Former owner\"),\n\n  /**\n   * Use for a person or organization that furnished financial support for the production of the work.\n   */\n  FUNDER(\"fnd\", \"Funder\"),\n\n  /**\n   * Use for a person responsible for geographic information system (GIS) development and integration with global positioning system data.\n   */\n  GEOGRAPHIC_INFORMATION_SPECIALIST(\"gis\", \"Geographic information specialist\"),\n\n  /**\n   * Use for a person or organization in memory or honor of whom a book, manuscript, etc. is donated.\n   */\n  HONOREE(\"hnr\", \"Honoree\"),\n\n  /**\n   * Use for a person who is invited or regularly leads a program (often broadcast) that includes other guests, performers, etc. (e.g., talk show host).\n   */\n  HOST(\"hst\", \"Host\"),\n\n  /**\n   * Use for a person or organization responsible for the decoration of a work (especially manuscript material) with precious metals or color, usually with elaborate designs and motifs.\n   */\n  ILLUMINATOR(\"ilu\", \"Illuminator\"),\n\n  /**\n   * Use for a person or organization who conceives, and perhaps also implements, a design or illustration, usually to accompany a written text.\n   */\n  ILLUSTRATOR(\"ill\", \"Illustrator\"),\n\n  /**\n   * Use for a person who signs a presentation statement.\n   */\n  INSCRIBER(\"ins\", \"Inscriber\"),\n\n  /**\n   * Use for a person or organization who principally plays an instrument in a musical or dramatic presentation or entertainment.\n   */\n  INSTRUMENTALIST(\"itr\", \"Instrumentalist\"),\n\n  /**\n   * Use for a person or organization who is interviewed at a consultation or meeting, usually by a reporter, pollster, or some other information gathering agent.\n   */\n  INTERVIEWEE(\"ive\", \"Interviewee\"),\n\n  /**\n   * Use for a person or organization who acts as a reporter, pollster, or other information gathering agent in a consultation or meeting involving one or more individuals.\n   */\n  INTERVIEWER(\"ivr\", \"Interviewer\"),\n\n  /**\n   * Use for a person or organization who first produces a particular useful item, or develops a new process for obtaining a known item or result.\n   */\n  INVENTOR(\"inv\", \"Inventor\"),\n\n  /**\n   * Use for an institution that provides scientific analyses of material samples.\n   */\n  LABORATORY(\"lbr\", \"Laboratory\"),\n\n  /**\n   * Use for a person or organization that manages or supervises work done in a controlled setting or environment.\n   */\n  LABORATORY_DIRECTOR(\"ldr\", \"Laboratory director\"),\n\n  /**\n   * Use for a person or organization whose work involves coordinating the arrangement of existing and proposed land features and structures.\n   */\n  LANDSCAPE_ARCHITECT(\"lsa\", \"Landscape architect\"),\n\n  /**\n   * Use to indicate that a person or organization takes primary responsibility for a particular activity or endeavor. Use with another relator term or code to show the greater importance this person or organization has regarding that particular role. If more than one relator is assigned to a heading, use the Lead relator only if it applies to all the relators.\n   */\n  LEAD(\"led\", \"Lead\"),\n\n  /**\n   * Use for a person or organization permitting the temporary use of a book, manuscript, etc., such as for photocopying or microfilming.\n   */\n  LENDER(\"len\", \"Lender\"),\n\n  /**\n   * Use for the party who files a libel in an ecclesiastical or admiralty case.\n   */\n  LIBELANT(\"lil\", \"Libelant\"),\n\n  /**\n   * Use for a libelant who takes an appeal from one ecclesiastical court or admiralty to another to reverse the judgment.\n   */\n  LIBELANT_APPELLANT(\"lit\", \"Libelant-appellant\"),\n\n  /**\n   * Use for a libelant against whom an appeal is taken from one ecclesiastical court or admiralty to another to reverse the judgment.\n   */\n  LIBELANT_APPELLEE(\"lie\", \"Libelant-appellee\"),\n\n  /**\n   * Use for a party against whom a libel has been filed in an ecclesiastical court or admiralty.\n   */\n  LIBELEE(\"lel\", \"Libelee\"),\n\n  /**\n   * Use for a libelee who takes an appeal from one ecclesiastical court or admiralty to another to reverse the judgment.\n   */\n  LIBELEE_APPELLANT(\"let\", \"Libelee-appellant\"),\n\n  /**\n   * Use for a libelee against whom an appeal is taken from one ecclesiastical court or admiralty to another to reverse the judgment.\n   */\n  LIBELEE_APPELLEE(\"lee\", \"Libelee-appellee\"),\n\n  /**\n   * Use for a person or organization who is a writer of the text of an opera, oratorio, etc.\n   */\n  LIBRETTIST(\"lbt\", \"Librettist\"),\n\n  /**\n   * Use for a person or organization who is an original recipient of the right to print or publish.\n   */\n  LICENSEE(\"lse\", \"Licensee\"),\n\n  /**\n   * Use for person or organization who is a signer of the license, imprimatur, etc.\n   */\n  LICENSOR(\"lso\", \"Licensor\"),\n\n  /**\n   * Use for a person or organization who designs the lighting scheme for a theatrical presentation, entertainment, motion picture, etc.\n   */\n  LIGHTING_DESIGNER(\"lgd\", \"Lighting designer\"),\n\n  /**\n   * Use for a person or organization who prepares the stone or plate for lithographic printing, including a graphic artist creating a design directly on the surface from which printing will be done.\n   */\n  LITHOGRAPHER(\"ltg\", \"Lithographer\"),\n\n  /**\n   * Use for a person or organization who is a writer of the text of a song.\n   */\n  LYRICIST(\"lyr\", \"Lyricist\"),\n\n  /**\n   * Use for a person or organization that makes an artifactual work (an object made or modified by one or more persons). Examples of artifactual works include vases, cannons or pieces of furniture.\n   */\n  MANUFACTURER(\"mfr\", \"Manufacturer\"),\n\n  /**\n   * Use for the named entity responsible for marbling paper, cloth, leather, etc. used in construction of a resource.\n   */\n  MARBLER(\"mrb\", \"Marbler\"),\n\n  /**\n   * Use for a person or organization performing the coding of SGML, HTML, or XML markup of metadata, text, etc.\n   */\n  MARKUP_EDITOR(\"mrk\", \"Markup editor\"),\n\n  /**\n   * Use for a person or organization primarily responsible for compiling and maintaining the original description of a metadata set (e.g., geospatial metadata set).\n   */\n  METADATA_CONTACT(\"mdc\", \"Metadata contact\"),\n\n  /**\n   * Use for a person or organization responsible for decorations, illustrations, letters, etc. cut on a metal surface for printing or decoration.\n   */\n  METAL_ENGRAVER(\"mte\", \"Metal-engraver\"),\n\n  /**\n   * Use for a person who leads a program (often broadcast) where topics are discussed, usually with participation of experts in fields related to the discussion.\n   */\n  MODERATOR(\"mod\", \"Moderator\"),\n\n  /**\n   * Use for a person or organization that supervises compliance with the contract and is responsible for the report and controls its distribution. Sometimes referred to as the grantee, or controlling agency.\n   */\n  MONITOR(\"mon\", \"Monitor\"),\n\n  /**\n   * Use for a person who transcribes or copies musical notation\n   */\n  MUSIC_COPYIST(\"mcp\", \"Music copyist\"),\n\n  /**\n   * Use for a person responsible for basic music decisions about a production, including coordinating the work of the composer, the sound editor, and sound mixers, selecting musicians, and organizing and/or conducting sound for rehearsals and performances.\n   */\n  MUSICAL_DIRECTOR(\"msd\", \"Musical director\"),\n\n  /**\n   * Use for a person or organization who performs music or contributes to the musical content of a work when it is not possible or desirable to identify the function more precisely.\n   */\n  MUSICIAN(\"mus\", \"Musician\"),\n\n  /**\n   * Use for a person who is a speaker relating the particulars of an act, occurrence, or course of events.\n   */\n  NARRATOR(\"nrt\", \"Narrator\"),\n\n  /**\n   * Use for a person or organization responsible for opposing a thesis or dissertation.\n   */\n  OPPONENT(\"opn\", \"Opponent\"),\n\n  /**\n   * Use for a person or organization responsible for organizing a meeting for which an item is the report or proceedings.\n   */\n  ORGANIZER_OF_MEETING(\"orm\", \"Organizer of meeting\"),\n\n  /**\n   * Use for a person or organization performing the work, i.e., the name of a person or organization associated with the intellectual content of the work. This category does not include the publisher or personal affiliation, or sponsor except where it is also the corporate author.\n   */\n  ORIGINATOR(\"org\", \"Originator\"),\n\n  /**\n   * Use for relator codes from other lists which have no equivalent in the MARC list or for terms which have not been assigned a code.\n   */\n  OTHER(\"oth\", \"Other\"),\n\n  /**\n   * Use for a person or organization that currently owns an item or collection.\n   */\n  OWNER(\"own\", \"Owner\"),\n\n  /**\n   * Use for a person or organization responsible for the production of paper, usually from wood, cloth, or other fibrous material.\n   */\n  PAPERMAKER(\"ppm\", \"Papermaker\"),\n\n  /**\n   * Use for a person or organization that applied for a patent.\n   */\n  PATENT_APPLICANT(\"pta\", \"Patent applicant\"),\n\n  /**\n   * Use for a person or organization that was granted the patent referred to by the item.\n   */\n  PATENT_HOLDER(\"pth\", \"Patent holder\"),\n\n  /**\n   * Use for a person or organization responsible for commissioning a work. Usually a patron uses his or her means or influence to support the work of artists, writers, etc. This includes those who commission and pay for individual works.\n   */\n  PATRON(\"pat\", \"Patron\"),\n\n  /**\n   * Use for a person or organization who exhibits musical or acting skills in a musical or dramatic presentation or entertainment, if specific codes for those functions ([act], [dnc], [itr], [voc], etc.) are not used. If specific codes are used, [prf] is used for a person whose principal skill is not known or specified.\n   */\n  PERFORMER(\"prf\", \"Performer\"),\n\n  /**\n   * Use for an authority (usually a government agency) that issues permits under which work is accomplished.\n   */\n  PERMITTING_AGENCY(\"pma\", \"Permitting agency\"),\n\n  /**\n   * Use for a person or organization responsible for taking photographs, whether they are used in their original form or as reproductions.\n   */\n  PHOTOGRAPHER(\"pht\", \"Photographer\"),\n\n  /**\n   * Use for the party who complains or sues in court in a personal action, usually in a legal proceeding.\n   */\n  PLAINTIFF(\"ptf\", \"Plaintiff\"),\n\n  /**\n   * Use for a plaintiff who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in a legal proceeding.\n   */\n  PLAINTIFF_APPELLANT(\"ptt\", \"Plaintiff-appellant\"),\n\n  /**\n   * Use for a plaintiff against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in a legal proceeding.\n   */\n  PLAINTIFF_APPELLEE(\"pte\", \"Plaintiff-appellee\"),\n\n  /**\n   * Use for a person or organization responsible for the production of plates, usually for the production of printed images and/or text.\n   */\n  PLATEMAKER(\"plt\", \"Platemaker\"),\n\n  /**\n   * Use for a person or organization who prints texts, whether from type or plates.\n   */\n  PRINTER(\"prt\", \"Printer\"),\n\n  /**\n   * Use for a person or organization who prints illustrations from plates.\n   */\n  PRINTER_OF_PLATES(\"pop\", \"Printer of plates\"),\n\n  /**\n   * Use for a person or organization who makes a relief, intaglio, or planographic printing surface.\n   */\n  PRINTMAKER(\"prm\", \"Printmaker\"),\n\n  /**\n   * Use for a person or organization primarily responsible for performing or initiating a process, such as is done with the collection of metadata sets.\n   */\n  PROCESS_CONTACT(\"prc\", \"Process contact\"),\n\n  /**\n   * Use for a person or organization responsible for the making of a motion picture, including business aspects, management of the productions, and the commercial success of the work.\n   */\n  PRODUCER(\"pro\", \"Producer\"),\n\n  /**\n   * Use for a person responsible for all technical and business matters in a production.\n   */\n  PRODUCTION_MANAGER(\"pmn\", \"Production manager\"),\n\n  /**\n   * Use for a person or organization associated with the production (props, lighting, special effects, etc.) of a musical or dramatic presentation or entertainment.\n   */\n  PRODUCTION_PERSONNEL(\"prd\", \"Production personnel\"),\n\n  /**\n   * Use for a person or organization responsible for the creation and/or maintenance of computer program design documents, source code, and machine-executable digital files and supporting documentation.\n   */\n  PROGRAMMER(\"prg\", \"Programmer\"),\n\n  /**\n   * Use for a person or organization with primary responsibility for all essential aspects of a project, or that manages a very large project that demands senior level responsibility, or that has overall responsibility for managing projects, or provides overall direction to a project manager.\n   */\n  PROJECT_DIRECTOR(\"pdr\", \"Project director\"),\n\n  /**\n   * Use for a person who corrects printed matter. For manuscripts, use Corrector [crr].\n   */\n  PROOFREADER(\"pfr\", \"Proofreader\"),\n\n  /**\n   * Use for the name of the place where a resource is published.\n   */\n  PUBLICATION_PLACE(\"pup\", \"Publication place\"),\n\n  /**\n   * Use for a person or organization that makes printed matter, often text, but also printed music, artwork, etc. available to the public.\n   */\n  PUBLISHER(\"pbl\", \"Publisher\"),\n\n  /**\n   * Use for a person or organization who presides over the elaboration of a collective work to ensure its coherence or continuity. This includes editors-in-chief, literary editors, editors of series, etc.\n   */\n  PUBLISHING_DIRECTOR(\"pbd\", \"Publishing director\"),\n\n  /**\n   * Use for a person or organization who manipulates, controls, or directs puppets or marionettes in a musical or dramatic presentation or entertainment.\n   */\n  PUPPETEER(\"ppt\", \"Puppeteer\"),\n\n  /**\n   * Use for a person or organization to whom correspondence is addressed.\n   */\n  RECIPIENT(\"rcp\", \"Recipient\"),\n\n  /**\n   * Use for a person or organization who supervises the technical aspects of a sound or video recording session.\n   */\n  RECORDING_ENGINEER(\"rce\", \"Recording engineer\"),\n\n  /**\n   * Use for a person or organization who writes or develops the framework for an item without being intellectually responsible for its content.\n   */\n  REDACTOR(\"red\", \"Redactor\"),\n\n  /**\n   * Use for a person or organization who prepares drawings of architectural designs (i.e., renderings) in accurate, representational perspective to show what the project will look like when completed.\n   */\n  RENDERER(\"ren\", \"Renderer\"),\n\n  /**\n   * Use for a person or organization who writes or presents reports of news or current events on air or in print.\n   */\n  REPORTER(\"rpt\", \"Reporter\"),\n\n  /**\n   * Use for an agency that hosts data or material culture objects and provides services to promote long term, consistent and shared use of those data or objects.\n   */\n  REPOSITORY(\"rps\", \"Repository\"),\n\n  /**\n   * Use for a person who directed or managed a research project.\n   */\n  RESEARCH_TEAM_HEAD(\"rth\", \"Research team head\"),\n\n  /**\n   * Use for a person who participated in a research project but whose role did not involve direction or management of it.\n   */\n  RESEARCH_TEAM_MEMBER(\"rtm\", \"Research team member\"),\n\n  /**\n   * Use for a person or organization responsible for performing research.\n   */\n  RESEARCHER(\"res\", \"Researcher\"),\n\n  /**\n   * Use for the party who makes an answer to the courts pursuant to an application for redress, usually in an equity proceeding.\n   */\n  RESPONDENT(\"rsp\", \"Respondent\"),\n\n  /**\n   * Use for a respondent who takes an appeal from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding.\n   */\n  RESPONDENT_APPELLANT(\"rst\", \"Respondent-appellant\"),\n\n  /**\n   * Use for a respondent against whom an appeal is taken from one court or jurisdiction to another to reverse the judgment, usually in an equity proceeding.\n   */\n  RESPONDENT_APPELLEE(\"rse\", \"Respondent-appellee\"),\n\n  /**\n   * Use for a person or organization legally responsible for the content of the published material.\n   */\n  RESPONSIBLE_PARTY(\"rpy\", \"Responsible party\"),\n\n  /**\n   * Use for a person or organization, other than the original choreographer or director, responsible for restaging a choreographic or dramatic work and who contributes minimal new content.\n   */\n  RESTAGER(\"rsg\", \"Restager\"),\n\n  /**\n   * Use for a person or organization responsible for the review of a book, motion picture, performance, etc.\n   */\n  REVIEWER(\"rev\", \"Reviewer\"),\n\n  /**\n   * Use for a person or organization responsible for parts of a work, often headings or opening parts of a manuscript, that appear in a distinctive color, usually red.\n   */\n  RUBRICATOR(\"rbr\", \"Rubricator\"),\n\n  /**\n   * Use for a person or organization who is the author of a motion picture screenplay.\n   */\n  SCENARIST(\"sce\", \"Scenarist\"),\n\n  /**\n   * Use for a person or organization who brings scientific, pedagogical, or historical competence to the conception and realization on a work, particularly in the case of audio-visual items.\n   */\n  SCIENTIFIC_ADVISOR(\"sad\", \"Scientific advisor\"),\n\n  /**\n   * Use for a person who is an amanuensis and for a writer of manuscripts proper. For a person who makes pen-facsimiles, use Facsimilist [fac].\n   */\n  SCRIBE(\"scr\", \"Scribe\"),\n\n  /**\n   * Use for a person or organization who models or carves figures that are three-dimensional representations.\n   */\n  SCULPTOR(\"scl\", \"Sculptor\"),\n\n  /**\n   * Use for a person or organization who is identified as the party of the second part. In the case of transfer of right, this is the assignee, transferee, licensee, grantee, etc. Multiple parties can be named jointly as the second party.\n   */\n  SECOND_PARTY(\"spy\", \"Second party\"),\n\n  /**\n   * Use for a person or organization who is a recorder, redactor, or other person responsible for expressing the views of a organization.\n   */\n  SECRETARY(\"sec\", \"Secretary\"),\n\n  /**\n   * Use for a person or organization who translates the rough sketches of the art director into actual architectural structures for a theatrical presentation, entertainment, motion picture, etc. Set designers draw the detailed guides and specifications for building the set.\n   */\n  SET_DESIGNER(\"std\", \"Set designer\"),\n\n  /**\n   * Use for a person whose signature appears without a presentation or other statement indicative of provenance. When there is a presentation statement, use Inscriber [ins].\n   */\n  SIGNER(\"sgn\", \"Signer\"),\n\n  /**\n   * Use for a person or organization who uses his/her/their voice with or without instrumental accompaniment to produce music. A performance may or may not include actual words.\n   */\n  SINGER(\"sng\", \"Singer\"),\n\n  /**\n   * Use for a person who produces and reproduces the sound score (both live and recorded), the installation of microphones, the setting of sound levels, and the coordination of sources of sound for a production.\n   */\n  SOUND_DESIGNER(\"sds\", \"Sound designer\"),\n\n  /**\n   * Use for a person who participates in a program (often broadcast) and makes a formalized contribution or presentation generally prepared in advance.\n   */\n  SPEAKER(\"spk\", \"Speaker\"),\n\n  /**\n   * Use for a person or organization that issued a contract or under the auspices of which a work has been written, printed, published, etc.\n   */\n  SPONSOR(\"spn\", \"Sponsor\"),\n\n  /**\n   * Use for a person who is in charge of everything that occurs on a performance stage, and who acts as chief of all crews and assistant to a director during rehearsals.\n   */\n  STAGE_MANAGER(\"stm\", \"Stage manager\"),\n\n  /**\n   * Use for an organization responsible for the development or enforcement of a standard.\n   */\n  STANDARDS_BODY(\"stn\", \"Standards body\"),\n\n  /**\n   * Use for a person or organization who creates a new plate for printing by molding or copying another printing surface.\n   */\n  STEREOTYPER(\"str\", \"Stereotyper\"),\n\n  /**\n   * Use for a person relaying a story with creative and/or theatrical interpretation.\n   */\n  STORYTELLER(\"stl\", \"Storyteller\"),\n\n  /**\n   * Use for a person or organization that supports (by allocating facilities, staff, or other resources) a project, program, meeting, event, data objects, material culture objects, or other entities capable of support.\n   */\n  SUPPORTING_HOST(\"sht\", \"Supporting host\"),\n\n  /**\n   * Use for a person or organization who does measurements of tracts of land, etc. to determine location, forms, and boundaries.\n   */\n  SURVEYOR(\"srv\", \"Surveyor\"),\n\n  /**\n   * Use for a person who, in the context of a resource, gives instruction in an intellectual subject or demonstrates while teaching physical skills.\n   */\n  TEACHER(\"tch\", \"Teacher\"),\n\n  /**\n   * Use for a person who is ultimately in charge of scenery, props, lights and sound for a production.\n   */\n  TECHNICAL_DIRECTOR(\"tcd\", \"Technical director\"),\n\n  /**\n   * Use for a person under whose supervision a degree candidate develops and presents a thesis, mémoire, or text of a dissertation.\n   */\n  THESIS_ADVISOR(\"ths\", \"Thesis advisor\"),\n\n  /**\n   * Use for a person who prepares a handwritten or typewritten copy from original material, including from dictated or orally recorded material. For makers of pen-facsimiles, use Facsimilist [fac].\n   */\n  TRANSCRIBER(\"trc\", \"Transcriber\"),\n\n  /**\n   * Use for a person or organization who renders a text from one language into another, or from an older form of a language into the modern form.\n   */\n  TRANSLATOR(\"trl\", \"Translator\"),\n\n  /**\n   * Use for a person or organization who designed the type face used in a particular item.\n   */\n  TYPE_DESIGNER(\"tyd\", \"Type designer\"),\n\n  /**\n   * Use for a person or organization primarily responsible for choice and arrangement of type used in an item. If the typographer is also responsible for other aspects of the graphic design of a book (e.g., Book designer [bkd]), codes for both functions may be needed.\n   */\n  TYPOGRAPHER(\"tyg\", \"Typographer\"),\n\n  /**\n   * Use for the name of a place where a university that is associated with a resource is located, for example, a university where an academic dissertation or thesis was presented.\n   */\n  UNIVERSITY_PLACE(\"uvp\", \"University place\"),\n\n  /**\n   * Use for a person or organization in charge of a video production, e.g. the video recording of a stage production as opposed to a commercial motion picture. The videographer may be the camera operator or may supervise one or more camera operators. Do not confuse with cinematographer.\n   */\n  VIDEOGRAPHER(\"vdg\", \"Videographer\"),\n\n  /**\n   * Use for a person or organization who principally exhibits singing skills in a musical or dramatic presentation or entertainment.\n   */\n  VOCALIST(\"voc\", \"Vocalist\"),\n\n  /**\n   * Use for a person who verifies the truthfulness of an event or action.\n   */\n  WITNESS(\"wit\", \"Witness\"),\n\n  /**\n   * Use for a person or organization who makes prints by cutting the image in relief on the end-grain of a wood block.\n   */\n  WOOD_ENGRAVER(\"wde\", \"Wood-engraver\"),\n\n  /**\n   * Use for a person or organization who makes prints by cutting the image in relief on the plank side of a wood block.\n   */\n  WOODCUTTER(\"wdc\", \"Woodcutter\"),\n\n  /**\n   * Use for a person or organization who writes significant material which accompanies a sound recording or other audiovisual material.\n   */\n  WRITER_OF_ACCOMPANYING_MATERIAL(\"wam\", \"Writer of accompanying material\");\n\n  private final String code;\n  private final String name;\n\n  Relator(String code, String name) {\n    this.code = code;\n    this.name = name;\n  }\n\n  public String getCode() {\n    return code;\n  }\n\n  public String getName() {\n    return name;\n  }\n\n  public static Relator byCode(String code) {\n    for (Relator relator : Relator.values()) {\n      if (relator.getCode().equalsIgnoreCase(code)) {\n        return relator;\n      }\n    }\n    return null;\n  }\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/Resource.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport me.ag2s.epublib.Constants;\nimport me.ag2s.epublib.util.IOUtil;\nimport me.ag2s.epublib.util.StringUtil;\nimport me.ag2s.epublib.util.commons.io.XmlStreamReader;\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.Reader;\nimport java.io.Serializable;\n\n/**\n * Represents a resource that is part of the epub.\n * A resource can be a html file, image, xml, etc.\n *\n * @author paul\n *\n */\npublic class Resource implements Serializable {\n\n  private static final long serialVersionUID = 1043946707835004037L;\n  private String id;\n  private String title;\n  private String href;\n\n\n\n  private String properties;\n  protected final String originalHref;\n  private MediaType mediaType;\n  private String inputEncoding;\n  protected byte[] data;\n\n  /**\n   * Creates an empty Resource with the given href.\n   *\n   * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8\n   *\n   * @param href The location of the resource within the epub. Example: \"chapter1.html\".\n   */\n  public Resource(String href) {\n    this(null, new byte[0], href, MediaTypes.determineMediaType(href));\n  }\n\n  /**\n   * Creates a Resource with the given data and MediaType.\n   * The href will be automatically generated.\n   *\n   * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8\n   *\n   * @param data The Resource's contents\n   * @param mediaType The MediaType of the Resource\n   */\n  public Resource(byte[] data, MediaType mediaType) {\n    this(null, data, null, mediaType);\n  }\n\n  /**\n   * Creates a resource with the given data at the specified href.\n   * The MediaType will be determined based on the href extension.\n   *\n   * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8\n   *\n   * @see MediaTypes#determineMediaType(String)\n   *\n   * @param data The Resource's contents\n   * @param href The location of the resource within the epub. Example: \"chapter1.html\".\n   */\n  public Resource(byte[] data, String href) {\n    this(null, data, href, MediaTypes.determineMediaType(href),\n        Constants.CHARACTER_ENCODING);\n  }\n\n  /**\n   * Creates a resource with the data from the given Reader at the specified href.\n   * The MediaType will be determined based on the href extension.\n   *\n   * @see MediaTypes#determineMediaType(String)\n   *\n   * @param in The Resource's contents\n   * @param href The location of the resource within the epub. Example: \"cover.jpg\".\n   */\n  public Resource(Reader in, String href) throws IOException {\n    this(null, IOUtil.toByteArray(in, Constants.CHARACTER_ENCODING), href,\n        MediaTypes.determineMediaType(href),\n        Constants.CHARACTER_ENCODING);\n  }\n\n  /**\n   * Creates a resource with the data from the given InputStream at the specified href.\n   * The MediaType will be determined based on the href extension.\n   *\n   * @see MediaTypes#determineMediaType(String)\n   *\n   * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8\n   *\n   * It is recommended to us the {@link #Resource(Reader, String)} method for creating textual\n   * (html/css/etc) resources to prevent encoding problems.\n   * Use this method only for binary Resources like images, fonts, etc.\n   *\n   *\n   * @param in The Resource's contents\n   * @param href The location of the resource within the epub. Example: \"cover.jpg\".\n   */\n  public Resource(InputStream in, String href) throws IOException {\n    this(null, IOUtil.toByteArray(in), href,\n        MediaTypes.determineMediaType(href));\n  }\n\n  /**\n   * Creates a resource with the given id, data, mediatype at the specified href.\n   * Assumes that if the data is of a text type (html/css/etc) then the encoding will be UTF-8\n   *\n   * @param id The id of the Resource. Internal use only. Will be auto-generated if it has a null-value.\n   * @param data The Resource's contents\n   * @param href The location of the resource within the epub. Example: \"chapter1.html\".\n   * @param mediaType The resources MediaType\n   */\n  public Resource(String id, byte[] data, String href, MediaType mediaType) {\n    this(id, data, href, mediaType, Constants.CHARACTER_ENCODING);\n  }\n  public Resource(String id, byte[] data, String href, String originalHref, MediaType mediaType) {\n    this(id, data, href, originalHref, mediaType, Constants.CHARACTER_ENCODING);\n  }\n\n\n  /**\n   * Creates a resource with the given id, data, mediatype at the specified href.\n   * If the data is of a text type (html/css/etc) then it will use the given inputEncoding.\n   *\n   * @param id The id of the Resource. Internal use only. Will be auto-generated if it has a null-value.\n   * @param data The Resource's contents\n   * @param href The location of the resource within the epub. Example: \"chapter1.html\".\n   * @param mediaType The resources MediaType\n   * @param inputEncoding If the data is of a text type (html/css/etc) then it will use the given inputEncoding.\n   */\n  public Resource(String id, byte[] data, String href, MediaType mediaType,\n      String inputEncoding) {\n    this.id = id;\n    this.href = href;\n    this.originalHref = href;\n    this.mediaType = mediaType;\n    this.inputEncoding = inputEncoding;\n    this.data = data;\n  }\n  public Resource(String id, byte[] data, String href, String originalHref, MediaType mediaType,\n      String inputEncoding) {\n    this.id = id;\n    this.href = href;\n    this.originalHref = originalHref;\n    this.mediaType = mediaType;\n    this.inputEncoding = inputEncoding;\n    this.data = data;\n  }\n\n  /**\n   * Gets the contents of the Resource as an InputStream.\n   *\n   * @return The contents of the Resource.\n   *\n   * @throws IOException IOException\n   */\n  public InputStream getInputStream() throws IOException {\n    return new ByteArrayInputStream(getData());\n  }\n\n  /**\n   * The contents of the resource as a byte[]\n   *\n   * @return The contents of the resource\n   */\n  public byte[] getData() throws IOException {\n    return data;\n  }\n\n  /**\n   * Tells this resource to release its cached data.\n   *\n   * If this resource was not lazy-loaded, this is a no-op.\n   */\n  public void close() {\n  }\n\n  /**\n   * Sets the data of the Resource.\n   * If the data is a of a different type then the original data then make sure to change the MediaType.\n   *\n   * @param data the data of the Resource\n   */\n  public void setData(byte[] data) {\n    this.data = data;\n  }\n\n  /**\n   * Returns the size of this resource in bytes.\n   *\n   * @return the size.\n   */\n  public long getSize() {\n    return data.length;\n  }\n\n  /**\n   * If the title is found by scanning the underlying html document then it is cached here.\n   *\n   * @return the title\n   */\n  public String getTitle() {\n    return title;\n  }\n\n  /**\n   * Sets the Resource's id: Make sure it is unique and a valid identifier.\n   *\n   * @param id Resource's id\n   */\n  public void setId(String id) {\n    this.id = id;\n  }\n\n  /**\n   * The resources Id.\n   *\n   * Must be both unique within all the resources of this book and a valid identifier.\n   * @return The resources Id.\n   */\n  public String getId() {\n    return id;\n  }\n\n  /**\n   * The location of the resource within the contents folder of the epub file.\n   *\n   * Example:<br/>\n   * images/cover.jpg<br/>\n   * content/chapter1.xhtml<br/>\n   *\n   * @return The location of the resource within the contents folder of the epub file.\n   */\n  public String getHref() {\n    return href;\n  }\n\n  /**\n   * Sets the Resource's href.\n   *\n   * @param href Resource's href.\n   */\n  public void setHref(String href) {\n    this.href = href;\n  }\n\n  /**\n   * The character encoding of the resource.\n   * Is allowed to be null for non-text resources like images.\n   *\n   * @return The character encoding of the resource.\n   */\n  public String getInputEncoding() {\n    return inputEncoding;\n  }\n\n  /**\n   * Sets the Resource's input character encoding.\n   *\n   * @param encoding Resource's input character encoding.\n   */\n  public void setInputEncoding(String encoding) {\n    this.inputEncoding = encoding;\n  }\n\n  /**\n   * Gets the contents of the Resource as Reader.\n   *\n   * Does all sorts of smart things (courtesy of apache commons io XMLStreamREader) to handle encodings, byte order markers, etc.\n   *\n   * @return the contents of the Resource as Reader.\n   * @throws IOException IOException\n   */\n  public Reader getReader() throws IOException {\n    return new XmlStreamReader(new ByteArrayInputStream(getData()),\n        getInputEncoding());\n  }\n\n  /**\n   * Gets the hashCode of the Resource's href.\n   *\n   */\n  public int hashCode() {\n    return href.hashCode();\n  }\n\n  /**\n   * Checks to see of the given resourceObject is a resource and whether its href is equal to this one.\n   *\n   * @return whether the given resourceObject is a resource and whether its href is equal to this one.\n   */\n  public boolean equals(Object resourceObject) {\n    if (!(resourceObject instanceof Resource)) {\n      return false;\n    }\n    return href.equals(((Resource) resourceObject).getHref());\n  }\n\n  /**\n   * This resource's mediaType.\n   *\n   * @return This resource's mediaType.\n   */\n  public MediaType getMediaType() {\n    return mediaType;\n  }\n\n  public void setMediaType(MediaType mediaType) {\n    this.mediaType = mediaType;\n  }\n\n  public void setTitle(String title) {\n    this.title = title;\n  }\n\n  public String getProperties() {\n    return properties;\n  }\n\n  public void setProperties(String properties) {\n    this.properties = properties;\n  }\n  @SuppressWarnings(\"NullableProblems\")\n  public String toString() {\n    return StringUtil.toString(\"id\", id,\n        \"title\", title,\n        \"encoding\", inputEncoding,\n        \"mediaType\", mediaType,\n        \"href\", href,\n        \"size\", (data == null ? 0 : data.length));\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/ResourceInputStream.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.FilterInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.zip.ZipFile;\n\n/**\n * A wrapper class for closing a ZipFile object when the InputStream derived\n * from it is closed.\n *\n * @author ttopalov\n */\npublic class ResourceInputStream extends FilterInputStream {\n\n  private final ZipFile zipFile;\n\n  /**\n   * Constructor.\n   *\n   * @param in\n   *            The InputStream object.\n   * @param zipFile\n   *            The ZipFile object.\n   */\n  public ResourceInputStream(InputStream in, ZipFile zipFile) {\n    super(in);\n    this.zipFile = zipFile;\n  }\n\n  @Override\n  public void close() throws IOException {\n    super.close();\n    zipFile.close();\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/ResourceReference.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.Serializable;\n\npublic class ResourceReference implements Serializable {\n\n  private static final long serialVersionUID = 2596967243557743048L;\n\n  protected Resource resource;\n\n  public ResourceReference(Resource resource) {\n    this.resource = resource;\n  }\n\n\n  public Resource getResource() {\n    return resource;\n  }\n\n  /**\n   * Besides setting the resource it also sets the fragmentId to null.\n   *\n   * @param resource resource\n   */\n  public void setResource(Resource resource) {\n    this.resource = resource;\n  }\n\n\n  /**\n   * The id of the reference referred to.\n   *\n   * null of the reference is null or has a null id itself.\n   *\n   * @return The id of the reference referred to.\n   */\n  public String getResourceId() {\n    if (resource != null) {\n      return resource.getId();\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/Resources.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport me.ag2s.epublib.Constants;\nimport me.ag2s.epublib.util.StringUtil;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * All the resources that make up the book.\n * XHTML files, images and epub xml documents must be here.\n *\n * @author paul\n */\npublic class Resources implements Serializable {\n\n    private static final long serialVersionUID = 2450876953383871451L;\n    private static final String IMAGE_PREFIX = \"image_\";\n    private static final String ITEM_PREFIX = \"item_\";\n    private int lastId = 1;\n\n    private Map<String, Resource> resources = new HashMap<>();\n\n    /**\n     * Adds a resource to the resources.\n     * <p>\n     * Fixes the resources id and href if necessary.\n     *\n     * @param resource resource\n     * @return the newly added resource\n     */\n    public Resource add(Resource resource) {\n        fixResourceHref(resource);\n        fixResourceId(resource);\n        this.resources.put(resource.getHref(), resource);\n        return resource;\n    }\n\n    /**\n     * Checks the id of the given resource and changes to a unique identifier if it isn't one already.\n     *\n     * @param resource resource\n     */\n    public void fixResourceId(Resource resource) {\n        String resourceId = resource.getId();\n\n        // first try and create a unique id based on the resource's href\n        if (StringUtil.isBlank(resource.getId())) {\n            resourceId = StringUtil.substringBeforeLast(resource.getHref(), '.');\n            resourceId = StringUtil.substringAfterLast(resourceId, '/');\n        }\n\n        resourceId = makeValidId(resourceId, resource);\n\n        // check if the id is unique. if not: create one from scratch\n        if (StringUtil.isBlank(resourceId) || containsId(resourceId)) {\n            resourceId = createUniqueResourceId(resource);\n        }\n        resource.setId(resourceId);\n    }\n\n    /**\n     * Check if the id is a valid identifier. if not: prepend with valid identifier\n     *\n     * @param resource resource\n     * @return a valid id\n     */\n    private String makeValidId(String resourceId, Resource resource) {\n        if (StringUtil.isNotBlank(resourceId) && !Character\n                .isJavaIdentifierStart(resourceId.charAt(0))) {\n            resourceId = getResourceItemPrefix(resource) + resourceId;\n        }\n        return resourceId;\n    }\n\n    private String getResourceItemPrefix(Resource resource) {\n        String result;\n        if (MediaTypes.isBitmapImage(resource.getMediaType())) {\n            result = IMAGE_PREFIX;\n        } else {\n            result = ITEM_PREFIX;\n        }\n        return result;\n    }\n\n    /**\n     * Creates a new resource id that is guaranteed to be unique for this set of Resources\n     *\n     * @param resource resource\n     * @return a new resource id that is guaranteed to be unique for this set of Resources\n     */\n    private String createUniqueResourceId(Resource resource) {\n        int counter = lastId;\n        if (counter == Integer.MAX_VALUE) {\n            if (resources.size() == Integer.MAX_VALUE) {\n                throw new IllegalArgumentException(\n                        \"Resources contains \" + Integer.MAX_VALUE\n                                + \" elements: no new elements can be added\");\n            } else {\n                counter = 1;\n            }\n        }\n        String prefix = getResourceItemPrefix(resource);\n        String result = prefix + counter;\n        while (containsId(result)) {\n            result = prefix + (++counter);\n        }\n        lastId = counter;\n        return result;\n    }\n\n    /**\n     * Whether the map of resources already contains a resource with the given id.\n     *\n     * @param id id\n     * @return Whether the map of resources already contains a resource with the given id.\n     */\n    public boolean containsId(String id) {\n        if (StringUtil.isBlank(id)) {\n            return false;\n        }\n        for (Resource resource : resources.values()) {\n            if (id.equals(resource.getId())) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Gets the resource with the given id.\n     *\n     * @param id id\n     * @return null if not found\n     */\n    public Resource getById(String id) {\n        if (StringUtil.isBlank(id)) {\n            return null;\n        }\n        for (Resource resource : resources.values()) {\n            if (id.equals(resource.getId())) {\n                return resource;\n            }\n        }\n        return null;\n    }\n\n    public Resource getByProperties(String properties) {\n        if (StringUtil.isBlank(properties)) {\n            return null;\n        }\n        for (Resource resource : resources.values()) {\n            if (properties.equals(resource.getProperties())) {\n                return resource;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Remove the resource with the given href.\n     *\n     * @param href href\n     * @return the removed resource, null if not found\n     */\n    public Resource remove(String href) {\n        return resources.remove(href);\n    }\n\n    private void fixResourceHref(Resource resource) {\n        if (StringUtil.isNotBlank(resource.getHref())\n                && !resources.containsKey(resource.getHref())) {\n            return;\n        }\n        if (StringUtil.isBlank(resource.getHref())) {\n            if (resource.getMediaType() == null) {\n                throw new IllegalArgumentException(\n                        \"Resource must have either a MediaType or a href\");\n            }\n            int i = 1;\n            String href = createHref(resource.getMediaType(), i);\n            while (resources.containsKey(href)) {\n                href = createHref(resource.getMediaType(), (++i));\n            }\n            resource.setHref(href);\n        }\n    }\n\n    private String createHref(MediaType mediaType, int counter) {\n        if (MediaTypes.isBitmapImage(mediaType)) {\n            return IMAGE_PREFIX + counter + mediaType.getDefaultExtension();\n        } else {\n            return ITEM_PREFIX + counter + mediaType.getDefaultExtension();\n        }\n    }\n\n\n    public boolean isEmpty() {\n        return resources.isEmpty();\n    }\n\n    /**\n     * The number of resources\n     *\n     * @return The number of resources\n     */\n    public int size() {\n        return resources.size();\n    }\n\n    /**\n     * The resources that make up this book.\n     * Resources can be xhtml pages, images, xml documents, etc.\n     *\n     * @return The resources that make up this book.\n     */\n    @SuppressWarnings(\"unused\")\n    public Map<String, Resource> getResourceMap() {\n        return resources;\n    }\n\n    public Collection<Resource> getAll() {\n        return resources.values();\n    }\n\n\n    /**\n     * Whether there exists a resource with the given href\n     *\n     * @param href href\n     * @return Whether there exists a resource with the given href\n     */\n    public boolean notContainsByHref(String href) {\n        if (StringUtil.isBlank(href)) {\n            return true;\n        } else {\n            return !resources.containsKey(\n                    StringUtil.substringBefore(href, Constants.FRAGMENT_SEPARATOR_CHAR));\n        }\n    }\n    /**\n     * Whether there exists a resource with the given href\n     *\n     * @param href href\n     * @return Whether there exists a resource with the given href\n     */\n    @SuppressWarnings(\"unused\")\n    public boolean containsByHref(String href) {\n        return !notContainsByHref(href);\n    }\n\n    /**\n     * Sets the collection of Resources to the given collection of resources\n     *\n     * @param resources resources\n     */\n    public void set(Collection<Resource> resources) {\n        this.resources.clear();\n        addAll(resources);\n    }\n\n    /**\n     * Adds all resources from the given Collection of resources to the existing collection.\n     *\n     * @param resources resources\n     */\n    public void addAll(Collection<Resource> resources) {\n        for (Resource resource : resources) {\n            fixResourceHref(resource);\n            this.resources.put(resource.getHref(), resource);\n        }\n    }\n\n    /**\n     * Sets the collection of Resources to the given collection of resources\n     *\n     * @param resources A map with as keys the resources href and as values the Resources\n     */\n    public void set(Map<String, Resource> resources) {\n        this.resources = new HashMap<>(resources);\n    }\n\n\n    /**\n     * First tries to find a resource with as id the given idOrHref, if that\n     * fails it tries to find one with the idOrHref as href.\n     *\n     * @param idOrHref idOrHref\n     * @return the found Resource\n     */\n    public Resource getByIdOrHref(String idOrHref) {\n        Resource resource = getById(idOrHref);\n        if (resource == null) {\n            resource = getByHref(idOrHref);\n        }\n        return resource;\n    }\n\n\n    /**\n     * Gets the resource with the given href.\n     * If the given href contains a fragmentId then that fragment id will be ignored.\n     *\n     * @param href href\n     * @return null if not found.\n     */\n    public Resource getByHref(String href) {\n        if (StringUtil.isBlank(href)) {\n            return null;\n        }\n        href = StringUtil.substringBefore(href, Constants.FRAGMENT_SEPARATOR_CHAR);\n        return resources.get(href);\n    }\n\n    /**\n     * Gets the first resource (random order) with the give mediatype.\n     * <p>\n     * Useful for looking up the table of contents as it's supposed to be the only resource with NCX mediatype.\n     *\n     * @param mediaType mediaType\n     * @return the first resource (random order) with the give mediatype.\n     */\n    public Resource findFirstResourceByMediaType(MediaType mediaType) {\n        return findFirstResourceByMediaType(resources.values(), mediaType);\n    }\n\n    /**\n     * Gets the first resource (random order) with the give mediatype.\n     * <p>\n     * Useful for looking up the table of contents as it's supposed to be the only resource with NCX mediatype.\n     *\n     * @param mediaType mediaType\n     * @return the first resource (random order) with the give mediatype.\n     */\n    public static Resource findFirstResourceByMediaType(\n            Collection<Resource> resources, MediaType mediaType) {\n        for (Resource resource : resources) {\n            if (resource.getMediaType() == mediaType) {\n                return resource;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * All resources that have the given MediaType.\n     *\n     * @param mediaType mediaType\n     * @return All resources that have the given MediaType.\n     */\n    public List<Resource> getResourcesByMediaType(MediaType mediaType) {\n        List<Resource> result = new ArrayList<>();\n        if (mediaType == null) {\n            return result;\n        }\n        for (Resource resource : getAll()) {\n            if (resource.getMediaType() == mediaType) {\n                result.add(resource);\n            }\n        }\n        return result;\n    }\n\n    /**\n     * All Resources that match any of the given list of MediaTypes\n     *\n     * @param mediaTypes mediaType\n     * @return All Resources that match any of the given list of MediaTypes\n     */\n    @SuppressWarnings(\"unused\")\n    public List<Resource> getResourcesByMediaTypes(MediaType[] mediaTypes) {\n        List<Resource> result = new ArrayList<>();\n        if (mediaTypes == null) {\n            return result;\n        }\n\n        // this is the fastest way of doing this according to\n        // http://stackoverflow.com/questions/1128723/in-java-how-can-i-test-if-an-array-contains-a-certain-value\n        List<MediaType> mediaTypesList = Arrays.asList(mediaTypes);\n        for (Resource resource : getAll()) {\n            if (mediaTypesList.contains(resource.getMediaType())) {\n                result.add(resource);\n            }\n        }\n        return result;\n    }\n\n\n    /**\n     * All resource hrefs\n     *\n     * @return all resource hrefs\n     */\n    public Collection<String> getAllHrefs() {\n        return resources.keySet();\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/Spine.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport me.ag2s.epublib.util.StringUtil;\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\n/**\n * The spine sections are the sections of the book in the order in which the book should be read.\n *\n * This contrasts with the Table of Contents sections which is an index into the Book's sections.\n *\n * @see TableOfContents\n *\n * @author paul\n */\npublic class Spine implements Serializable {\n\n  private static final long serialVersionUID = 3878483958947357246L;\n  private Resource tocResource;\n  private List<SpineReference> spineReferences;\n\n  public Spine() {\n    this(new ArrayList<>());\n  }\n\n  /**\n   * Creates a spine out of all the resources in the table of contents.\n   *\n   * @param tableOfContents tableOfContents\n   */\n  public Spine(TableOfContents tableOfContents) {\n    this.spineReferences = createSpineReferences(\n        tableOfContents.getAllUniqueResources());\n  }\n\n  public Spine(List<SpineReference> spineReferences) {\n    this.spineReferences = spineReferences;\n  }\n\n  public static List<SpineReference> createSpineReferences(\n      Collection<Resource> resources) {\n    List<SpineReference> result = new ArrayList<>(\n            resources.size());\n    for (Resource resource : resources) {\n      result.add(new SpineReference(resource));\n    }\n    return result;\n  }\n\n  public List<SpineReference> getSpineReferences() {\n    return spineReferences;\n  }\n\n  public void setSpineReferences(List<SpineReference> spineReferences) {\n    this.spineReferences = spineReferences;\n  }\n\n  /**\n   * Gets the resource at the given index.\n   * Null if not found.\n   *\n   * @param index index\n   * @return the resource at the given index.\n   */\n  public Resource getResource(int index) {\n    if (index < 0 || index >= spineReferences.size()) {\n      return null;\n    }\n    return spineReferences.get(index).getResource();\n  }\n\n  /**\n   * Finds the first resource that has the given resourceId.\n   *\n   * Null if not found.\n   *\n   * @param resourceId resourceId\n   * @return the first resource that has the given resourceId.\n   */\n  public int findFirstResourceById(String resourceId) {\n    if (StringUtil.isBlank(resourceId)) {\n      return -1;\n    }\n\n    for (int i = 0; i < spineReferences.size(); i++) {\n      SpineReference spineReference = spineReferences.get(i);\n      if (resourceId.equals(spineReference.getResourceId())) {\n        return i;\n      }\n    }\n    return -1;\n  }\n\n  /**\n   * Adds the given spineReference to the spine references and returns it.\n   *\n   * @param spineReference spineReference\n   * @return the given spineReference\n   */\n  public SpineReference addSpineReference(SpineReference spineReference) {\n    if (spineReferences == null) {\n      this.spineReferences = new ArrayList<>();\n    }\n    spineReferences.add(spineReference);\n    return spineReference;\n  }\n\n  /**\n   * Adds the given resource to the spine references and returns it.\n   *\n   * @return the given spineReference\n   */\n  @SuppressWarnings(\"unused\")\n  public SpineReference addResource(Resource resource) {\n    return addSpineReference(new SpineReference(resource));\n  }\n\n  /**\n   * The number of elements in the spine.\n   *\n   * @return The number of elements in the spine.\n   */\n  public int size() {\n    return spineReferences.size();\n  }\n\n  /**\n   * As per the epub file format the spine officially maintains a reference to the Table of Contents.\n   * The epubwriter will look for it here first, followed by some clever tricks to find it elsewhere if not found.\n   * Put it here to be sure of the expected behaviours.\n   *\n   * @param tocResource tocResource\n   */\n  public void setTocResource(Resource tocResource) {\n    this.tocResource = tocResource;\n  }\n\n  /**\n   * The resource containing the XML for the tableOfContents.\n   * When saving an epub file this resource needs to be in this place.\n   *\n   * @return The resource containing the XML for the tableOfContents.\n   */\n  public Resource getTocResource() {\n    return tocResource;\n  }\n\n  /**\n   * The position within the spine of the given resource.\n   *\n   * @param currentResource currentResource\n   * @return something &lt; 0 if not found.\n   *\n   */\n  public int getResourceIndex(Resource currentResource) {\n    if (currentResource == null) {\n      return -1;\n    }\n    return getResourceIndex(currentResource.getHref());\n  }\n\n  /**\n   * The first position within the spine of a resource with the given href.\n   *\n   * @return something &lt; 0 if not found.\n   *\n   */\n  public int getResourceIndex(String resourceHref) {\n    int result = -1;\n    if (StringUtil.isBlank(resourceHref)) {\n      return result;\n    }\n    for (int i = 0; i < spineReferences.size(); i++) {\n      if (resourceHref.equals(spineReferences.get(i).getResource().getHref())) {\n        result = i;\n        break;\n      }\n    }\n    return result;\n  }\n\n  /**\n   * Whether the spine has any references\n   * @return Whether the spine has any references\n   */\n  public boolean isEmpty() {\n    return spineReferences.isEmpty();\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/SpineReference.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.Serializable;\n\n\n/**\n * A Section of a book.\n * Represents both an item in the package document and a item in the index.\n *\n * @author paul\n */\npublic class SpineReference extends ResourceReference implements Serializable {\n\n    private static final long serialVersionUID = -7921609197351510248L;\n    private boolean linear;//default = true;\n\n    public SpineReference(Resource resource) {\n        this(resource, true);\n    }\n\n\n    public SpineReference(Resource resource, boolean linear) {\n        super(resource);\n        this.linear = linear;\n    }\n\n    /**\n     * Linear denotes whether the section is Primary or Auxiliary.\n     * Usually the cover page has linear set to false and all the other sections\n     * have it set to true.\n     * <p>\n     * It's an optional property that readers may also ignore.\n     *\n     * <blockquote>primary or auxiliary is useful for Reading Systems which\n     * opt to present auxiliary content differently than primary content.\n     * For example, a Reading System might opt to render auxiliary content in\n     * a popup window apart from the main window which presents the primary\n     * content. (For an example of the types of content that may be considered\n     * auxiliary, refer to the example below and the subsequent discussion.)</blockquote>\n     *\n     * @return whether the section is Primary or Auxiliary.\n     * @see <a href=\"http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.4\">OPF Spine specification</a>\n     */\n    public boolean isLinear() {\n        return linear;\n    }\n\n    public void setLinear(boolean linear) {\n        this.linear = linear;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/TOCReference.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Comparator;\nimport java.util.List;\n\n/**\n * An item in the Table of Contents.\n *\n * @see TableOfContents\n *\n * @author paul\n */\npublic class TOCReference extends TitledResourceReference\n    implements Serializable {\n\n  private static final long serialVersionUID = 5787958246077042456L;\n  private List<TOCReference> children;\n  private static final Comparator<TOCReference> COMPARATOR_BY_TITLE_IGNORE_CASE = (tocReference1, tocReference2) -> String.CASE_INSENSITIVE_ORDER.compare(tocReference1.getTitle(), tocReference2.getTitle());\n  @Deprecated\n  public TOCReference() {\n    this(null, null, null);\n  }\n\n  public TOCReference(String name, Resource resource) {\n    this(name, resource, null);\n  }\n\n  public TOCReference(String name, Resource resource, String fragmentId) {\n    this(name, resource, fragmentId, new ArrayList<>());\n  }\n\n  public TOCReference(String title, Resource resource, String fragmentId,\n      List<TOCReference> children) {\n    super(resource, title, fragmentId);\n    this.children = children;\n  }\n  @SuppressWarnings(\"unused\")\n  public static Comparator<TOCReference> getComparatorByTitleIgnoreCase() {\n    return COMPARATOR_BY_TITLE_IGNORE_CASE;\n  }\n\n  public List<TOCReference> getChildren() {\n    return children;\n  }\n\n  public TOCReference addChildSection(TOCReference childSection) {\n    this.children.add(childSection);\n    return childSection;\n  }\n\n  public void setChildren(List<TOCReference> children) {\n    this.children = children;\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/TableOfContents.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * The table of contents of the book.\n * The TableOfContents is a tree structure at the root it is a list of TOCReferences, each if which may have as children another list of TOCReferences.\n *\n * The table of contents is used by epub as a quick index to chapters and sections within chapters.\n * It may contain duplicate entries, may decide to point not to certain chapters, etc.\n *\n * See the spine for the complete list of sections in the order in which they should be read.\n *\n * @see Spine\n *\n * @author paul\n */\npublic class TableOfContents implements Serializable {\n\n  private static final long serialVersionUID = -3147391239966275152L;\n\n  public static final String DEFAULT_PATH_SEPARATOR = \"/\";\n\n  private List<TOCReference> tocReferences;\n\n  public TableOfContents() {\n    this(new ArrayList<>());\n  }\n\n  public TableOfContents(List<TOCReference> tocReferences) {\n    this.tocReferences = tocReferences;\n  }\n\n  public List<TOCReference> getTocReferences() {\n    return tocReferences;\n  }\n\n  public void setTocReferences(List<TOCReference> tocReferences) {\n    this.tocReferences = tocReferences;\n  }\n\n  /**\n   * Calls addTOCReferenceAtLocation after splitting the path using the DEFAULT_PATH_SEPARATOR.\n   * @return the new TOCReference\n   */\n  @SuppressWarnings(\"unused\")\n  public TOCReference addSection(Resource resource, String path) {\n    return addSection(resource, path, DEFAULT_PATH_SEPARATOR);\n  }\n\n  /**\n   * Calls addTOCReferenceAtLocation after splitting the path using the given pathSeparator.\n   *\n   * @param resource resource\n   * @param path path\n   * @param pathSeparator pathSeparator\n   * @return the new TOCReference\n   */\n  public TOCReference addSection(Resource resource, String path,\n      String pathSeparator) {\n    String[] pathElements = path.split(pathSeparator);\n    return addSection(resource, pathElements);\n  }\n\n  /**\n   * Finds the first TOCReference in the given list that has the same title as the given Title.\n   *\n   * @param title title\n   * @param tocReferences tocReferences\n   * @return null if not found.\n   */\n  private static TOCReference findTocReferenceByTitle(String title,\n      List<TOCReference> tocReferences) {\n    for (TOCReference tocReference : tocReferences) {\n      if (title.equals(tocReference.getTitle())) {\n        return tocReference;\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Adds the given Resources to the TableOfContents at the location specified by the pathElements.\n   *\n   * Example:\n   * Calling this method with a Resource and new String[] {\"chapter1\", \"paragraph1\"} will result in the following:\n   * <ul>\n   * <li>a TOCReference with the title \"chapter1\" at the root level.<br/>\n   * If this TOCReference did not yet exist it will have been created and does not point to any resource</li>\n   * <li>A TOCReference that has the title \"paragraph1\". This TOCReference will be the child of TOCReference \"chapter1\" and\n   * will point to the given Resource</li>\n   * </ul>\n   *\n   * @param resource resource\n   * @param pathElements pathElements\n   * @return the new TOCReference\n   */\n  public TOCReference addSection(Resource resource, String[] pathElements) {\n    if (pathElements == null || pathElements.length == 0) {\n      return null;\n    }\n    TOCReference result = null;\n    List<TOCReference> currentTocReferences = this.tocReferences;\n    for (String currentTitle : pathElements) {\n      result = findTocReferenceByTitle(currentTitle, currentTocReferences);\n      if (result == null) {\n        result = new TOCReference(currentTitle, null);\n        currentTocReferences.add(result);\n      }\n      currentTocReferences = result.getChildren();\n    }\n    result.setResource(resource);\n    return result;\n  }\n\n  /**\n   * Adds the given Resources to the TableOfContents at the location specified by the pathElements.\n   *\n   * Example:\n   * Calling this method with a Resource and new int[] {0, 0} will result in the following:\n   * <ul>\n   * <li>a TOCReference at the root level.<br/>\n   * If this TOCReference did not yet exist it will have been created with a title of \"\" and does not point to any resource</li>\n   * <li>A TOCReference that points to the given resource and is a child of the previously created TOCReference.<br/>\n   * If this TOCReference didn't exist yet it will be created and have a title of \"\"</li>\n   * </ul>\n   *\n   * @param resource resource\n   * @param pathElements pathElements\n   * @return the new TOCReference\n   */\n  @SuppressWarnings(\"unused\")\n  public TOCReference addSection(Resource resource, int[] pathElements,\n      String sectionTitlePrefix, String sectionNumberSeparator) {\n    if (pathElements == null || pathElements.length == 0) {\n      return null;\n    }\n    TOCReference result = null;\n    List<TOCReference> currentTocReferences = this.tocReferences;\n    for (int i = 0; i < pathElements.length; i++) {\n      int currentIndex = pathElements[i];\n      if (currentIndex > 0 && currentIndex < (currentTocReferences.size()\n          - 1)) {\n        result = currentTocReferences.get(currentIndex);\n      } else {\n        result = null;\n      }\n      if (result == null) {\n        paddTOCReferences(currentTocReferences, pathElements, i,\n            sectionTitlePrefix, sectionNumberSeparator);\n        result = currentTocReferences.get(currentIndex);\n      }\n      currentTocReferences = result.getChildren();\n    }\n    result.setResource(resource);\n    return result;\n  }\n\n  private void paddTOCReferences(List<TOCReference> currentTocReferences,\n      int[] pathElements, int pathPos, String sectionPrefix,\n      String sectionNumberSeparator) {\n    for (int i = currentTocReferences.size(); i <= pathElements[pathPos]; i++) {\n      String sectionTitle = createSectionTitle(pathElements, pathPos, i,\n          sectionPrefix,\n          sectionNumberSeparator);\n      currentTocReferences.add(new TOCReference(sectionTitle, null));\n    }\n  }\n\n  private String createSectionTitle(int[] pathElements, int pathPos,\n      int lastPos,\n      String sectionPrefix, String sectionNumberSeparator) {\n    StringBuilder title = new StringBuilder(sectionPrefix);\n    for (int i = 0; i < pathPos; i++) {\n      if (i > 0) {\n        title.append(sectionNumberSeparator);\n      }\n      title.append(pathElements[i] + 1);\n    }\n    if (pathPos > 0) {\n      title.append(sectionNumberSeparator);\n    }\n    title.append(lastPos + 1);\n    return title.toString();\n  }\n\n  public TOCReference addTOCReference(TOCReference tocReference) {\n    if (tocReferences == null) {\n      tocReferences = new ArrayList<>();\n    }\n    tocReferences.add(tocReference);\n    return tocReference;\n  }\n\n  /**\n   * All unique references (unique by href) in the order in which they are referenced to in the table of contents.\n   *\n   * @return All unique references (unique by href) in the order in which they are referenced to in the table of contents.\n   */\n  public List<Resource> getAllUniqueResources() {\n    Set<String> uniqueHrefs = new HashSet<>();\n    List<Resource> result = new ArrayList<>();\n    getAllUniqueResources(uniqueHrefs, result, tocReferences);\n    return result;\n  }\n\n  private static void getAllUniqueResources(Set<String> uniqueHrefs,\n      List<Resource> result, List<TOCReference> tocReferences) {\n    for (TOCReference tocReference : tocReferences) {\n      Resource resource = tocReference.getResource();\n      if (resource != null && !uniqueHrefs.contains(resource.getHref())) {\n        uniqueHrefs.add(resource.getHref());\n        result.add(resource);\n      }\n      getAllUniqueResources(uniqueHrefs, result, tocReference.getChildren());\n    }\n  }\n\n  /**\n   * The total number of references in this table of contents.\n   *\n   * @return The total number of references in this table of contents.\n   */\n  public int size() {\n    return getTotalSize(tocReferences);\n  }\n\n  private static int getTotalSize(Collection<TOCReference> tocReferences) {\n    int result = tocReferences.size();\n    for (TOCReference tocReference : tocReferences) {\n      result += getTotalSize(tocReference.getChildren());\n    }\n    return result;\n  }\n\n  /**\n   * The maximum depth of the reference tree\n   * @return The maximum depth of the reference tree\n   */\n  public int calculateDepth() {\n    return calculateDepth(tocReferences, 0);\n  }\n\n  private int calculateDepth(List<TOCReference> tocReferences,\n      int currentDepth) {\n    int maxChildDepth = 0;\n    for (TOCReference tocReference : tocReferences) {\n      int childDepth = calculateDepth(tocReference.getChildren(), 1);\n      if (childDepth > maxChildDepth) {\n        maxChildDepth = childDepth;\n      }\n    }\n    return currentDepth + maxChildDepth;\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/domain/TitledResourceReference.java",
    "content": "package me.ag2s.epublib.domain;\n\nimport java.io.Serializable;\n\nimport me.ag2s.epublib.Constants;\nimport me.ag2s.epublib.util.StringUtil;\n\npublic class TitledResourceReference extends ResourceReference\n        implements Serializable {\n\n    private static final long serialVersionUID = 3918155020095190080L;\n    private String fragmentId;\n    private String title;\n\n    /**\n     * 这会使title为null\n     *\n     * @param resource resource\n     */\n    @Deprecated\n    @SuppressWarnings(\"unused\")\n    public TitledResourceReference(Resource resource) {\n        this(resource, null);\n    }\n\n    public TitledResourceReference(Resource resource, String title) {\n        this(resource, title, null);\n    }\n\n    public TitledResourceReference(Resource resource, String title,\n                                   String fragmentId) {\n        super(resource);\n        this.title = title;\n        this.fragmentId = fragmentId;\n    }\n\n    public String getFragmentId() {\n        return fragmentId;\n    }\n\n    public void setFragmentId(String fragmentId) {\n        this.fragmentId = fragmentId;\n    }\n\n    public String getTitle() {\n        return title;\n    }\n\n    public void setTitle(String title) {\n        this.title = title;\n    }\n\n\n    /**\n     * If the fragmentId is blank it returns the resource href, otherwise\n     * it returns the resource href + '#' + the fragmentId.\n     *\n     * @return If the fragmentId is blank it returns the resource href,\n     * otherwise it returns the resource href + '#' + the fragmentId.\n     */\n    public String getCompleteHref() {\n        if (StringUtil.isBlank(fragmentId)) {\n            return resource.getHref();\n        } else {\n            return resource.getHref() + Constants.FRAGMENT_SEPARATOR_CHAR\n                    + fragmentId;\n        }\n    }\n\n    @Override\n    public Resource getResource() {\n        //resource为null时不设置标题\n        if(this.resource!=null&&this.title!=null){\n            resource.setTitle(title);\n        }\n\n        return resource;\n    }\n\n    public void setResource(Resource resource, String fragmentId) {\n        super.setResource(resource);\n        this.fragmentId = fragmentId;\n    }\n\n    /**\n     * Sets the resource to the given resource and sets the fragmentId to null.\n     */\n    public void setResource(Resource resource) {\n        setResource(resource, null);\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/BookProcessor.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport me.ag2s.epublib.domain.EpubBook;\n\n/**\n * Post-processes a book.\n *\n * Can be used to clean up a book after reading or before writing.\n *\n * @author paul\n */\npublic interface BookProcessor {\n\n  /**\n   * A BookProcessor that returns the input book unchanged.\n   */\n  BookProcessor IDENTITY_BOOKPROCESSOR = book -> book;\n\n    EpubBook processBook(EpubBook book);\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport me.ag2s.epublib.domain.EpubBook;\n\n/**\n * A book processor that combines several other bookprocessors\n * <p>\n * Fixes coverpage/coverimage.\n * Cleans up the XHTML.\n *\n * @author paul.siegmann\n */\n@SuppressWarnings(\"unused declaration\")\npublic class BookProcessorPipeline implements BookProcessor {\n\n  private static final String TAG= BookProcessorPipeline.class.getName();\n  private List<BookProcessor> bookProcessors;\n\n  public BookProcessorPipeline() {\n    this(null);\n  }\n\n  public BookProcessorPipeline(List<BookProcessor> bookProcessingPipeline) {\n    this.bookProcessors = bookProcessingPipeline;\n  }\n\n  @Override\n  public EpubBook processBook(EpubBook book) {\n    if (bookProcessors == null) {\n      return book;\n    }\n    for (BookProcessor bookProcessor : bookProcessors) {\n      try {\n        book = bookProcessor.processBook(book);\n      } catch (Exception e) {\n        // Log.e(TAG, e.getMessage(), e);\n        e.printStackTrace();\n      }\n    }\n    return book;\n  }\n\n  public void addBookProcessor(BookProcessor bookProcessor) {\n    if (this.bookProcessors == null) {\n      bookProcessors = new ArrayList<>();\n    }\n    this.bookProcessors.add(bookProcessor);\n  }\n\n  public void addBookProcessors(Collection<BookProcessor> bookProcessors) {\n    if (this.bookProcessors == null) {\n      this.bookProcessors = new ArrayList<>();\n    }\n    this.bookProcessors.addAll(bookProcessors);\n  }\n\n\n  public List<BookProcessor> getBookProcessors() {\n    return bookProcessors;\n  }\n\n\n  public void setBookProcessingPipeline(\n      List<BookProcessor> bookProcessingPipeline) {\n    this.bookProcessors = bookProcessingPipeline;\n  }\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/DOMUtil.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\nimport org.w3c.dom.Text;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport me.ag2s.epublib.util.StringUtil;\n\n/**\n * Utility methods for working with the DOM.\n *\n * @author paul\n */\n// package\nclass DOMUtil {\n\n    /**\n     * First tries to get the attribute value by doing an getAttributeNS on the element, if that gets an empty element it does a getAttribute without namespace.\n     *\n     * @param element   element\n     * @param namespace namespace\n     * @param attribute attribute\n     * @return String Attribute\n     */\n    public static String getAttribute(Element element, String namespace,\n                                      String attribute) {\n        String result = element.getAttributeNS(namespace, attribute);\n        if (StringUtil.isEmpty(result)) {\n            result = element.getAttribute(attribute);\n        }\n        return result;\n    }\n\n    /**\n     * Gets all descendant elements of the given parentElement with the given namespace and tagname and returns their text child as a list of String.\n     *\n     * @param parentElement parentElement\n     * @param namespace     namespace\n     * @param tagName       tagName\n     * @return List<String>\n     */\n    public static List<String> getElementsTextChild(Element parentElement,\n                                                    String namespace, String tagName) {\n        NodeList elements = parentElement\n                .getElementsByTagNameNS(namespace, tagName);\n        //ArrayList 初始化时指定长度提高性能\n        List<String> result = new ArrayList<>(elements.getLength());\n        for (int i = 0; i < elements.getLength(); i++) {\n            result.add(getTextChildrenContent((Element) elements.item(i)));\n        }\n        return result;\n    }\n\n    /**\n     * Finds in the current document the first element with the given namespace and elementName and with the given findAttributeName and findAttributeValue.\n     * It then returns the value of the given resultAttributeName.\n     *\n     * @param document            document\n     * @param namespace           namespace\n     * @param elementName         elementName\n     * @param findAttributeName   findAttributeName\n     * @param findAttributeValue  findAttributeValue\n     * @param resultAttributeName resultAttributeName\n     * @return String value\n     */\n    public static String getFindAttributeValue(Document document,\n                                               String namespace, String elementName, String findAttributeName,\n                                               String findAttributeValue, String resultAttributeName) {\n        NodeList metaTags = document.getElementsByTagNameNS(namespace, elementName);\n        for (int i = 0; i < metaTags.getLength(); i++) {\n            Element metaElement = (Element) metaTags.item(i);\n            if (findAttributeValue\n                    .equalsIgnoreCase(metaElement.getAttribute(findAttributeName))\n                    && StringUtil\n                    .isNotBlank(metaElement.getAttribute(resultAttributeName))) {\n                return metaElement.getAttribute(resultAttributeName);\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Gets the first element that is a child of the parentElement and has the given namespace and tagName\n     *\n     * @param parentElement parentElement\n     * @param namespace     namespace\n     * @param tagName       tagName\n     * @return Element\n     */\n    public static NodeList getElementsByTagNameNS(Element parentElement,\n                                                  String namespace, String tagName) {\n        NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName);\n        if (nodes.getLength() != 0) {\n            return nodes;\n        }\n        nodes = parentElement.getElementsByTagName(tagName);\n        if (nodes.getLength() == 0) {\n            return null;\n        }\n        return nodes;\n    }\n    /**\n     * Gets the first element that is a child of the parentElement and has the given namespace and tagName\n     *\n     * @param parentElement parentElement\n     * @param namespace     namespace\n     * @param tagName       tagName\n     * @return Element\n     */\n    public static NodeList getElementsByTagNameNS(Document parentElement,\n                                                  String namespace, String tagName) {\n        NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName);\n        if (nodes.getLength() != 0) {\n            return nodes;\n        }\n        nodes = parentElement.getElementsByTagName(tagName);\n        if (nodes.getLength() == 0) {\n            return null;\n        }\n        return nodes;\n    }\n\n    /**\n     * Gets the first element that is a child of the parentElement and has the given namespace and tagName\n     *\n     * @param parentElement parentElement\n     * @param namespace     namespace\n     * @param tagName       tagName\n     * @return Element\n     */\n    public static Element getFirstElementByTagNameNS(Element parentElement,\n                                                     String namespace, String tagName) {\n        NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName);\n        if (nodes.getLength() != 0) {\n            return (Element) nodes.item(0);\n        }\n        nodes = parentElement.getElementsByTagName(tagName);\n        if (nodes.getLength() == 0) {\n            return null;\n        }\n        return (Element) nodes.item(0);\n    }\n\n    /**\n     * The contents of all Text nodes that are children of the given parentElement.\n     * The result is trim()-ed.\n     * <p>\n     * The reason for this more complicated procedure instead of just returning the data of the firstChild is that\n     * when the text is Chinese characters then on Android each Characater is represented in the DOM as\n     * an individual Text node.\n     *\n     * @param parentElement parentElement\n     * @return String value\n     */\n    public static String getTextChildrenContent(Element parentElement) {\n        if (parentElement == null) {\n            return null;\n        }\n        StringBuilder result = new StringBuilder();\n        NodeList childNodes = parentElement.getChildNodes();\n        for (int i = 0; i < childNodes.getLength(); i++) {\n            Node node = childNodes.item(i);\n            if ((node == null) ||\n                    (node.getNodeType() != Node.TEXT_NODE)) {\n                continue;\n            }\n            result.append(((Text) node).getData());\n        }\n        return result.toString().trim();\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/EpubProcessorSupport.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport me.ag2s.epublib.Constants;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.UnsupportedEncodingException;\nimport java.io.Writer;\nimport java.net.URL;\nimport java.util.Objects;\n\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\nimport javax.xml.parsers.ParserConfigurationException;\n\nimport org.xml.sax.EntityResolver;\nimport org.xml.sax.InputSource;\nimport org.xmlpull.v1.XmlPullParserFactory;\nimport org.xmlpull.v1.XmlSerializer;\n\n/**\n * Various low-level support methods for reading/writing epubs.\n *\n * @author paul.siegmann\n */\npublic class EpubProcessorSupport {\n\n    private static final String TAG = EpubProcessorSupport.class.getName();\n\n    protected static DocumentBuilderFactory documentBuilderFactory;\n\n    static {\n        init();\n    }\n\n    static class EntityResolverImpl implements EntityResolver {\n\n        private String previousLocation;\n\n        @Override\n        public InputSource resolveEntity(String publicId, String systemId)\n                throws IOException {\n            String resourcePath;\n            if (systemId.startsWith(\"http:\")) {\n                URL url = new URL(systemId);\n                resourcePath = \"dtd/\" + url.getHost() + url.getPath();\n                previousLocation = resourcePath\n                        .substring(0, resourcePath.lastIndexOf('/'));\n            } else {\n                resourcePath =\n                        previousLocation + systemId.substring(systemId.lastIndexOf('/'));\n            }\n\n            if (Objects.requireNonNull(this.getClass().getClassLoader()).getResource(resourcePath) == null) {\n                throw new RuntimeException(\n                        \"remote resource is not cached : [\" + systemId\n                                + \"] cannot continue\");\n            }\n\n            InputStream in = Objects.requireNonNull(EpubProcessorSupport.class.getClassLoader())\n                    .getResourceAsStream(resourcePath);\n            return new InputSource(in);\n        }\n    }\n\n\n    private static void init() {\n        EpubProcessorSupport.documentBuilderFactory = DocumentBuilderFactory\n                .newInstance();\n        documentBuilderFactory.setNamespaceAware(true);\n        documentBuilderFactory.setValidating(false);\n    }\n\n    public static XmlSerializer createXmlSerializer(OutputStream out)\n            throws UnsupportedEncodingException {\n        return createXmlSerializer(\n                new OutputStreamWriter(out, Constants.CHARACTER_ENCODING));\n    }\n\n    public static XmlSerializer createXmlSerializer(Writer out) {\n        XmlSerializer result = null;\n        try {\n            /*\n             * Disable XmlPullParserFactory here before it doesn't work when\n             * building native image using GraalVM\n             */\n            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();\n            factory.setValidating(true);\n            result = factory.newSerializer();\n\n            //result = new KXmlSerializer();\n            result.setFeature(\n                    \"http://xmlpull.org/v1/doc/features.html#indent-output\", true);\n            result.setOutput(out);\n        } catch (Exception e) {\n            e.printStackTrace();\n            // Log.e(TAG,\n            //         \"When creating XmlSerializer: \" + e.getClass().getName() + \": \" + e\n            //                 .getMessage());\n        }\n        return result;\n    }\n\n    /**\n     * Gets an EntityResolver that loads dtd's and such from the epub4j classpath.\n     * In order to enable the loading of relative urls the given EntityResolver contains the previousLocation.\n     * Because of a new EntityResolver is created every time this method is called.\n     * Fortunately the EntityResolver created uses up very little memory per instance.\n     *\n     * @return an EntityResolver that loads dtd's and such from the epub4j classpath.\n     */\n    public static EntityResolver getEntityResolver() {\n        return new EntityResolverImpl();\n    }\n\n    @SuppressWarnings(\"unused\")\n    public DocumentBuilderFactory getDocumentBuilderFactory() {\n        return documentBuilderFactory;\n    }\n\n    /**\n     * Creates a DocumentBuilder that looks up dtd's and schema's from epub4j's classpath.\n     *\n     * @return a DocumentBuilder that looks up dtd's and schema's from epub4j's classpath.\n     */\n    public static DocumentBuilder createDocumentBuilder() {\n        DocumentBuilder result = null;\n        try {\n            result = documentBuilderFactory.newDocumentBuilder();\n            result.setEntityResolver(getEntityResolver());\n        } catch (ParserConfigurationException e) {\n            e.printStackTrace();\n            // Log.e(TAG, e.getMessage());\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/EpubReader.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.zip.ZipFile;\nimport java.util.zip.ZipInputStream;\n\nimport me.ag2s.epublib.Constants;\nimport me.ag2s.epublib.domain.EpubBook;\nimport me.ag2s.epublib.domain.MediaType;\nimport me.ag2s.epublib.domain.MediaTypes;\nimport me.ag2s.epublib.domain.Resource;\nimport me.ag2s.epublib.domain.Resources;\nimport me.ag2s.epublib.util.ResourceUtil;\nimport me.ag2s.epublib.util.StringUtil;\n\n/**\n * Reads an epub file.\n *\n * @author paul\n */\n@SuppressWarnings(\"ALL\")\npublic class EpubReader {\n\n    private static final String TAG = EpubReader.class.getName();\n    private final BookProcessor bookProcessor = BookProcessor.IDENTITY_BOOKPROCESSOR;\n\n    public EpubBook readEpub(InputStream in) throws IOException {\n        return readEpub(in, Constants.CHARACTER_ENCODING);\n    }\n\n    public EpubBook readEpub(ZipInputStream in) throws IOException {\n        return readEpub(in, Constants.CHARACTER_ENCODING);\n    }\n\n    public EpubBook readEpub(ZipFile zipfile) throws IOException {\n        return readEpub(zipfile, Constants.CHARACTER_ENCODING);\n    }\n\n    /**\n     * Read epub from inputstream\n     *\n     * @param in       the inputstream from which to read the epub\n     * @param encoding the encoding to use for the html files within the epub\n     * @return the Book as read from the inputstream\n     * @throws IOException IOException\n     */\n    public EpubBook readEpub(InputStream in, String encoding) throws IOException {\n        return readEpub(new ZipInputStream(in), encoding);\n    }\n\n\n    /**\n     * Reads this EPUB without loading any resources into memory.\n     *\n     * @param zipFile  the file to load\n     * @param encoding the encoding for XHTML files\n     * @return this Book without loading all resources into memory.\n     * @throws IOException IOException\n     */\n    public EpubBook readEpubLazy(ZipFile zipFile, String encoding)\n            throws IOException {\n        return readEpubLazy(zipFile, encoding,\n                Arrays.asList(MediaTypes.mediaTypes));\n    }\n\n    public EpubBook readEpub(ZipInputStream in, String encoding) throws IOException {\n        return readEpub(ResourcesLoader.loadResources(in, encoding));\n    }\n\n    public EpubBook readEpub(ZipFile in, String encoding) throws IOException {\n        return readEpub(ResourcesLoader.loadResources(in, encoding));\n    }\n\n    /**\n     * Reads this EPUB without loading all resources into memory.\n     *\n     * @param zipFile         the file to load\n     * @param encoding        the encoding for XHTML files\n     * @param lazyLoadedTypes a list of the MediaType to load lazily\n     * @return this Book without loading all resources into memory.\n     * @throws IOException IOException\n     */\n    public EpubBook readEpubLazy(ZipFile zipFile, String encoding,\n                                 List<MediaType> lazyLoadedTypes) throws IOException {\n        Resources resources = ResourcesLoader\n                .loadResources(zipFile, encoding, lazyLoadedTypes);\n        return readEpub(resources);\n    }\n\n    public EpubBook readEpub(Resources resources) {\n        return readEpub(resources, new EpubBook());\n    }\n\n    public EpubBook readEpub(Resources resources, EpubBook result) {\n        if (result == null) {\n            result = new EpubBook();\n        }\n        handleMimeType(result, resources);\n        String packageResourceHref = getPackageResourceHref(resources);\n        Resource packageResource = processPackageResource(packageResourceHref,\n                result, resources);\n        result.setOpfResource(packageResource);\n        Resource ncxResource = processNcxResource(packageResource, result);\n        result.setNcxResource(ncxResource);\n        result = postProcessBook(result);\n        return result;\n    }\n\n\n    private EpubBook postProcessBook(EpubBook book) {\n        if (bookProcessor != null) {\n            book = bookProcessor.processBook(book);\n        }\n        return book;\n    }\n\n    private Resource processNcxResource(Resource packageResource, EpubBook book) {\n        System.out.println(TAG + \" OPF:getHref()\" + packageResource.getHref());\n        if (book.isEpub3()) {\n            return NCXDocumentV3.read(book, this);\n        } else {\n            return NCXDocumentV2.read(book, this);\n        }\n\n    }\n\n    private Resource processPackageResource(String packageResourceHref, EpubBook book,\n                                            Resources resources) {\n        Resource packageResource = resources.remove(packageResourceHref);\n        try {\n            PackageDocumentReader.read(packageResource, this, book, resources);\n        } catch (Exception e) {\n            e.printStackTrace();\n            // Log.e(TAG, e.getMessage(), e);\n        }\n        return packageResource;\n    }\n\n    private String getPackageResourceHref(Resources resources) {\n        String defaultResult = \"OEBPS/content.opf\";\n        String result = defaultResult;\n\n        Resource containerResource = resources.remove(\"META-INF/container.xml\");\n        if (containerResource == null) {\n            return result;\n        }\n        try {\n            Document document = ResourceUtil.getAsDocument(containerResource);\n            Element rootFileElement = (Element) ((Element) document\n                    .getDocumentElement().getElementsByTagName(\"rootfiles\").item(0))\n                    .getElementsByTagName(\"rootfile\").item(0);\n            result = rootFileElement.getAttribute(\"full-path\");\n        } catch (Exception e) {\n            e.printStackTrace();\n            // Log.e(TAG, e.getMessage(), e);\n        }\n        if (StringUtil.isBlank(result)) {\n            result = defaultResult;\n        }\n        return result;\n    }\n\n    private void handleMimeType(EpubBook result, Resources resources) {\n        resources.remove(\"mimetype\");\n        //result.setResources(resources);\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/EpubWriter.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport org.xmlpull.v1.XmlSerializer;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.Writer;\nimport java.util.zip.CRC32;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipOutputStream;\n\nimport me.ag2s.epublib.domain.EpubBook;\nimport me.ag2s.epublib.domain.MediaTypes;\nimport me.ag2s.epublib.domain.Resource;\nimport me.ag2s.epublib.util.IOUtil;\n\n/**\n * Generates an epub file. Not thread-safe, single use object.\n *\n * @author paul\n */\npublic class EpubWriter {\n\n  private static final String TAG= EpubWriter.class.getName();\n\n  // package\n  static final String EMPTY_NAMESPACE_PREFIX = \"\";\n\n  private BookProcessor bookProcessor;\n\n  public EpubWriter() {\n    this(BookProcessor.IDENTITY_BOOKPROCESSOR);\n  }\n\n\n  public EpubWriter(BookProcessor bookProcessor) {\n    this.bookProcessor = bookProcessor;\n  }\n\n\n  public void write(EpubBook book, OutputStream out) throws IOException {\n    book = processBook(book);\n    ZipOutputStream resultStream = new ZipOutputStream(out);\n    writeMimeType(resultStream);\n    writeContainer(resultStream);\n    initTOCResource(book);\n    writeResources(book, resultStream);\n    writePackageDocument(book, resultStream);\n    resultStream.close();\n  }\n\n  private EpubBook processBook(EpubBook book) {\n    if (bookProcessor != null) {\n      book = bookProcessor.processBook(book);\n    }\n    return book;\n  }\n\n  private void initTOCResource(EpubBook book) {\n    Resource tocResource;\n    try {\n      if (book.isEpub3()) {\n        tocResource = NCXDocumentV3.createNCXResource(book);\n      } else {\n        tocResource = NCXDocumentV2.createNCXResource(book);\n      }\n\n      Resource currentTocResource = book.getSpine().getTocResource();\n      if (currentTocResource != null) {\n        book.getResources().remove(currentTocResource.getHref());\n      }\n      book.getSpine().setTocResource(tocResource);\n      book.getResources().add(tocResource);\n    } catch (Exception e) {\n      e.printStackTrace();\n      // Log.e(TAG,\n      //     \"Error writing table of contents: \"\n      //         + ex.getClass().getName() + \": \" + ex.getMessage(), ex);\n    }\n  }\n\n\n  private void writeResources(EpubBook book, ZipOutputStream resultStream) {\n    for (Resource resource : book.getResources().getAll()) {\n      writeResource(resource, resultStream);\n    }\n  }\n\n  /**\n   * Writes the resource to the resultStream.\n   *\n   * @param resource resource\n   * @param  resultStream resultStream\n   */\n  private void writeResource(Resource resource, ZipOutputStream resultStream) {\n    if (resource == null) {\n      return;\n    }\n    try {\n      resultStream.putNextEntry(new ZipEntry(\"OEBPS/\" + resource.getHref()));\n      InputStream inputStream = resource.getInputStream();\n\n      IOUtil.copy(inputStream, resultStream);\n      inputStream.close();\n    } catch (Exception e) {\n      e.printStackTrace();\n      // Log.e(TAG,e.getMessage(), e);\n    }\n  }\n\n\n  private void writePackageDocument(EpubBook book, ZipOutputStream resultStream)\n          throws IOException {\n    resultStream.putNextEntry(new ZipEntry(\"OEBPS/content.opf\"));\n    XmlSerializer xmlSerializer = EpubProcessorSupport\n            .createXmlSerializer(resultStream);\n    PackageDocumentWriter.write(this, xmlSerializer, book);\n    xmlSerializer.flush();\n//\t\tString resultAsString = result.toString();\n//\t\tresultStream.write(resultAsString.getBytes(Constants.ENCODING));\n  }\n\n  /**\n   * Writes the META-INF/container.xml file.\n   *\n   * @param  resultStream resultStream\n   * @throws IOException IOException\n   */\n  private void writeContainer(ZipOutputStream resultStream) throws IOException {\n    resultStream.putNextEntry(new ZipEntry(\"META-INF/container.xml\"));\n    Writer out = new OutputStreamWriter(resultStream);\n    out.write(\"<?xml version=\\\"1.0\\\"?>\\n\");\n    out.write(\n        \"<container version=\\\"1.0\\\" xmlns=\\\"urn:oasis:names:tc:opendocument:xmlns:container\\\">\\n\");\n    out.write(\"\\t<rootfiles>\\n\");\n    out.write(\n        \"\\t\\t<rootfile full-path=\\\"OEBPS/content.opf\\\" media-type=\\\"application/oebps-package+xml\\\"/>\\n\");\n    out.write(\"\\t</rootfiles>\\n\");\n    out.write(\"</container>\");\n    out.flush();\n  }\n\n  /**\n   * Stores the mimetype as an uncompressed file in the ZipOutputStream.\n   *\n   * @param  resultStream resultStream\n   * @throws IOException IOException\n   */\n  private void writeMimeType(ZipOutputStream resultStream) throws IOException {\n    ZipEntry mimetypeZipEntry = new ZipEntry(\"mimetype\");\n    mimetypeZipEntry.setMethod(ZipEntry.STORED);\n    byte[] mimetypeBytes = MediaTypes.EPUB.getName().getBytes();\n    mimetypeZipEntry.setSize(mimetypeBytes.length);\n    mimetypeZipEntry.setCrc(calculateCrc(mimetypeBytes));\n    resultStream.putNextEntry(mimetypeZipEntry);\n    resultStream.write(mimetypeBytes);\n  }\n\n  private long calculateCrc(byte[] data) {\n    CRC32 crc = new CRC32();\n    crc.update(data);\n    return crc.getValue();\n  }\n\n  String getNcxId() {\n    return \"ncx\";\n  }\n\n  String getNcxHref() {\n    return \"toc.ncx\";\n  }\n\n  String getNcxMediaType() {\n    return MediaTypes.NCX.getName();\n  }\n\n\n  @SuppressWarnings(\"unused\")\n  public BookProcessor getBookProcessor() {\n    return bookProcessor;\n  }\n\n  @SuppressWarnings(\"unused\")\n  public void setBookProcessor(BookProcessor bookProcessor) {\n    this.bookProcessor = bookProcessor;\n  }\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/HtmlProcessor.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport me.ag2s.epublib.domain.Resource;\nimport java.io.OutputStream;\n@SuppressWarnings(\"unused\")\npublic interface HtmlProcessor {\n\n  void processHtmlResource(Resource resource, OutputStream out);\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\nimport org.xmlpull.v1.XmlSerializer;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLDecoder;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipOutputStream;\n\nimport me.ag2s.epublib.Constants;\nimport me.ag2s.epublib.domain.Author;\nimport me.ag2s.epublib.domain.EpubBook;\nimport me.ag2s.epublib.domain.Identifier;\nimport me.ag2s.epublib.domain.MediaTypes;\nimport me.ag2s.epublib.domain.Resource;\nimport me.ag2s.epublib.domain.TOCReference;\nimport me.ag2s.epublib.domain.TableOfContents;\nimport me.ag2s.epublib.util.ResourceUtil;\nimport me.ag2s.epublib.util.StringUtil;\n\n/**\n * Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/\n *\n * @author paul\n */\npublic class NCXDocumentV2 {\n\n    public static final String NAMESPACE_NCX = \"http://www.daisy.org/z3986/2005/ncx/\";\n    @SuppressWarnings(\"unused\")\n    public static final String PREFIX_NCX = \"ncx\";\n    public static final String NCX_ITEM_ID = \"ncx\";\n    public static final String DEFAULT_NCX_HREF = \"toc.ncx\";\n    public static final String PREFIX_DTB = \"dtb\";\n\n    private static final String TAG = NCXDocumentV2.class.getName();\n\n    private interface NCXTags {\n\n        String ncx = \"ncx\";\n        String meta = \"meta\";\n        String navPoint = \"navPoint\";\n        String navMap = \"navMap\";\n        String navLabel = \"navLabel\";\n        String content = \"content\";\n        String text = \"text\";\n        String docTitle = \"docTitle\";\n        String docAuthor = \"docAuthor\";\n        String head = \"head\";\n    }\n\n    private interface NCXAttributes {\n\n        String src = \"src\";\n        String name = \"name\";\n        String content = \"content\";\n        String id = \"id\";\n        String playOrder = \"playOrder\";\n        String clazz = \"class\";\n        String version = \"version\";\n    }\n\n    private interface NCXAttributeValues {\n\n        String chapter = \"chapter\";\n        String version = \"2005-1\";\n\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static Resource read(EpubBook book, EpubReader epubReader) {\n        Resource ncxResource = null;\n        if (book.getSpine().getTocResource() == null) {\n            // Log.e(TAG, \"Book does not contain a table of contents file\");\n            System.err.println(TAG + \" Book does not contain a table of contents file\");\n            return null;\n        }\n        try {\n            ncxResource = book.getSpine().getTocResource();\n            if (ncxResource == null) {\n                return null;\n            }\n            // Log.d(TAG, ncxResource.getHref());\n            System.out.println(TAG + \" ncxResource.getHref()\" + ncxResource.getHref());\n            Document ncxDocument = ResourceUtil.getAsDocument(ncxResource);\n            Element navMapElement = DOMUtil\n                    .getFirstElementByTagNameNS(ncxDocument.getDocumentElement(),\n                            NAMESPACE_NCX, NCXTags.navMap);\n            if (navMapElement == null) {\n                return null;\n            }\n\n            TableOfContents tableOfContents = new TableOfContents(\n                    readTOCReferences(navMapElement.getChildNodes(), book));\n            book.setTableOfContents(tableOfContents);\n        } catch (Exception e) {\n            e.printStackTrace();\n            // Log.e(TAG, e.getMessage(), e);\n        }\n        return ncxResource;\n    }\n\n    static List<TOCReference> readTOCReferences(NodeList navpoints,\n                                                EpubBook book) {\n        if (navpoints == null) {\n            return new ArrayList<>();\n        }\n        List<TOCReference> result = new ArrayList<>(\n                navpoints.getLength());\n        for (int i = 0; i < navpoints.getLength(); i++) {\n            Node node = navpoints.item(i);\n            if (node.getNodeType() != Document.ELEMENT_NODE) {\n                continue;\n            }\n            if (!(node.getLocalName().equals(NCXTags.navPoint))) {\n                continue;\n            }\n            TOCReference tocReference = readTOCReference((Element) node, book);\n            result.add(tocReference);\n        }\n        return result;\n    }\n\n    static TOCReference readTOCReference(Element navpointElement, EpubBook book) {\n        String label = readNavLabel(navpointElement);\n        //Log.d(TAG,\"label:\"+label);\n        String tocResourceRoot = StringUtil\n                .substringBeforeLast(book.getSpine().getTocResource().getHref(), '/');\n        if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref()\n                .length()) {\n            tocResourceRoot = \"\";\n        } else {\n            tocResourceRoot = tocResourceRoot + \"/\";\n        }\n        String reference = StringUtil\n                .collapsePathDots(tocResourceRoot + readNavReference(navpointElement));\n        String href = StringUtil\n                .substringBefore(reference, Constants.FRAGMENT_SEPARATOR_CHAR);\n        String fragmentId = StringUtil\n                .substringAfter(reference, Constants.FRAGMENT_SEPARATOR_CHAR);\n        Resource resource = book.getResources().getByHref(href);\n        if (resource == null) {\n            System.err.println(TAG + \" Resource with href \" + href + \" in NCX document not found\");\n            // Log.e(TAG, \"Resource with href \" + href + \" in NCX document not found\");\n        }\n        System.out.println(TAG + \" label:\" + label);\n        System.out.println(TAG + \" href:\" + href);\n        System.out.println(TAG + \" fragmentId:\" + fragmentId);\n        TOCReference result = new TOCReference(label, resource, fragmentId);\n        List<TOCReference> childTOCReferences = readTOCReferences(\n                navpointElement.getChildNodes(), book);\n        result.setChildren(childTOCReferences);\n        return result;\n    }\n\n    private static String readNavReference(Element navpointElement) {\n        Element contentElement = DOMUtil\n                .getFirstElementByTagNameNS(navpointElement, NAMESPACE_NCX,\n                        NCXTags.content);\n        if (contentElement == null) {\n            return null;\n        }\n        String result = DOMUtil\n                .getAttribute(contentElement, NAMESPACE_NCX, NCXAttributes.src);\n        try {\n            result = URLDecoder.decode(result, Constants.CHARACTER_ENCODING);\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n            // Log.e(TAG, e.getMessage());\n        }\n        return result;\n    }\n\n    private static String readNavLabel(Element navpointElement) {\n        //Log.d(TAG,navpointElement.getTagName());\n        Element navLabel = DOMUtil\n                .getFirstElementByTagNameNS(navpointElement, NAMESPACE_NCX,\n                        NCXTags.navLabel);\n        assert navLabel != null;\n        return DOMUtil.getTextChildrenContent(DOMUtil\n                .getFirstElementByTagNameNS(navLabel, NAMESPACE_NCX, NCXTags.text));\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static void write(EpubWriter epubWriter, EpubBook book,\n                             ZipOutputStream resultStream) throws IOException {\n        resultStream\n                .putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref()));\n        XmlSerializer out = EpubProcessorSupport.createXmlSerializer(resultStream);\n        write(out, book);\n        out.flush();\n    }\n\n\n    /**\n     * Generates a resource containing an xml document containing the table of contents of the book in ncx format.\n     *\n     * @param xmlSerializer the serializer used\n     * @param book          the book to serialize\n     * @throws IOException              IOException\n     * @throws IllegalStateException    IllegalStateException\n     * @throws IllegalArgumentException IllegalArgumentException\n     */\n    public static void write(XmlSerializer xmlSerializer, EpubBook book)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),\n                book.getMetadata().getAuthors(), book.getTableOfContents());\n    }\n\n    public static Resource createNCXResource(EpubBook book)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        return createNCXResource(book.getMetadata().getIdentifiers(),\n                book.getTitle(), book.getMetadata().getAuthors(),\n                book.getTableOfContents());\n    }\n\n    public static Resource createNCXResource(List<Identifier> identifiers,\n                                             String title, List<Author> authors, TableOfContents tableOfContents)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        ByteArrayOutputStream data = new ByteArrayOutputStream();\n        XmlSerializer out = EpubProcessorSupport.createXmlSerializer(data);\n        write(out, identifiers, title, authors, tableOfContents);\n        return new Resource(NCX_ITEM_ID, data.toByteArray(),\n                DEFAULT_NCX_HREF, MediaTypes.NCX);\n    }\n\n    public static void write(XmlSerializer serializer,\n                             List<Identifier> identifiers, String title, List<Author> authors,\n                             TableOfContents tableOfContents)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        serializer.startDocument(Constants.CHARACTER_ENCODING, false);\n        serializer.setPrefix(EpubWriter.EMPTY_NAMESPACE_PREFIX, NAMESPACE_NCX);\n        serializer.startTag(NAMESPACE_NCX, NCXTags.ncx);\n//\t\tserializer.writeNamespace(\"ncx\", NAMESPACE_NCX);\n//\t\tserializer.attribute(\"xmlns\", NAMESPACE_NCX);\n        serializer\n                .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.version,\n                        NCXAttributeValues.version);\n        serializer.startTag(NAMESPACE_NCX, NCXTags.head);\n\n        for (Identifier identifier : identifiers) {\n            writeMetaElement(identifier.getScheme(), identifier.getValue(),\n                    serializer);\n        }\n\n        writeMetaElement(\"generator\", Constants.EPUB_GENERATOR_NAME, serializer);\n        writeMetaElement(\"depth\", String.valueOf(tableOfContents.calculateDepth()),\n                serializer);\n        writeMetaElement(\"totalPageCount\", \"0\", serializer);\n        writeMetaElement(\"maxPageNumber\", \"0\", serializer);\n\n        serializer.endTag(NAMESPACE_NCX, \"head\");\n\n        serializer.startTag(NAMESPACE_NCX, NCXTags.docTitle);\n        serializer.startTag(NAMESPACE_NCX, NCXTags.text);\n        // write the first title\n        serializer.text(StringUtil.defaultIfNull(title));\n        serializer.endTag(NAMESPACE_NCX, NCXTags.text);\n        serializer.endTag(NAMESPACE_NCX, NCXTags.docTitle);\n\n        for (Author author : authors) {\n            serializer.startTag(NAMESPACE_NCX, NCXTags.docAuthor);\n            serializer.startTag(NAMESPACE_NCX, NCXTags.text);\n            serializer.text(author.getLastname() + \", \" + author.getFirstname());\n            serializer.endTag(NAMESPACE_NCX, NCXTags.text);\n            serializer.endTag(NAMESPACE_NCX, NCXTags.docAuthor);\n        }\n\n        serializer.startTag(NAMESPACE_NCX, NCXTags.navMap);\n        writeNavPoints(tableOfContents.getTocReferences(), 1, serializer);\n        serializer.endTag(NAMESPACE_NCX, NCXTags.navMap);\n\n        serializer.endTag(NAMESPACE_NCX, \"ncx\");\n        serializer.endDocument();\n    }\n\n\n    private static void writeMetaElement(String dtbName, String content,\n                                         XmlSerializer serializer)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        serializer.startTag(NAMESPACE_NCX, NCXTags.meta);\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.name,\n                PREFIX_DTB + \":\" + dtbName);\n        serializer\n                .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.content,\n                        content);\n        serializer.endTag(NAMESPACE_NCX, NCXTags.meta);\n    }\n\n    private static int writeNavPoints(List<TOCReference> tocReferences,\n                                      int playOrder,\n                                      XmlSerializer serializer)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        for (TOCReference tocReference : tocReferences) {\n            if (tocReference.getResource() == null) {\n                playOrder = writeNavPoints(tocReference.getChildren(), playOrder,\n                        serializer);\n                continue;\n            }\n            writeNavPointStart(tocReference, playOrder, serializer);\n            playOrder++;\n            if (!tocReference.getChildren().isEmpty()) {\n                playOrder = writeNavPoints(tocReference.getChildren(), playOrder,\n                        serializer);\n            }\n            writeNavPointEnd(tocReference, serializer);\n        }\n        return playOrder;\n    }\n\n\n    private static void writeNavPointStart(TOCReference tocReference,\n                                           int playOrder, XmlSerializer serializer)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        serializer.startTag(NAMESPACE_NCX, NCXTags.navPoint);\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.id,\n                \"navPoint-\" + playOrder);\n        serializer\n                .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.playOrder,\n                        String.valueOf(playOrder));\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.clazz,\n                NCXAttributeValues.chapter);\n        serializer.startTag(NAMESPACE_NCX, NCXTags.navLabel);\n        serializer.startTag(NAMESPACE_NCX, NCXTags.text);\n        serializer.text(tocReference.getTitle());\n        serializer.endTag(NAMESPACE_NCX, NCXTags.text);\n        serializer.endTag(NAMESPACE_NCX, NCXTags.navLabel);\n        serializer.startTag(NAMESPACE_NCX, NCXTags.content);\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.src,\n                tocReference.getCompleteHref());\n        serializer.endTag(NAMESPACE_NCX, NCXTags.content);\n    }\n    @SuppressWarnings(\"unused\")\n    private static void writeNavPointEnd(TOCReference tocReference,\n                                         XmlSerializer serializer)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        serializer.endTag(NAMESPACE_NCX, NCXTags.navPoint);\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\nimport org.xmlpull.v1.XmlSerializer;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLDecoder;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport me.ag2s.epublib.Constants;\nimport me.ag2s.epublib.domain.Author;\nimport me.ag2s.epublib.domain.EpubBook;\nimport me.ag2s.epublib.domain.Identifier;\nimport me.ag2s.epublib.domain.MediaType;\nimport me.ag2s.epublib.domain.MediaTypes;\nimport me.ag2s.epublib.domain.Resource;\nimport me.ag2s.epublib.domain.TOCReference;\nimport me.ag2s.epublib.domain.TableOfContents;\nimport me.ag2s.epublib.util.ResourceUtil;\nimport me.ag2s.epublib.util.StringUtil;\n\n/**\n * Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/\n *\n * @author Ag2S20150909\n */\n\npublic class NCXDocumentV3 {\n    public static final String NAMESPACE_XHTML = \"http://www.w3.org/1999/xhtml\";\n    public static final String NAMESPACE_EPUB = \"http://www.idpf.org/2007/ops\";\n    public static final String LANGUAGE = \"en\";\n    @SuppressWarnings(\"unused\")\n    public static final String PREFIX_XHTML = \"html\";\n    public static final String NCX_ITEM_ID = \"htmltoc\";\n    public static final String DEFAULT_NCX_HREF = \"toc.xhtml\";\n    public static final String V3_NCX_PROPERTIES = \"nav\";\n    public static final MediaType V3_NCX_MEDIATYPE = MediaTypes.XHTML;\n\n    private static final String TAG = NCXDocumentV3.class.getName();\n\n    private interface XHTMLTgs {\n        String html = \"html\";\n        String head = \"head\";\n        String title = \"title\";\n        String meta = \"meta\";\n        String link = \"link\";\n        String body = \"body\";\n        String h1 = \"h1\";\n        String h2 = \"h2\";\n        String nav = \"nav\";\n        String ol = \"ol\";\n        String li = \"li\";\n        String a = \"a\";\n        String span = \"span\";\n    }\n\n    private interface XHTMLAttributes {\n        String xmlns = \"xmlns\";\n        String xmlns_epub = \"xmlns:epub\";\n        String lang = \"lang\";\n        String xml_lang = \"xml:lang\";\n        String rel = \"rel\";\n        String type = \"type\";\n        String epub_type = \"epub:type\";//nav的必须属性\n        String id = \"id\";\n        String role = \"role\";\n        String href = \"href\";\n        String http_equiv = \"http-equiv\";\n        String content = \"content\";\n\n    }\n\n    private interface XHTMLAttributeValues {\n        String Content_Type = \"Content-Type\";\n        String HTML_UTF8 = \"text/html; charset=utf-8\";\n        String lang = \"en\";\n        String epub_type = \"toc\";\n        String role_toc = \"doc-toc\";\n\n    }\n\n\n    /**\n     * 解析epub的目录文件\n     *\n     * @param book       Book\n     * @param epubReader epubreader\n     * @return Resource\n     */\n    @SuppressWarnings(\"unused\")\n    public static Resource read(EpubBook book, EpubReader epubReader) {\n        Resource ncxResource = null;\n        if (book.getSpine().getTocResource() == null) {\n            // Log.e(TAG, \"Book does not contain a table of contents file\");\n            System.err.println(TAG + \" Book does not contain a table of contents file\");\n            return null;\n        }\n        try {\n            ncxResource = book.getSpine().getTocResource();\n            if (ncxResource == null) {\n                return null;\n            }\n            //一些epub 3 文件没有按照epub3的标准使用删除掉ncx目录文件\n            if (ncxResource.getHref().endsWith(\".ncx\")){\n                // Log.v(TAG,\"该epub文件不标准，使用了epub2的目录文件\");\n                System.err.println(TAG + \" 该epub文件不标准，使用了epub2的目录文件\");\n                return NCXDocumentV2.read(book, epubReader);\n            }\n            // Log.d(TAG, ncxResource.getHref());\n            System.out.println(TAG + \" \" + ncxResource.getHref());\n\n            Document ncxDocument = ResourceUtil.getAsDocument(ncxResource);\n            // Log.d(TAG, ncxDocument.getNodeName());\n            System.out.println(TAG + \" \" + ncxDocument.getNodeName());\n\n            Element navMapElement = (Element) ncxDocument.getElementsByTagName(XHTMLTgs.nav).item(0);\n            if(navMapElement==null){\n                // Log.d(TAG,\"epub3目录文件未发现nav节点，尝试使用epub2的规则解析\");\n                System.out.println(TAG + \" \" + \"epub3目录文件未发现nav节点，尝试使用epub2的规则解析\");\n                return NCXDocumentV2.read(book, epubReader);\n            }\n            navMapElement = (Element) navMapElement.getElementsByTagName(XHTMLTgs.ol).item(0);\n            // Log.d(TAG, navMapElement.getTagName());\n            System.out.println(TAG + \" \" + navMapElement.getTagName());\n\n            TableOfContents tableOfContents = new TableOfContents(\n                    readTOCReferences(navMapElement.getChildNodes(), book));\n            // Log.d(TAG, tableOfContents.toString());\n            System.out.println(TAG + \" \" + tableOfContents.toString());\n            book.setTableOfContents(tableOfContents);\n        } catch (Exception e) {\n            e.printStackTrace();\n            // Log.e(TAG, e.getMessage(), e);\n        }\n        return ncxResource;\n    }\n\n    private static List<TOCReference> doToc(Node n, EpubBook book) {\n        List<TOCReference> result = new ArrayList<>();\n\n        if (n == null || n.getNodeType() != Document.ELEMENT_NODE) {\n            return result;\n        } else {\n            Element el = (Element) n;\n            NodeList nodeList = el.getElementsByTagName(XHTMLTgs.li);\n            for (int i = 0; i < nodeList.getLength(); i++) {\n                result.add(readTOCReference((Element) nodeList.item(i), book));\n            }\n        }\n        return result;\n    }\n\n\n    static List<TOCReference> readTOCReferences(NodeList navpoints,\n                                                EpubBook book) {\n        if (navpoints == null) {\n            return new ArrayList<>();\n        }\n        //Log.d(TAG, \"readTOCReferences:navpoints.getLength()\" + navpoints.getLength());\n        List<TOCReference> result = new ArrayList<>(navpoints.getLength());\n        for (int i = 0; i < navpoints.getLength(); i++) {\n            Node node = navpoints.item(i);\n            //如果该node是null,或者不是Element,跳出本次循环\n            if (node == null || node.getNodeType() != Document.ELEMENT_NODE) {\n                continue;\n            }\n\n            Element el = (Element) node;\n            //如果该Element的name为”li“,将其添加到目录结果\n            if (el.getTagName().equals(XHTMLTgs.li)) {\n                result.add(readTOCReference(el, book));\n            }\n\n        }\n\n\n        return result;\n    }\n\n\n    static TOCReference readTOCReference(Element navpointElement, EpubBook book) {\n        //章节的名称\n        String label = readNavLabel(navpointElement);\n        //Log.d(TAG, \"label:\" + label);\n        String tocResourceRoot = StringUtil\n                .substringBeforeLast(book.getSpine().getTocResource().getHref(), '/');\n        if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref()\n                .length()) {\n            tocResourceRoot = \"\";\n        } else {\n            tocResourceRoot = tocResourceRoot + \"/\";\n        }\n\n        String reference = StringUtil\n                .collapsePathDots(tocResourceRoot + readNavReference(navpointElement));\n        String href = StringUtil\n                .substringBefore(reference, Constants.FRAGMENT_SEPARATOR_CHAR);\n        String fragmentId = StringUtil\n                .substringAfter(reference, Constants.FRAGMENT_SEPARATOR_CHAR);\n        Resource resource = book.getResources().getByHref(href);\n        if (resource == null) {\n            System.err.println(TAG + \" \" + \"Resource with href \" + href + \" in NCX document not found\");\n            // Log.e(TAG, \"Resource with href \" + href + \" in NCX document not found\");\n        }\n\n        System.out.println(TAG + \" label:\" + label);\n        System.out.println(TAG + \" href:\" + href);\n        System.out.println(TAG + \" fragmentId:\" + fragmentId);\n\n        //父级目录\n        TOCReference result = new TOCReference(label, resource, fragmentId);\n        //解析子级目录\n        List<TOCReference> childTOCReferences = doToc(navpointElement, book);\n        //readTOCReferences(\n        //navpointElement.getChildNodes(), book);\n        result.setChildren(childTOCReferences);\n        return result;\n    }\n\n    /**\n     * 获取目录节点的href\n     *\n     * @param navpointElement navpointElement\n     * @return String\n     */\n    private static String readNavReference(Element navpointElement) {\n        //https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav\n        //父级节点必须是 \"li\"\n        //Log.d(TAG, \"readNavReference:\" + navpointElement.getTagName());\n\n        Element contentElement = DOMUtil\n                .getFirstElementByTagNameNS(navpointElement, \"\", XHTMLTgs.a);\n        if (contentElement == null) {\n            return null;\n        }\n        String result = DOMUtil\n                .getAttribute(contentElement, \"\", XHTMLAttributes.href);\n        try {\n            result = URLDecoder.decode(result, Constants.CHARACTER_ENCODING);\n        } catch (UnsupportedEncodingException e) {\n            // Log.e(TAG, e.getMessage());\n            e.printStackTrace();\n        }\n\n        return result;\n\n    }\n\n    /**\n     * 获取目录节点里面的章节名\n     *\n     * @param navpointElement navpointElement\n     * @return String\n     */\n    private static String readNavLabel(Element navpointElement) {\n        //https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav\n        //父级节点必须是 \"li\"\n        //Log.d(TAG, \"readNavLabel:\" + navpointElement.getTagName());\n        String label;\n        Element labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, \"\", \"a\");\n        assert labelElement != null;\n        label = labelElement.getTextContent();\n        if (StringUtil.isNotBlank(label)) {\n            return label;\n        } else {\n            labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, \"\", \"span\");\n        }\n        assert labelElement != null;\n        label = labelElement.getTextContent();\n        //如果通过 a 标签无法获取章节列表,则是无href章节名\n        return label;\n\n    }\n\n    public static Resource createNCXResource(EpubBook book)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        return createNCXResource(book.getMetadata().getIdentifiers(),\n                book.getTitle(), book.getMetadata().getAuthors(),\n                book.getTableOfContents());\n    }\n\n    public static Resource createNCXResource(List<Identifier> identifiers,\n                                             String title, List<Author> authors, TableOfContents tableOfContents)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        ByteArrayOutputStream data = new ByteArrayOutputStream();\n        XmlSerializer out = EpubProcessorSupport.createXmlSerializer(data);\n        write(out, identifiers, title, authors, tableOfContents);\n\n        Resource resource = new Resource(NCX_ITEM_ID, data.toByteArray(),\n                DEFAULT_NCX_HREF, V3_NCX_MEDIATYPE);\n        resource.setProperties(V3_NCX_PROPERTIES);\n        return resource;\n    }\n\n    /**\n     * Generates a resource containing an xml document containing the table of contents of the book in ncx format.\n     *\n     * @param xmlSerializer the serializer used\n     * @param book          the book to serialize\n     * @throws IOException              IOException\n     * @throws IllegalStateException    IllegalStateException\n     * @throws IllegalArgumentException IllegalArgumentException\n     */\n    public static void write(XmlSerializer xmlSerializer, EpubBook book)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),\n                book.getMetadata().getAuthors(), book.getTableOfContents());\n    }\n\n    /**\n     * 写入\n     *\n     * @param serializer      serializer\n     * @param identifiers     identifiers\n     * @param title           title\n     * @param authors         authors\n     * @param tableOfContents tableOfContents\n     */\n    @SuppressWarnings(\"unused\")\n    public static void write(XmlSerializer serializer,\n                             List<Identifier> identifiers, String title, List<Author> authors,\n                             TableOfContents tableOfContents) throws IllegalArgumentException, IllegalStateException, IOException {\n        serializer.startDocument(Constants.CHARACTER_ENCODING, false);\n        serializer.setPrefix(EpubWriter.EMPTY_NAMESPACE_PREFIX, NAMESPACE_XHTML);\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.html);\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xmlns_epub, NAMESPACE_EPUB);\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xml_lang, XHTMLAttributeValues.lang);\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.lang, LANGUAGE);\n        //写入头部head标签\n        writeHead(title, serializer);\n        //body开始\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.body);\n        //h1开始\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h1);\n        serializer.text(title);\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h1);\n        //h1关闭\n        //nav开始\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.nav);\n        serializer.attribute(\"\", XHTMLAttributes.epub_type, XHTMLAttributeValues.epub_type);\n        serializer.attribute(\"\", XHTMLAttributes.id, XHTMLAttributeValues.epub_type);\n        serializer.attribute(\"\", XHTMLAttributes.role, XHTMLAttributeValues.role_toc);\n        //h2开始\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h2);\n        serializer.text(\"目录\");\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h2);\n\n\n        writeNavPoints(tableOfContents.getTocReferences(), 1, serializer);\n\n\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.nav);\n\n        //body关闭\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.body);\n\n\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.html);\n        serializer.endDocument();\n\n    }\n\n    private static int writeNavPoints(List<TOCReference> tocReferences,\n                                      int playOrder,\n                                      XmlSerializer serializer) throws IOException {\n        writeOlStart(serializer);\n        for (TOCReference tocReference : tocReferences) {\n            if (tocReference.getResource() == null) {\n                playOrder = writeNavPoints(tocReference.getChildren(), playOrder,\n                        serializer);\n                continue;\n            }\n\n\n            writeNavPointStart(tocReference, serializer);\n\n            playOrder++;\n            if (!tocReference.getChildren().isEmpty()) {\n                playOrder = writeNavPoints(tocReference.getChildren(), playOrder,\n                        serializer);\n            }\n\n            writeNavPointEnd(tocReference, serializer);\n        }\n        writeOlSEnd(serializer);\n        return playOrder;\n    }\n\n    private static void writeNavPointStart(TOCReference tocReference, XmlSerializer serializer) throws IOException {\n        writeLiStart(serializer);\n        String title = tocReference.getTitle();\n        String href = tocReference.getCompleteHref();\n        if (StringUtil.isNotBlank(href)) {\n            writeLabel(title, href, serializer);\n        } else {\n            writeLabel(title, serializer);\n        }\n    }\n\n    @SuppressWarnings(\"unused\")\n    private static void writeNavPointEnd(TOCReference tocReference,\n                                         XmlSerializer serializer) throws IOException {\n        writeLiEnd(serializer);\n    }\n\n    protected static void writeLabel(String title, String href, XmlSerializer serializer) throws IOException {\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.a);\n        serializer.attribute(\"\", XHTMLAttributes.href, href);\n        //attribute必须在Text之前设置。\n        serializer.text(title);\n        //serializer.attribute(NAMESPACE_XHTML, XHTMLAttributes.href, href);\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.a);\n    }\n\n    protected static void writeLabel(String title, XmlSerializer serializer) throws IOException {\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.span);\n        serializer.text(title);\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.span);\n    }\n\n    private static void writeLiStart(XmlSerializer serializer) throws IOException {\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.li);\n        // Log.d(TAG, \"writeLiStart\");\n        System.out.println(TAG + \" writeLiStart\");\n    }\n\n    private static void writeLiEnd(XmlSerializer serializer) throws IOException {\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.li);\n        // Log.d(TAG, \"writeLiEND\");\n        System.out.println(TAG + \" writeLiEND\");\n    }\n\n    private static void writeOlStart(XmlSerializer serializer) throws IOException {\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.ol);\n        // Log.d(TAG, \"writeOlStart\");\n        System.out.println(TAG + \" writeOlStart\");\n    }\n\n    private static void writeOlSEnd(XmlSerializer serializer) throws IOException {\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.ol);\n        // Log.d(TAG, \"writeOlEnd\");\n        System.out.println(TAG + \" writeOlEnd\");\n    }\n\n    private static void writeHead(String title, XmlSerializer serializer) throws IOException {\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.head);\n        //title\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.title);\n        serializer.text(StringUtil.defaultIfNull(title));\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.title);\n        //link\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.link);\n        serializer.attribute(\"\", XHTMLAttributes.rel, \"stylesheet\");\n        serializer.attribute(\"\", XHTMLAttributes.type, \"text/css\");\n        serializer.attribute(\"\", XHTMLAttributes.href, \"css/style.css\");\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.link);\n\n        //meta\n        serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.meta);\n        serializer.attribute(\"\", XHTMLAttributes.http_equiv, XHTMLAttributeValues.Content_Type);\n        serializer.attribute(\"\", XHTMLAttributes.content, XHTMLAttributeValues.HTML_UTF8);\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.meta);\n\n        serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.head);\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/PackageDocumentBase.java",
    "content": "package me.ag2s.epublib.epub;\n\n\n/**\n * Functionality shared by the PackageDocumentReader and the PackageDocumentWriter\n *\n * @author paul\n *\n */\npublic class PackageDocumentBase {\n\n  public static final String BOOK_ID_ID = \"duokan-book-id\";\n  public static final String NAMESPACE_OPF = \"http://www.idpf.org/2007/opf\";\n  public static final String NAMESPACE_DUBLIN_CORE = \"http://purl.org/dc/elements/1.1/\";\n  public static final String PREFIX_DUBLIN_CORE = \"dc\";\n  //public static final String PREFIX_OPF = \"opf\";\n  //在EPUB3标准中，packge前面没有opf头，一些epub阅读器也不支持opf头。\n  //Some Epub Reader not reconize op:packge,So just let it empty;\n  public static final String PREFIX_OPF = \"\";\n  //添加 version 变量来区分Epub文件的版本\n  //Add the version field to distinguish the version of EPUB file\n  public static final String version=\"version\";\n  public static final String dateFormat = \"yyyy-MM-dd\";\n\n  protected interface DCTags {\n\n    String title = \"title\";\n    String creator = \"creator\";\n    String subject = \"subject\";\n    String description = \"description\";\n    String publisher = \"publisher\";\n    String contributor = \"contributor\";\n    String date = \"date\";\n    String type = \"type\";\n    String format = \"format\";\n    String identifier = \"identifier\";\n    String source = \"source\";\n    String language = \"language\";\n    String relation = \"relation\";\n    String coverage = \"coverage\";\n    String rights = \"rights\";\n  }\n\n  protected interface DCAttributes {\n\n    String scheme = \"scheme\";\n    String id = \"id\";\n  }\n\n  protected interface OPFTags {\n\n    String metadata = \"metadata\";\n    String meta = \"meta\";\n    String manifest = \"manifest\";\n    String packageTag = \"package\";\n    String itemref = \"itemref\";\n    String spine = \"spine\";\n    String reference = \"reference\";\n    String guide = \"guide\";\n    String item = \"item\";\n  }\n\n  protected interface OPFAttributes {\n\n    String uniqueIdentifier = \"unique-identifier\";\n    String idref = \"idref\";\n    String name = \"name\";\n    String content = \"content\";\n    String type = \"type\";\n    String href = \"href\";\n    String linear = \"linear\";\n    String event = \"event\";\n    String role = \"role\";\n    String file_as = \"file-as\";\n    String id = \"id\";\n    String media_type = \"media-type\";\n    String title = \"title\";\n    String toc = \"toc\";\n    String version = \"version\";\n    String scheme = \"scheme\";\n    String property = \"property\";\n    //add for epub3\n    /**\n     * add for epub3\n     */\n    String properties=\"properties\";\n  }\n\n  protected interface OPFValues {\n\n    String meta_cover = \"cover\";\n    String reference_cover = \"cover\";\n    String no = \"no\";\n    String generator = \"generator\";\n    String duokan = \"duokan-body-font\";\n  }\n}"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataReader.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport javax.xml.namespace.QName;\n\nimport me.ag2s.epublib.domain.Author;\nimport me.ag2s.epublib.domain.Date;\nimport me.ag2s.epublib.domain.Identifier;\nimport me.ag2s.epublib.domain.Metadata;\nimport me.ag2s.epublib.util.StringUtil;\n\n/**\n * Reads the package document metadata.\n * <p>\n * In its own separate class because the PackageDocumentReader became a bit large and unwieldy.\n *\n * @author paul\n */\n// package\nclass PackageDocumentMetadataReader extends PackageDocumentBase {\n\n    private static final String TAG = PackageDocumentMetadataReader.class.getName();\n\n    public static Metadata readMetadata(Document packageDocument) {\n        Metadata result = new Metadata();\n        Element metadataElement = DOMUtil\n                .getFirstElementByTagNameNS(packageDocument.getDocumentElement(),\n                        NAMESPACE_OPF, OPFTags.metadata);\n        if (metadataElement == null) {\n            // Log.e(TAG, \"Package does not contain element \" + OPFTags.metadata);\n            System.err.println(TAG + \" \" + \"Package does not contain element \" + OPFTags.metadata);\n            return result;\n        }\n        result.setTitles(DOMUtil\n                .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,\n                        DCTags.title));\n        result.setPublishers(DOMUtil\n                .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,\n                        DCTags.publisher));\n        result.setDescriptions(DOMUtil\n                .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,\n                        DCTags.description));\n        result.setRights(DOMUtil\n                .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,\n                        DCTags.rights));\n        result.setTypes(DOMUtil\n                .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,\n                        DCTags.type));\n        result.setSubjects(DOMUtil\n                .getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,\n                        DCTags.subject));\n        result.setIdentifiers(readIdentifiers(metadataElement));\n        result.setAuthors(readCreators(metadataElement));\n        result.setContributors(readContributors(metadataElement));\n        result.setDates(readDates(metadataElement));\n        result.setOtherProperties(readOtherProperties(metadataElement));\n        result.setMetaAttributes(readMetaProperties(metadataElement));\n        Element languageTag = DOMUtil\n                .getFirstElementByTagNameNS(metadataElement, NAMESPACE_DUBLIN_CORE,\n                        DCTags.language);\n        if (languageTag != null) {\n            result.setLanguage(DOMUtil.getTextChildrenContent(languageTag));\n        }\n\n        return result;\n    }\n\n    /**\n     * consumes meta tags that have a property attribute as defined in the standard. For example:\n     * &lt;meta property=\"rendition:layout\"&gt;pre-paginated&lt;/meta&gt;\n     *\n     * @param metadataElement metadataElement\n     * @return Map<QName, String>\n     */\n    private static Map<QName, String> readOtherProperties(\n            Element metadataElement) {\n        Map<QName, String> result = new HashMap<>();\n\n        NodeList metaTags = metadataElement.getElementsByTagName(OPFTags.meta);\n        for (int i = 0; i < metaTags.getLength(); i++) {\n            Node metaNode = metaTags.item(i);\n            Node property = metaNode.getAttributes()\n                    .getNamedItem(OPFAttributes.property);\n            if (property != null) {\n                String name = property.getNodeValue();\n                String value = metaNode.getTextContent();\n                result.put(new QName(name), value);\n            }\n        }\n\n        return result;\n    }\n\n    /**\n     * consumes meta tags that have a property attribute as defined in the standard. For example:\n     * &lt;meta property=\"rendition:layout\"&gt;pre-paginated&lt;/meta&gt;\n     *\n     * @param metadataElement metadataElement\n     * @return Map<String, String>\n     */\n    private static Map<String, String> readMetaProperties(\n            Element metadataElement) {\n        Map<String, String> result = new HashMap<>();\n\n        NodeList metaTags = metadataElement.getElementsByTagName(OPFTags.meta);\n        for (int i = 0; i < metaTags.getLength(); i++) {\n            Element metaElement = (Element) metaTags.item(i);\n            String name = metaElement.getAttribute(OPFAttributes.name);\n            String value = metaElement.getAttribute(OPFAttributes.content);\n            result.put(name, value);\n        }\n\n        return result;\n    }\n\n    private static String getBookIdId(Document document) {\n        Element packageElement = DOMUtil\n                .getFirstElementByTagNameNS(document.getDocumentElement(),\n                        NAMESPACE_OPF, OPFTags.packageTag);\n        if (packageElement == null) {\n            return null;\n        }\n        return DOMUtil.getAttribute(packageElement, NAMESPACE_OPF, OPFAttributes.uniqueIdentifier);\n\n    }\n\n    private static List<Author> readCreators(Element metadataElement) {\n        return readAuthors(DCTags.creator, metadataElement);\n    }\n\n    private static List<Author> readContributors(Element metadataElement) {\n        return readAuthors(DCTags.contributor, metadataElement);\n    }\n\n    private static List<Author> readAuthors(String authorTag,\n                                            Element metadataElement) {\n        NodeList elements = metadataElement\n                .getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, authorTag);\n        List<Author> result = new ArrayList<>(elements.getLength());\n        for (int i = 0; i < elements.getLength(); i++) {\n            Element authorElement = (Element) elements.item(i);\n            Author author = createAuthor(authorElement);\n            if (author != null) {\n                result.add(author);\n            }\n        }\n        return result;\n\n    }\n\n    private static List<Date> readDates(Element metadataElement) {\n        NodeList elements = metadataElement\n                .getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, DCTags.date);\n        List<Date> result = new ArrayList<>(elements.getLength());\n        for (int i = 0; i < elements.getLength(); i++) {\n            Element dateElement = (Element) elements.item(i);\n            Date date;\n            try {\n                date = new Date(DOMUtil.getTextChildrenContent(dateElement),\n                        DOMUtil.getAttribute(dateElement, NAMESPACE_OPF, OPFAttributes.event));\n                result.add(date);\n            } catch (IllegalArgumentException e) {\n                // Log.e(TAG, e.getMessage());\n                e.printStackTrace();\n            }\n        }\n        return result;\n\n    }\n\n    private static Author createAuthor(Element authorElement) {\n        String authorString = DOMUtil.getTextChildrenContent(authorElement);\n        if (StringUtil.isBlank(authorString)) {\n            return null;\n        }\n        int spacePos = authorString.lastIndexOf(' ');\n        Author result;\n        if (spacePos < 0) {\n            result = new Author(authorString);\n        } else {\n            result = new Author(authorString.substring(0, spacePos),\n                    authorString.substring(spacePos + 1));\n        }\n        result.setRole(\n                DOMUtil.getAttribute(authorElement, NAMESPACE_OPF, OPFAttributes.role));\n        return result;\n    }\n\n\n    private static List<Identifier> readIdentifiers(Element metadataElement) {\n        NodeList identifierElements = metadataElement\n                .getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, DCTags.identifier);\n        if (identifierElements.getLength() == 0) {\n            // Log.e(TAG, \"Package does not contain element \" + DCTags.identifier);\n            System.err.println(TAG + \" \" + \"Package does not contain element \" + DCTags.identifier);\n            return new ArrayList<>();\n        }\n        String bookIdId = getBookIdId(metadataElement.getOwnerDocument());\n        List<Identifier> result = new ArrayList<>(\n                identifierElements.getLength());\n        for (int i = 0; i < identifierElements.getLength(); i++) {\n            Element identifierElement = (Element) identifierElements.item(i);\n            String schemeName = DOMUtil.getAttribute(identifierElement, NAMESPACE_OPF, DCAttributes.scheme);\n            String identifierValue = DOMUtil\n                    .getTextChildrenContent(identifierElement);\n            if (StringUtil.isBlank(identifierValue)) {\n                continue;\n            }\n            Identifier identifier = new Identifier(schemeName, identifierValue);\n            if (identifierElement.getAttribute(\"id\").equals(bookIdId)) {\n                identifier.setBookId(true);\n            }\n            result.add(identifier);\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport org.xmlpull.v1.XmlSerializer;\n\nimport java.io.IOException;\nimport java.util.List;\nimport java.util.Map;\n\nimport javax.xml.namespace.QName;\n\nimport me.ag2s.epublib.Constants;\nimport me.ag2s.epublib.domain.Author;\nimport me.ag2s.epublib.domain.Date;\nimport me.ag2s.epublib.domain.EpubBook;\nimport me.ag2s.epublib.domain.Identifier;\nimport me.ag2s.epublib.util.StringUtil;\n\npublic class PackageDocumentMetadataWriter extends PackageDocumentBase {\n\n  /**\n   * Writes the book's metadata.\n   *\n   * @param book       book\n   * @param serializer serializer\n   * @throws IOException              IOException\n   * @throws IllegalStateException    IllegalStateException\n   * @throws IllegalArgumentException IllegalArgumentException\n   */\n  public static void writeMetaData(EpubBook book, XmlSerializer serializer)\n          throws IllegalArgumentException, IllegalStateException, IOException {\n    serializer.startTag(NAMESPACE_OPF, OPFTags.metadata);\n    serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE);\n    serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF);\n\n    writeIdentifiers(book.getMetadata().getIdentifiers(), serializer);\n    writeSimpleMetdataElements(DCTags.title, book.getMetadata().getTitles(),\n            serializer);\n    writeSimpleMetdataElements(DCTags.subject, book.getMetadata().getSubjects(),\n            serializer);\n    writeSimpleMetdataElements(DCTags.description,\n        book.getMetadata().getDescriptions(), serializer);\n    writeSimpleMetdataElements(DCTags.publisher,\n        book.getMetadata().getPublishers(), serializer);\n    writeSimpleMetdataElements(DCTags.type, book.getMetadata().getTypes(),\n        serializer);\n    writeSimpleMetdataElements(DCTags.rights, book.getMetadata().getRights(),\n        serializer);\n\n    // write authors\n    for (Author author : book.getMetadata().getAuthors()) {\n      serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.creator);\n      serializer.attribute(NAMESPACE_OPF, OPFAttributes.role,\n          author.getRelator().getCode());\n      serializer.attribute(NAMESPACE_OPF, OPFAttributes.file_as,\n          author.getLastname() + \", \" + author.getFirstname());\n      serializer.text(author.getFirstname() + \" \" + author.getLastname());\n      serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.creator);\n    }\n\n    // write contributors\n    for (Author author : book.getMetadata().getContributors()) {\n      serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.contributor);\n      serializer.attribute(NAMESPACE_OPF, OPFAttributes.role,\n          author.getRelator().getCode());\n      serializer.attribute(NAMESPACE_OPF, OPFAttributes.file_as,\n          author.getLastname() + \", \" + author.getFirstname());\n      serializer.text(author.getFirstname() + \" \" + author.getLastname());\n      serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.contributor);\n    }\n\n    // write dates\n    for (Date date : book.getMetadata().getDates()) {\n      serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.date);\n      if (date.getEvent() != null) {\n        serializer.attribute(NAMESPACE_OPF, OPFAttributes.event,\n            date.getEvent().toString());\n      }\n      serializer.text(date.getValue());\n      serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.date);\n    }\n\n    // write language\n    if (StringUtil.isNotBlank(book.getMetadata().getLanguage())) {\n      serializer.startTag(NAMESPACE_DUBLIN_CORE, \"language\");\n      serializer.text(book.getMetadata().getLanguage());\n      serializer.endTag(NAMESPACE_DUBLIN_CORE, \"language\");\n    }\n\n    // write other properties\n    if (book.getMetadata().getOtherProperties() != null) {\n      for (Map.Entry<QName, String> mapEntry : book.getMetadata()\n          .getOtherProperties().entrySet()) {\n        serializer.startTag(mapEntry.getKey().getNamespaceURI(), OPFTags.meta);\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX,\n            OPFAttributes.property, mapEntry.getKey().getLocalPart());\n        serializer.text(mapEntry.getValue());\n        serializer.endTag(mapEntry.getKey().getNamespaceURI(), OPFTags.meta);\n\n      }\n    }\n\n    // write coverimage\n    if (book.getCoverImage() != null) { // write the cover image\n      serializer.startTag(NAMESPACE_OPF, OPFTags.meta);\n      serializer\n          .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name,\n              OPFValues.meta_cover);\n      serializer\n          .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content,\n              book.getCoverImage().getId());\n      serializer.endTag(NAMESPACE_OPF, OPFTags.meta);\n    }\n\n    // write generator\n    serializer.startTag(NAMESPACE_OPF, OPFTags.meta);\n    serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name,\n        OPFValues.generator);\n    serializer\n        .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content,\n            Constants.EPUB_GENERATOR_NAME);\n    serializer.endTag(NAMESPACE_OPF, OPFTags.meta);\n\n    // write duokan\n    serializer.startTag(NAMESPACE_OPF, OPFTags.meta);\n    serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name,\n            OPFValues.duokan);\n    serializer\n            .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content,\n                    Constants.EPUB_DUOKAN_NAME);\n    serializer.endTag(NAMESPACE_OPF, OPFTags.meta);\n\n    serializer.endTag(NAMESPACE_OPF, OPFTags.metadata);\n  }\n\n  private static void writeSimpleMetdataElements(String tagName,\n      List<String> values, XmlSerializer serializer)\n      throws IllegalArgumentException, IllegalStateException, IOException {\n    for (String value : values) {\n      if (StringUtil.isBlank(value)) {\n        continue;\n      }\n      serializer.startTag(NAMESPACE_DUBLIN_CORE, tagName);\n      serializer.text(value);\n      serializer.endTag(NAMESPACE_DUBLIN_CORE, tagName);\n    }\n  }\n\n\n  /**\n   * Writes out the complete list of Identifiers to the package document.\n   * The first identifier for which the bookId is true is made the bookId identifier.\n   * If no identifier has bookId == true then the first bookId identifier is written as the primary.\n   *\n   * @param identifiers identifiers\n   * @param serializer serializer\n   * @throws IllegalStateException e\n   * @throws IllegalArgumentException e\n   * @\n   */\n  private static void writeIdentifiers(List<Identifier> identifiers,\n      XmlSerializer serializer)\n      throws IllegalArgumentException, IllegalStateException, IOException {\n    Identifier bookIdIdentifier = Identifier.getBookIdIdentifier(identifiers);\n    if (bookIdIdentifier == null) {\n      return;\n    }\n\n    serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);\n    serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, DCAttributes.id,\n        BOOK_ID_ID);\n    serializer.attribute(NAMESPACE_OPF, OPFAttributes.scheme,\n        bookIdIdentifier.getScheme());\n    serializer.text(bookIdIdentifier.getValue());\n    serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);\n\n    for (Identifier identifier : identifiers.subList(1, identifiers.size())) {\n      if (identifier == bookIdIdentifier) {\n        continue;\n      }\n      serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);\n      serializer.attribute(NAMESPACE_OPF, \"scheme\", identifier.getScheme());\n      serializer.text(identifier.getValue());\n      serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.NodeList;\nimport org.xml.sax.SAXException;\n\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLDecoder;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport me.ag2s.epublib.Constants;\nimport me.ag2s.epublib.domain.EpubBook;\nimport me.ag2s.epublib.domain.Guide;\nimport me.ag2s.epublib.domain.GuideReference;\nimport me.ag2s.epublib.domain.MediaType;\nimport me.ag2s.epublib.domain.MediaTypes;\nimport me.ag2s.epublib.domain.Resource;\nimport me.ag2s.epublib.domain.Resources;\nimport me.ag2s.epublib.domain.Spine;\nimport me.ag2s.epublib.domain.SpineReference;\nimport me.ag2s.epublib.util.ResourceUtil;\nimport me.ag2s.epublib.util.StringUtil;\n\n/**\n * Reads the opf package document as defined by namespace http://www.idpf.org/2007/opf\n *\n * @author paul\n */\npublic class PackageDocumentReader extends PackageDocumentBase {\n\n    private static final String TAG = PackageDocumentReader.class.getName();\n    private static final String[] POSSIBLE_NCX_ITEM_IDS = new String[]{\"toc\",\n            \"ncx\", \"ncxtoc\", \"htmltoc\"};\n\n\n    public static void read(\n            Resource packageResource, EpubReader epubReader, EpubBook book,\n            Resources resources)\n            throws SAXException, IOException {\n        Document packageDocument = ResourceUtil.getAsDocument(packageResource);\n        String packageHref = packageResource.getHref();\n        resources = fixHrefs(packageHref, resources);\n        readGuide(packageDocument, epubReader, book, resources);\n\n        // Books sometimes use non-identifier ids. We map these here to legal ones\n        Map<String, String> idMapping = new HashMap<>();\n        String version = DOMUtil.getAttribute(packageDocument.getDocumentElement(), PREFIX_OPF, PackageDocumentBase.version);\n\n        resources = readManifest(packageDocument, packageHref, epubReader,\n                resources, idMapping);\n        book.setResources(resources);\n        book.setVersion(version);\n        readCover(packageDocument, book);\n        book.setMetadata(\n                PackageDocumentMetadataReader.readMetadata(packageDocument));\n        book.setSpine(readSpine(packageDocument, book.getResources(), idMapping));\n\n        // if we did not find a cover page then we make the first page of the book the cover page\n        if (book.getCoverPage() == null && book.getSpine().size() > 0) {\n            book.setCoverPage(book.getSpine().getResource(0));\n        }\n    }\n\n//\tprivate static Resource readCoverImage(Element metadataElement, Resources resources) {\n//\t\tString coverResourceId = DOMUtil.getFindAttributeValue(metadataElement.getOwnerDocument(), NAMESPACE_OPF, OPFTags.meta, OPFAttributes.name, OPFValues.meta_cover, OPFAttributes.content);\n//\t\tif (StringUtil.isBlank(coverResourceId)) {\n//\t\t\treturn null;\n//\t\t}\n//\t\tResource coverResource = resources.getByIdOrHref(coverResourceId);\n//\t\treturn coverResource;\n//\t}\n\n\n    /**\n     * Reads the manifest containing the resource ids, hrefs and mediatypes.\n     *\n     * @param packageDocument e\n     * @param packageHref     e\n     * @param epubReader      e\n     * @param resources       e\n     * @param idMapping       e\n     * @return a Map with resources, with their id's as key.\n     */\n    @SuppressWarnings(\"unused\")\n    private static Resources readManifest(Document packageDocument,\n                                          String packageHref,\n                                          EpubReader epubReader, Resources resources,\n                                          Map<String, String> idMapping) {\n        Element manifestElement = DOMUtil\n                .getFirstElementByTagNameNS(packageDocument.getDocumentElement(),\n                        NAMESPACE_OPF, OPFTags.manifest);\n        Resources result = new Resources();\n        if (manifestElement == null) {\n            // Log.e(TAG,\n            //         \"Package document does not contain element \" + OPFTags.manifest);\n            System.err.println(TAG + \" \" + \"Package does not contain element \" + OPFTags.manifest);\n            return result;\n        }\n        NodeList itemElements = manifestElement\n                .getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.item);\n        for (int i = 0; i < itemElements.getLength(); i++) {\n            Element itemElement = (Element) itemElements.item(i);\n            String id = DOMUtil\n                    .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.id);\n            String href = DOMUtil\n                    .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.href);\n\n            try {\n                href = URLDecoder.decode(href, Constants.CHARACTER_ENCODING);\n            } catch (UnsupportedEncodingException e) {\n                // Log.e(TAG, e.getMessage());\n                e.printStackTrace();\n            }\n            String mediaTypeName = DOMUtil\n                    .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.media_type);\n            Resource resource = resources.remove(href);\n            if (resource == null) {\n                // Log.e(TAG, \"resource with href '\" + href + \"' not found\");\n                System.err.println(TAG + \" \" + \"resource with href '\" + href + \"' not found\");\n                continue;\n            }\n            resource.setId(id);\n            //for epub3\n            String properties = DOMUtil.getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.properties);\n            resource.setProperties(properties);\n\n            MediaType mediaType = MediaTypes.getMediaTypeByName(mediaTypeName);\n            if (mediaType != null) {\n                resource.setMediaType(mediaType);\n            }\n            result.add(resource);\n            idMapping.put(id, resource.getId());\n        }\n        return result;\n    }\n\n\n    /**\n     * Reads the book's guide.\n     * Here some more attempts are made at finding the cover page.\n     *\n     * @param packageDocument r\n     * @param epubReader      r\n     * @param book            r\n     * @param resources       g\n     */\n    @SuppressWarnings(\"unused\")\n    private static void readGuide(Document packageDocument,\n                                  EpubReader epubReader, EpubBook book, Resources resources) {\n        Element guideElement = DOMUtil\n                .getFirstElementByTagNameNS(packageDocument.getDocumentElement(),\n                        NAMESPACE_OPF, OPFTags.guide);\n        if (guideElement == null) {\n            return;\n        }\n        Guide guide = book.getGuide();\n        NodeList guideReferences = guideElement\n                .getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.reference);\n        for (int i = 0; i < guideReferences.getLength(); i++) {\n            Element referenceElement = (Element) guideReferences.item(i);\n            String resourceHref = DOMUtil\n                    .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.href);\n            if (StringUtil.isBlank(resourceHref)) {\n                continue;\n            }\n            Resource resource = resources.getByHref(StringUtil\n                    .substringBefore(resourceHref, Constants.FRAGMENT_SEPARATOR_CHAR));\n            if (resource == null) {\n                // Log.e(TAG, \"Guide is referencing resource with href \" + resourceHref\n                //         + \" which could not be found\");\n                System.err.println(TAG + \" \" + \"Guide is referencing resource with href \" + resourceHref\n                + \" which could not be found\");\n                continue;\n            }\n            String type = DOMUtil\n                    .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.type);\n            if (StringUtil.isBlank(type)) {\n                // Log.e(TAG, \"Guide is referencing resource with href \" + resourceHref\n                //         + \" which is missing the 'type' attribute\");\n                System.err.println(TAG + \" \" + \"Guide is referencing resource with href \" + resourceHref\n                + \" which is missing the 'type' attribute\");\n                continue;\n            }\n            String title = DOMUtil\n                    .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.title);\n            if (GuideReference.COVER.equalsIgnoreCase(type)) {\n                continue; // cover is handled elsewhere\n            }\n            GuideReference reference = new GuideReference(resource, type, title,\n                    StringUtil\n                            .substringAfter(resourceHref, Constants.FRAGMENT_SEPARATOR_CHAR));\n            guide.addReference(reference);\n        }\n    }\n\n\n    /**\n     * Strips off the package prefixes up to the href of the packageHref.\n     * <p>\n     * Example:\n     * If the packageHref is \"OEBPS/content.opf\" then a resource href like \"OEBPS/foo/bar.html\" will be turned into \"foo/bar.html\"\n     *\n     * @param packageHref     f\n     * @param resourcesByHref g\n     * @return The stripped package href\n     */\n    static Resources fixHrefs(String packageHref,\n                              Resources resourcesByHref) {\n        int lastSlashPos = packageHref.lastIndexOf('/');\n        if (lastSlashPos < 0) {\n            return resourcesByHref;\n        }\n        Resources result = new Resources();\n        for (Resource resource : resourcesByHref.getAll()) {\n            if (StringUtil.isNotBlank(resource.getHref())\n                    && resource.getHref().length() > lastSlashPos) {\n                resource.setHref(resource.getHref().substring(lastSlashPos + 1));\n            }\n            result.add(resource);\n        }\n        return result;\n    }\n\n    /**\n     * Reads the document's spine, containing all sections in reading order.\n     *\n     * @param packageDocument b\n     * @param resources       b\n     * @param idMapping       b\n     * @return the document's spine, containing all sections in reading order.\n     */\n    private static Spine readSpine(Document packageDocument, Resources resources,\n                                   Map<String, String> idMapping) {\n\n        Element spineElement = DOMUtil\n                .getFirstElementByTagNameNS(packageDocument.getDocumentElement(),\n                        NAMESPACE_OPF, OPFTags.spine);\n        if (spineElement == null) {\n            // Log.e(TAG, \"Element \" + OPFTags.spine\n            //         + \" not found in package document, generating one automatically\");\n            System.err.println(TAG + \" \" + \"Element \" + OPFTags.spine\n                + \" not found in package document, generating one automatically\");\n            return generateSpineFromResources(resources);\n        }\n        Spine result = new Spine();\n        String tocResourceId = DOMUtil.getAttribute(spineElement, NAMESPACE_OPF, OPFAttributes.toc);\n        // Log.v(TAG,tocResourceId);\n        System.out.println(TAG + \" \" + tocResourceId);\n        result.setTocResource(findTableOfContentsResource(tocResourceId, resources));\n        NodeList spineNodes = DOMUtil.getElementsByTagNameNS(packageDocument, NAMESPACE_OPF, OPFTags.itemref);\n        if(spineNodes==null){\n            // Log.e(TAG,\"spineNodes is null\");\n            System.err.println(TAG + \" \" + \"spineNodes is null\");\n            return result;\n        }\n        List<SpineReference> spineReferences = new ArrayList<>(spineNodes.getLength());\n        for (int i = 0; i < spineNodes.getLength(); i++) {\n            Element spineItem = (Element) spineNodes.item(i);\n            String itemref = DOMUtil.getAttribute(spineItem, NAMESPACE_OPF, OPFAttributes.idref);\n            if (StringUtil.isBlank(itemref)) {\n                // Log.e(TAG, \"itemref with missing or empty idref\"); // XXX\n                System.err.println(TAG + \" \" + \"itemref with missing or empty idref\");\n                continue;\n            }\n            String id = idMapping.get(itemref);\n            if (id == null) {\n                id = itemref;\n            }\n\n            Resource resource = resources.getByIdOrHref(id);\n            if (resource == null) {\n                // Log.e(TAG, \"resource with id '\" + id + \"' not found\");\n                System.err.println(TAG + \" \" + \"resource with id '\" + id + \"' not found\");\n                continue;\n            }\n\n            SpineReference spineReference = new SpineReference(resource);\n            if (OPFValues.no.equalsIgnoreCase(DOMUtil\n                    .getAttribute(spineItem, NAMESPACE_OPF, OPFAttributes.linear))) {\n                spineReference.setLinear(false);\n            }\n            spineReferences.add(spineReference);\n        }\n        result.setSpineReferences(spineReferences);\n        return result;\n    }\n\n    /**\n     * Creates a spine out of all resources in the resources.\n     * The generated spine consists of all XHTML pages in order of their href.\n     *\n     * @param resources f\n     * @return a spine created out of all resources in the resources.\n     */\n    private static Spine generateSpineFromResources(Resources resources) {\n        Spine result = new Spine();\n        List<String> resourceHrefs = new ArrayList<>(resources.getAllHrefs());\n        Collections.sort(resourceHrefs, String.CASE_INSENSITIVE_ORDER);\n        for (String resourceHref : resourceHrefs) {\n            Resource resource = resources.getByHref(resourceHref);\n            if (resource.getMediaType() == MediaTypes.NCX) {\n                result.setTocResource(resource);\n            } else if (resource.getMediaType() == MediaTypes.XHTML) {\n                result.addSpineReference(new SpineReference(resource));\n            }\n        }\n        return result;\n    }\n\n\n    /**\n     * The spine tag should contain a 'toc' attribute with as value the resource id of the table of contents resource.\n     * <p>\n     * Here we try several ways of finding this table of contents resource.\n     * We try the given attribute value, some often-used ones and finally look through all resources for the first resource with the table of contents mimetype.\n     *\n     * @param tocResourceId g\n     * @param resources     g\n     * @return the Resource containing the table of contents\n     */\n    static Resource findTableOfContentsResource(String tocResourceId,\n                                                Resources resources) {\n        Resource tocResource;\n        //一些epub3的文件为了兼容epub2,保留的epub2的目录文件，这里优先选择epub3的xml目录\n        tocResource = resources.getByProperties(\"nav\");\n        if (tocResource != null) {\n            return tocResource;\n        }\n\n        if (StringUtil.isNotBlank(tocResourceId)) {\n            tocResource = resources.getByIdOrHref(tocResourceId);\n        }\n\n        if (tocResource != null) {\n            return tocResource;\n        }\n\n        // get the first resource with the NCX mediatype\n        tocResource = resources.findFirstResourceByMediaType(MediaTypes.NCX);\n\n        if (tocResource == null) {\n            for (String possibleNcxItemId : POSSIBLE_NCX_ITEM_IDS) {\n                tocResource = resources.getByIdOrHref(possibleNcxItemId);\n                if (tocResource != null) {\n                    break;\n                }\n                tocResource = resources\n                        .getByIdOrHref(possibleNcxItemId.toUpperCase());\n                if (tocResource != null) {\n                    break;\n                }\n            }\n        }\n\n\n        if (tocResource == null) {\n            System.err.println(TAG + \" \" +\n                    \"Could not find table of contents resource. Tried resource with id '\"\n                            + tocResourceId + \"', \" + Constants.DEFAULT_TOC_ID + \", \"\n                            + Constants.DEFAULT_TOC_ID.toUpperCase()\n                            + \" and any NCX resource.\");\n        }\n        return tocResource;\n    }\n\n\n    /**\n     * Find all resources that have something to do with the coverpage and the cover image.\n     * Search the meta tags and the guide references\n     *\n     * @param packageDocument s\n     * @return all resources that have something to do with the coverpage and the cover image.\n     */\n    // package\n    static Set<String> findCoverHrefs(Document packageDocument) {\n\n        Set<String> result = new HashSet<>();\n\n        // try and find a meta tag with name = 'cover' and a non-blank id\n        String coverResourceId = DOMUtil\n                .getFindAttributeValue(packageDocument, NAMESPACE_OPF,\n                        OPFTags.meta, OPFAttributes.name, OPFValues.meta_cover,\n                        OPFAttributes.content);\n\n        if (StringUtil.isNotBlank(coverResourceId)) {\n            String coverHref = DOMUtil\n                    .getFindAttributeValue(packageDocument, NAMESPACE_OPF,\n                            OPFTags.item, OPFAttributes.id, coverResourceId,\n                            OPFAttributes.href);\n            if (StringUtil.isNotBlank(coverHref)) {\n                result.add(coverHref);\n            } else {\n                result.add(\n                        coverResourceId); // maybe there was a cover href put in the cover id attribute\n            }\n        }\n        // try and find a reference tag with type is 'cover' and reference is not blank\n        String coverHref = DOMUtil\n                .getFindAttributeValue(packageDocument, NAMESPACE_OPF,\n                        OPFTags.reference, OPFAttributes.type, OPFValues.reference_cover,\n                        OPFAttributes.href);\n        if (StringUtil.isNotBlank(coverHref)) {\n            result.add(coverHref);\n        }\n        return result;\n    }\n\n    /**\n     * Finds the cover resource in the packageDocument and adds it to the book if found.\n     * Keeps the cover resource in the resources map\n     *\n     * @param packageDocument s\n     * @param book            x\n     */\n    private static void readCover(Document packageDocument, EpubBook book) {\n\n        Collection<String> coverHrefs = findCoverHrefs(packageDocument);\n        for (String coverHref : coverHrefs) {\n            Resource resource = book.getResources().getByHref(coverHref);\n            if (resource == null) {\n                System.err.println(TAG + \" \" + \"Cover resource \" + coverHref + \" not found\");\n                continue;\n            }\n            if (resource.getMediaType() == MediaTypes.XHTML) {\n                book.setCoverPage(resource);\n            } else if (MediaTypes.isBitmapImage(resource.getMediaType())) {\n                book.setCoverImage(resource);\n            }\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport org.xmlpull.v1.XmlSerializer;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport me.ag2s.epublib.Constants;\nimport me.ag2s.epublib.domain.EpubBook;\nimport me.ag2s.epublib.domain.Guide;\nimport me.ag2s.epublib.domain.GuideReference;\nimport me.ag2s.epublib.domain.MediaTypes;\nimport me.ag2s.epublib.domain.Resource;\nimport me.ag2s.epublib.domain.Spine;\nimport me.ag2s.epublib.domain.SpineReference;\nimport me.ag2s.epublib.util.StringUtil;\n\n/**\n * Writes the opf package document as defined by namespace http://www.idpf.org/2007/opf\n *\n * @author paul\n */\npublic class PackageDocumentWriter extends PackageDocumentBase {\n\n    private static final String TAG = PackageDocumentWriter.class.getName();\n\n    public static void write(EpubWriter epubWriter, XmlSerializer serializer,\n                             EpubBook book) {\n        try {\n            serializer.startDocument(Constants.CHARACTER_ENCODING, false);\n            serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF);\n            serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE);\n            serializer.startTag(NAMESPACE_OPF, OPFTags.packageTag);\n            serializer\n                    .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.version,\n                            book.getVersion());\n            serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX,\n                    OPFAttributes.uniqueIdentifier, BOOK_ID_ID);\n\n            PackageDocumentMetadataWriter.writeMetaData(book, serializer);\n\n            writeManifest(book, epubWriter, serializer);\n            writeSpine(book, epubWriter, serializer);\n            writeGuide(book, epubWriter, serializer);\n\n            serializer.endTag(NAMESPACE_OPF, OPFTags.packageTag);\n            serializer.endDocument();\n            serializer.flush();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    /**\n     * Writes the package's spine.\n     *\n     * @param book e\n     * @param epubWriter g\n     * @param serializer g\n     * @throws IOException g\n     * @throws IllegalStateException g\n     * @throws IllegalArgumentException 1@throws XMLStreamException\n     */\n    @SuppressWarnings(\"unused\")\n    private static void writeSpine(EpubBook book, EpubWriter epubWriter,\n                                   XmlSerializer serializer)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        serializer.startTag(NAMESPACE_OPF, OPFTags.spine);\n        Resource tocResource = book.getSpine().getTocResource();\n        String tocResourceId = tocResource.getId();\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.toc,\n                tocResourceId);\n\n        if (book.getCoverPage() != null // there is a cover page\n                && book.getSpine().findFirstResourceById(book.getCoverPage().getId())\n                < 0) { // cover page is not already in the spine\n            // write the cover html file\n            serializer.startTag(NAMESPACE_OPF, OPFTags.itemref);\n            serializer\n                    .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref,\n                            book.getCoverPage().getId());\n            serializer\n                    .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear,\n                            \"no\");\n            serializer.endTag(NAMESPACE_OPF, OPFTags.itemref);\n        }\n        writeSpineItems(book.getSpine(), serializer);\n        serializer.endTag(NAMESPACE_OPF, OPFTags.spine);\n    }\n\n\n    private static void writeManifest(EpubBook book, EpubWriter epubWriter,\n                                      XmlSerializer serializer)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        serializer.startTag(NAMESPACE_OPF, OPFTags.manifest);\n\n        serializer.startTag(NAMESPACE_OPF, OPFTags.item);\n\n        //For EPUB3\n        if (book.isEpub3()) {\n            serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.properties, NCXDocumentV3.V3_NCX_PROPERTIES);\n            serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, NCXDocumentV3.NCX_ITEM_ID);\n            serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, NCXDocumentV3.DEFAULT_NCX_HREF);\n            serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, NCXDocumentV3.V3_NCX_MEDIATYPE.getName());\n        } else {\n            serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id,\n                    epubWriter.getNcxId());\n            serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, epubWriter.getNcxHref());\n            serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, epubWriter.getNcxMediaType());\n        }\n\n        serializer.endTag(NAMESPACE_OPF, OPFTags.item);\n\n//\t\twriteCoverResources(book, serializer);\n\n        for (Resource resource : getAllResourcesSortById(book)) {\n            writeItem(book, resource, serializer);\n        }\n\n        serializer.endTag(NAMESPACE_OPF, OPFTags.manifest);\n    }\n\n    private static List<Resource> getAllResourcesSortById(EpubBook book) {\n        List<Resource> allResources = new ArrayList<>(\n                book.getResources().getAll());\n        Collections.sort(allResources, (resource1, resource2) -> resource1.getId().compareToIgnoreCase(resource2.getId()));\n        return allResources;\n    }\n\n    /**\n     * Writes a resources as an item element\n     *\n     * @param resource   g\n     * @param serializer g\n     * @throws IOException              g\n     * @throws IllegalStateException    g\n     * @throws IllegalArgumentException 1@throws XMLStreamException\n     */\n    private static void writeItem(EpubBook book, Resource resource,\n                                  XmlSerializer serializer)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        if (resource == null ||\n                (resource.getMediaType() == MediaTypes.NCX\n                        && book.getSpine().getTocResource() != null)) {\n            return;\n        }\n        if (StringUtil.isBlank(resource.getId())) {\n//      log.error(\"resource id must not be empty (href: \" + resource.getHref()\n//          + \", mediatype:\" + resource.getMediaType() + \")\");\n            System.err.println(TAG + \" \" + \"resource id must not be empty (href: \" + resource.getHref()\n                    + \", mediatype:\" + resource.getMediaType() + \")\");\n            return;\n        }\n        if (StringUtil.isBlank(resource.getHref())) {\n//      log.error(\"resource href must not be empty (id: \" + resource.getId()\n//          + \", mediatype:\" + resource.getMediaType() + \")\");\n            System.err.println(TAG + \" \" + \"resource href must not be empty (id: \" + resource.getId()\n                    + \", mediatype:\" + resource.getMediaType() + \")\");\n            return;\n        }\n        if (resource.getMediaType() == null) {\n//      log.error(\"resource mediatype must not be empty (id: \" + resource.getId()\n//          + \", href:\" + resource.getHref() + \")\");\n            System.err.println(TAG + \" \" + \"resource mediatype must not be empty (id: \" + resource.getId()\n                    + \", href:\" + resource.getHref() + \")\");\n            return;\n        }\n        serializer.startTag(NAMESPACE_OPF, OPFTags.item);\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id,\n                resource.getId());\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href,\n                resource.getHref());\n        serializer\n                .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type,\n                        resource.getMediaType().getName());\n        serializer.endTag(NAMESPACE_OPF, OPFTags.item);\n    }\n\n    /**\n     * List all spine references\n     *\n     * @throws IOException f\n     * @throws IllegalStateException f\n     * @throws IllegalArgumentException f\n     */\n    @SuppressWarnings(\"unused\")\n    private static void writeSpineItems(Spine spine, XmlSerializer serializer)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        for (SpineReference spineReference : spine.getSpineReferences()) {\n            serializer.startTag(NAMESPACE_OPF, OPFTags.itemref);\n            serializer\n                    .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref,\n                            spineReference.getResourceId());\n            if (!spineReference.isLinear()) {\n                serializer\n                        .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear,\n                                OPFValues.no);\n            }\n            serializer.endTag(NAMESPACE_OPF, OPFTags.itemref);\n        }\n    }\n\n    private static void writeGuide(EpubBook book, EpubWriter epubWriter,\n                                   XmlSerializer serializer)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        serializer.startTag(NAMESPACE_OPF, OPFTags.guide);\n        ensureCoverPageGuideReferenceWritten(book.getGuide(), epubWriter,\n                serializer);\n        for (GuideReference reference : book.getGuide().getReferences()) {\n            writeGuideReference(reference, serializer);\n        }\n        serializer.endTag(NAMESPACE_OPF, OPFTags.guide);\n    }\n\n    @SuppressWarnings(\"unused\")\n    private static void ensureCoverPageGuideReferenceWritten(Guide guide,\n                                                             EpubWriter epubWriter, XmlSerializer serializer)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        if (!(guide.getGuideReferencesByType(GuideReference.COVER).isEmpty())) {\n            return;\n        }\n        Resource coverPage = guide.getCoverPage();\n        if (coverPage != null) {\n            writeGuideReference(\n                    new GuideReference(guide.getCoverPage(), GuideReference.COVER,\n                            GuideReference.COVER), serializer);\n        }\n    }\n\n\n    private static void writeGuideReference(GuideReference reference,\n                                            XmlSerializer serializer)\n            throws IllegalArgumentException, IllegalStateException, IOException {\n        if (reference == null) {\n            return;\n        }\n        serializer.startTag(NAMESPACE_OPF, OPFTags.reference);\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.type,\n                reference.getType());\n        serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href,\n                reference.getCompleteHref());\n        if (StringUtil.isNotBlank(reference.getTitle())) {\n            serializer\n                    .attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.title,\n                            reference.getTitle());\n        }\n        serializer.endTag(NAMESPACE_OPF, OPFTags.reference);\n    }\n}"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/epub/ResourcesLoader.java",
    "content": "package me.ag2s.epublib.epub;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Enumeration;\nimport java.util.List;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipException;\nimport java.util.zip.ZipFile;\nimport java.util.zip.ZipInputStream;\n\nimport me.ag2s.epublib.domain.EpubResourceProvider;\nimport me.ag2s.epublib.domain.LazyResource;\nimport me.ag2s.epublib.domain.LazyResourceProvider;\nimport me.ag2s.epublib.domain.MediaType;\nimport me.ag2s.epublib.domain.MediaTypes;\nimport me.ag2s.epublib.domain.Resource;\nimport me.ag2s.epublib.domain.Resources;\nimport me.ag2s.epublib.util.CollectionUtil;\nimport me.ag2s.epublib.util.ResourceUtil;\n\n\n/**\n * Loads Resources from inputStreams, ZipFiles, etc\n *\n * @author paul\n */\npublic class ResourcesLoader {\n\n    private static final String TAG = ResourcesLoader.class.getName();\n\n\n    /**\n     * Loads the entries of the zipFile as resources.\n     * <p>\n     * The MediaTypes that are in the lazyLoadedTypes will not get their\n     * contents loaded, but are stored as references to entries into the\n     * ZipFile and are loaded on demand by the Resource system.\n     *\n     * @param zipFile             import epub zipfile\n     * @param defaultHtmlEncoding epub xhtml default encoding\n     * @param lazyLoadedTypes     lazyLoadedTypes\n     * @return Resources\n     * @throws IOException IOException\n     */\n    public static Resources loadResources(ZipFile zipFile,\n                                          String defaultHtmlEncoding,\n                                          List<MediaType> lazyLoadedTypes) throws IOException {\n\n        LazyResourceProvider resourceProvider =\n                new EpubResourceProvider(zipFile.getName());\n\n        Resources result = new Resources();\n        Enumeration<? extends ZipEntry> entries = zipFile.entries();\n\n        while (entries.hasMoreElements()) {\n            ZipEntry zipEntry = entries.nextElement();\n\n            if (zipEntry == null || zipEntry.isDirectory()) {\n                continue;\n            }\n\n            String href = zipEntry.getName();\n\n            Resource resource;\n\n            if (shouldLoadLazy(href, lazyLoadedTypes)) {\n                resource = new LazyResource(resourceProvider, zipEntry.getSize(), href);\n            } else {\n                resource = ResourceUtil\n                        .createResource(zipEntry, zipFile.getInputStream(zipEntry));\n                /*掌上书苑有很多自制书OPF的nameSpace格式不标准，强制修复成正确的格式*/\n                if (href.endsWith(\"opf\")) {\n                    String string = new String(resource.getData()).replace(\"smlns=\\\"\", \"xmlns=\\\"\");\n                    resource.setData(string.getBytes());\n                }\n\n            }\n\n            if (resource.getMediaType() == MediaTypes.XHTML) {\n                resource.setInputEncoding(defaultHtmlEncoding);\n            }\n            result.add(resource);\n        }\n\n        return result;\n    }\n\n    /**\n     * Whether the given href will load a mediaType that is in the\n     * collection of lazilyLoadedMediaTypes.\n     *\n     * @param href                   href\n     * @param lazilyLoadedMediaTypes lazilyLoadedMediaTypes\n     * @return Whether the given href will load a mediaType that is\n     * in the collection of lazilyLoadedMediaTypes.\n     */\n    private static boolean shouldLoadLazy(String href,\n                                          Collection<MediaType> lazilyLoadedMediaTypes) {\n        if (CollectionUtil.isEmpty(lazilyLoadedMediaTypes)) {\n            return false;\n        }\n        MediaType mediaType = MediaTypes.determineMediaType(href);\n        return lazilyLoadedMediaTypes.contains(mediaType);\n    }\n\n    /**\n     * Loads all entries from the ZipInputStream as Resources.\n     * <p>\n     * Loads the contents of all ZipEntries into memory.\n     * Is fast, but may lead to memory problems when reading large books\n     * on devices with small amounts of memory.\n     *\n     * @param zipInputStream      zipInputStream\n     * @param defaultHtmlEncoding defaultHtmlEncoding\n     * @return Resources\n     * @throws IOException IOException\n     */\n    public static Resources loadResources(ZipInputStream zipInputStream,\n                                          String defaultHtmlEncoding) throws IOException {\n        Resources result = new Resources();\n        ZipEntry zipEntry;\n        do {\n            // get next valid zipEntry\n            zipEntry = getNextZipEntry(zipInputStream);\n            if ((zipEntry == null) || zipEntry.isDirectory()) {\n                continue;\n            }\n            String href = zipEntry.getName();\n\n            // store resource\n            Resource resource = ResourceUtil.createResource(zipEntry, zipInputStream);\n            ///*掌上书苑有很多自制书OPF的nameSpace格式不标准，强制修复成正确的格式*/\n            if (href.endsWith(\"opf\")) {\n                String string = new String(resource.getData()).replace(\"smlns=\\\"\", \"xmlns=\\\"\");\n                resource.setData(string.getBytes());\n            }\n            if (resource.getMediaType() == MediaTypes.XHTML) {\n                resource.setInputEncoding(defaultHtmlEncoding);\n            }\n            result.add(resource);\n        } while (zipEntry != null);\n\n        return result;\n    }\n\n\n    private static ZipEntry getNextZipEntry(ZipInputStream zipInputStream)\n            throws IOException {\n        try {\n            return zipInputStream.getNextEntry();\n        } catch (ZipException e) {\n            //see <a href=\"https://github.com/psiegman/epublib/issues/122\">Issue #122 Infinite loop</a>.\n            //when reading a file that is not a real zip archive or a zero length file, zipInputStream.getNextEntry()\n            //throws an exception and does not advance, so loadResources enters an infinite loop\n            //log.error(\"Invalid or damaged zip file.\", e);\n            // Log.e(TAG, e.getLocalizedMessage());\n            e.printStackTrace();\n            try {\n                zipInputStream.closeEntry();\n            } catch (Exception ignored) {\n            }\n            throw e;\n        }\n    }\n\n    /**\n     * Loads all entries from the ZipInputStream as Resources.\n     * <p>\n     * Loads the contents of all ZipEntries into memory.\n     * Is fast, but may lead to memory problems when reading large books\n     * on devices with small amounts of memory.\n     *\n     * @param zipFile             zipFile\n     * @param defaultHtmlEncoding defaultHtmlEncoding\n     * @return Resources\n     * @throws IOException IOException\n     */\n    public static Resources loadResources(ZipFile zipFile, String defaultHtmlEncoding) throws IOException {\n        List<MediaType> ls = new ArrayList<>();\n        return loadResources(zipFile, defaultHtmlEncoding, ls);\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/CollectionUtil.java",
    "content": "package me.ag2s.epublib.util;\n\nimport java.util.Collection;\nimport java.util.Enumeration;\nimport java.util.Iterator;\nimport java.util.List;\n\npublic class CollectionUtil {\n\n  /**\n   * Wraps an Enumeration around an Iterator\n   * @author paul.siegmann\n   *\n   * @param <T>\n   */\n  private static class IteratorEnumerationAdapter<T> implements Enumeration<T> {\n\n    private final Iterator<T> iterator;\n\n    public IteratorEnumerationAdapter(Iterator<T> iter) {\n      this.iterator = iter;\n    }\n\n    @Override\n    public boolean hasMoreElements() {\n      return iterator.hasNext();\n    }\n\n    @Override\n    public T nextElement() {\n      return iterator.next();\n    }\n  }\n\n  /**\n   * Creates an Enumeration out of the given Iterator.\n   * @param <T>  g\n   * @param it g\n   * @return an Enumeration created out of the given Iterator.\n   */\n  @SuppressWarnings(\"unused\")\n  public static <T> Enumeration<T> createEnumerationFromIterator(\n      Iterator<T> it) {\n    return new IteratorEnumerationAdapter<>(it);\n  }\n\n\n  /**\n   * Returns the first element of the list, null if the list is null or empty.\n   *\n   * @param <T> f\n   * @param list f\n   * @return the first element of the list, null if the list is null or empty.\n   */\n  public static <T> T first(List<T> list) {\n    if (list == null || list.isEmpty()) {\n      return null;\n    }\n    return list.get(0);\n  }\n\n  /**\n   * Whether the given collection is null or has no elements.\n   *\n   * @param collection g\n   * @return Whether the given collection is null or has no elements.\n   */\n  public static boolean isEmpty(Collection<?> collection) {\n    return collection == null || collection.isEmpty();\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/IOUtil.java",
    "content": "package me.ag2s.epublib.util;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.Closeable;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.Reader;\nimport java.io.StringWriter;\nimport java.io.Writer;\nimport java.net.HttpURLConnection;\nimport java.net.URLConnection;\nimport java.nio.ByteBuffer;\nimport java.nio.CharBuffer;\nimport java.nio.channels.ReadableByteChannel;\nimport java.nio.charset.Charset;\n\nimport me.ag2s.epublib.epub.PackageDocumentReader;\nimport me.ag2s.epublib.util.commons.io.IOConsumer;\n\n/**\n * Most of the functions herein are re-implementations of the ones in\n * apache io IOUtils.\n * <p>\n * The reason for re-implementing this is that the functions are fairly simple\n * and using my own implementation saves the inclusion of a 200Kb jar file.\n */\npublic class IOUtil {\n    private static final String TAG = IOUtil.class.getName();\n\n    /**\n     * Represents the end-of-file (or stream).\n     *\n     * @since 2.5 (made public)\n     */\n    public static final int EOF = -1;\n\n\n    public static final int DEFAULT_BUFFER_SIZE = 1024 * 8;\n    private static final byte[] SKIP_BYTE_BUFFER = new byte[DEFAULT_BUFFER_SIZE];\n\n    // Allocated in the relevant skip method if necessary.\n    /*\n     * These buffers are static and are shared between threads.\n     * This is possible because the buffers are write-only - the contents are never read.\n     *\n     * N.B. there is no need to synchronize when creating these because:\n     * - we don't care if the buffer is created multiple times (the data is ignored)\n     * - we always use the same size buffer, so if it it is recreated it will still be OK\n     * (if the buffer size were variable, we would need to synch. to ensure some other thread\n     * did not create a smaller one)\n     */\n    private static char[] SKIP_CHAR_BUFFER;\n\n    /**\n     * Gets the contents of the Reader as a byte[], with the given character encoding.\n     *\n     * @param in       g\n     * @param encoding g\n     * @return the contents of the Reader as a byte[], with the given character encoding.\n     * @throws IOException g\n     */\n    public static byte[] toByteArray(Reader in, String encoding)\n            throws IOException {\n        StringWriter out = new StringWriter();\n        copy(in, out);\n        out.flush();\n        return out.toString().getBytes(encoding);\n    }\n\n    /**\n     * Returns the contents of the InputStream as a byte[]\n     *\n     * @param in f\n     * @return the contents of the InputStream as a byte[]\n     * @throws IOException f\n     */\n    public static byte[] toByteArray(InputStream in) throws IOException {\n        ByteArrayOutputStream result = new ByteArrayOutputStream();\n        copy(in, result);\n        result.flush();\n        return result.toByteArray();\n    }\n\n\n    /**\n     * Reads data from the InputStream, using the specified buffer size.\n     * <p>\n     * This is meant for situations where memory is tight, since\n     * it prevents buffer expansion.\n     *\n     * @param in   the stream to read data from\n     * @param size the size of the array to create\n     * @return the array, or null\n     * @throws IOException f\n     */\n    public static byte[] toByteArray(InputStream in, int size)\n            throws IOException {\n\n        try {\n            ByteArrayOutputStream result;\n\n            if (size > 0) {\n                result = new ByteArrayOutputStream(size);\n            } else {\n                result = new ByteArrayOutputStream();\n            }\n\n            copy(in, result);\n            result.flush();\n            return result.toByteArray();\n        } catch (OutOfMemoryError error) {\n            //Return null so it gets loaded lazily.\n            return null;\n        }\n\n    }\n\n\n    /**\n     * if totalNrRead &lt; 0 then totalNrRead is returned, if\n     * (nrRead + totalNrRead) &lt; Integer.MAX_VALUE then nrRead + totalNrRead\n     * is returned, -1 otherwise.\n     *\n     * @param nrRead       f\n     * @param totalNrNread f\n     * @return if totalNrRead &lt; 0 then totalNrRead is returned, if\n     * (nrRead + totalNrRead) &lt; Integer.MAX_VALUE then nrRead + totalNrRead\n     * is returned, -1 otherwise.\n     */\n    protected static int calcNewNrReadSize(int nrRead, int totalNrNread) {\n        if (totalNrNread < 0) {\n            return totalNrNread;\n        }\n        if (totalNrNread > (Integer.MAX_VALUE - nrRead)) {\n            return -1;\n        } else {\n            return (totalNrNread + nrRead);\n        }\n    }\n\n    //\n    public static void copy(InputStream in, OutputStream result) throws IOException {\n        copy(in, result,DEFAULT_BUFFER_SIZE);\n    }\n\n    /**\n     * Copies bytes from an <code>InputStream</code> to an <code>OutputStream</code> using an internal buffer of the\n     * given size.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a <code>BufferedInputStream</code>.\n     * </p>\n     *\n     * @param input      the <code>InputStream</code> to read from\n     * @param output     the <code>OutputStream</code> to write to\n     * @param bufferSize the bufferSize used to copy from the input to the output\n     * @return the number of bytes copied. or {@code 0} if {@code input is null}.\n     * @throws NullPointerException if the output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 2.5\n     */\n    public static long copy(final InputStream input, final OutputStream output, final int bufferSize)\n            throws IOException {\n        return copyLarge(input, output, new byte[bufferSize]);\n    }\n\n    /**\n     * Copies bytes from an <code>InputStream</code> to chars on a\n     * <code>Writer</code> using the default character encoding of the platform.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedInputStream</code>.\n     * <p>\n     * This method uses {@link InputStreamReader}.\n     *\n     * @param input  the <code>InputStream</code> to read from\n     * @param output the <code>Writer</code> to write to\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 1.1\n     * @deprecated 2.5 use {@link #copy(InputStream, Writer, Charset)} instead\n     */\n    @Deprecated\n    public static void copy(final InputStream input, final Writer output)\n            throws IOException {\n        copy(input, output, Charset.defaultCharset());\n    }\n\n    /**\n     * Copies bytes from an <code>InputStream</code> to chars on a\n     * <code>Writer</code> using the specified character encoding.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedInputStream</code>.\n     * <p>\n     * This method uses {@link InputStreamReader}.\n     *\n     * @param input        the <code>InputStream</code> to read from\n     * @param output       the <code>Writer</code> to write to\n     * @param inputCharset the charset to use for the input stream, null means platform default\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 2.3\n     */\n    public static void copy(final InputStream input, final Writer output, final Charset inputCharset)\n            throws IOException {\n        final InputStreamReader in = new InputStreamReader(input, inputCharset.name());\n        copy(in, output);\n    }\n\n    /**\n     * Copies bytes from an <code>InputStream</code> to chars on a\n     * <code>Writer</code> using the specified character encoding.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedInputStream</code>.\n     * <p>\n     * Character encoding names can be found at\n     * <a href=\"http://www.iana.org/assignments/character-sets\">IANA</a>.\n     * <p>\n     * This method uses {@link InputStreamReader}.\n     *\n     * @param input            the <code>InputStream</code> to read from\n     * @param output           the <code>Writer</code> to write to\n     * @param inputCharsetName the name of the requested charset for the InputStream, null means platform default\n     * @throws NullPointerException                         if the input or output is null\n     * @throws IOException                                  if an I/O error occurs\n     * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io\n     *                                                      .UnsupportedEncodingException} in version 2.2 if the\n     *                                                      encoding is not supported.\n     * @since 1.1\n     */\n    public static void copy(final InputStream input, final Writer output, final String inputCharsetName)\n            throws IOException {\n        copy(input, output, Charset.forName(inputCharsetName));\n    }\n\n    /**\n     * Copies chars from a <code>Reader</code> to a <code>Appendable</code>.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedReader</code>.\n     * <p>\n     * Large streams (over 2GB) will return a chars copied value of\n     * <code>-1</code> after the copy has completed since the correct\n     * number of chars cannot be returned as an int. For large streams\n     * use the <code>copyLarge(Reader, Writer)</code> method.\n     *\n     * @param input  the <code>Reader</code> to read from\n     * @param output the <code>Appendable</code> to write to\n     * @return the number of characters copied, or -1 if &gt; Integer.MAX_VALUE\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 2.7\n     */\n    public static long copy(final Reader input, final Appendable output) throws IOException {\n        return copy(input, output, CharBuffer.allocate(DEFAULT_BUFFER_SIZE));\n    }\n\n    /**\n     * Copies chars from a <code>Reader</code> to an <code>Appendable</code>.\n     * <p>\n     * This method uses the provided buffer, so there is no need to use a\n     * <code>BufferedReader</code>.\n     * </p>\n     *\n     * @param input  the <code>Reader</code> to read from\n     * @param output the <code>Appendable</code> to write to\n     * @param buffer the buffer to be used for the copy\n     * @return the number of characters copied\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 2.7\n     */\n    public static long copy(final Reader input, final Appendable output, final CharBuffer buffer) throws IOException {\n        long count = 0;\n        int n;\n        while (EOF != (n = input.read(buffer))) {\n            buffer.flip();\n            output.append(buffer, 0, n);\n            count += n;\n        }\n        return count;\n    }\n\n    /**\n     * Copies chars from a <code>Reader</code> to bytes on an\n     * <code>OutputStream</code> using the default character encoding of the\n     * platform, and calling flush.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedReader</code>.\n     * <p>\n     * Due to the implementation of OutputStreamWriter, this method performs a\n     * flush.\n     * <p>\n     * This method uses {@link OutputStreamWriter}.\n     *\n     * @param input  the <code>Reader</code> to read from\n     * @param output the <code>OutputStream</code> to write to\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 1.1\n     * @deprecated 2.5 use {@link #copy(Reader, OutputStream, Charset)} instead\n     */\n    @Deprecated\n    public static void copy(final Reader input, final OutputStream output)\n            throws IOException {\n        copy(input, output, Charset.defaultCharset());\n    }\n\n    /**\n     * Copies chars from a <code>Reader</code> to bytes on an\n     * <code>OutputStream</code> using the specified character encoding, and\n     * calling flush.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedReader</code>.\n     * </p>\n     * <p>\n     * Due to the implementation of OutputStreamWriter, this method performs a\n     * flush.\n     * </p>\n     * <p>\n     * This method uses {@link OutputStreamWriter}.\n     * </p>\n     *\n     * @param input         the <code>Reader</code> to read from\n     * @param output        the <code>OutputStream</code> to write to\n     * @param outputCharset the charset to use for the OutputStream, null means platform default\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 2.3\n     */\n    public static void copy(final Reader input, final OutputStream output, final Charset outputCharset)\n            throws IOException {\n        final OutputStreamWriter out = new OutputStreamWriter(output, outputCharset.name());\n        copy(input, out);\n        // XXX Unless anyone is planning on rewriting OutputStreamWriter,\n        // we have to flush here.\n        out.flush();\n    }\n\n    /**\n     * Copies chars from a <code>Reader</code> to bytes on an\n     * <code>OutputStream</code> using the specified character encoding, and\n     * calling flush.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedReader</code>.\n     * <p>\n     * Character encoding names can be found at\n     * <a href=\"http://www.iana.org/assignments/character-sets\">IANA</a>.\n     * <p>\n     * Due to the implementation of OutputStreamWriter, this method performs a\n     * flush.\n     * <p>\n     * This method uses {@link OutputStreamWriter}.\n     *\n     * @param input             the <code>Reader</code> to read from\n     * @param output            the <code>OutputStream</code> to write to\n     * @param outputCharsetName the name of the requested charset for the OutputStream, null means platform default\n     * @throws NullPointerException                         if the input or output is null\n     * @throws IOException                                  if an I/O error occurs\n     * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io\n     *                                                      .UnsupportedEncodingException} in version 2.2 if the\n     *                                                      encoding is not supported.\n     * @since 1.1\n     */\n    public static void copy(final Reader input, final OutputStream output, final String outputCharsetName)\n            throws IOException {\n        copy(input, output, Charset.forName(outputCharsetName));\n    }\n\n    /**\n     * Copies chars from a <code>Reader</code> to a <code>Writer</code>.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedReader</code>.\n     * <p>\n     * Large streams (over 2GB) will return a chars copied value of\n     * <code>-1</code> after the copy has completed since the correct\n     * number of chars cannot be returned as an int. For large streams\n     * use the <code>copyLarge(Reader, Writer)</code> method.\n     *\n     * @param input  the <code>Reader</code> to read from\n     * @param output the <code>Writer</code> to write to\n     * @return the number of characters copied, or -1 if &gt; Integer.MAX_VALUE\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 1.1\n     */\n    public static int copy(final Reader input, final Writer output) throws IOException {\n        final long count = copyLarge(input, output);\n        if (count > Integer.MAX_VALUE) {\n            return -1;\n        }\n        return (int) count;\n    }\n\n    /**\n     * Copies bytes from a large (over 2GB) <code>InputStream</code> to an\n     * <code>OutputStream</code>.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedInputStream</code>.\n     * </p>\n     * <p>\n     * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.\n     * </p>\n     *\n     * @param input  the <code>InputStream</code> to read from\n     * @param output the <code>OutputStream</code> to write to\n     * @return the number of bytes copied. or {@code 0} if {@code input is null}.\n     * @throws NullPointerException if the output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 1.3\n     */\n    public static long copyLarge(final InputStream input, final OutputStream output)\n            throws IOException {\n        return copy(input, output, DEFAULT_BUFFER_SIZE);\n    }\n\n    /**\n     * Copies bytes from a large (over 2GB) <code>InputStream</code> to an\n     * <code>OutputStream</code>.\n     * <p>\n     * This method uses the provided buffer, so there is no need to use a\n     * <code>BufferedInputStream</code>.\n     * </p>\n     *\n     * @param input  the <code>InputStream</code> to read from\n     * @param output the <code>OutputStream</code> to write to\n     * @param buffer the buffer to use for the copy\n     * @return the number of bytes copied. or {@code 0} if {@code input is null}.\n     * @throws IOException if an I/O error occurs\n     * @since 2.2\n     */\n    public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer)\n            throws IOException {\n        long count = 0;\n        if (input != null) {\n            int n;\n            while (EOF != (n = input.read(buffer))) {\n                output.write(buffer, 0, n);\n                count += n;\n            }\n            //input.close();\n        }\n        return count;\n    }\n\n    /**\n     * Copies some or all bytes from a large (over 2GB) <code>InputStream</code> to an\n     * <code>OutputStream</code>, optionally skipping input bytes.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedInputStream</code>.\n     * </p>\n     * <p>\n     * Note that the implementation uses {@link #skip(InputStream, long)}.\n     * This means that the method may be considerably less efficient than using the actual skip implementation,\n     * this is done to guarantee that the correct number of characters are skipped.\n     * </p>\n     * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.\n     *\n     * @param input       the <code>InputStream</code> to read from\n     * @param output      the <code>OutputStream</code> to write to\n     * @param inputOffset : number of bytes to skip from input before copying\n     *                    -ve values are ignored\n     * @param length      : number of bytes to copy. -ve means all\n     * @return the number of bytes copied\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 2.2\n     */\n    public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset,\n                                 final long length) throws IOException {\n        return copyLarge(input, output, inputOffset, length, new byte[DEFAULT_BUFFER_SIZE]);\n    }\n\n    /**\n     * Copies some or all bytes from a large (over 2GB) <code>InputStream</code> to an\n     * <code>OutputStream</code>, optionally skipping input bytes.\n     * <p>\n     * This method uses the provided buffer, so there is no need to use a\n     * <code>BufferedInputStream</code>.\n     * </p>\n     * <p>\n     * Note that the implementation uses {@link #skip(InputStream, long)}.\n     * This means that the method may be considerably less efficient than using the actual skip implementation,\n     * this is done to guarantee that the correct number of characters are skipped.\n     * </p>\n     *\n     * @param input       the <code>InputStream</code> to read from\n     * @param output      the <code>OutputStream</code> to write to\n     * @param inputOffset : number of bytes to skip from input before copying\n     *                    -ve values are ignored\n     * @param length      : number of bytes to copy. -ve means all\n     * @param buffer      the buffer to use for the copy\n     * @return the number of bytes copied\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 2.2\n     */\n    public static long copyLarge(final InputStream input, final OutputStream output,\n                                 final long inputOffset, final long length, final byte[] buffer) throws IOException {\n        if (inputOffset > 0) {\n            skipFully(input, inputOffset);\n        }\n        if (length == 0) {\n            return 0;\n        }\n        final int bufferLength = buffer.length;\n        int bytesToRead = bufferLength;\n        if (length > 0 && length < bufferLength) {\n            bytesToRead = (int) length;\n        }\n        int read;\n        long totalRead = 0;\n        while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) {\n            output.write(buffer, 0, read);\n            totalRead += read;\n            if (length > 0) { // only adjust length if not reading to the end\n                // Note the cast must work because buffer.length is an integer\n                bytesToRead = (int) Math.min(length - totalRead, bufferLength);\n            }\n        }\n        return totalRead;\n    }\n\n    /**\n     * Copies chars from a large (over 2GB) <code>Reader</code> to a <code>Writer</code>.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedReader</code>.\n     * <p>\n     * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.\n     *\n     * @param input  the <code>Reader</code> to read from\n     * @param output the <code>Writer</code> to write to\n     * @return the number of characters copied\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 1.3\n     */\n    public static long copyLarge(final Reader input, final Writer output) throws IOException {\n        return copyLarge(input, output, new char[DEFAULT_BUFFER_SIZE]);\n    }\n\n    /**\n     * Copies chars from a large (over 2GB) <code>Reader</code> to a <code>Writer</code>.\n     * <p>\n     * This method uses the provided buffer, so there is no need to use a\n     * <code>BufferedReader</code>.\n     * <p>\n     *\n     * @param input  the <code>Reader</code> to read from\n     * @param output the <code>Writer</code> to write to\n     * @param buffer the buffer to be used for the copy\n     * @return the number of characters copied\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 2.2\n     */\n    public static long copyLarge(final Reader input, final Writer output, final char[] buffer) throws IOException {\n        long count = 0;\n        int n;\n        while (EOF != (n = input.read(buffer))) {\n            output.write(buffer, 0, n);\n            count += n;\n        }\n        return count;\n    }\n\n    /**\n     * Copies some or all chars from a large (over 2GB) <code>InputStream</code> to an\n     * <code>OutputStream</code>, optionally skipping input chars.\n     * <p>\n     * This method buffers the input internally, so there is no need to use a\n     * <code>BufferedReader</code>.\n     * <p>\n     * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}.\n     *\n     * @param input       the <code>Reader</code> to read from\n     * @param output      the <code>Writer</code> to write to\n     * @param inputOffset : number of chars to skip from input before copying\n     *                    -ve values are ignored\n     * @param length      : number of chars to copy. -ve means all\n     * @return the number of chars copied\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 2.2\n     */\n    public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length)\n            throws IOException {\n        return copyLarge(input, output, inputOffset, length, new char[DEFAULT_BUFFER_SIZE]);\n    }\n\n    /**\n     * Copies some or all chars from a large (over 2GB) <code>InputStream</code> to an\n     * <code>OutputStream</code>, optionally skipping input chars.\n     * <p>\n     * This method uses the provided buffer, so there is no need to use a\n     * <code>BufferedReader</code>.\n     * <p>\n     *\n     * @param input       the <code>Reader</code> to read from\n     * @param output      the <code>Writer</code> to write to\n     * @param inputOffset : number of chars to skip from input before copying\n     *                    -ve values are ignored\n     * @param length      : number of chars to copy. -ve means all\n     * @param buffer      the buffer to be used for the copy\n     * @return the number of chars copied\n     * @throws NullPointerException if the input or output is null\n     * @throws IOException          if an I/O error occurs\n     * @since 2.2\n     */\n    public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length,\n                                 final char[] buffer)\n            throws IOException {\n        if (inputOffset > 0) {\n            skipFully(input, inputOffset);\n        }\n        if (length == 0) {\n            return 0;\n        }\n        int bytesToRead = buffer.length;\n        if (length > 0 && length < buffer.length) {\n            bytesToRead = (int) length;\n        }\n        int read;\n        long totalRead = 0;\n        while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) {\n            output.write(buffer, 0, read);\n            totalRead += read;\n            if (length > 0) { // only adjust length if not reading to the end\n                // Note the cast must work because buffer.length is an integer\n                bytesToRead = (int) Math.min(length - totalRead, buffer.length);\n            }\n        }\n        return totalRead;\n    }\n\n    /**\n     * Skips bytes from an input byte stream.\n     * This implementation guarantees that it will read as many bytes\n     * as possible before giving up; this may not always be the case for\n     * skip() implementations in subclasses of {@link InputStream}.\n     * <p>\n     * Note that the implementation uses {@link InputStream#read(byte[], int, int)} rather\n     * than delegating to {@link InputStream#skip(long)}.\n     * This means that the method may be considerably less efficient than using the actual skip implementation,\n     * this is done to guarantee that the correct number of bytes are skipped.\n     * </p>\n     *\n     * @param input  byte stream to skip\n     * @param toSkip number of bytes to skip.\n     * @return number of bytes actually skipped.\n     * @throws IOException              if there is a problem reading the file\n     * @throws IllegalArgumentException if toSkip is negative\n     * @see InputStream#skip(long)\n     * @see <a href=\"https://issues.apache.org/jira/browse/IO-203\">IO-203 - Add skipFully() method for InputStreams</a>\n     * @since 2.0\n     */\n    public static long skip(final InputStream input, final long toSkip) throws IOException {\n        if (toSkip < 0) {\n            throw new IllegalArgumentException(\"Skip count must be non-negative, actual: \" + toSkip);\n        }\n        /*\n         * N.B. no need to synchronize access to SKIP_BYTE_BUFFER: - we don't care if the buffer is created multiple\n         * times (the data is ignored) - we always use the same size buffer, so if it it is recreated it will still be\n         * OK (if the buffer size were variable, we would need to synch. to ensure some other thread did not create a\n         * smaller one)\n         */\n        long remain = toSkip;\n        while (remain > 0) {\n            // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()\n            final long n = input.read(SKIP_BYTE_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length));\n            if (n < 0) { // EOF\n                break;\n            }\n            remain -= n;\n        }\n        return toSkip - remain;\n    }\n\n    /**\n     * Skips bytes from a ReadableByteChannel.\n     * This implementation guarantees that it will read as many bytes\n     * as possible before giving up.\n     *\n     * @param input  ReadableByteChannel to skip\n     * @param toSkip number of bytes to skip.\n     * @return number of bytes actually skipped.\n     * @throws IOException              if there is a problem reading the ReadableByteChannel\n     * @throws IllegalArgumentException if toSkip is negative\n     * @since 2.5\n     */\n    public static long skip(final ReadableByteChannel input, final long toSkip) throws IOException {\n        if (toSkip < 0) {\n            throw new IllegalArgumentException(\"Skip count must be non-negative, actual: \" + toSkip);\n        }\n        final ByteBuffer skipByteBuffer = ByteBuffer.allocate((int) Math.min(toSkip, SKIP_BYTE_BUFFER.length));\n        long remain = toSkip;\n        while (remain > 0) {\n            skipByteBuffer.position(0);\n            skipByteBuffer.limit((int) Math.min(remain, SKIP_BYTE_BUFFER.length));\n            final int n = input.read(skipByteBuffer);\n            if (n == EOF) {\n                break;\n            }\n            remain -= n;\n        }\n        return toSkip - remain;\n    }\n\n    /**\n     * Skips characters from an input character stream.\n     * This implementation guarantees that it will read as many characters\n     * as possible before giving up; this may not always be the case for\n     * skip() implementations in subclasses of {@link Reader}.\n     * <p>\n     * Note that the implementation uses {@link Reader#read(char[], int, int)} rather\n     * than delegating to {@link Reader#skip(long)}.\n     * This means that the method may be considerably less efficient than using the actual skip implementation,\n     * this is done to guarantee that the correct number of characters are skipped.\n     * </p>\n     *\n     * @param input  character stream to skip\n     * @param toSkip number of characters to skip.\n     * @return number of characters actually skipped.\n     * @throws IOException              if there is a problem reading the file\n     * @throws IllegalArgumentException if toSkip is negative\n     * @see Reader#skip(long)\n     * @see <a href=\"https://issues.apache.org/jira/browse/IO-203\">IO-203 - Add skipFully() method for InputStreams</a>\n     * @since 2.0\n     */\n    public static long skip(final Reader input, final long toSkip) throws IOException {\n        if (toSkip < 0) {\n            throw new IllegalArgumentException(\"Skip count must be non-negative, actual: \" + toSkip);\n        }\n        /*\n         * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data\n         * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer\n         * size were variable, we would need to synch. to ensure some other thread did not create a smaller one)\n         */\n        if (SKIP_CHAR_BUFFER == null) {\n            SKIP_CHAR_BUFFER = new char[SKIP_BYTE_BUFFER.length];\n        }\n        long remain = toSkip;\n        while (remain > 0) {\n            // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip()\n            final long n = input.read(SKIP_CHAR_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length));\n            if (n < 0) { // EOF\n                break;\n            }\n            remain -= n;\n        }\n        return toSkip - remain;\n    }\n\n    /**\n     * Skips the requested number of bytes or fail if there are not enough left.\n     * <p>\n     * This allows for the possibility that {@link InputStream#skip(long)} may\n     * not skip as many bytes as requested (most likely because of reaching EOF).\n     * <p>\n     * Note that the implementation uses {@link #skip(InputStream, long)}.\n     * This means that the method may be considerably less efficient than using the actual skip implementation,\n     * this is done to guarantee that the correct number of characters are skipped.\n     * </p>\n     *\n     * @param input  stream to skip\n     * @param toSkip the number of bytes to skip\n     * @throws IOException              if there is a problem reading the file\n     * @throws IllegalArgumentException if toSkip is negative\n     * @throws EOFException             if the number of bytes skipped was incorrect\n     * @see InputStream#skip(long)\n     * @since 2.0\n     */\n    public static void skipFully(final InputStream input, final long toSkip) throws IOException {\n        if (toSkip < 0) {\n            throw new IllegalArgumentException(\"Bytes to skip must not be negative: \" + toSkip);\n        }\n        final long skipped = skip(input, toSkip);\n        if (skipped != toSkip) {\n            throw new EOFException(\"Bytes to skip: \" + toSkip + \" actual: \" + skipped);\n        }\n    }\n\n    /**\n     * Skips the requested number of bytes or fail if there are not enough left.\n     *\n     * @param input  ReadableByteChannel to skip\n     * @param toSkip the number of bytes to skip\n     * @throws IOException              if there is a problem reading the ReadableByteChannel\n     * @throws IllegalArgumentException if toSkip is negative\n     * @throws EOFException             if the number of bytes skipped was incorrect\n     * @since 2.5\n     */\n    public static void skipFully(final ReadableByteChannel input, final long toSkip) throws IOException {\n        if (toSkip < 0) {\n            throw new IllegalArgumentException(\"Bytes to skip must not be negative: \" + toSkip);\n        }\n        final long skipped = skip(input, toSkip);\n        if (skipped != toSkip) {\n            throw new EOFException(\"Bytes to skip: \" + toSkip + \" actual: \" + skipped);\n        }\n    }\n\n    /**\n     * Skips the requested number of characters or fail if there are not enough left.\n     * <p>\n     * This allows for the possibility that {@link Reader#skip(long)} may\n     * not skip as many characters as requested (most likely because of reaching EOF).\n     * <p>\n     * Note that the implementation uses {@link #skip(Reader, long)}.\n     * This means that the method may be considerably less efficient than using the actual skip implementation,\n     * this is done to guarantee that the correct number of characters are skipped.\n     * </p>\n     *\n     * @param input  stream to skip\n     * @param toSkip the number of characters to skip\n     * @throws IOException              if there is a problem reading the file\n     * @throws IllegalArgumentException if toSkip is negative\n     * @throws EOFException             if the number of characters skipped was incorrect\n     * @see Reader#skip(long)\n     * @since 2.0\n     */\n    public static void skipFully(final Reader input, final long toSkip) throws IOException {\n        final long skipped = skip(input, toSkip);\n        if (skipped != toSkip) {\n            throw new EOFException(\"Chars to skip: \" + toSkip + \" actual: \" + skipped);\n        }\n    }\n\n    /**\n     * Returns the length of the given array in a null-safe manner.\n     *\n     * @param array an array or null\n     * @return the array length -- or 0 if the given array is null.\n     * @since 2.7\n     */\n    public static int length(final byte[] array) {\n        return array == null ? 0 : array.length;\n    }\n\n    /**\n     * Returns the length of the given array in a null-safe manner.\n     *\n     * @param array an array or null\n     * @return the array length -- or 0 if the given array is null.\n     * @since 2.7\n     */\n    public static int length(final char[] array) {\n        return array == null ? 0 : array.length;\n    }\n\n    /**\n     * Returns the length of the given CharSequence in a null-safe manner.\n     *\n     * @param csq a CharSequence or null\n     * @return the CharSequence length -- or 0 if the given CharSequence is null.\n     * @since 2.7\n     */\n    public static int length(final CharSequence csq) {\n        return csq == null ? 0 : csq.length();\n    }\n\n    /**\n     * Returns the length of the given array in a null-safe manner.\n     *\n     * @param array an array or null\n     * @return the array length -- or 0 if the given array is null.\n     * @since 2.7\n     */\n    public static int length(final Object[] array) {\n        return array == null ? 0 : array.length;\n    }\n\n    /**\n     * Closes the given {@link Closeable} as a null-safe operation.\n     *\n     * @param closeable The resource to close, may be null.\n     * @throws IOException if an I/O error occurs.\n     * @since 2.7\n     */\n    public static void close(final Closeable closeable) throws IOException {\n        if (closeable != null) {\n            closeable.close();\n        }\n    }\n\n    /**\n     * Closes the given {@link Closeable} as a null-safe operation.\n     *\n     * @param closeables The resource(s) to close, may be null.\n     * @throws IOException if an I/O error occurs.\n     * @since 2.8.0\n     */\n    public static void close(final Closeable... closeables) throws IOException {\n        if (closeables != null) {\n            for (final Closeable closeable : closeables) {\n                close(closeable);\n            }\n        }\n    }\n\n    /**\n     * Closes the given {@link Closeable} as a null-safe operation.\n     *\n     * @param closeable The resource to close, may be null.\n     * @param consumer  Consume the IOException thrown by {@link Closeable#close()}.\n     * @throws IOException if an I/O error occurs.\n     * @since 2.7\n     */\n    public static void close(final Closeable closeable, final IOConsumer<IOException> consumer) throws IOException {\n        if (closeable != null) {\n            try {\n                closeable.close();\n            } catch (final IOException e) {\n                if (consumer != null) {\n                    consumer.accept(e);\n                }\n            }\n        }\n    }\n\n    /**\n     * Closes a URLConnection.\n     *\n     * @param conn the connection to close.\n     * @since 2.4\n     */\n    public static void close(final URLConnection conn) {\n        if (conn instanceof HttpURLConnection) {\n            ((HttpURLConnection) conn).disconnect();\n        }\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static String Stream2String(InputStream inputStream) {\n        ByteArrayOutputStream result = new ByteArrayOutputStream();\n        try {\n            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];\n            int length;\n            while ((length = inputStream.read(buffer)) != -1) {\n                result.write(buffer, 0, length);\n            }\n            return result.toString();\n        } catch (Exception e) {\n            return e.getLocalizedMessage();\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/NoCloseOutputStream.java",
    "content": "package me.ag2s.epublib.util;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\n\n/**\n * OutputStream with the close() disabled.\n * We write multiple documents to a ZipOutputStream.\n * Some of the formatters call a close() after writing their data.\n * We don't want them to do that, so we wrap regular OutputStreams in this NoCloseOutputStream.\n *\n * @author paul\n */\n@SuppressWarnings(\"unused\")\npublic class NoCloseOutputStream extends OutputStream {\n\n  private final OutputStream outputStream;\n\n  public NoCloseOutputStream(OutputStream outputStream) {\n    this.outputStream = outputStream;\n  }\n\n  @Override\n  public void write(int b) throws IOException {\n    outputStream.write(b);\n  }\n\n  /**\n   * A close() that does not call it's parent's close()\n   */\n  public void close() {\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/NoCloseWriter.java",
    "content": "package me.ag2s.epublib.util;\n\nimport java.io.IOException;\nimport java.io.Writer;\n\n/**\n * Writer with the close() disabled.\n * We write multiple documents to a ZipOutputStream.\n * Some of the formatters call a close() after writing their data.\n * We don't want them to do that, so we wrap regular Writers in this NoCloseWriter.\n *\n * @author paul\n */\n@SuppressWarnings(\"unused\")\npublic class NoCloseWriter extends Writer {\n\n  private final Writer writer;\n\n  public NoCloseWriter(Writer writer) {\n    this.writer = writer;\n  }\n\n  @Override\n  public void close() {\n  }\n\n  @Override\n  public void flush() throws IOException {\n    writer.flush();\n  }\n\n  @Override\n  public void write(char[] cbuf, int off, int len) throws IOException {\n    writer.write(cbuf, off, len);\n  }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/ResourceUtil.java",
    "content": "package me.ag2s.epublib.util;\n\nimport org.w3c.dom.Document;\nimport org.xml.sax.InputSource;\nimport org.xml.sax.SAXException;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.Reader;\nimport java.io.UnsupportedEncodingException;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipInputStream;\n\nimport javax.xml.parsers.DocumentBuilder;\n\nimport me.ag2s.epublib.Constants;\nimport me.ag2s.epublib.domain.MediaType;\nimport me.ag2s.epublib.domain.MediaTypes;\nimport me.ag2s.epublib.domain.Resource;\nimport me.ag2s.epublib.epub.EpubProcessorSupport;\n\n/**\n * Various resource utility methods\n *\n * @author paul\n */\npublic class ResourceUtil {\n    /**\n     * 快速创建HTML类型的Resource\n     *\n     * @param title 章节的标题\n     * @param txt   章节的正文\n     * @param model html模板\n     * @return 返回Resource\n     */\n    public static Resource createChapterResource(String title, String txt, String model, String href) {\n        if (title.contains(\"\\n\")) {\n            title = \"<span class=\\\"chapter-sequence-number\\\">\" + title.replaceFirst(\"\\\\s*\\\\n\\\\s*\", \"</span><br />\");\n        } else {\n            title = title.replaceFirst(\"\\\\s+\", \"</span><br />\");\n            if (title.contains(\"</span>\"))\n                title = \"<span class=\\\"chapter-sequence-number\\\">\" + title;\n        }\n        String html = model.replace(\"{title}\", title)\n                .replace(\"{content}\", StringUtil.formatHtml(txt));\n        return new Resource(html.getBytes(), href);\n    }\n\n    public static Resource createPublicResource(String name, String author, String intro, String kind, String wordCount, String model, String href) {\n        String html = model.replace(\"{name}\", name)\n                .replace(\"{author}\", author)\n                .replace(\"{kind}\", kind == null ? \"\" : kind)\n                .replace(\"{wordCount}\", wordCount == null ? \"\" : wordCount)\n                .replace(\"{intro}\", StringUtil.formatHtml(intro == null ? \"\" : intro));\n        return new Resource(html.getBytes(), href);\n    }\n\n    /**\n     * 快速从File创建Resource\n     *\n     * @param file File\n     * @return Resource\n     * @throws IOException IOException\n     */\n\n    @SuppressWarnings(\"unused\")\n    public static Resource createResource(File file) throws IOException {\n        if (file == null) {\n            return null;\n        }\n        MediaType mediaType = MediaTypes.determineMediaType(file.getName());\n        byte[] data = IOUtil.toByteArray(new FileInputStream(file));\n        return new Resource(data, mediaType);\n    }\n\n\n    /**\n     * 创建一个只带标题的HTMl类型的Resource,常用于封面页，大卷页\n     *\n     * @param title v\n     * @param href  v\n     * @return a resource with as contents a html page with the given title.\n     */\n    @SuppressWarnings(\"unused\")\n    public static Resource createResource(String title, String href) {\n        String content =\n                \"<html><head><title>\" + title + \"</title></head><body><h1>\" + title\n                        + \"</h1></body></html>\";\n        return new Resource(null, content.getBytes(), href, MediaTypes.XHTML,\n                Constants.CHARACTER_ENCODING);\n    }\n\n    /**\n     * Creates a resource out of the given zipEntry and zipInputStream.\n     *\n     * @param zipEntry       v\n     * @param zipInputStream v\n     * @return a resource created out of the given zipEntry and zipInputStream.\n     * @throws IOException v\n     */\n    public static Resource createResource(ZipEntry zipEntry,\n                                          ZipInputStream zipInputStream) throws IOException {\n        return new Resource(zipInputStream, zipEntry.getName());\n\n    }\n\n    public static Resource createResource(ZipEntry zipEntry,\n                                          InputStream zipInputStream) throws IOException {\n        return new Resource(zipInputStream, zipEntry.getName());\n\n    }\n\n    /**\n     * Converts a given string from given input character encoding to the requested output character encoding.\n     *\n     * @param inputEncoding  v\n     * @param outputEncoding v\n     * @param input          v\n     * @return the string from given input character encoding converted to the requested output character encoding.\n     * @throws UnsupportedEncodingException v\n     */\n    @SuppressWarnings(\"unused\")\n    public static byte[] recode(String inputEncoding, String outputEncoding,\n                                byte[] input) throws UnsupportedEncodingException {\n        return new String(input, inputEncoding).getBytes(outputEncoding);\n    }\n\n    /**\n     * Gets the contents of the Resource as an InputSource in a null-safe manner.\n     */\n    @SuppressWarnings(\"unused\")\n    public static InputSource getInputSource(Resource resource)\n            throws IOException {\n        if (resource == null) {\n            return null;\n        }\n        Reader reader = resource.getReader();\n        if (reader == null) {\n            return null;\n        }\n        return new InputSource(reader);\n    }\n\n\n    /**\n     * Reads parses the xml therein and returns the result as a Document\n     */\n    public static Document getAsDocument(Resource resource)\n            throws SAXException, IOException {\n        return getAsDocument(resource,\n                EpubProcessorSupport.createDocumentBuilder());\n    }\n\n    /**\n     * Reads the given resources inputstream, parses the xml therein and returns the result as a Document\n     *\n     * @param resource        v\n     * @param documentBuilder v\n     * @return the document created from the given resource\n     * @throws UnsupportedEncodingException v\n     * @throws SAXException                 v\n     * @throws IOException                  v\n     */\n    public static Document getAsDocument(Resource resource,\n                                         DocumentBuilder documentBuilder)\n            throws UnsupportedEncodingException, SAXException, IOException {\n        InputSource inputSource = getInputSource(resource);\n        if (inputSource == null) {\n            return null;\n        }\n        return documentBuilder.parse(inputSource);\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/StringUtil.java",
    "content": "package me.ag2s.epublib.util;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * Various String utility functions.\n * <p>\n * Most of the functions herein are re-implementations of the ones in apache\n * commons StringUtils. The reason for re-implementing this is that the\n * functions are fairly simple and using my own implementation saves the\n * inclusion of a 200Kb jar file.\n *\n * @author paul.siegmann\n */\npublic class StringUtil {\n\n    /**\n     * Changes a path containing '..', '.' and empty dirs into a path that\n     * doesn't. X/foo/../Y is changed into 'X/Y', etc. Does not handle invalid\n     * paths like \"../\".\n     *\n     * @param path path\n     * @return the normalized path\n     */\n    public static String collapsePathDots(String path) {\n        String[] stringParts = path.split(\"/\");\n        List<String> parts = new ArrayList<>(Arrays.asList(stringParts));\n        for (int i = 0; i < parts.size() - 1; i++) {\n            String currentDir = parts.get(i);\n            if (currentDir.length() == 0 || currentDir.equals(\".\")) {\n                parts.remove(i);\n                i--;\n            } else if (currentDir.equals(\"..\")) {\n                parts.remove(i - 1);\n                parts.remove(i - 1);\n                i -= 2;\n            }\n        }\n        StringBuilder result = new StringBuilder();\n        if (path.startsWith(\"/\")) {\n            result.append('/');\n        }\n        for (int i = 0; i < parts.size(); i++) {\n            result.append(parts.get(i));\n            if (i < (parts.size() - 1)) {\n                result.append('/');\n            }\n        }\n        return result.toString();\n    }\n\n    /**\n     * Whether the String is not null, not zero-length and does not contain of\n     * only whitespace.\n     *\n     * @param text text\n     * @return Whether the String is not null, not zero-length and does not contain of\n     */\n    public static boolean isNotBlank(String text) {\n        return !isBlank(text);\n    }\n\n    /**\n     * Whether the String is null, zero-length and does contain only whitespace.\n     *\n     * @return Whether the String is null, zero-length and does contain only whitespace.\n     */\n    public static boolean isBlank(String text) {\n        if (isEmpty(text)) {\n            return true;\n        }\n        for (int i = 0; i < text.length(); i++) {\n            if (!Character.isWhitespace(text.charAt(i))) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Whether the given string is null or zero-length.\n     *\n     * @param text the input for this method\n     * @return Whether the given string is null or zero-length.\n     */\n    public static boolean isEmpty(String text) {\n        return (text == null) || (text.length() == 0);\n    }\n\n    /**\n     * Whether the given source string ends with the given suffix, ignoring\n     * case.\n     *\n     * @param source source\n     * @param suffix suffix\n     * @return Whether the given source string ends with the given suffix, ignoring case.\n     */\n    public static boolean endsWithIgnoreCase(String source, String suffix) {\n        if (isEmpty(suffix)) {\n            return true;\n        }\n        if (isEmpty(source)) {\n            return false;\n        }\n        if (suffix.length() > source.length()) {\n            return false;\n        }\n        return source.substring(source.length() - suffix.length())\n                .toLowerCase().endsWith(suffix.toLowerCase());\n    }\n\n    /**\n     * If the given text is null return \"\", the original text otherwise.\n     *\n     * @param text text\n     * @return If the given text is null \"\", the original text otherwise.\n     */\n    public static String defaultIfNull(String text) {\n        return defaultIfNull(text, \"\");\n    }\n\n    /**\n     * If the given text is null return \"\", the given defaultValue otherwise.\n     *\n     * @param text         d\n     * @param defaultValue d\n     * @return If the given text is null \"\", the given defaultValue otherwise.\n     */\n    public static String defaultIfNull(String text, String defaultValue) {\n        if (text == null) {\n            return defaultValue;\n        }\n        return text;\n    }\n\n    /**\n     * Null-safe string comparator\n     *\n     * @param text1 d\n     * @param text2 d\n     * @return whether the two strings are equal\n     */\n    public static boolean equals(String text1, String text2) {\n        if (text1 == null) {\n            return (text2 == null);\n        }\n        return text1.equals(text2);\n    }\n\n    /**\n     * Pretty toString printer.\n     *\n     * @param keyValues d\n     * @return a string representation of the input values\n     */\n    public static String toString(Object... keyValues) {\n        StringBuilder result = new StringBuilder();\n        result.append('[');\n        for (int i = 0; i < keyValues.length; i += 2) {\n            if (i > 0) {\n                result.append(\", \");\n            }\n            result.append(keyValues[i]);\n            result.append(\": \");\n            Object value = null;\n            if ((i + 1) < keyValues.length) {\n                value = keyValues[i + 1];\n            }\n            if (value == null) {\n                result.append(\"<null>\");\n            } else {\n                result.append('\\'');\n                result.append(value);\n                result.append('\\'');\n            }\n        }\n        result.append(']');\n        return result.toString();\n    }\n\n    public static int hashCode(String... values) {\n        int result = 31;\n        for (String value : values) {\n            result ^= String.valueOf(value).hashCode();\n        }\n        return result;\n    }\n\n    /**\n     * Gives the substring of the given text before the given separator.\n     * <p>\n     * If the text does not contain the given separator then the given text is\n     * returned.\n     *\n     * @param text      d\n     * @param separator d\n     * @return the substring of the given text before the given separator.\n     */\n    public static String substringBefore(String text, char separator) {\n        if (isEmpty(text)) {\n            return text;\n        }\n        int sepPos = text.indexOf(separator);\n        if (sepPos < 0) {\n            return text;\n        }\n        return text.substring(0, sepPos);\n    }\n\n    /**\n     * Gives the substring of the given text before the last occurrence of the\n     * given separator.\n     * <p>\n     * If the text does not contain the given separator then the given text is\n     * returned.\n     *\n     * @param text      d\n     * @param separator d\n     * @return the substring of the given text before the last occurrence of the given separator.\n     */\n    public static String substringBeforeLast(String text, char separator) {\n        if (isEmpty(text)) {\n            return text;\n        }\n        int cPos = text.lastIndexOf(separator);\n        if (cPos < 0) {\n            return text;\n        }\n        return text.substring(0, cPos);\n    }\n\n    /**\n     * Gives the substring of the given text after the last occurrence of the\n     * given separator.\n     * <p>\n     * If the text does not contain the given separator then \"\" is returned.\n     *\n     * @param text      d\n     * @param separator d\n     * @return the substring of the given text after the last occurrence of the given separator.\n     */\n    public static String substringAfterLast(String text, char separator) {\n        if (isEmpty(text)) {\n            return text;\n        }\n        int cPos = text.lastIndexOf(separator);\n        if (cPos < 0) {\n            return \"\";\n        }\n        return text.substring(cPos + 1);\n    }\n\n    /**\n     * Gives the substring of the given text after the given separator.\n     * <p>\n     * If the text does not contain the given separator then \"\" is returned.\n     *\n     * @param text the input text\n     * @param c    the separator char\n     * @return the substring of the given text after the given separator.\n     */\n    public static String substringAfter(String text, char c) {\n        if (isEmpty(text)) {\n            return text;\n        }\n        int cPos = text.indexOf(c);\n        if (cPos < 0) {\n            return \"\";\n        }\n        return text.substring(cPos + 1);\n    }\n\n    public static String formatHtml(String text) {\n        StringBuilder body = new StringBuilder();\n        for (String s : text.split(\"\\\\r?\\\\n\")) {\n            s = s.replaceAll(\"^\\\\s+|\\\\s+$\", \"\");\n            if (s.length() > 0) {\n                //段落为一张图片才认定为图片章节/漫画并启用多看单图优化，否则认定为普通文字夹杂着的图片文字。\n                if (s.matches(\"(?i)^<img\\\\s([^>]+)/?>$\")) {\n                    body.append(s.replaceAll(\"(?i)^<img\\\\s([^>]+)/?>$\",\n                            \"<div class=\\\"duokan-image-single\\\"><img class=\\\"picture-80\\\" $1/></div>\"));\n                } else {\n                    body.append(\"<p>\").append(s).append(\"</p>\");\n                }\n            }\n        }\n        return body.toString();\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/commons/io/BOMInputStream.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage me.ag2s.epublib.util.commons.io;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Arrays;\nimport java.util.Comparator;\nimport java.util.List;\n\nimport me.ag2s.epublib.util.IOUtil;\n\nimport static me.ag2s.epublib.util.IOUtil.EOF;\n\n\n/**\n * This class is used to wrap a stream that includes an encoded {@link ByteOrderMark} as its first bytes.\n *\n * This class detects these bytes and, if required, can automatically skip them and return the subsequent byte as the\n * first byte in the stream.\n *\n * The {@link ByteOrderMark} implementation has the following pre-defined BOMs:\n * <ul>\n * <li>UTF-8 - {@link ByteOrderMark#UTF_8}</li>\n * <li>UTF-16BE - {@link ByteOrderMark#UTF_16LE}</li>\n * <li>UTF-16LE - {@link ByteOrderMark#UTF_16BE}</li>\n * <li>UTF-32BE - {@link ByteOrderMark#UTF_32LE}</li>\n * <li>UTF-32LE - {@link ByteOrderMark#UTF_32BE}</li>\n * </ul>\n *\n *\n * <h2>Example 1 - Detect and exclude a UTF-8 BOM</h2>\n *\n * <pre>\n * BOMInputStream bomIn = new BOMInputStream(in);\n * if (bomIn.hasBOM()) {\n *     // has a UTF-8 BOM\n * }\n * </pre>\n *\n * <h2>Example 2 - Detect a UTF-8 BOM (but don't exclude it)</h2>\n *\n * <pre>\n * boolean include = true;\n * BOMInputStream bomIn = new BOMInputStream(in, include);\n * if (bomIn.hasBOM()) {\n *     // has a UTF-8 BOM\n * }\n * </pre>\n *\n * <h2>Example 3 - Detect Multiple BOMs</h2>\n *\n * <pre>\n * BOMInputStream bomIn = new BOMInputStream(in,\n *   ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE,\n *   ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE\n *   );\n * if (bomIn.hasBOM() == false) {\n *     // No BOM found\n * } else if (bomIn.hasBOM(ByteOrderMark.UTF_16LE)) {\n *     // has a UTF-16LE BOM\n * } else if (bomIn.hasBOM(ByteOrderMark.UTF_16BE)) {\n *     // has a UTF-16BE BOM\n * } else if (bomIn.hasBOM(ByteOrderMark.UTF_32LE)) {\n *     // has a UTF-32LE BOM\n * } else if (bomIn.hasBOM(ByteOrderMark.UTF_32BE)) {\n *     // has a UTF-32BE BOM\n * }\n * </pre>\n *\n * @see ByteOrderMark\n * @see <a href=\"http://en.wikipedia.org/wiki/Byte_order_mark\">Wikipedia - Byte Order Mark</a>\n * @since 2.0\n */\npublic class BOMInputStream extends ProxyInputStream {\n    private final boolean include;\n    /**\n     * BOMs are sorted from longest to shortest.\n     */\n    private final List<ByteOrderMark> boms;\n    private ByteOrderMark byteOrderMark;\n    private int[] firstBytes;\n    private int fbLength;\n    private int fbIndex;\n    private int markFbIndex;\n    private boolean markedAtStart;\n\n    /**\n     * Constructs a new BOM InputStream that excludes a {@link ByteOrderMark#UTF_8} BOM.\n     *\n     * @param delegate\n     *            the InputStream to delegate to\n     */\n    @SuppressWarnings(\"unused\")\n    public BOMInputStream(final InputStream delegate) {\n        this(delegate, false, ByteOrderMark.UTF_8);\n    }\n\n    /**\n     * Constructs a new BOM InputStream that detects a a {@link ByteOrderMark#UTF_8} and optionally includes it.\n     *\n     * @param delegate\n     *            the InputStream to delegate to\n     * @param include\n     *            true to include the UTF-8 BOM or false to exclude it\n     */\n    @SuppressWarnings(\"unused\")\n    public BOMInputStream(final InputStream delegate, final boolean include) {\n        this(delegate, include, ByteOrderMark.UTF_8);\n    }\n\n    /**\n     * Constructs a new BOM InputStream that excludes the specified BOMs.\n     *\n     * @param delegate\n     *            the InputStream to delegate to\n     * @param boms\n     *            The BOMs to detect and exclude\n     */\n    @SuppressWarnings(\"unused\")\n    public BOMInputStream(final InputStream delegate, final ByteOrderMark... boms) {\n        this(delegate, false, boms);\n    }\n\n    /**\n     * Compares ByteOrderMark objects in descending length order.\n     */\n    private static final Comparator<ByteOrderMark> ByteOrderMarkLengthComparator = (bom1, bom2) -> {\n        final int len1 = bom1.length();\n        final int len2 = bom2.length();\n        return Integer.compare(len2, len1);\n    };\n\n    /**\n     * Constructs a new BOM InputStream that detects the specified BOMs and optionally includes them.\n     *\n     * @param delegate\n     *            the InputStream to delegate to\n     * @param include\n     *            true to include the specified BOMs or false to exclude them\n     * @param boms\n     *            The BOMs to detect and optionally exclude\n     */\n    public BOMInputStream(final InputStream delegate, final boolean include, final ByteOrderMark... boms) {\n        super(delegate);\n        if (IOUtil.length(boms) == 0) {\n            throw new IllegalArgumentException(\"No BOMs specified\");\n        }\n        this.include = include;\n        final List<ByteOrderMark> list = Arrays.asList(boms);\n        // Sort the BOMs to match the longest BOM first because some BOMs have the same starting two bytes.\n        // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n        //     list.sort(ByteOrderMarkLengthComparator);\n        // }\n        this.boms = list;\n\n    }\n\n    /**\n     * Indicates whether the stream contains one of the specified BOMs.\n     *\n     * @return true if the stream has one of the specified BOMs, otherwise false if it does not\n     * @throws IOException\n     *             if an error reading the first bytes of the stream occurs\n     */\n    @SuppressWarnings(\"unused\")\n    public boolean hasBOM() throws IOException {\n        return getBOM() != null;\n    }\n\n    /**\n     * Indicates whether the stream contains the specified BOM.\n     *\n     * @param bom\n     *            The BOM to check for\n     * @return true if the stream has the specified BOM, otherwise false if it does not\n     * @throws IllegalArgumentException\n     *             if the BOM is not one the stream is configured to detect\n     * @throws IOException\n     *             if an error reading the first bytes of the stream occurs\n     */\n    @SuppressWarnings(\"unused\")\n    public boolean hasBOM(final ByteOrderMark bom) throws IOException {\n        if (!boms.contains(bom)) {\n            throw new IllegalArgumentException(\"Stream not configure to detect \" + bom);\n        }\n        getBOM();\n        return byteOrderMark != null && byteOrderMark.equals(bom);\n    }\n\n    /**\n     * Return the BOM (Byte Order Mark).\n     *\n     * @return The BOM or null if none\n     * @throws IOException\n     *             if an error reading the first bytes of the stream occurs\n     */\n    public ByteOrderMark getBOM() throws IOException {\n        if (firstBytes == null) {\n            fbLength = 0;\n            // BOMs are sorted from longest to shortest\n            final int maxBomSize = boms.get(0).length();\n            firstBytes = new int[maxBomSize];\n            // Read first maxBomSize bytes\n            for (int i = 0; i < firstBytes.length; i++) {\n                firstBytes[i] = in.read();\n                fbLength++;\n                if (firstBytes[i] < 0) {\n                    break;\n                }\n            }\n            // match BOM in firstBytes\n            byteOrderMark = find();\n            if (byteOrderMark != null) {\n                if (!include) {\n                    if (byteOrderMark.length() < firstBytes.length) {\n                        fbIndex = byteOrderMark.length();\n                    } else {\n                        fbLength = 0;\n                    }\n                }\n            }\n        }\n        return byteOrderMark;\n    }\n\n    /**\n     * Return the BOM charset Name - {@link ByteOrderMark#getCharsetName()}.\n     *\n     * @return The BOM charset Name or null if no BOM found\n     * @throws IOException\n     *             if an error reading the first bytes of the stream occurs\n     *\n     */\n    public String getBOMCharsetName() throws IOException {\n        getBOM();\n        return byteOrderMark == null ? null : byteOrderMark.getCharsetName();\n    }\n\n    /**\n     * This method reads and either preserves or skips the first bytes in the stream. It behaves like the single-byte\n     * <code>read()</code> method, either returning a valid byte or -1 to indicate that the initial bytes have been\n     * processed already.\n     *\n     * @return the byte read (excluding BOM) or -1 if the end of stream\n     * @throws IOException\n     *             if an I/O error occurs\n     */\n    private int readFirstBytes() throws IOException {\n        getBOM();\n        return fbIndex < fbLength ? firstBytes[fbIndex++] : EOF;\n    }\n\n    /**\n     * Find a BOM with the specified bytes.\n     *\n     * @return The matched BOM or null if none matched\n     */\n    private ByteOrderMark find() {\n        for (final ByteOrderMark bom : boms) {\n            if (matches(bom)) {\n                return bom;\n            }\n        }\n        return null;\n    }\n\n    /**\n     * Check if the bytes match a BOM.\n     *\n     * @param bom\n     *            The BOM\n     * @return true if the bytes match the bom, otherwise false\n     */\n    private boolean matches(final ByteOrderMark bom) {\n        // if (bom.length() != fbLength) {\n        // return false;\n        // }\n        // firstBytes may be bigger than the BOM bytes\n        for (int i = 0; i < bom.length(); i++) {\n            if (bom.get(i) != firstBytes[i]) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    // ----------------------------------------------------------------------------\n    // Implementation of InputStream\n    // ----------------------------------------------------------------------------\n\n    /**\n     * Invokes the delegate's <code>read()</code> method, detecting and optionally skipping BOM.\n     *\n     * @return the byte read (excluding BOM) or -1 if the end of stream\n     * @throws IOException\n     *             if an I/O error occurs\n     */\n    @Override\n    public int read() throws IOException {\n        final int b = readFirstBytes();\n        return b >= 0 ? b : in.read();\n    }\n\n    /**\n     * Invokes the delegate's <code>read(byte[], int, int)</code> method, detecting and optionally skipping BOM.\n     *\n     * @param buf\n     *            the buffer to read the bytes into\n     * @param off\n     *            The start offset\n     * @param len\n     *            The number of bytes to read (excluding BOM)\n     * @return the number of bytes read or -1 if the end of stream\n     * @throws IOException\n     *             if an I/O error occurs\n     */\n    @Override\n    public int read(final byte[] buf, int off, int len) throws IOException {\n        int firstCount = 0;\n        int b = 0;\n        while (len > 0 && b >= 0) {\n            b = readFirstBytes();\n            if (b >= 0) {\n                buf[off++] = (byte) (b & 0xFF);\n                len--;\n                firstCount++;\n            }\n        }\n        final int secondCount = in.read(buf, off, len);\n        return secondCount < 0 ? firstCount > 0 ? firstCount : EOF : firstCount + secondCount;\n    }\n\n    /**\n     * Invokes the delegate's <code>read(byte[])</code> method, detecting and optionally skipping BOM.\n     *\n     * @param buf\n     *            the buffer to read the bytes into\n     * @return the number of bytes read (excluding BOM) or -1 if the end of stream\n     * @throws IOException\n     *             if an I/O error occurs\n     */\n    @Override\n    public int read(final byte[] buf) throws IOException {\n        return read(buf, 0, buf.length);\n    }\n\n    /**\n     * Invokes the delegate's <code>mark(int)</code> method.\n     *\n     * @param readlimit\n     *            read ahead limit\n     */\n    @Override\n    public synchronized void mark(final int readlimit) {\n        markFbIndex = fbIndex;\n        markedAtStart = firstBytes == null;\n        in.mark(readlimit);\n    }\n\n    /**\n     * Invokes the delegate's <code>reset()</code> method.\n     *\n     * @throws IOException\n     *             if an I/O error occurs\n     */\n    @Override\n    public synchronized void reset() throws IOException {\n        fbIndex = markFbIndex;\n        if (markedAtStart) {\n            firstBytes = null;\n        }\n\n        in.reset();\n    }\n\n    /**\n     * Invokes the delegate's <code>skip(long)</code> method, detecting and optionally skipping BOM.\n     *\n     * @param n\n     *            the number of bytes to skip\n     * @return the number of bytes to skipped or -1 if the end of stream\n     * @throws IOException\n     *             if an I/O error occurs\n     */\n    @Override\n    public long skip(final long n) throws IOException {\n        int skipped = 0;\n        while ((n > skipped) && (readFirstBytes() >= 0)) {\n            skipped++;\n        }\n        return in.skip(n - skipped) + skipped;\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/commons/io/ByteOrderMark.java",
    "content": "package me.ag2s.epublib.util.commons.io;\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport java.io.Serializable;\nimport java.util.Locale;\n\n/**\n * Byte Order Mark (BOM) representation - see {@link BOMInputStream}.\n *\n * @see BOMInputStream\n * @see <a href=\"http://en.wikipedia.org/wiki/Byte_order_mark\">Wikipedia: Byte Order Mark</a>\n * @see <a href=\"http://www.w3.org/TR/2006/REC-xml-20060816/#sec-guessing\">W3C: Autodetection of Character Encodings\n *      (Non-Normative)</a>\n * @since 2.0\n */\npublic class ByteOrderMark implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    /** UTF-8 BOM */\n    public static final ByteOrderMark UTF_8    = new ByteOrderMark(\"UTF-8\",    0xEF, 0xBB, 0xBF);\n\n    /** UTF-16BE BOM (Big-Endian) */\n    public static final ByteOrderMark UTF_16BE = new ByteOrderMark(\"UTF-16BE\", 0xFE, 0xFF);\n\n    /** UTF-16LE BOM (Little-Endian) */\n    public static final ByteOrderMark UTF_16LE = new ByteOrderMark(\"UTF-16LE\", 0xFF, 0xFE);\n\n    /**\n     * UTF-32BE BOM (Big-Endian)\n     * @since 2.2\n     */\n    public static final ByteOrderMark UTF_32BE = new ByteOrderMark(\"UTF-32BE\", 0x00, 0x00, 0xFE, 0xFF);\n\n    /**\n     * UTF-32LE BOM (Little-Endian)\n     * @since 2.2\n     */\n    public static final ByteOrderMark UTF_32LE = new ByteOrderMark(\"UTF-32LE\", 0xFF, 0xFE, 0x00, 0x00);\n\n    /**\n     * Unicode BOM character; external form depends on the encoding.\n     * @see <a href=\"http://unicode.org/faq/utf_bom.html#BOM\">Byte Order Mark (BOM) FAQ</a>\n     * @since 2.5\n     */\n    @SuppressWarnings(\"unused\")\n    public static final char UTF_BOM = '\\uFEFF';\n\n    private final String charsetName;\n    private final int[] bytes;\n\n    /**\n     * Construct a new BOM.\n     *\n     * @param charsetName The name of the charset the BOM represents\n     * @param bytes The BOM's bytes\n     * @throws IllegalArgumentException if the charsetName is null or\n     * zero length\n     * @throws IllegalArgumentException if the bytes are null or zero\n     * length\n     */\n    public ByteOrderMark(final String charsetName, final int... bytes) {\n        if (charsetName == null || charsetName.isEmpty()) {\n            throw new IllegalArgumentException(\"No charsetName specified\");\n        }\n        if (bytes == null || bytes.length == 0) {\n            throw new IllegalArgumentException(\"No bytes specified\");\n        }\n        this.charsetName = charsetName;\n        this.bytes = new int[bytes.length];\n        System.arraycopy(bytes, 0, this.bytes, 0, bytes.length);\n    }\n\n    /**\n     * Return the name of the {@link java.nio.charset.Charset} the BOM represents.\n     *\n     * @return the character set name\n     */\n    public String getCharsetName() {\n        return charsetName;\n    }\n\n    /**\n     * Return the length of the BOM's bytes.\n     *\n     * @return the length of the BOM's bytes\n     */\n    public int length() {\n        return bytes.length;\n    }\n\n    /**\n     * The byte at the specified position.\n     *\n     * @param pos The position\n     * @return The specified byte\n     */\n    public int get(final int pos) {\n        return bytes[pos];\n    }\n\n    /**\n     * Return a copy of the BOM's bytes.\n     *\n     * @return a copy of the BOM's bytes\n     */\n    public byte[] getBytes() {\n        final byte[] copy = new byte[bytes.length];\n        for (int i = 0; i < bytes.length; i++) {\n            copy[i] = (byte)bytes[i];\n        }\n        return copy;\n    }\n\n    /**\n     * Indicates if this BOM's bytes equals another.\n     *\n     * @param obj The object to compare to\n     * @return true if the bom's bytes are equal, otherwise\n     * false\n     */\n    @Override\n    public boolean equals(final Object obj) {\n        if (!(obj instanceof ByteOrderMark)) {\n            return false;\n        }\n        final ByteOrderMark bom = (ByteOrderMark)obj;\n        if (bytes.length != bom.length()) {\n            return false;\n        }\n        for (int i = 0; i < bytes.length; i++) {\n            if (bytes[i] != bom.get(i)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Return the hashcode for this BOM.\n     *\n     * @return the hashcode for this BOM.\n     * @see java.lang.Object#hashCode()\n     */\n    @Override\n    public int hashCode() {\n        int hashCode = getClass().hashCode();\n        for (final int b : bytes) {\n            hashCode += b;\n        }\n        return hashCode;\n    }\n\n    /**\n     * Provide a String representation of the BOM.\n     *\n     * @return the length of the BOM's bytes\n     */\n    @Override\n    @SuppressWarnings(\"NullableProblems\")\n    public String toString() {\n        final StringBuilder builder = new StringBuilder();\n        builder.append(getClass().getSimpleName());\n        builder.append('[');\n        builder.append(charsetName);\n        builder.append(\": \");\n        for (int i = 0; i < bytes.length; i++) {\n            if (i > 0) {\n                builder.append(\",\");\n            }\n            builder.append(\"0x\");\n            builder.append(Integer.toHexString(0xFF & bytes[i]).toUpperCase(Locale.ROOT));\n        }\n        builder.append(']');\n        return builder.toString();\n    }\n\n}"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/commons/io/IOConsumer.java",
    "content": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage me.ag2s.epublib.util.commons.io;\n\nimport java.io.IOException;\nimport java.util.Objects;\nimport java.util.function.Consumer;\n\n/**\n * Like {@link Consumer} but throws {@link IOException}.\n *\n * @param <T> the type of the input to the operations.\n * @since 2.7\n */\n@FunctionalInterface\npublic interface IOConsumer<T> {\n\n    /**\n     * Performs this operation on the given argument.\n     *\n     * @param t the input argument\n     * @throws IOException if an I/O error occurs.\n     */\n    void accept(T t) throws IOException;\n\n    /**\n     * Returns a composed {@code IoConsumer} that performs, in sequence, this operation followed by the {@code after}\n     * operation. If performing either operation throws an exception, it is relayed to the caller of the composed\n     * operation. If performing this operation throws an exception, the {@code after} operation will not be performed.\n     *\n     * @param after the operation to perform after this operation\n     * @return a composed {@code Consumer} that performs in sequence this operation followed by the {@code after}\n     *         operation\n     * @throws NullPointerException if {@code after} is null\n     */\n    @SuppressWarnings(\"unused\")\n    default IOConsumer<T> andThen(final IOConsumer<? super T> after) {\n        Objects.requireNonNull(after);\n        return (final T t) -> {\n            accept(t);\n            after.accept(t);\n        };\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/commons/io/ProxyInputStream.java",
    "content": "package me.ag2s.epublib.util.commons.io;\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport java.io.FilterInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\nimport me.ag2s.epublib.util.IOUtil;\n\nimport static me.ag2s.epublib.util.IOUtil.EOF;\n\n\n/**\n * A Proxy stream which acts as expected, that is it passes the method\n * calls on to the proxied stream and doesn't change which methods are\n * being called.\n * <p>\n * It is an alternative base class to FilterInputStream\n * to increase reusability, because FilterInputStream changes the\n * methods being called, such as read(byte[]) to read(byte[], int, int).\n * </p>\n * <p>\n * See the protected methods for ways in which a subclass can easily decorate\n * a stream with custom pre-, post- or error processing functionality.\n * </p>\n */\npublic abstract class ProxyInputStream extends FilterInputStream {\n\n    /**\n     * Constructs a new ProxyInputStream.\n     *\n     * @param proxy the InputStream to delegate to\n     */\n    public ProxyInputStream(final InputStream proxy) {\n        super(proxy);\n        // the proxy is stored in a protected superclass variable named 'in'\n    }\n\n    /**\n     * Invokes the delegate's <code>read()</code> method.\n     *\n     * @return the byte read or -1 if the end of stream\n     * @throws IOException if an I/O error occurs\n     */\n    @Override\n    public int read() throws IOException {\n        try {\n            beforeRead(1);\n            final int b = in.read();\n            afterRead(b != EOF ? 1 : EOF);\n            return b;\n        } catch (final IOException e) {\n            handleIOException(e);\n            return EOF;\n        }\n    }\n\n    /**\n     * Invokes the delegate's <code>read(byte[])</code> method.\n     *\n     * @param bts the buffer to read the bytes into\n     * @return the number of bytes read or EOF if the end of stream\n     * @throws IOException if an I/O error occurs\n     */\n    @Override\n    public int read(final byte[] bts) throws IOException {\n        try {\n            beforeRead(IOUtil.length(bts));\n            final int n = in.read(bts);\n            afterRead(n);\n            return n;\n        } catch (final IOException e) {\n            handleIOException(e);\n            return EOF;\n        }\n    }\n\n    /**\n     * Invokes the delegate's <code>read(byte[], int, int)</code> method.\n     *\n     * @param bts the buffer to read the bytes into\n     * @param off The start offset\n     * @param len The number of bytes to read\n     * @return the number of bytes read or -1 if the end of stream\n     * @throws IOException if an I/O error occurs\n     */\n    @Override\n    public int read(final byte[] bts, final int off, final int len) throws IOException {\n        try {\n            beforeRead(len);\n            final int n = in.read(bts, off, len);\n            afterRead(n);\n            return n;\n        } catch (final IOException e) {\n            handleIOException(e);\n            return EOF;\n        }\n    }\n\n    /**\n     * Invokes the delegate's <code>skip(long)</code> method.\n     *\n     * @param ln the number of bytes to skip\n     * @return the actual number of bytes skipped\n     * @throws IOException if an I/O error occurs\n     */\n    @Override\n    public long skip(final long ln) throws IOException {\n        try {\n            return in.skip(ln);\n        } catch (final IOException e) {\n            handleIOException(e);\n            return 0;\n        }\n    }\n\n    /**\n     * Invokes the delegate's <code>available()</code> method.\n     *\n     * @return the number of available bytes\n     * @throws IOException if an I/O error occurs\n     */\n    @Override\n    public int available() throws IOException {\n        try {\n            return super.available();\n        } catch (final IOException e) {\n            handleIOException(e);\n            return 0;\n        }\n    }\n\n    /**\n     * Invokes the delegate's <code>close()</code> method.\n     *\n     * @throws IOException if an I/O error occurs\n     */\n    @Override\n    public void close() throws IOException {\n        IOUtil.close(in, this::handleIOException);\n    }\n\n    /**\n     * Invokes the delegate's <code>mark(int)</code> method.\n     *\n     * @param readlimit read ahead limit\n     */\n    @Override\n    public synchronized void mark(final int readlimit) {\n        in.mark(readlimit);\n    }\n\n    /**\n     * Invokes the delegate's <code>reset()</code> method.\n     *\n     * @throws IOException if an I/O error occurs\n     */\n    @Override\n    public synchronized void reset() throws IOException {\n        try {\n            in.reset();\n        } catch (final IOException e) {\n            handleIOException(e);\n        }\n    }\n\n    /**\n     * Invokes the delegate's <code>markSupported()</code> method.\n     *\n     * @return true if mark is supported, otherwise false\n     */\n    @Override\n    public boolean markSupported() {\n        return in.markSupported();\n    }\n\n    /**\n     * Invoked by the read methods before the call is proxied. The number\n     * of bytes that the caller wanted to read (1 for the {@link #read()}\n     * method, buffer length for {@link #read(byte[])}, etc.) is given as\n     * an argument.\n     * <p>\n     * Subclasses can override this method to add common pre-processing\n     * functionality without having to override all the read methods.\n     * The default implementation does nothing.\n     * <p>\n     * Note this method is <em>not</em> called from {@link #skip(long)} or\n     * {@link #reset()}. You need to explicitly override those methods if\n     * you want to add pre-processing steps also to them.\n     *\n     * @param n number of bytes that the caller asked to be read\n     * @since 2.0\n     */\n    @SuppressWarnings(\"unused\")\n\n    protected void beforeRead(final int n) {\n        // no-op\n    }\n\n    /**\n     * Invoked by the read methods after the proxied call has returned\n     * successfully. The number of bytes returned to the caller (or -1 if\n     * the end of stream was reached) is given as an argument.\n     * <p>\n     * Subclasses can override this method to add common post-processing\n     * functionality without having to override all the read methods.\n     * The default implementation does nothing.\n     * <p>\n     * Note this method is <em>not</em> called from {@link #skip(long)} or\n     * {@link #reset()}. You need to explicitly override those methods if\n     * you want to add post-processing steps also to them.\n     *\n     * @param n number of bytes read, or -1 if the end of stream was reached\n     * @since 2.0\n     */\n    @SuppressWarnings(\"unused\")\n    protected void afterRead(final int n) {\n        // no-op\n    }\n\n    /**\n     * Handle any IOExceptions thrown.\n     * <p>\n     * This method provides a point to implement custom exception\n     * handling. The default behavior is to re-throw the exception.\n     *\n     * @param e The IOException thrown\n     * @throws IOException if an I/O error occurs\n     * @since 2.0\n     */\n    protected void handleIOException(final IOException e) throws IOException {\n        throw e;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReader.java",
    "content": "package me.ag2s.epublib.util.commons.io;\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.Reader;\nimport java.io.StringReader;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.net.URLConnection;\nimport java.text.MessageFormat;\nimport java.util.Locale;\nimport java.util.Objects;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport me.ag2s.epublib.util.IOUtil;\n\n\n/**\n * Character stream that handles all the necessary Voodoo to figure out the\n * charset encoding of the XML document within the stream.\n * <p>\n * IMPORTANT: This class is not related in any way to the org.xml.sax.XMLReader.\n * This one IS a character stream.\n * </p>\n * <p>\n * All this has to be done without consuming characters from the stream, if not\n * the XML parser will not recognized the document as a valid XML. This is not\n * 100% true, but it's close enough (UTF-8 BOM is not handled by all parsers\n * right now, XmlStreamReader handles it and things work in all parsers).\n * </p>\n * <p>\n * The XmlStreamReader class handles the charset encoding of XML documents in\n * Files, raw streams and HTTP streams by offering a wide set of constructors.\n * </p>\n * <p>\n * By default the charset encoding detection is lenient, the constructor with\n * the lenient flag can be used for a script (following HTTP MIME and XML\n * specifications). All this is nicely explained by Mark Pilgrim in his blog, <a\n * href=\"http://diveintomark.org/archives/2004/02/13/xml-media-types\">\n * Determining the character encoding of a feed</a>.\n * </p>\n * <p>\n * Originally developed for <a href=\"http://rome.dev.java.net\">ROME</a> under\n * Apache License 2.0.\n * </p>\n *\n * //@seerr XmlStreamWriter\n * @since 2.0\n */\npublic class XmlStreamReader extends Reader {\n    private static final int BUFFER_SIZE = IOUtil.DEFAULT_BUFFER_SIZE;\n\n    private static final String UTF_8 = \"UTF-8\";\n\n    private static final String US_ASCII = \"US-ASCII\";\n\n    private static final String UTF_16BE = \"UTF-16BE\";\n\n    private static final String UTF_16LE = \"UTF-16LE\";\n\n    private static final String UTF_32BE = \"UTF-32BE\";\n\n    private static final String UTF_32LE = \"UTF-32LE\";\n\n    private static final String UTF_16 = \"UTF-16\";\n\n    private static final String UTF_32 = \"UTF-32\";\n\n    private static final String EBCDIC = \"CP1047\";\n\n    private static final ByteOrderMark[] BOMS = new ByteOrderMark[] {\n            ByteOrderMark.UTF_8,\n            ByteOrderMark.UTF_16BE,\n            ByteOrderMark.UTF_16LE,\n            ByteOrderMark.UTF_32BE,\n            ByteOrderMark.UTF_32LE\n    };\n\n    // UTF_16LE and UTF_32LE have the same two starting BOM bytes.\n    private static final ByteOrderMark[] XML_GUESS_BYTES = new ByteOrderMark[] {\n            new ByteOrderMark(UTF_8,    0x3C, 0x3F, 0x78, 0x6D),\n            new ByteOrderMark(UTF_16BE, 0x00, 0x3C, 0x00, 0x3F),\n            new ByteOrderMark(UTF_16LE, 0x3C, 0x00, 0x3F, 0x00),\n            new ByteOrderMark(UTF_32BE, 0x00, 0x00, 0x00, 0x3C,\n                    0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x6D),\n            new ByteOrderMark(UTF_32LE, 0x3C, 0x00, 0x00, 0x00,\n                    0x3F, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x6D, 0x00, 0x00, 0x00),\n            new ByteOrderMark(EBCDIC,   0x4C, 0x6F, 0xA7, 0x94)\n    };\n\n    private final Reader reader;\n\n    private final String encoding;\n\n    private final String defaultEncoding;\n\n    /**\n     * Returns the default encoding to use if none is set in HTTP content-type,\n     * XML prolog and the rules based on content-type are not adequate.\n     * <p>\n     * If it is NULL the content-type based rules are used.\n     *\n     * @return the default encoding to use.\n     */\n    public String getDefaultEncoding() {\n        return defaultEncoding;\n    }\n\n    /**\n     * Creates a Reader for a File.\n     * <p>\n     * It looks for the UTF-8 BOM first, if none sniffs the XML prolog charset,\n     * if this is also missing defaults to UTF-8.\n     * <p>\n     * It does a lenient charset encoding detection, check the constructor with\n     * the lenient parameter for details.\n     *\n     * @param file File to create a Reader from.\n     * @throws IOException thrown if there is a problem reading the file.\n     */\n    @SuppressWarnings(\"unused\")\n    public XmlStreamReader(final File file) throws IOException {\n        this(new FileInputStream(Objects.requireNonNull(file)));\n    }\n\n    /**\n     * Creates a Reader for a raw InputStream.\n     * <p>\n     * It follows the same logic used for files.\n     * <p>\n     * It does a lenient charset encoding detection, check the constructor with\n     * the lenient parameter for details.\n     *\n     * @param inputStream InputStream to create a Reader from.\n     * @throws IOException thrown if there is a problem reading the stream.\n     */\n    public XmlStreamReader(final InputStream inputStream) throws IOException {\n        this(inputStream, true);\n    }\n\n    /**\n     * Creates a Reader for a raw InputStream.\n     * <p>\n     * It follows the same logic used for files.\n     * <p>\n     * If lenient detection is indicated and the detection above fails as per\n     * specifications it then attempts the following:\n     * <p>\n     * If the content type was 'text/html' it replaces it with 'text/xml' and\n     * tries the detection again.\n     * <p>\n     * Else if the XML prolog had a charset encoding that encoding is used.\n     * <p>\n     * Else if the content type had a charset encoding that encoding is used.\n     * <p>\n     * Else 'UTF-8' is used.\n     * <p>\n     * If lenient detection is indicated an XmlStreamReaderException is never\n     * thrown.\n     *\n     * @param inputStream InputStream to create a Reader from.\n     * @param lenient indicates if the charset encoding detection should be\n     *        relaxed.\n     * @throws IOException thrown if there is a problem reading the stream.\n     * @throws XmlStreamReaderException thrown if the charset encoding could not\n     *         be determined according to the specs.\n     */\n    public XmlStreamReader(final InputStream inputStream, final boolean lenient) throws IOException {\n        this(inputStream, lenient, null);\n    }\n\n    /**\n     * Creates a Reader for a raw InputStream.\n     * <p>\n     * It follows the same logic used for files.\n     * <p>\n     * If lenient detection is indicated and the detection above fails as per\n     * specifications it then attempts the following:\n     * <p>\n     * If the content type was 'text/html' it replaces it with 'text/xml' and\n     * tries the detection again.\n     * <p>\n     * Else if the XML prolog had a charset encoding that encoding is used.\n     * <p>\n     * Else if the content type had a charset encoding that encoding is used.\n     * <p>\n     * Else 'UTF-8' is used.\n     * <p>\n     * If lenient detection is indicated an XmlStreamReaderException is never\n     * thrown.\n     *\n     * @param inputStream InputStream to create a Reader from.\n     * @param lenient indicates if the charset encoding detection should be\n     *        relaxed.\n     * @param defaultEncoding The default encoding\n     * @throws IOException thrown if there is a problem reading the stream.\n     * @throws XmlStreamReaderException thrown if the charset encoding could not\n     *         be determined according to the specs.\n     */\n    public XmlStreamReader(final InputStream inputStream, final boolean lenient, final String defaultEncoding)\n            throws IOException {\n        Objects.requireNonNull(inputStream, \"inputStream\");\n        this.defaultEncoding = defaultEncoding;\n        final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS);\n        final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);\n        this.encoding = doRawStream(bom, pis, lenient);\n        this.reader = new InputStreamReader(pis, encoding);\n    }\n\n    /**\n     * Creates a Reader using the InputStream of a URL.\n     * <p>\n     * If the URL is not of type HTTP and there is not 'content-type' header in\n     * the fetched data it uses the same logic used for Files.\n     * <p>\n     * If the URL is a HTTP Url or there is a 'content-type' header in the\n     * fetched data it uses the same logic used for an InputStream with\n     * content-type.\n     * <p>\n     * It does a lenient charset encoding detection, check the constructor with\n     * the lenient parameter for details.\n     *\n     * @param url URL to create a Reader from.\n     * @throws IOException thrown if there is a problem reading the stream of\n     *         the URL.\n     */\n    @SuppressWarnings(\"unused\")\n    public XmlStreamReader(final URL url) throws IOException {\n        this(Objects.requireNonNull(url, \"url\").openConnection(), null);\n    }\n\n    /**\n     * Creates a Reader using the InputStream of a URLConnection.\n     * <p>\n     * If the URLConnection is not of type HttpURLConnection and there is not\n     * 'content-type' header in the fetched data it uses the same logic used for\n     * files.\n     * <p>\n     * If the URLConnection is a HTTP Url or there is a 'content-type' header in\n     * the fetched data it uses the same logic used for an InputStream with\n     * content-type.\n     * <p>\n     * It does a lenient charset encoding detection, check the constructor with\n     * the lenient parameter for details.\n     *\n     * @param conn URLConnection to create a Reader from.\n     * @param defaultEncoding The default encoding\n     * @throws IOException thrown if there is a problem reading the stream of\n     *         the URLConnection.\n     */\n    public XmlStreamReader(final URLConnection conn, final String defaultEncoding) throws IOException {\n        Objects.requireNonNull(conn, \"conm\");\n        this.defaultEncoding = defaultEncoding;\n        final boolean lenient = true;\n        final String contentType = conn.getContentType();\n        final InputStream inputStream = conn.getInputStream();\n        final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS);\n        final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);\n        if (conn instanceof HttpURLConnection || contentType != null) {\n            this.encoding = processHttpStream(bom, pis, contentType, lenient);\n        } else {\n            this.encoding = doRawStream(bom, pis, lenient);\n        }\n        this.reader = new InputStreamReader(pis, encoding);\n    }\n\n    /**\n     * Creates a Reader using an InputStream and the associated content-type\n     * header.\n     * <p>\n     * First it checks if the stream has BOM. If there is not BOM checks the\n     * content-type encoding. If there is not content-type encoding checks the\n     * XML prolog encoding. If there is not XML prolog encoding uses the default\n     * encoding mandated by the content-type MIME type.\n     * <p>\n     * It does a lenient charset encoding detection, check the constructor with\n     * the lenient parameter for details.\n     *\n     * @param inputStream InputStream to create the reader from.\n     * @param httpContentType content-type header to use for the resolution of\n     *        the charset encoding.\n     * @throws IOException thrown if there is a problem reading the file.\n     */\n    public XmlStreamReader(final InputStream inputStream, final String httpContentType)\n            throws IOException {\n        this(inputStream, httpContentType, true);\n    }\n\n    /**\n     * Creates a Reader using an InputStream and the associated content-type\n     * header. This constructor is lenient regarding the encoding detection.\n     * <p>\n     * First it checks if the stream has BOM. If there is not BOM checks the\n     * content-type encoding. If there is not content-type encoding checks the\n     * XML prolog encoding. If there is not XML prolog encoding uses the default\n     * encoding mandated by the content-type MIME type.\n     * <p>\n     * If lenient detection is indicated and the detection above fails as per\n     * specifications it then attempts the following:\n     * <p>\n     * If the content type was 'text/html' it replaces it with 'text/xml' and\n     * tries the detection again.\n     * <p>\n     * Else if the XML prolog had a charset encoding that encoding is used.\n     * <p>\n     * Else if the content type had a charset encoding that encoding is used.\n     * <p>\n     * Else 'UTF-8' is used.\n     * <p>\n     * If lenient detection is indicated an XmlStreamReaderException is never\n     * thrown.\n     *\n     * @param inputStream InputStream to create the reader from.\n     * @param httpContentType content-type header to use for the resolution of\n     *        the charset encoding.\n     * @param lenient indicates if the charset encoding detection should be\n     *        relaxed.\n     * @param defaultEncoding The default encoding\n     * @throws IOException thrown if there is a problem reading the file.\n     * @throws XmlStreamReaderException thrown if the charset encoding could not\n     *         be determined according to the specs.\n     */\n    public XmlStreamReader(final InputStream inputStream, final String httpContentType,\n                           final boolean lenient, final String defaultEncoding) throws IOException {\n        Objects.requireNonNull(inputStream, \"inputStream\");\n        this.defaultEncoding = defaultEncoding;\n        final BOMInputStream bom = new BOMInputStream(new BufferedInputStream(inputStream, BUFFER_SIZE), false, BOMS);\n        final BOMInputStream pis = new BOMInputStream(bom, true, XML_GUESS_BYTES);\n        this.encoding = processHttpStream(bom, pis, httpContentType, lenient);\n        this.reader = new InputStreamReader(pis, encoding);\n    }\n\n    /**\n     * Creates a Reader using an InputStream and the associated content-type\n     * header. This constructor is lenient regarding the encoding detection.\n     * <p>\n     * First it checks if the stream has BOM. If there is not BOM checks the\n     * content-type encoding. If there is not content-type encoding checks the\n     * XML prolog encoding. If there is not XML prolog encoding uses the default\n     * encoding mandated by the content-type MIME type.\n     * <p>\n     * If lenient detection is indicated and the detection above fails as per\n     * specifications it then attempts the following:\n     * <p>\n     * If the content type was 'text/html' it replaces it with 'text/xml' and\n     * tries the detection again.\n     * <p>\n     * Else if the XML prolog had a charset encoding that encoding is used.\n     * <p>\n     * Else if the content type had a charset encoding that encoding is used.\n     * <p>\n     * Else 'UTF-8' is used.\n     * <p>\n     * If lenient detection is indicated an XmlStreamReaderException is never\n     * thrown.\n     *\n     * @param inputStream InputStream to create the reader from.\n     * @param httpContentType content-type header to use for the resolution of\n     *        the charset encoding.\n     * @param lenient indicates if the charset encoding detection should be\n     *        relaxed.\n     * @throws IOException thrown if there is a problem reading the file.\n     * @throws XmlStreamReaderException thrown if the charset encoding could not\n     *         be determined according to the specs.\n     */\n    public XmlStreamReader(final InputStream inputStream, final String httpContentType,\n                           final boolean lenient) throws IOException {\n        this(inputStream, httpContentType, lenient, null);\n    }\n\n    /**\n     * Returns the charset encoding of the XmlStreamReader.\n     *\n     * @return charset encoding.\n     */\n    public String getEncoding() {\n        return encoding;\n    }\n\n    /**\n     * Invokes the underlying reader's <code>read(char[], int, int)</code> method.\n     * @param buf the buffer to read the characters into\n     * @param offset The start offset\n     * @param len The number of bytes to read\n     * @return the number of characters read or -1 if the end of stream\n     * @throws IOException if an I/O error occurs\n     */\n    @Override\n    public int read(final char[] buf, final int offset, final int len) throws IOException {\n        return reader.read(buf, offset, len);\n    }\n\n    /**\n     * Closes the XmlStreamReader stream.\n     *\n     * @throws IOException thrown if there was a problem closing the stream.\n     */\n    @Override\n    public void close() throws IOException {\n        reader.close();\n    }\n\n    /**\n     * Process the raw stream.\n     *\n     * @param bom BOMInputStream to detect byte order marks\n     * @param pis BOMInputStream to guess XML encoding\n     * @param lenient indicates if the charset encoding detection should be\n     *        relaxed.\n     * @return the encoding to be used\n     * @throws IOException thrown if there is a problem reading the stream.\n     */\n    private String doRawStream(final BOMInputStream bom, final BOMInputStream pis, final boolean lenient)\n            throws IOException {\n        final String bomEnc      = bom.getBOMCharsetName();\n        final String xmlGuessEnc = pis.getBOMCharsetName();\n        final String xmlEnc = getXmlProlog(pis, xmlGuessEnc);\n        try {\n            return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc);\n        } catch (final XmlStreamReaderException ex) {\n            if (lenient) {\n                return doLenientDetection(null, ex);\n            }\n            throw ex;\n        }\n    }\n\n    /**\n     * Process a HTTP stream.\n     *\n     * @param bom BOMInputStream to detect byte order marks\n     * @param pis BOMInputStream to guess XML encoding\n     * @param httpContentType The HTTP content type\n     * @param lenient indicates if the charset encoding detection should be\n     *        relaxed.\n     * @return the encoding to be used\n     * @throws IOException thrown if there is a problem reading the stream.\n     */\n    private String processHttpStream(final BOMInputStream bom, final BOMInputStream pis, final String httpContentType,\n                                     final boolean lenient) throws IOException {\n        final String bomEnc = bom.getBOMCharsetName();\n        final String xmlGuessEnc = pis.getBOMCharsetName();\n        final String xmlEnc = getXmlProlog(pis, xmlGuessEnc);\n        try {\n            return calculateHttpEncoding(httpContentType, bomEnc, xmlGuessEnc, xmlEnc, lenient);\n        } catch (final XmlStreamReaderException ex) {\n            if (lenient) {\n                return doLenientDetection(httpContentType, ex);\n            }\n            throw ex;\n        }\n    }\n\n    /**\n     * Do lenient detection.\n     *\n     * @param httpContentType content-type header to use for the resolution of\n     *        the charset encoding.\n     * @param ex The thrown exception\n     * @return the encoding\n     * @throws IOException thrown if there is a problem reading the stream.\n     */\n    private String doLenientDetection(String httpContentType,\n                                      XmlStreamReaderException ex) throws IOException {\n        if (httpContentType != null && httpContentType.startsWith(\"text/html\")) {\n            httpContentType = httpContentType.substring(\"text/html\".length());\n            httpContentType = \"text/xml\" + httpContentType;\n            try {\n                return calculateHttpEncoding(httpContentType, ex.getBomEncoding(),\n                        ex.getXmlGuessEncoding(), ex.getXmlEncoding(), true);\n            } catch (final XmlStreamReaderException ex2) {\n                ex = ex2;\n            }\n        }\n        String encoding = ex.getXmlEncoding();\n        if (encoding == null) {\n            encoding = ex.getContentTypeEncoding();\n        }\n        if (encoding == null) {\n            encoding = defaultEncoding == null ? UTF_8 : defaultEncoding;\n        }\n        return encoding;\n    }\n\n    /**\n     * Calculate the raw encoding.\n     *\n     * @param bomEnc BOM encoding\n     * @param xmlGuessEnc XML Guess encoding\n     * @param xmlEnc XML encoding\n     * @return the raw encoding\n     * @throws IOException thrown if there is a problem reading the stream.\n     */\n    String calculateRawEncoding(final String bomEnc, final String xmlGuessEnc,\n                                final String xmlEnc) throws IOException {\n\n        // BOM is Null\n        if (bomEnc == null) {\n            if (xmlGuessEnc == null || xmlEnc == null) {\n                return defaultEncoding == null ? UTF_8 : defaultEncoding;\n            }\n            if (xmlEnc.equals(UTF_16) &&\n                    (xmlGuessEnc.equals(UTF_16BE) || xmlGuessEnc.equals(UTF_16LE))) {\n                return xmlGuessEnc;\n            }\n            return xmlEnc;\n        }\n\n        // BOM is UTF-8\n        if (bomEnc.equals(UTF_8)) {\n            if (xmlGuessEnc != null && !xmlGuessEnc.equals(UTF_8)) {\n                final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);\n                throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);\n            }\n            if (xmlEnc != null && !xmlEnc.equals(UTF_8)) {\n                final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);\n                throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);\n            }\n            return bomEnc;\n        }\n\n        // BOM is UTF-16BE or UTF-16LE\n        if (bomEnc.equals(UTF_16BE) || bomEnc.equals(UTF_16LE)) {\n            if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) {\n                final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);\n                throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);\n            }\n            if (xmlEnc != null && !xmlEnc.equals(UTF_16) && !xmlEnc.equals(bomEnc)) {\n                final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);\n                throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);\n            }\n            return bomEnc;\n        }\n\n        // BOM is UTF-32BE or UTF-32LE\n        if (bomEnc.equals(UTF_32BE) || bomEnc.equals(UTF_32LE)) {\n            if (xmlGuessEnc != null && !xmlGuessEnc.equals(bomEnc)) {\n                final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);\n                throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);\n            }\n            if (xmlEnc != null && !xmlEnc.equals(UTF_32) && !xmlEnc.equals(bomEnc)) {\n                final String msg = MessageFormat.format(RAW_EX_1, bomEnc, xmlGuessEnc, xmlEnc);\n                throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);\n            }\n            return bomEnc;\n        }\n\n        // BOM is something else\n        final String msg = MessageFormat.format(RAW_EX_2, bomEnc, xmlGuessEnc, xmlEnc);\n        throw new XmlStreamReaderException(msg, bomEnc, xmlGuessEnc, xmlEnc);\n    }\n\n\n    /**\n     * Calculate the HTTP encoding.\n     *\n     * @param httpContentType The HTTP content type\n     * @param bomEnc BOM encoding\n     * @param xmlGuessEnc XML Guess encoding\n     * @param xmlEnc XML encoding\n     * @param lenient indicates if the charset encoding detection should be\n     *        relaxed.\n     * @return the HTTP encoding\n     * @throws IOException thrown if there is a problem reading the stream.\n     */\n    String calculateHttpEncoding(final String httpContentType,\n                                 final String bomEnc, final String xmlGuessEnc, final String xmlEnc,\n                                 final boolean lenient) throws IOException {\n\n        // Lenient and has XML encoding\n        if (lenient && xmlEnc != null) {\n            return xmlEnc;\n        }\n\n        // Determine mime/encoding content types from HTTP Content Type\n        final String cTMime = getContentTypeMime(httpContentType);\n        final String cTEnc  = getContentTypeEncoding(httpContentType);\n        final boolean appXml  = isAppXml(cTMime);\n        final boolean textXml = isTextXml(cTMime);\n\n        // Mime type NOT \"application/xml\" or \"text/xml\"\n        if (!appXml && !textXml) {\n            final String msg = MessageFormat.format(HTTP_EX_3, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);\n            throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);\n        }\n\n        // No content type encoding\n        if (cTEnc == null) {\n            if (appXml) {\n                return calculateRawEncoding(bomEnc, xmlGuessEnc, xmlEnc);\n            }\n            return defaultEncoding == null ? US_ASCII : defaultEncoding;\n        }\n\n        // UTF-16BE or UTF-16LE content type encoding\n        if (cTEnc.equals(UTF_16BE) || cTEnc.equals(UTF_16LE)) {\n            if (bomEnc != null) {\n                final String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);\n                throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);\n            }\n            return cTEnc;\n        }\n\n        // UTF-16 content type encoding\n        if (cTEnc.equals(UTF_16)) {\n            if (bomEnc != null && bomEnc.startsWith(UTF_16)) {\n                return bomEnc;\n            }\n            final String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);\n            throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);\n        }\n\n        // UTF-32BE or UTF-132E content type encoding\n        if (cTEnc.equals(UTF_32BE) || cTEnc.equals(UTF_32LE)) {\n            if (bomEnc != null) {\n                final String msg = MessageFormat.format(HTTP_EX_1, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);\n                throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);\n            }\n            return cTEnc;\n        }\n\n        // UTF-32 content type encoding\n        if (cTEnc.equals(UTF_32)) {\n            if (bomEnc != null && bomEnc.startsWith(UTF_32)) {\n                return bomEnc;\n            }\n            final String msg = MessageFormat.format(HTTP_EX_2, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);\n            throw new XmlStreamReaderException(msg, cTMime, cTEnc, bomEnc, xmlGuessEnc, xmlEnc);\n        }\n\n        return cTEnc;\n    }\n\n    /**\n     * Returns MIME type or NULL if httpContentType is NULL.\n     *\n     * @param httpContentType the HTTP content type\n     * @return The mime content type\n     */\n    static String getContentTypeMime(final String httpContentType) {\n        String mime = null;\n        if (httpContentType != null) {\n            final int i = httpContentType.indexOf(\";\");\n            if (i >= 0) {\n                mime = httpContentType.substring(0, i);\n            } else {\n                mime = httpContentType;\n            }\n            mime = mime.trim();\n        }\n        return mime;\n    }\n\n    private static final Pattern CHARSET_PATTERN = Pattern\n            .compile(\"charset=[\\\"']?([.[^; \\\"']]*)[\\\"']?\");\n\n    /**\n     * Returns charset parameter value, NULL if not present, NULL if\n     * httpContentType is NULL.\n     *\n     * @param httpContentType the HTTP content type\n     * @return The content type encoding (upcased)\n     */\n    static String getContentTypeEncoding(final String httpContentType) {\n        String encoding = null;\n        if (httpContentType != null) {\n            final int i = httpContentType.indexOf(\";\");\n            if (i > -1) {\n                final String postMime = httpContentType.substring(i + 1);\n                final Matcher m = CHARSET_PATTERN.matcher(postMime);\n                encoding = m.find() ? m.group(1) : null;\n                encoding = encoding != null ? encoding.toUpperCase(Locale.ROOT) : null;\n            }\n        }\n        return encoding;\n    }\n\n    /**\n     * Pattern capturing the encoding of the \"xml\" processing instruction.\n     */\n    public static final Pattern ENCODING_PATTERN = Pattern.compile(\n            \"<\\\\?xml.*encoding[\\\\s]*=[\\\\s]*((?:\\\".[^\\\"]*\\\")|(?:'.[^']*'))\",\n            Pattern.MULTILINE);\n\n    /**\n     * Returns the encoding declared in the <?xml encoding=...?>, NULL if none.\n     *\n     * @param inputStream InputStream to create the reader from.\n     * @param guessedEnc guessed encoding\n     * @return the encoding declared in the <?xml encoding=...?>\n     * @throws IOException thrown if there is a problem reading the stream.\n     */\n    private static String getXmlProlog(final InputStream inputStream, final String guessedEnc)\n            throws IOException {\n        String encoding = null;\n        if (guessedEnc != null) {\n            final byte[] bytes = new byte[BUFFER_SIZE];\n            inputStream.mark(BUFFER_SIZE);\n            int offset = 0;\n            int max = BUFFER_SIZE;\n            int c = inputStream.read(bytes, offset, max);\n            int firstGT = -1;\n            String xmlProlog = \"\"; // avoid possible NPE warning (cannot happen; this just silences the warning)\n            while (c != -1 && firstGT == -1 && offset < BUFFER_SIZE) {\n                offset += c;\n                max -= c;\n                c = inputStream.read(bytes, offset, max);\n                xmlProlog = new String(bytes, 0, offset, guessedEnc);\n                firstGT = xmlProlog.indexOf('>');\n            }\n            if (firstGT == -1) {\n                if (c == -1) {\n                    throw new IOException(\"Unexpected end of XML stream\");\n                }\n                throw new IOException(\n                        \"XML prolog or ROOT element not found on first \"\n                                + offset + \" bytes\");\n            }\n            final int bytesRead = offset;\n            if (bytesRead > 0) {\n                inputStream.reset();\n                final BufferedReader bReader = new BufferedReader(new StringReader(\n                        xmlProlog.substring(0, firstGT + 1)));\n                final StringBuffer prolog = new StringBuffer();\n                String line;\n                while ((line = bReader.readLine()) != null) {\n                    prolog.append(line);\n                }\n                final Matcher m = ENCODING_PATTERN.matcher(prolog);\n                if (m.find()) {\n                    encoding = Objects.requireNonNull(m.group(1)).toUpperCase(Locale.ROOT);\n                    encoding = encoding.substring(1, encoding.length() - 1);\n                }\n            }\n        }\n        return encoding;\n    }\n\n    /**\n     * Indicates if the MIME type belongs to the APPLICATION XML family.\n     *\n     * @param mime The mime type\n     * @return true if the mime type belongs to the APPLICATION XML family,\n     * otherwise false\n     */\n    static boolean isAppXml(final String mime) {\n        return mime != null &&\n                (mime.equals(\"application/xml\") ||\n                        mime.equals(\"application/xml-dtd\") ||\n                        mime.equals(\"application/xml-external-parsed-entity\") ||\n                        mime.startsWith(\"application/\") && mime.endsWith(\"+xml\"));\n    }\n\n    /**\n     * Indicates if the MIME type belongs to the TEXT XML family.\n     *\n     * @param mime The mime type\n     * @return true if the mime type belongs to the TEXT XML family,\n     * otherwise false\n     */\n    static boolean isTextXml(final String mime) {\n        return mime != null &&\n                (mime.equals(\"text/xml\") ||\n                        mime.equals(\"text/xml-external-parsed-entity\") ||\n                        mime.startsWith(\"text/\") && mime.endsWith(\"+xml\"));\n    }\n\n    private static final String RAW_EX_1 =\n            \"Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] encoding mismatch\";\n\n    private static final String RAW_EX_2 =\n            \"Invalid encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] unknown BOM\";\n\n    private static final String HTTP_EX_1 =\n            \"Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], BOM must be NULL\";\n\n    private static final String HTTP_EX_2 =\n            \"Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], encoding mismatch\";\n\n    private static final String HTTP_EX_3 =\n            \"Invalid encoding, CT-MIME [{0}] CT-Enc [{1}] BOM [{2}] XML guess [{3}] XML prolog [{4}], Invalid MIME\";\n\n}"
  },
  {
    "path": "src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReaderException.java",
    "content": "package me.ag2s.epublib.util.commons.io;\n\n/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOTICE file distributed with\n * this work for additional information regarding copyright ownership.\n * The ASF licenses this file to You under the Apache License, Version 2.0\n * (the \"License\"); you may not use this file except in compliance with\n * the License.  You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport java.io.IOException;\n\n/**\n * The XmlStreamReaderException is thrown by the XmlStreamReader constructors if\n * the charset encoding can not be determined according to the XML 1.0\n * specification and RFC 3023.\n * <p>\n * The exception returns the unconsumed InputStream to allow the application to\n * do an alternate processing with the stream. Note that the original\n * InputStream given to the XmlStreamReader cannot be used as that one has been\n * already read.\n * </p>\n *\n * @since 2.0\n */\npublic class XmlStreamReaderException extends IOException {\n\n    private static final long serialVersionUID = 1L;\n\n    private final String bomEncoding;\n\n    private final String xmlGuessEncoding;\n\n    private final String xmlEncoding;\n\n    private final String contentTypeMime;\n\n    private final String contentTypeEncoding;\n\n    /**\n     * Creates an exception instance if the charset encoding could not be\n     * determined.\n     * <p>\n     * Instances of this exception are thrown by the XmlStreamReader.\n     * </p>\n     *\n     * @param msg message describing the reason for the exception.\n     * @param bomEnc BOM encoding.\n     * @param xmlGuessEnc XML guess encoding.\n     * @param xmlEnc XML prolog encoding.\n     */\n    public XmlStreamReaderException(final String msg, final String bomEnc,\n                                    final String xmlGuessEnc, final String xmlEnc) {\n        this(msg, null, null, bomEnc, xmlGuessEnc, xmlEnc);\n    }\n\n    /**\n     * Creates an exception instance if the charset encoding could not be\n     * determined.\n     * <p>\n     * Instances of this exception are thrown by the XmlStreamReader.\n     * </p>\n     *\n     * @param msg message describing the reason for the exception.\n     * @param ctMime MIME type in the content-type.\n     * @param ctEnc encoding in the content-type.\n     * @param bomEnc BOM encoding.\n     * @param xmlGuessEnc XML guess encoding.\n     * @param xmlEnc XML prolog encoding.\n     */\n    public XmlStreamReaderException(final String msg, final String ctMime, final String ctEnc,\n                                    final String bomEnc, final String xmlGuessEnc, final String xmlEnc) {\n        super(msg);\n        contentTypeMime = ctMime;\n        contentTypeEncoding = ctEnc;\n        bomEncoding = bomEnc;\n        xmlGuessEncoding = xmlGuessEnc;\n        xmlEncoding = xmlEnc;\n    }\n\n    /**\n     * Returns the BOM encoding found in the InputStream.\n     *\n     * @return the BOM encoding, null if none.\n     */\n    public String getBomEncoding() {\n        return bomEncoding;\n    }\n\n    /**\n     * Returns the encoding guess based on the first bytes of the InputStream.\n     *\n     * @return the encoding guess, null if it couldn't be guessed.\n     */\n    public String getXmlGuessEncoding() {\n        return xmlGuessEncoding;\n    }\n\n    /**\n     * Returns the encoding found in the XML prolog of the InputStream.\n     *\n     * @return the encoding of the XML prolog, null if none.\n     */\n    public String getXmlEncoding() {\n        return xmlEncoding;\n    }\n\n    /**\n     * Returns the MIME type in the content-type used to attempt determining the\n     * encoding.\n     *\n     * @return the MIME type in the content-type, null if there was not\n     *         content-type or the encoding detection did not involve HTTP.\n     */\n    public String getContentTypeMime() {\n        return contentTypeMime;\n    }\n\n    /**\n     * Returns the encoding in the content-type used to attempt determining the\n     * encoding.\n     *\n     * @return the encoding in the content-type, null if there was not\n     *         content-type, no encoding in it or the encoding detection did not\n     *         involve HTTP.\n     */\n    public String getContentTypeEncoding() {\n        return contentTypeEncoding;\n    }\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/umdlib/domain/UmdBook.java",
    "content": "package me.ag2s.umdlib.domain;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\n\nimport me.ag2s.umdlib.tool.WrapOutputStream;\n\npublic class UmdBook {\n\n    public int getNum() {\n        return num;\n    }\n\n    public void setNum(int num) {\n        this.num = num;\n    }\n\n    private int num;\n\n\n    /** Header Part of UMD book */\n    private UmdHeader header = new UmdHeader();\n    /**\n     * Detail chapters Part of UMD book\n     * (include Titles & Contents of each chapter)\n     */\n    private UmdChapters chapters = new UmdChapters();\n\n    /** Cover Part of UMD book (for example, and JPEG file) */\n    private UmdCover cover = new UmdCover();\n\n    /** End Part of UMD book */\n    private UmdEnd end = new UmdEnd();\n\n    /**\n     * Build the UMD file.\n     * @param os\n     * @throws IOException\n     */\n    public void buildUmd(OutputStream os) throws IOException {\n        WrapOutputStream wos = new WrapOutputStream(os);\n\n        header.buildHeader(wos);\n        chapters.buildChapters(wos);\n        cover.buildCover(wos);\n        end.buildEnd(wos);\n    }\n\n    public UmdHeader getHeader() {\n        return header;\n    }\n\n    public void setHeader(UmdHeader header) {\n        this.header = header;\n    }\n\n    public UmdChapters getChapters() {\n        return chapters;\n    }\n\n    public void setChapters(UmdChapters chapters) {\n        this.chapters = chapters;\n    }\n\n    public UmdCover getCover() {\n    return cover;\n    }\n\n    public void setCover(UmdCover cover) {\n    this.cover = cover;\n    }\n\n    public UmdEnd getEnd() {\n        return end;\n    }\n\n    public void setEnd(UmdEnd end) {\n        this.end = end;\n    }\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/umdlib/domain/UmdChapters.java",
    "content": "package me.ag2s.umdlib.domain;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.zip.DeflaterOutputStream;\n\nimport me.ag2s.umdlib.tool.UmdUtils;\nimport me.ag2s.umdlib.tool.WrapOutputStream;\n\n/**\n * It includes all titles and contents of each chapter in the UMD file.\n * And the content has been compressed by zlib.\n * \n * @author Ray Liang (liangguanhui@qq.com)\n * 2009-12-20\n */\npublic class UmdChapters {\n\t\n\tprivate static final int DEFAULT_CHUNK_INIT_SIZE = 32768;\n\tprivate int TotalContentLen;\n\n\tpublic List<byte[]> getTitles() {\n\t\treturn titles;\n\t}\n\n\tprivate List<byte[]> titles = new ArrayList<>();\n\tpublic List<Integer> contentLengths = new ArrayList<>();\n\tpublic ByteArrayOutputStream contents = new ByteArrayOutputStream();\n\n\tpublic void addTitle(String s){\n\t\ttitles.add(UmdUtils.stringToUnicodeBytes(s));\n\t}\n\tpublic void addTitle(byte[] s){\n      titles.add(s);\n\t}\n\tpublic void addContentLength(Integer integer){\n\t\tcontentLengths.add(integer);\n\t}\n\tpublic int getContentLength(int index){\n\t\treturn contentLengths.get(index);\n\t}\n\n\tpublic byte[] getContent(int index) {\n\t\tint st=contentLengths.get(index);\n\t\tbyte[] b=contents.toByteArray();\n\t\tint end=index+1<contentLengths.size()?contentLengths.get(index+1): getTotalContentLen();\n\t\tSystem.out.println(\"总长度:\"+contents.size());\n\t\tSystem.out.println(\"起始值:\"+st);\n\t\tSystem.out.println(\"结束值:\"+end);\n\t\tbyte[] bAr=new byte[end-st];\n\t\tSystem.arraycopy(b,st,bAr,0,bAr.length);\n\t\treturn bAr;\n\n\t}\n\tpublic String getContentString(int index) {\n\t\treturn UmdUtils.unicodeBytesToString(getContent(index)).replace((char) 0x2029, '\\n');\n\n\t}\n\tpublic String getTitle(int index){\n\t\treturn UmdUtils.unicodeBytesToString(titles.get(index));\n\t}\n\n\t\n\tpublic void buildChapters(WrapOutputStream wos) throws IOException {\n\t\twriteChaptersHead(wos);\n\t\twriteChaptersContentOffset(wos);\n\t\twriteChaptersTitles(wos);\n\t\twriteChaptersChunks(wos);\n\t}\n\t\n\tprivate void writeChaptersHead(WrapOutputStream wos) throws IOException {\n\t\twos.writeBytes('#', 0x0b, 0, 0, 0x09);\n\t\twos.writeInt(contents.size());\n\t}\n\t\n\tprivate void writeChaptersContentOffset(WrapOutputStream wos) throws IOException {\n\t\twos.writeBytes('#', 0x83, 0, 0, 0x09);\n\t\tbyte[] rb = UmdUtils.genRandomBytes(4);\n\t\twos.writeBytes(rb); //random numbers\n\t\twos.write('$');\n\t\twos.writeBytes(rb); //random numbers\n\t\t\n\t\twos.writeInt(contentLengths.size() * 4 + 9);  // about the count of chapters\n\t\tint offset = 0;\n\t\tfor (Integer n : contentLengths) {\n\t\t\twos.writeInt(offset);\n\t\t\toffset += n;\n\t\t}\n\t}\n\t\n\tprivate void writeChaptersTitles(WrapOutputStream wos) throws IOException {\n\t\twos.writeBytes('#', 0x84, 0, 0x01, 0x09);\n\t\tbyte[] rb = UmdUtils.genRandomBytes(4);\n\t\twos.writeBytes(rb); //random numbers\n\t\twos.write('$');\n\t\twos.writeBytes(rb); //random numbers\n\t\t\n\t\tint totalTitlesLen = 0;\n\t\tfor (byte[] t : titles) {\n\t\t\ttotalTitlesLen += t.length;\n\t\t}\n\t\t\n\t\t// about the length of the titles\n\t\twos.writeInt(totalTitlesLen + titles.size() + 9);  \n\t\t\n\t\tfor (byte[] t : titles) {\n\t\t\twos.writeByte(t.length);\n\t\t\twos.write(t);\n\t\t}\n\t}\n\t\n\tprivate void writeChaptersChunks(WrapOutputStream wos) throws IOException {\n\t\tbyte[] allContents = contents.toByteArray();\n\t\t\n\t\tbyte[] zero16 = new byte[16];\n\t\tArrays.fill(zero16, 0, zero16.length, (byte) 0);\n\t\t\n\t\t// write each package of content\n\t\tint startPos = 0;\n\t\tint len = 0;\n\t\tint left = 0;\n\t\tint chunkCnt = 0;\n\t\tByteArrayOutputStream bos = new ByteArrayOutputStream(DEFAULT_CHUNK_INIT_SIZE + 256);\n\t\tList<byte[]> chunkRbList = new ArrayList<byte[]>();\n\t\t\n\t\twhile(startPos < allContents.length) {\n\t\t\tleft = allContents.length - startPos;\n\t\t\tlen = DEFAULT_CHUNK_INIT_SIZE < left ? DEFAULT_CHUNK_INIT_SIZE : left; \n\t\t\t\n\t\t\tbos.reset();\n\t\t\tDeflaterOutputStream zos = new DeflaterOutputStream(bos);\n\t\t\tzos.write(allContents, startPos, len);\n\t\t\tzos.close();\n\t\t\tbyte[] chunk = bos.toByteArray();\n\t\t\t\n\t\t\tbyte[] rb = UmdUtils.genRandomBytes(4);\n\t\t\twos.writeByte('$');\n\t\t\twos.writeBytes(rb);  // 4 random\n\t\t\tchunkRbList.add(rb);\n\t\t\twos.writeInt(chunk.length + 9);\n\t\t\twos.write(chunk);\n\t\t\t\n\t\t\t// end of each chunk\n\t\t\twos.writeBytes('#', 0xF1, 0, 0, 0x15);\n\t\t\twos.write(zero16);\n\t\t\t\n\t\t\tstartPos += len;\n\t\t\tchunkCnt++;\n\t\t}\n\t\t\n\t\t// end of all chunks\n\t\twos.writeBytes('#', 0x81, 0, 0x01, 0x09);\n\t\twos.writeBytes(0, 0, 0, 0); //random numbers\n\t\twos.write('$');\n\t\twos.writeBytes(0, 0, 0, 0); //random numbers\n\t\twos.writeInt(chunkCnt * 4 + 9);\n\t\tfor (int i = chunkCnt - 1; i >= 0; i--) {\n\t\t\t// random. They are as the same as random numbers in the begin of each chunk\n\t\t\t// use desc order to output these random\n\t\t\twos.writeBytes(chunkRbList.get(i));\n\t\t}\n\t}\n\t\n\tpublic void addChapter(String title, String content) {\n\t\ttitles.add(UmdUtils.stringToUnicodeBytes(title));\n\t\tbyte[] b = UmdUtils.stringToUnicodeBytes(content);\n\t\tcontentLengths.add(b.length);\n\t\ttry {\n\t\t\tcontents.write(b);\n\t\t} catch (IOException e) {\n\t\t\tthrow new RuntimeException(e);\n\t\t}\n\t}\n\t\n\tpublic void addFile(File f, String title) throws IOException {\n\t\tbyte[] temp = UmdUtils.readFile(f);\n\t\tString s = new String(temp);\n\t\taddChapter(title, s);\n\t}\n\t\n\tpublic void addFile(File f) throws IOException {\n\t\tString s = f.getName();\n\t\tint idx = s.lastIndexOf('.');\n\t\tif (idx >= 0) {\n\t\t\ts = s.substring(0, idx);\n\t\t}\n\t\taddFile(f, s);\n\t}\n\t\n\tpublic void clearChapters() {\n\t\ttitles.clear();\n\t\tcontentLengths.clear();\n\t\tcontents.reset();\n\t}\n\n\tpublic int getTotalContentLen() {\n\t\treturn TotalContentLen;\n\t}\n\n\tpublic void setTotalContentLen(int totalContentLen) {\n\t\tTotalContentLen = totalContentLen;\n\t}\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/umdlib/domain/UmdCover.java",
    "content": "package me.ag2s.umdlib.domain;\n\n\nimport java.io.File;\nimport java.io.IOException;\n\nimport me.ag2s.umdlib.tool.UmdUtils;\nimport me.ag2s.umdlib.tool.WrapOutputStream;\n\n\n/**\n * This is the cover part of the UMD file.\n * <P>\n * NOTICE: if the \"coverData\" is empty, it will be skipped when building UMD file.\n * </P>\n * There are 3 ways to load the image data:\n * <ol>\n *     <li>new constructor function of UmdCover.</li>\n *     <li>use UmdCover.load function.</li>\n *     <li>use UmdCover.initDefaultCover, it will generate a simple image with text.</li>\n * </ol>\n * @author Ray Liang (liangguanhui@qq.com)\n * 2009-12-20\n */\npublic class UmdCover {\n\t\n\tprivate static int DEFAULT_COVER_WIDTH = 120;\n\tprivate static int DEFAULT_COVER_HEIGHT = 160;\n\t\n\tprivate byte[] coverData;\n\n\tpublic UmdCover() {\n\t}\n\n\tpublic UmdCover(byte[] coverData) {\n\t\tthis.coverData = coverData;\n\t}\n\t\n\tpublic void load(File f) throws IOException {\n\t\tthis.coverData = UmdUtils.readFile(f);\n\t}\n\n\tpublic void load(String fileName) throws IOException {\n\t\tload(new File(fileName));\n\t}\n\t\n\tpublic void initDefaultCover(String title) throws IOException {\n//\t\tBufferedImage img = new BufferedImage(DEFAULT_COVER_WIDTH, DEFAULT_COVER_HEIGHT, BufferedImage.TYPE_INT_RGB);\n//\t\tGraphics g = img.getGraphics();\n//\t\tg.setColor(Color.BLACK);\n//\t\tg.fillRect(0, 0, img.getWidth(), img.getHeight());\n//\t\tg.setColor(Color.WHITE);\n//\t\tg.setFont(new Font(\"����\", Font.PLAIN, 12));\n//\n//\t\tFontMetrics fm = g.getFontMetrics();\n//\t\tint ascent = fm.getAscent();\n//\t\tint descent = fm.getDescent();\n//\t\tint strWidth = fm.stringWidth(title);\n//\t\tint x = (img.getWidth() - strWidth) / 2;\n//\t\tint y = (img.getHeight() - ascent - descent) / 2;\n//\t\tg.drawString(title, x, y);\n//\t\tg.dispose();\n//\n//\t\tByteArrayOutputStream baos = new ByteArrayOutputStream();\n//\n//\t\tJPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(baos);\n//\t\tJPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(img);\n//\t\tparam.setQuality(0.5f, false);\n//\t\tencoder.setJPEGEncodeParam(param);\n//\t\tencoder.encode(img);\n//\n//\t\tcoverData = baos.toByteArray();\n\t}\n\t\n\tpublic void buildCover(WrapOutputStream wos) throws IOException {\n\t\tif (coverData == null || coverData.length == 0) {\n\t\t\treturn;\n\t\t}\n\t\twos.writeBytes('#', 0x82, 0, 0x01, 0x0A, 0x01);\n\t\tbyte[] rb = UmdUtils.genRandomBytes(4);\n\t\twos.writeBytes(rb); //random numbers\n\t\twos.write('$');\n\t\twos.writeBytes(rb); //random numbers\n\t\twos.writeInt(coverData.length + 9);\n\t\twos.write(coverData);\n\t}\n\n\tpublic byte[] getCoverData() {\n\t\treturn coverData;\n\t}\n\n\tpublic void setCoverData(byte[] coverData) {\n\t\tthis.coverData = coverData;\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/umdlib/domain/UmdEnd.java",
    "content": "package me.ag2s.umdlib.domain;\n\nimport java.io.IOException;\n\nimport me.ag2s.umdlib.tool.WrapOutputStream;\n\n/**\n * End part of UMD book, nothing to be special\n * \n * @author Ray Liang (liangguanhui@qq.com)\n * 2009-12-20\n */\npublic class UmdEnd {\n\t\n\tpublic void buildEnd(WrapOutputStream wos) throws IOException {\n\t\twos.writeBytes('#', 0x0C, 0, 0x01, 0x09);\n\t\twos.writeInt(wos.getWritten() + 4);\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/umdlib/domain/UmdHeader.java",
    "content": "package me.ag2s.umdlib.domain;\n\n\nimport java.io.IOException;\n\nimport me.ag2s.umdlib.tool.UmdUtils;\nimport me.ag2s.umdlib.tool.WrapOutputStream;\n\n/**\n * Header of UMD file.\n * It includes a lot of properties of header.\n * All the properties are String type.\n * \n * @author Ray Liang (liangguanhui@qq.com)\n * 2009-12-20\n */\npublic class UmdHeader {\n\tpublic byte getUmdType() {\n\t\treturn umdType;\n\t}\n\n\tpublic void setUmdType(byte umdType) {\n\t\tthis.umdType = umdType;\n\t}\n\n\tprivate byte umdType;\n\tprivate String title;\n\n\tprivate String author;\n\t\n\tprivate String year;\n\t\n\tprivate String month;\n\t\n\tprivate String day;\n\t\n\tprivate String bookType;\n\n\tprivate String bookMan;\n\t\n\tprivate String shopKeeper;\n\tprivate final static byte B_type_umd = (byte) 0x01;\n\tprivate final static byte B_type_title = (byte) 0x02;\n\tprivate final static byte B_type_author = (byte) 0x03;\n\tprivate final static byte B_type_year = (byte) 0x04;\n\tprivate final static byte B_type_month = (byte) 0x05;\n\tprivate final static byte B_type_day = (byte) 0x06;\n\tprivate final static byte B_type_bookType = (byte) 0x07;\n\tprivate final static byte B_type_bookMan = (byte) 0x08;\n\tprivate final static byte B_type_shopKeeper = (byte) 0x09;\n\t\n\tpublic void buildHeader(WrapOutputStream wos) throws IOException {\n\t\twos.writeBytes(0x89, 0x9b, 0x9a, 0xde); // UMD file type flags\n\t\twos.writeByte('#');\n\t\twos.writeBytes(0x01, 0x00, 0x00, 0x08); // Unknown\n\t\twos.writeByte(0x01); //0x01 is text type; while 0x02 is Image type.\n\t\twos.writeBytes(UmdUtils.genRandomBytes(2)); //random number\n\t\t\n\t\t// start properties output\n\t\tbuildType(wos, B_type_title, getTitle());\n\t\tbuildType(wos, B_type_author, getAuthor());\n\t\tbuildType(wos, B_type_year, getYear());\n\t\tbuildType(wos, B_type_month, getMonth());\n\t\tbuildType(wos, B_type_day, getDay());\n\t\tbuildType(wos, B_type_bookType, getBookType());\n\t\tbuildType(wos, B_type_bookMan, getBookMan());\n\t\tbuildType(wos, B_type_shopKeeper, getShopKeeper());\n\t}\n\t\n\tpublic void buildType(WrapOutputStream wos, byte type, String content) throws IOException {\n\t\tif (content == null || content.length() == 0) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\twos.writeBytes('#', type, 0, 0);\n\t\t\n\t\tbyte[] temp = UmdUtils.stringToUnicodeBytes(content);\n\t\twos.writeByte(temp.length + 5);\n\t\twos.write(temp);\n\t}\n\n\n\n\tpublic String getTitle() {\n\t\treturn title;\n\t}\n\n\tpublic void setTitle(String title) {\n\t\tthis.title = title;\n\t}\n\n\tpublic String getAuthor() {\n\t\treturn author;\n\t}\n\n\tpublic void setAuthor(String author) {\n\t\tthis.author = author;\n\t}\n\n\tpublic String getBookMan() {\n\t\treturn bookMan;\n\t}\n\n\tpublic void setBookMan(String bookMan) {\n\t\tthis.bookMan = bookMan;\n\t}\n\n\tpublic String getShopKeeper() {\n\t\treturn shopKeeper;\n\t}\n\n\tpublic void setShopKeeper(String shopKeeper) {\n\t\tthis.shopKeeper = shopKeeper;\n\t}\n\n\tpublic String getYear() {\n\t\treturn year;\n\t}\n\n\tpublic void setYear(String year) {\n\t\tthis.year = year;\n\t}\n\n\tpublic String getMonth() {\n\t\treturn month;\n\t}\n\n\tpublic void setMonth(String month) {\n\t\tthis.month = month;\n\t}\n\n\tpublic String getDay() {\n\t\treturn day;\n\t}\n\n\tpublic void setDay(String day) {\n\t\tthis.day = day;\n\t}\n\n\tpublic String getBookType() {\n\t\treturn bookType;\n\t}\n\n\tpublic void setBookType(String bookType) {\n\t\tthis.bookType = bookType;\n\t}\n\t\n\t@Override\n\tpublic String toString() {\n\t\treturn \"UmdHeader{\" +\n\t\t\t\t\"umdType=\" + umdType +\n\t\t\t\t\", title='\" + title + '\\'' +\n\t\t\t\t\", author='\" + author + '\\'' +\n\t\t\t\t\", year='\" + year + '\\'' +\n\t\t\t\t\", month='\" + month + '\\'' +\n\t\t\t\t\", day='\" + day + '\\'' +\n\t\t\t\t\", bookType='\" + bookType + '\\'' +\n\t\t\t\t\", bookMan='\" + bookMan + '\\'' +\n\t\t\t\t\", shopKeeper='\" + shopKeeper + '\\'' +\n\t\t\t\t'}';\n\t}\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/umdlib/tool/StreamReader.java",
    "content": "package me.ag2s.umdlib.tool;\n\nimport java.io.FileInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\npublic class StreamReader {\n    private InputStream is;\n\n    public long getOffset() {\n        return offset;\n    }\n\n    public void setOffset(long offset) {\n        this.offset = offset;\n    }\n\n    public long getSize() {\n        return size;\n    }\n\n    public void setSize(long size) {\n        this.size = size;\n    }\n\n    private long offset;\n    private long size;\n\n    private void incCount(int value) {\n        int temp = (int) (offset + value);\n        if (temp < 0) {\n            temp = Integer.MAX_VALUE;\n        }\n        offset = temp;\n    }\n    public StreamReader(InputStream inputStream) throws IOException {\n        this.is=inputStream;\n        //this.size=inputStream.getChannel().size();\n    }\n\n    public short readUint8() throws IOException {\n        byte[] b=new byte[1];\n        is.read(b);\n        incCount(1);\n        return (short) ((b[0] & 0xFF));\n\n    }\n\n    public byte readByte() throws IOException {\n        byte[] b=new byte[1];\n        is.read(b);\n        incCount(1);\n        return b[0];\n    }\n    public byte[] readBytes(int len) throws IOException {\n        if (len<1){\n            System.out.println(len);\n            throw new IllegalArgumentException(\"Length must > 0: \" + len);\n        }\n        byte[] b=new byte[len];\n        is.read(b);\n        incCount(len);\n        return b;\n    }\n    public String readHex(int len) throws IOException {\n        if (len<1){\n            System.out.println(len);\n            throw new IllegalArgumentException(\"Length must > 0: \" + len);\n        }\n        byte[] b=new byte[len];\n        is.read(b);\n        incCount(len);\n       return UmdUtils.toHex(b);\n    }\n\n    public short readShort() throws IOException {\n        byte[] b=new byte[2];\n        is.read(b);\n        incCount(2);\n        short x = (short) (((b[0] & 0xFF) <<  8) | ((b[1] & 0xFF) <<  0));\n        return x;\n    }\n    public short readShortLe() throws IOException {\n        byte[] b=new byte[2];\n        is.read(b);\n        incCount(2);\n        short x = (short) (((b[1] & 0xFF) <<  8) | ((b[0] & 0xFF) <<  0));\n        return x;\n    }\n    public int readInt() throws IOException {\n        byte[] b=new byte[4];\n        is.read(b);\n        incCount(4);\n        int x = ((b[0] & 0xFF) << 24) | ((b[1] & 0xFF) << 16) |\n                ((b[2] & 0xFF) <<  8) | ((b[3] & 0xFF) <<  0);\n        return x;\n    }\n    public int readIntLe() throws IOException {\n        byte[] b=new byte[4];\n        is.read(b);\n        incCount(4);\n        int x = ((b[3] & 0xFF) << 24) | ((b[2] & 0xFF) << 16) |\n                ((b[1] & 0xFF) <<  8) | ((b[0] & 0xFF) <<  0);\n        return x;\n    }\n    public void skip(int len) throws IOException {\n        readBytes(len);\n    }\n\n\n    public byte[] read(byte[] b) throws IOException {\n        is.read(b);\n        incCount(b.length);\n        return b;\n    }\n\n    public byte[] read(byte[] b, int off, int len) throws IOException {\n        is.read(b, off, len);\n        incCount(len);\n        return b;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/umdlib/tool/UmdUtils.java",
    "content": "\npackage me.ag2s.umdlib.tool;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedOutputStream;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.util.Random;\nimport java.util.zip.InflaterInputStream;\n\n\npublic class UmdUtils {\n\n\tprivate static final int EOF = -1;\n\tprivate static final int BUFFER_SIZE = 8 * 1024;\n\n\n\t/**\n\t * 将字符串编码成Unicode形式的byte[]\n\t * @param s 要编码的字符串\n\t * @return 编码好的byte[]\n\t */\n\tpublic static byte[] stringToUnicodeBytes(String s) {\n\t\tif (s == null) {\n\t\t\tthrow new NullPointerException();\n\t\t}\n\t\t\n\t\tint len = s.length();\n\t\tbyte[] ret = new byte[len * 2];\n\t\tint a, b, c;\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tc = s.charAt(i);\n\t\t\ta = c >> 8;\n\t\t\tb = c & 0xFF;\n\t\t\tif (a < 0) {\n\t\t\t\ta += 0xFF;\n\t\t\t}\n\t\t\tif (b < 0) {\n\t\t\t\tb += 0xFF;\n\t\t\t}\n\t\t\tret[i * 2] = (byte) b;\n\t\t\tret[i * 2 + 1] = (byte) a;\n\t\t}\n\t\treturn ret;\n\t}\n\n\t/**\n\t * 将编码成Unicode形式的byte[]解码成原始字符串\n\t * @param bytes 编码成Unicode形式的byte[]\n\t * @return 原始字符串\n\t */\n\tpublic static String unicodeBytesToString(byte[] bytes){\n\t\tchar[] s=new char[bytes.length/2];\n\t\tStringBuilder sb=new StringBuilder();\n\t\tint a,b,c;\n\t\tfor(int i=0;i<s.length;i++){\n\t\t\ta=bytes[i*2+1];\n\t\t\tb=bytes[i*2];\n\t\t\tc=(a&0xff)<<8|(b&0xff);\n\t\t\tif(c<0){\n\t\t\t\tc+=0xffff;\n\t\t\t}\n\t\t\tchar[] c1=Character.toChars(c);\n\t\t\tsb.append(c1);\n\n\t\t}\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 将byte[]转化成Hex形式\n\t * @param bArr byte[]\n\t * @return 目标HEX字符串\n\t */\n\tpublic static String toHex(byte[] bArr){\n\t\tStringBuilder sb = new StringBuilder(bArr.length);\n\t\tString sTmp;\n\n\t\tfor (int i = 0; i < bArr.length; i++) {\n\t\t\tsTmp = Integer.toHexString(0xFF & bArr[i]);\n\t\t\tif (sTmp.length() < 2)\n\t\t\t\tsb.append(0);\n\t\t\tsb.append(sTmp.toUpperCase());\n\t\t}\n\n\t\treturn sb.toString();\n\t}\n\n\t/**\n\t * 解压缩zip的byte[]\n\t * @param compress zippered byte[]\n\t * @return decompressed byte[]\n\t * @throws Exception 解码时失败时\n\t */\n\tpublic static byte[] decompress(byte[] compress) throws Exception {\n\t\tByteArrayInputStream bais = new ByteArrayInputStream(compress);\n\t\tInflaterInputStream iis = new InflaterInputStream(bais);\n\t\tByteArrayOutputStream baos = new ByteArrayOutputStream();\n\t\tint c = 0;\n\t\tbyte[] buf = new byte[BUFFER_SIZE];\n\t\twhile (true) {\n\t\t\tc = iis.read(buf);\n\n\t\t\tif (c == EOF)\n\t\t\t\tbreak;\n\t\t\tbaos.write(buf, 0, c);\n\t\t}\n\t\tbaos.flush();\n\t\treturn baos.toByteArray();\n\t}\n\n\n\n\t\n\tpublic static void saveFile(File f, byte[] content) throws IOException {\n\t\tFileOutputStream fos = new FileOutputStream(f);\n\t\ttry {\n\t\t\tBufferedOutputStream bos = new BufferedOutputStream(fos);\n\t\t\tbos.write(content);\n\t\t\tbos.flush();\n\t\t} finally {\n\t\t\tfos.close();\n\t\t}\n\t}\n\t\n\tpublic static byte[] readFile(File f) throws IOException {\n\t\tFileInputStream fis = new FileInputStream(f);\n\t\ttry {\n\t\t\tByteArrayOutputStream baos = new ByteArrayOutputStream();\n\t\t\tBufferedInputStream bis = new BufferedInputStream(fis);\n\t\t\tint ch;\n\t\t\twhile ((ch = bis.read()) >= 0) {\n\t\t\t\tbaos.write(ch);\n\t\t\t}\n\t\t\tbaos.flush();\n\t\t\treturn baos.toByteArray();\n\t\t} finally {\n\t\t\tfis.close();\n\t\t}\n\t}\n\t\n\tprivate static Random random = new Random();\n\t\n\tpublic static byte[] genRandomBytes(int len) {\n\t\tif (len <= 0) {\n\t\t\tthrow new IllegalArgumentException(\"Length must > 0: \" + len);\n\t\t}\n\t\tbyte[] ret = new byte[len];\n\t\tfor (int i = 0; i < ret.length; i++) {\n\t\t\tret[i] = (byte) random.nextInt(256);\n\t\t}\n\t\treturn ret;\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/umdlib/tool/WrapOutputStream.java",
    "content": "package me.ag2s.umdlib.tool;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\n\npublic class WrapOutputStream extends OutputStream {\n\t\n\tprivate OutputStream os;\n\tprivate int written;\n\n\tpublic WrapOutputStream(OutputStream os) {\n\t\tthis.os = os;\n\t}\n\t\n    private void incCount(int value) {\n        int temp = written + value;\n        if (temp < 0) {\n            temp = Integer.MAX_VALUE;\n        }\n        written = temp;\n    }\n\t\n    // it is different from the writeInt of DataOutputStream\n\tpublic void writeInt(int v) throws IOException {\n        os.write((v >>>  0) & 0xFF);\n        os.write((v >>>  8) & 0xFF);\n        os.write((v >>> 16) & 0xFF);\n        os.write((v >>> 24) & 0xFF);\n\t\tincCount(4);\n\t}\n\t\n\tpublic void writeByte(byte b) throws IOException {\n\t\twrite(b);\n\t}\n\t\n\tpublic void writeByte(int n) throws IOException {\n\t\twrite(n);\n\t}\n\t\n\tpublic void writeBytes(byte ... bytes) throws IOException {\n\t\twrite(bytes);\n\t}\n\t\n\tpublic void writeBytes(int ... vals) throws IOException {\n\t\tfor (int v : vals) {\n\t\t\twrite(v);\n\t\t}\n\t}\n\n\tpublic void write(byte[] b, int off, int len) throws IOException {\n\t\tos.write(b, off, len);\n\t\tincCount(len);\n\t}\n\n\tpublic void write(byte[] b) throws IOException {\n\t\tos.write(b);\n\t\tincCount(b.length);\n\t}\n\n\tpublic void write(int b) throws IOException {\n\t\tos.write(b);\n\t\tincCount(1);\n\t}\n\t\n\t/////////////////////////////////////////////////\n\n\tpublic void close() throws IOException {\n\t\tos.close();\n\t}\n\n\tpublic void flush() throws IOException {\n\t\tos.flush();\n\t}\n\n\tpublic boolean equals(Object obj) {\n\t\treturn os.equals(obj);\n\t}\n\n\tpublic int hashCode() {\n\t\treturn os.hashCode();\n\t}\n\n\tpublic String toString() {\n\t\treturn os.toString();\n\t}\n\n\tpublic int getWritten() {\n\t\treturn written;\n\t}\n\n}\n"
  },
  {
    "path": "src/main/java/me/ag2s/umdlib/umd/UmdReader.java",
    "content": "package me.ag2s.umdlib.umd;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n\nimport me.ag2s.umdlib.domain.UmdBook;\nimport me.ag2s.umdlib.domain.UmdCover;\nimport me.ag2s.umdlib.domain.UmdHeader;\nimport me.ag2s.umdlib.tool.StreamReader;\nimport me.ag2s.umdlib.tool.UmdUtils;\n\n/**\n * UMD格式的电子书解析\n * 格式规范参考：\n * http://blog.sina.com.cn/s/blog_7c8dc2d501018o5d.html\n * http://blog.sina.com.cn/s/blog_7c8dc2d501018o5l.html\n *\n */\n\npublic class UmdReader {\n    UmdBook book;\n    InputStream inputStream;\n    int _AdditionalCheckNumber;\n    int _TotalContentLen;\n    boolean end = false;\n\n\n    public synchronized UmdBook read(InputStream inputStream) throws Exception {\n\n        book = new UmdBook();\n        this.inputStream=inputStream;\n        StreamReader reader = new StreamReader(inputStream);\n        UmdHeader umdHeader = new UmdHeader();\n        book.setHeader(umdHeader);\n        if (reader.readIntLe() != 0xde9a9b89) {\n            throw new IOException(\"Wrong header\");\n        }\n        short num1 = -1;\n        byte ch = reader.readByte();\n        while (ch == 35) {\n            //int num2=reader.readByte();\n            short segType = reader.readShortLe();\n            byte segFlag = reader.readByte();\n            short len = (short) (reader.readUint8() - 5);\n\n            System.out.println(\"块标识:\" + segType);\n            //short length1 = reader.readByte();\n            ReadSection(segType, segFlag, len, reader, umdHeader);\n\n            if ((int) segType == 241 || (int) segType == 10) {\n                segType = num1;\n            }\n            for (ch = reader.readByte(); ch == 36; ch = reader.readByte()) {\n                //int num3 = reader.readByte();\n                System.out.println(ch);\n                int additionalCheckNumber = reader.readIntLe();\n                int length2 = (reader.readIntLe() - 9);\n                ReadAdditionalSection(segType, additionalCheckNumber, length2, reader);\n            }\n            num1 = segType;\n\n        }\n        System.out.println(book.getHeader().toString());\n        return book;\n\n    }\n\n    private void ReadAdditionalSection(short segType, int additionalCheckNumber, int length, StreamReader reader) throws Exception {\n        switch (segType) {\n            case 14:\n                //this._TotalImageList.Add((object) Image.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length))));\n                break;\n            case 15:\n                //this._TotalImageList.Add((object) Image.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length))));\n                break;\n            case 129:\n                reader.readBytes(length);\n                break;\n            case 130:\n                //byte[] covers = reader.readBytes(length);\n                book.setCover(new UmdCover(reader.readBytes(length)));\n                //this._Book.Cover = BitmapImage.FromStream((Stream) new MemoryStream(reader.ReadBytes((int) length)));\n                break;\n            case 131:\n                System.out.println(length / 4);\n                book.setNum(length / 4);\n                for (int i = 0; i < length / 4; ++i) {\n                    book.getChapters().addContentLength(reader.readIntLe());\n                }\n                break;\n            case 132:\n                //System.out.println(length/4);\n                System.out.println(_AdditionalCheckNumber);\n                System.out.println(additionalCheckNumber);\n                if (this._AdditionalCheckNumber != additionalCheckNumber) {\n                    System.out.println(length);\n                    book.getChapters().contents.write(UmdUtils.decompress(reader.readBytes(length)));\n                    book.getChapters().contents.flush();\n                    break;\n                } else {\n                    for (int i = 0; i < book.getNum(); i++) {\n                        short len = reader.readUint8();\n                        byte[] title = reader.readBytes(len);\n                        //System.out.println(UmdUtils.unicodeBytesToString(title));\n                        book.getChapters().addTitle(title);\n                    }\n                }\n\n\n                break;\n            default:\n                    /*Console.WriteLine(\"未知内容\");\n                    Console.WriteLine(\"Seg Type = \" + (object) segType);\n                    Console.WriteLine(\"Seg Len = \" + (object) length);\n                    Console.WriteLine(\"content = \" + (object) reader.ReadBytes((int) length));*/\n                break;\n        }\n    }\n\n    public void ReadSection(short segType, byte segFlag, short length, StreamReader reader, UmdHeader header) throws IOException {\n        switch (segType) {\n            case 1://umd文件头 DCTS_CMD_ID_VERSION\n                header.setUmdType(reader.readByte());\n                reader.readBytes(2);//Random 2\n                System.out.println(\"UMD文件类型:\" + header.getUmdType());\n                break;\n            case 2://文件标题 DCTS_CMD_ID_TITLE\n                header.setTitle(UmdUtils.unicodeBytesToString(reader.readBytes(length)));\n                System.out.println(\"文件标题:\" + header.getTitle());\n                break;\n            case 3://作者\n                header.setAuthor(UmdUtils.unicodeBytesToString(reader.readBytes(length)));\n                System.out.println(\"作者:\" + header.getAuthor());\n                break;\n            case 4://年\n                header.setYear(UmdUtils.unicodeBytesToString(reader.readBytes(length)));\n                System.out.println(\"年:\" + header.getYear());\n                break;\n            case 5://月\n                header.setMonth(UmdUtils.unicodeBytesToString(reader.readBytes(length)));\n                System.out.println(\"月:\" + header.getMonth());\n                break;\n            case 6://日\n                header.setDay(UmdUtils.unicodeBytesToString(reader.readBytes(length)));\n                System.out.println(\"日:\" + header.getDay());\n                break;\n            case 7://小说类型\n                header.setBookType(UmdUtils.unicodeBytesToString(reader.readBytes(length)));\n                System.out.println(\"小说类型:\" + header.getBookType());\n                break;\n            case 8://出版商\n                header.setBookMan(UmdUtils.unicodeBytesToString(reader.readBytes(length)));\n                System.out.println(\"出版商:\" + header.getBookMan());\n                break;\n            case 9:// 零售商\n                header.setShopKeeper(UmdUtils.unicodeBytesToString(reader.readBytes(length)));\n                System.out.println(\"零售商:\" + header.getShopKeeper());\n                break;\n            case 10://CONTENT ID\n                System.out.println(\"CONTENT ID:\" + reader.readHex(length));\n                break;\n            case 11:\n                //内容长度 DCTS_CMD_ID_FILE_LENGTH\n                _TotalContentLen = reader.readIntLe();\n                book.getChapters().setTotalContentLen(_TotalContentLen);\n                System.out.println(\"内容长度:\" + _TotalContentLen);\n                break;\n            case 12://UMD文件结束\n                end = true;\n                int num2 = reader.readIntLe();\n                System.out.println(\"整个文件长度\" + num2);\n                break;\n            case 13:\n                break;\n            case 14:\n                int num3 = (int) reader.readByte();\n                break;\n            case 15:\n                reader.readBytes(length);\n                break;\n            case 129://正文\n            case 131://章节偏移\n                _AdditionalCheckNumber = reader.readIntLe();\n                System.out.println(\"章节偏移:\" + _AdditionalCheckNumber);\n                break;\n            case 132://章节标题，正文\n                _AdditionalCheckNumber = reader.readIntLe();\n                System.out.println(\"章节标题，正文:\" + _AdditionalCheckNumber);\n                break;\n            case 130://封面（jpg）\n                int num4 = (int) reader.readByte();\n                _AdditionalCheckNumber = reader.readIntLe();\n                break;\n            case 135://页面偏移（Page Offset）\n                reader.readUint8();//fontSize 一字节 字体大小\n                reader.readUint8();//screenWidth 屏幕宽度\n                reader.readBytes(4);//BlockRandom 指向一个页面偏移数据块\n                break;\n            case 240://CDS KEY\n                break;\n            case 241://许可证(LICENCE KEY)\n                //System.out.println(\"整个文件长度\" + length);\n                System.out.println(\"许可证(LICENCE KEY):\" + reader.readHex(16));\n                break;\n            default:\n                if (length > 0) {\n                    byte[] numArray = reader.readBytes(length);\n                }\n\n\n        }\n    }\n\n\n    @Override\n    public String toString() {\n        return \"UmdReader{\" +\n                \"book=\" + book +\n                '}';\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/kxml2/io/KXmlParser.java",
    "content": "/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The  above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE. */\n\n// Contributors: Paul Hackenberger (unterminated entity handling in relaxed mode)\n\npackage org.kxml2.io;\n\nimport java.io.*;\nimport java.util.*;\n\nimport org.xmlpull.v1.*;\n\n/** A simple, pull based XML parser. This classe replaces the kXML 1\n    XmlParser class and the corresponding event classes. */\n\npublic class KXmlParser implements XmlPullParser {\n\n    private Object location;\n\tstatic final private String UNEXPECTED_EOF = \"Unexpected EOF\";\n    static final private String ILLEGAL_TYPE = \"Wrong event type\";\n    static final private int LEGACY = 999;\n    static final private int XML_DECL = 998;\n\n    // general\n\n    private String version;\n    private Boolean standalone;\n\n    private boolean processNsp;\n    private boolean relaxed;\n    private Hashtable entityMap;\n    private int depth;\n    private String[] elementStack = new String[16];\n    private String[] nspStack = new String[8];\n    private int[] nspCounts = new int[4];\n\n    // source\n\n    private Reader reader;\n    private String encoding;\n    private char[] srcBuf;\n\n    private int srcPos;\n    private int srcCount;\n\n    private int line;\n    private int column;\n\n    // txtbuffer\n\n    /** Target buffer for storing incoming text (including aggregated resolved entities) */\n    private char[] txtBuf = new char[128];\n    /** Write position  */\n    private int txtPos;\n\n    // Event-related\n\n    private int type;\n    private boolean isWhitespace;\n    private String namespace;\n    private String prefix;\n    private String name;\n\n    private boolean degenerated;\n    private int attributeCount;\n    private String[] attributes = new String[16];\n//    private int stackMismatch = 0;\n    private String error;\n\n    /** \n     * A separate peek buffer seems simpler than managing\n     * wrap around in the first level read buffer */\n\n    private int[] peek = new int[2];\n    private int peekCount;\n    private boolean wasCR;\n\n    private boolean unresolved;\n    private boolean token;\n\n    public KXmlParser() {\n        srcBuf =\n            new char[Runtime.getRuntime().freeMemory() >= 1048576 ? 8192 : 128];\n    }\n\n    private final boolean isProp(String n1, boolean prop, String n2) {\n        if (!n1.startsWith(\"http://xmlpull.org/v1/doc/\"))\n            return false;\n        if (prop)\n            return n1.substring(42).equals(n2);\n        else\n            return n1.substring(40).equals(n2);\n    }\n\n    private final boolean adjustNsp() throws XmlPullParserException {\n\n        boolean any = false;\n\n        for (int i = 0; i < attributeCount << 2; i += 4) {\n            // * 4 - 4; i >= 0; i -= 4) {\n\n            String attrName = attributes[i + 2];\n            int cut = attrName.indexOf(':');\n            String prefix;\n\n            if (cut != -1) {\n                prefix = attrName.substring(0, cut);\n                attrName = attrName.substring(cut + 1);\n            }\n            else if (attrName.equals(\"xmlns\")) {\n                prefix = attrName;\n                attrName = null;\n            }\n            else\n                continue;\n\n            if (!prefix.equals(\"xmlns\")) {\n                any = true;\n            }\n            else {\n                int j = (nspCounts[depth]++) << 1;\n\n                nspStack = ensureCapacity(nspStack, j + 2);\n                nspStack[j] = attrName;\n                nspStack[j + 1] = attributes[i + 3];\n\n                if (attrName != null && attributes[i + 3].equals(\"\"))\n                    error(\"illegal empty namespace\");\n\n                //  prefixMap = new PrefixMap (prefixMap, attrName, attr.getValue ());\n\n                //System.out.println (prefixMap);\n\n                System.arraycopy(\n                    attributes,\n                    i + 4,\n                    attributes,\n                    i,\n                    ((--attributeCount) << 2) - i);\n\n                i -= 4;\n            }\n        }\n\n        if (any) {\n            for (int i = (attributeCount << 2) - 4; i >= 0; i -= 4) {\n\n                String attrName = attributes[i + 2];\n                int cut = attrName.indexOf(':');\n\n                if (cut == 0 && !relaxed)\n                    throw new RuntimeException(\n                        \"illegal attribute name: \" + attrName + \" at \" + this);\n\n                else if (cut != -1) {\n                    String attrPrefix = attrName.substring(0, cut);\n\n                    attrName = attrName.substring(cut + 1);\n\n                    String attrNs = getNamespace(attrPrefix);\n\n                    if (attrNs == null && !relaxed)\n                        throw new RuntimeException(\n                            \"Undefined Prefix: \" + attrPrefix + \" in \" + this);\n\n                    attributes[i] = attrNs;\n                    attributes[i + 1] = attrPrefix;\n                    attributes[i + 2] = attrName;\n\n                    /*\n                                        if (!relaxed) {\n                                            for (int j = (attributeCount << 2) - 4; j > i; j -= 4)\n                                                if (attrName.equals(attributes[j + 2])\n                                                    && attrNs.equals(attributes[j]))\n                                                    exception(\n                                                        \"Duplicate Attribute: {\"\n                                                            + attrNs\n                                                            + \"}\"\n                                                            + attrName);\n                                        }\n                        */\n                }\n            }\n        }\n\n        int cut = name.indexOf(':');\n\n        if (cut == 0)\n            error(\"illegal tag name: \" + name);\n\n        if (cut != -1) {\n            prefix = name.substring(0, cut);\n            name = name.substring(cut + 1);\n        }\n\n        this.namespace = getNamespace(prefix);\n\n        if (this.namespace == null) {\n            if (prefix != null)\n                error(\"undefined prefix: \" + prefix);\n            this.namespace = NO_NAMESPACE;\n        }\n\n        return any;\n    }\n\n    private final String[] ensureCapacity(String[] arr, int required) {\n        if (arr.length >= required)\n            return arr;\n        String[] bigger = new String[required + 16];\n        System.arraycopy(arr, 0, bigger, 0, arr.length);\n        return bigger;\n    }\n\n    private final void error(String desc) throws XmlPullParserException {\n        if (relaxed) {\n            if (error == null)\n                error = \"ERR: \" + desc;\n        }\n        else\n            exception(desc);\n    }\n\n    private final void exception(String desc) throws XmlPullParserException {\n        throw new XmlPullParserException(\n            desc.length() < 100 ? desc : desc.substring(0, 100) + \"\\n\",\n            this,\n            null);\n    }\n\n    /** \n     * common base for next and nextToken. Clears the state, except from \n     * txtPos and whitespace. Does not set the type variable */\n\n    private final void nextImpl() throws IOException, XmlPullParserException {\n\n        if (reader == null)\n            exception(\"No Input specified\");\n\n        if (type == END_TAG)\n            depth--;\n\n        while (true) {\n            attributeCount = -1;\n\n\t\t\t// degenerated needs to be handled before error because of possible\n\t\t\t// processor expectations(!)\n\n\t\t\tif (degenerated) {\n\t\t\t\tdegenerated = false;\n\t\t\t\ttype = END_TAG;\n\t\t\t\treturn;\n\t\t\t}\n\n\n            if (error != null) {\n                for (int i = 0; i < error.length(); i++)\n                    push(error.charAt(i));\n                //\t\t\t\ttext = error;\n                error = null;\n                type = COMMENT;\n                return;\n            }\n\n\n//            if (relaxed\n//                && (stackMismatch > 0 || (peek(0) == -1 && depth > 0))) {\n//                int sp = (depth - 1) << 2;\n//                type = END_TAG;\n//                namespace = elementStack[sp];\n//                prefix = elementStack[sp + 1];\n//                name = elementStack[sp + 2];\n//                if (stackMismatch != 1)\n//                    error = \"missing end tag /\" + name + \" inserted\";\n//                if (stackMismatch > 0)\n//                    stackMismatch--;\n//                return;\n//            }\n\n            prefix = null;\n            name = null;\n            namespace = null;\n            //            text = null;\n\n            type = peekType();\n\n            switch (type) {\n\n                case ENTITY_REF :\n                    pushEntity();\n                    return;\n\n                case START_TAG :\n                    parseStartTag(false);\n                    return;\n\n                case END_TAG :\n                    parseEndTag();\n                    return;\n\n                case END_DOCUMENT :\n                    return;\n\n                case TEXT :\n                    pushText('<', !token);\n                    if (depth == 0) {\n                        if (isWhitespace)\n                            type = IGNORABLE_WHITESPACE;\n                        // make exception switchable for instances.chg... !!!!\n                        //\telse \n                        //    exception (\"text '\"+getText ()+\"' not allowed outside root element\");\n                    }\n                    return;\n\n                default :\n                    type = parseLegacy(token);\n                    if (type != XML_DECL)\n                        return;\n            }\n        }\n    }\n\n    private final int parseLegacy(boolean push)\n        throws IOException, XmlPullParserException {\n\n        String req = \"\";\n        int term;\n        int result;\n        int prev = 0;\n\n        read(); // <\n        int c = read();\n\n        if (c == '?') {\n            if ((peek(0) == 'x' || peek(0) == 'X')\n                && (peek(1) == 'm' || peek(1) == 'M')) {\n\n                if (push) {\n                    push(peek(0));\n                    push(peek(1));\n                }\n                read();\n                read();\n\n                if ((peek(0) == 'l' || peek(0) == 'L') && peek(1) <= ' ') {\n\n                    if (line != 1 || column > 4)\n                        error(\"PI must not start with xml\");\n\n                    parseStartTag(true);\n\n                    if (attributeCount < 1 || !\"version\".equals(attributes[2]))\n                        error(\"version expected\");\n\n                    version = attributes[3];\n\n                    int pos = 1;\n\n                    if (pos < attributeCount\n                        && \"encoding\".equals(attributes[2 + 4])) {\n                        encoding = attributes[3 + 4];\n                        pos++;\n                    }\n\n                    if (pos < attributeCount\n                        && \"standalone\".equals(attributes[4 * pos + 2])) {\n                        String st = attributes[3 + 4 * pos];\n                        if (\"yes\".equals(st))\n                            standalone = new Boolean(true);\n                        else if (\"no\".equals(st))\n                            standalone = new Boolean(false);\n                        else\n                            error(\"illegal standalone value: \" + st);\n                        pos++;\n                    }\n\n                    if (pos != attributeCount)\n                        error(\"illegal xmldecl\");\n\n                    isWhitespace = true;\n                    txtPos = 0;\n\n                    return XML_DECL;\n                }\n            }\n\n            /*            int c0 = read ();\n                        int c1 = read ();\n                        int */\n\n            term = '?';\n            result = PROCESSING_INSTRUCTION;\n        }\n        else if (c == '!') {\n            if (peek(0) == '-') {\n                result = COMMENT;\n                req = \"--\";\n                term = '-';\n            }\n            else if (peek(0) == '[') {\n                result = CDSECT;\n                req = \"[CDATA[\";\n                term = ']';\n                push = true;\n            }\n            else {\n                result = DOCDECL;\n                req = \"DOCTYPE\";\n                term = -1;\n            }\n        }\n        else {\n            error(\"illegal: <\" + c);\n            return COMMENT;\n        }\n\n        for (int i = 0; i < req.length(); i++)\n            read(req.charAt(i));\n\n        if (result == DOCDECL)\n            parseDoctype(push);\n        else {\n            while (true) {\n                c = read();\n                if (c == -1){\n                    error(UNEXPECTED_EOF);\n                    return COMMENT;\n                }\n\n                if (push)\n                    push(c);\n\n                if ((term == '?' || c == term)\n                    && peek(0) == term\n                    && peek(1) == '>')\n                    break;\n\n                prev = c;\n            }\n\n            if (term == '-' && prev == '-' && !relaxed)\n                error(\"illegal comment delimiter: --->\");\n\n            read();\n            read();\n\n            if (push && term != '?')\n                txtPos--;\n\n        }\n        return result;\n    }\n\n    /** precondition: &lt! consumed */\n\n    private final void parseDoctype(boolean push)\n        throws IOException, XmlPullParserException {\n\n        int nesting = 1;\n        boolean quoted = false;\n\n        // read();\n\n        while (true) {\n            int i = read();\n            switch (i) {\n\n                case -1 :\n                    error(UNEXPECTED_EOF);\n                    return;\n\n                case '\\'' :\n                    quoted = !quoted;\n                    break;\n\n                case '<' :\n                    if (!quoted)\n                        nesting++;\n                    break;\n\n                case '>' :\n                    if (!quoted) {\n                        if ((--nesting) == 0)\n                            return;\n                    }\n                    break;\n            }\n            if (push)\n                push(i);\n        }\n    }\n\n    /* precondition: &lt;/ consumed */\n\n    private final void parseEndTag()\n        throws IOException, XmlPullParserException {\n\n        read(); // '<'\n        read(); // '/'\n        name = readName();\n        skip();\n        read('>');\n\n        int sp = (depth - 1) << 2;\n\n        if (depth == 0) {\n            error(\"element stack empty\");\n            type = COMMENT;\n            return;\n        }\n\n        if (!relaxed) {\n          if (!name.equals(elementStack[sp + 3])) {\n            error(\"expected: /\" + elementStack[sp + 3] + \" read: \" + name);\n\n\t\t\t// become case insensitive in relaxed mode\n\n//            int probe = sp;\n//            while (probe >= 0 && !name.toLowerCase().equals(elementStack[probe + 3].toLowerCase())) {\n//                stackMismatch++;\n//                probe -= 4;\n//            }\n//\n//            if (probe < 0) {\n//                stackMismatch = 0;\n//                //\t\t\ttext = \"unexpected end tag ignored\";\n//                type = COMMENT;\n//                return;\n//            }\n        }\n\n        namespace = elementStack[sp];\n        prefix = elementStack[sp + 1];\n        name = elementStack[sp + 2];\n        }\n    }\n\n    private final int peekType() throws IOException {\n        switch (peek(0)) {\n            case -1 :\n                return END_DOCUMENT;\n            case '&' :\n                return ENTITY_REF;\n            case '<' :\n                switch (peek(1)) {\n                    case '/' :\n                        return END_TAG;\n                    case '?' :\n                    case '!' :\n                        return LEGACY;\n                    default :\n                        return START_TAG;\n                }\n            default :\n                return TEXT;\n        }\n    }\n\n    private final String get(int pos) {\n        return new String(txtBuf, pos, txtPos - pos);\n    }\n\n    /*\n    private final String pop (int pos) {\n    String result = new String (txtBuf, pos, txtPos - pos);\n    txtPos = pos;\n    return result;\n    }\n    */\n\n    private final void push(int c) {\n\n        isWhitespace &= c <= ' ';\n\n        if (txtPos + 1 >= txtBuf.length) { // +1 to have enough space for 2 surrogates, if needed\n            char[] bigger = new char[txtPos * 4 / 3 + 4];\n            System.arraycopy(txtBuf, 0, bigger, 0, txtPos);\n            txtBuf = bigger;\n        }\n\n        if (c > 0xffff) {\n            // write high Unicode value as surrogate pair\n            int offset = c - 0x010000;\n            txtBuf[txtPos++] = (char)((offset >>> 10) + 0xd800); // high surrogate\n            txtBuf[txtPos++] = (char)((offset & 0x3ff) + 0xdc00); // low surrogate\n        } else {\n            txtBuf[txtPos++] = (char) c;\n        }\n    }\n\n    /** Sets name and attributes */\n\n    private final void parseStartTag(boolean xmldecl)\n        throws IOException, XmlPullParserException {\n\n        if (!xmldecl)\n            read();\n        name = readName();\n        attributeCount = 0;\n\n        while (true) {\n            skip();\n\n            int c = peek(0);\n\n            if (xmldecl) {\n                if (c == '?') {\n                    read();\n                    read('>');\n                    return;\n                }\n            }\n            else {\n                if (c == '/') {\n                    degenerated = true;\n                    read();\n                    skip();\n                    read('>');\n                    break;\n                }\n\n                if (c == '>' && !xmldecl) {\n                    read();\n                    break;\n                }\n            }\n\n            if (c == -1) {\n                error(UNEXPECTED_EOF);\n                //type = COMMENT;\n                return;\n            }\n\n            String attrName = readName();\n\n            if (attrName.length() == 0) {\n                error(\"attr name expected\");\n               //type = COMMENT;\n                break;\n            }\n\n            int i = (attributeCount++) << 2;\n\n            attributes = ensureCapacity(attributes, i + 4);\n\n            attributes[i++] = \"\";\n            attributes[i++] = null;\n            attributes[i++] = attrName;\n\n            skip();\n\n            if (peek(0) != '=') {\n            \tif(!relaxed){\n            \t\terror(\"Attr.value missing f. \"+attrName);\n            \t}\n                attributes[i] = attrName;\n            }\n            else {\n                read('=');\n                skip();\n                int delimiter = peek(0);\n\n                if (delimiter != '\\'' && delimiter != '\"') {\n                \tif(!relaxed){\n                \t\terror(\"attr value delimiter missing!\");\n                \t}\n                    delimiter = ' ';\n                }\n\t\t\t\telse \n\t\t\t\t\tread();\n\t\t\t\t\n                int p = txtPos;\n                pushText(delimiter, true);\n\n                attributes[i] = get(p);\n                txtPos = p;\n\n                if (delimiter != ' ')\n                    read(); // skip endquote\n            }\n        }\n\n        int sp = depth++ << 2;\n\n        elementStack = ensureCapacity(elementStack, sp + 4);\n        elementStack[sp + 3] = name;\n\n        if (depth >= nspCounts.length) {\n            int[] bigger = new int[depth + 4];\n            System.arraycopy(nspCounts, 0, bigger, 0, nspCounts.length);\n            nspCounts = bigger;\n        }\n\n        nspCounts[depth] = nspCounts[depth - 1];\n\n        /*\n        \t\tif(!relaxed){\n                for (int i = attributeCount - 1; i > 0; i--) {\n                    for (int j = 0; j < i; j++) {\n                        if (getAttributeName(i).equals(getAttributeName(j)))\n                            exception(\"Duplicate Attribute: \" + getAttributeName(i));\n                    }\n                }\n        \t\t}\n        */\n        if (processNsp)\n            adjustNsp();\n        else\n            namespace = \"\";\n\n        elementStack[sp] = namespace;\n        elementStack[sp + 1] = prefix;\n        elementStack[sp + 2] = name;\n    }\n\n    /** \n     * result: isWhitespace; if the setName parameter is set,\n     * the name of the entity is stored in \"name\" */\n\n    private final void pushEntity()\n        throws IOException, XmlPullParserException {\n\n        push(read()); // &\n        \n        \n        int pos = txtPos;\n\n        while (true) {\n            int c = peek(0);\n            if (c == ';') {\n              read();\n              break;\n            }\n            if (c < 128\n                && (c < '0' || c > '9')\n                && (c < 'a' || c > 'z')\n                && (c < 'A' || c > 'Z')\n                && c != '_'\n                && c != '-'\n                && c != '#') {\n            \tif(!relaxed){\n            \t\terror(\"unterminated entity ref\");\n            \t}\n            \t\n            \tSystem.out.println(\"broken entitiy: \"+get(pos-1));\n            \t\n                //; ends with:\"+(char)c);           \n//                if (c != -1)\n//                    push(c);\n                return;\n            }\n\n            push(read());\n        }\n\n        String code = get(pos);\n        txtPos = pos - 1;\n        if (token && type == ENTITY_REF){\n            name = code;\n        }\n\n        if (code.charAt(0) == '#') {\n            int c =\n                (code.charAt(1) == 'x'\n                    ? Integer.parseInt(code.substring(2), 16)\n                    : Integer.parseInt(code.substring(1)));\n            push(c);\n            return;\n        }\n\n        String result = (String) entityMap.get(code);\n\n        unresolved = result == null;\n\n        if (unresolved) {\n            if (!token)\n                error(\"unresolved: &\" + code + \";\");\n        }\n        else {\n            for (int i = 0; i < result.length(); i++)\n                push(result.charAt(i));\n        }\n    }\n\n    /** types:\n    '<': parse to any token (for nextToken ())\n    '\"': parse to quote\n    ' ': parse to whitespace or '>'\n    */\n\n    private final void pushText(int delimiter, boolean resolveEntities)\n        throws IOException, XmlPullParserException {\n\n        int next = peek(0);\n        int cbrCount = 0;\n\n        while (next != -1 && next != delimiter) { // covers eof, '<', '\"'\n\n            if (delimiter == ' ')\n                if (next <= ' ' || next == '>')\n                    break;\n\n            if (next == '&') {\n                if (!resolveEntities)\n                    break;\n\n                pushEntity();\n            }\n            else if (next == '\\n' && type == START_TAG) {\n                read();\n                push(' ');\n            }\n            else\n                push(read());\n\n            if (next == '>' && cbrCount >= 2 && delimiter != ']')\n                error(\"Illegal: ]]>\");\n\n            if (next == ']')\n                cbrCount++;\n            else\n                cbrCount = 0;\n\n            next = peek(0);\n        }\n    }\n\n    private final void read(char c)\n        throws IOException, XmlPullParserException {\n        int a = read();\n        if (a != c)\n            error(\"expected: '\" + c + \"' actual: '\" + ((char) a) + \"'\");\n    }\n\n    private final int read() throws IOException {\n        int result;\n\n        if (peekCount == 0)\n            result = peek(0);\n        else {\n            result = peek[0];\n            peek[0] = peek[1];\n        }\n        //\t\telse {\n        //\t\t\tresult = peek[0]; \n        //\t\t\tSystem.arraycopy (peek, 1, peek, 0, peekCount-1);\n        //\t\t}\n        peekCount--;\n\n        column++;\n\n        if (result == '\\n') {\n\n            line++;\n            column = 1;\n        }\n\n        return result;\n    }\n\n    /** Does never read more than needed */\n\n    private final int peek(int pos) throws IOException {\n\n        while (pos >= peekCount) {\n\n            int nw;\n\n            if (srcBuf.length <= 1)\n                nw = reader.read();\n            else if (srcPos < srcCount)\n                nw = srcBuf[srcPos++];\n            else {\n                srcCount = reader.read(srcBuf, 0, srcBuf.length);\n                if (srcCount <= 0)\n                    nw = -1;\n                else\n                    nw = srcBuf[0];\n\n                srcPos = 1;\n            }\n\n            if (nw == '\\r') {\n                wasCR = true;\n                peek[peekCount++] = '\\n';\n            }\n            else {\n                if (nw == '\\n') {\n                    if (!wasCR)\n                        peek[peekCount++] = '\\n';\n                }\n                else\n                    peek[peekCount++] = nw;\n\n                wasCR = false;\n            }\n        }\n\n        return peek[pos];\n    }\n\n    private final String readName()\n        throws IOException, XmlPullParserException {\n\n        int pos = txtPos;\n        int c = peek(0);\n        if ((c < 'a' || c > 'z')\n            && (c < 'A' || c > 'Z')\n            && c != '_'\n            && c != ':'\n            && c < 0x0c0\n            && !relaxed)\n            error(\"name expected\");\n\n        do {\n            push(read());\n            c = peek(0);\n        }\n        while ((c >= 'a' && c <= 'z')\n            || (c >= 'A' && c <= 'Z')\n            || (c >= '0' && c <= '9')\n            || c == '_'\n            || c == '-'\n            || c == ':'\n            || c == '.'\n            || c >= 0x0b7);\n\n        String result = get(pos);\n        txtPos = pos;\n        return result;\n    }\n\n    private final void skip() throws IOException {\n\n        while (true) {\n            int c = peek(0);\n            if (c > ' ' || c == -1)\n                break;\n            read();\n        }\n    }\n\n    //  public part starts here...\n\n    public void setInput(Reader reader) throws XmlPullParserException {\n        this.reader = reader;\n\n        line = 1;\n        column = 0;\n        type = START_DOCUMENT;\n        name = null;\n        namespace = null;\n        degenerated = false;\n        attributeCount = -1;\n        encoding = null;\n        version = null;\n        standalone = null;\n\n        if (reader == null)\n            return;\n\n        srcPos = 0;\n        srcCount = 0;\n        peekCount = 0;\n        depth = 0;\n\n        entityMap = new Hashtable();\n        entityMap.put(\"amp\", \"&\");\n        entityMap.put(\"apos\", \"'\");\n        entityMap.put(\"gt\", \">\");\n        entityMap.put(\"lt\", \"<\");\n        entityMap.put(\"quot\", \"\\\"\");\n    }\n\n    public void setInput(InputStream is, String _enc)\n        throws XmlPullParserException {\n\n        srcPos = 0;\n        srcCount = 0;\n        String enc = _enc;\n\n        if (is == null)\n            throw new IllegalArgumentException();\n\n        try {\n\n            if (enc == null) {\n                // read four bytes \n\n                int chk = 0;\n\n                while (srcCount < 4) {\n                    int i = is.read();\n                    if (i == -1)\n                        break;\n                    chk = (chk << 8) | i;\n                    srcBuf[srcCount++] = (char) i;\n                }\n\n                if (srcCount == 4) {\n                    switch (chk) {\n                        case 0x00000FEFF :\n                            enc = \"UTF-32BE\";\n                            srcCount = 0;\n                            break;\n\n                        case 0x0FFFE0000 :\n                            enc = \"UTF-32LE\";\n                            srcCount = 0;\n                            break;\n\n                        case 0x03c :\n                            enc = \"UTF-32BE\";\n                            srcBuf[0] = '<';\n                            srcCount = 1;\n                            break;\n\n                        case 0x03c000000 :\n                            enc = \"UTF-32LE\";\n                            srcBuf[0] = '<';\n                            srcCount = 1;\n                            break;\n\n                        case 0x0003c003f :\n                            enc = \"UTF-16BE\";\n                            srcBuf[0] = '<';\n                            srcBuf[1] = '?';\n                            srcCount = 2;\n                            break;\n\n                        case 0x03c003f00 :\n                            enc = \"UTF-16LE\";\n                            srcBuf[0] = '<';\n                            srcBuf[1] = '?';\n                            srcCount = 2;\n                            break;\n\n                        case 0x03c3f786d :\n                            while (true) {\n                                int i = is.read();\n                                if (i == -1)\n                                    break;\n                                srcBuf[srcCount++] = (char) i;\n                                if (i == '>') {\n                                    String s = new String(srcBuf, 0, srcCount);\n                                    int i0 = s.indexOf(\"encoding\");\n                                    if (i0 != -1) {\n                                        while (s.charAt(i0) != '\"'\n                                            && s.charAt(i0) != '\\'')\n                                            i0++;\n                                        char deli = s.charAt(i0++);\n                                        int i1 = s.indexOf(deli, i0);\n                                        enc = s.substring(i0, i1);\n                                    }\n                                    break;\n                                }\n                            }\n\n                        default :\n                            if ((chk & 0x0ffff0000) == 0x0FEFF0000) {\n                                enc = \"UTF-16BE\";\n                                srcBuf[0] =\n                                    (char) ((srcBuf[2] << 8) | srcBuf[3]);\n                                srcCount = 1;\n                            }\n                            else if ((chk & 0x0ffff0000) == 0x0fffe0000) {\n                                enc = \"UTF-16LE\";\n                                srcBuf[0] =\n                                    (char) ((srcBuf[3] << 8) | srcBuf[2]);\n                                srcCount = 1;\n                            }\n                            else if ((chk & 0x0ffffff00) == 0x0EFBBBF00) {\n                                enc = \"UTF-8\";\n                                srcBuf[0] = srcBuf[3];\n                                srcCount = 1;\n                            }\n                    }\n                }\n            }\n\n            if (enc == null)\n                enc = \"UTF-8\";\n\n            int sc = srcCount;\n            setInput(new InputStreamReader(is, enc));\n            encoding = _enc;\n            srcCount = sc;\n        }\n        catch (Exception e) {\n            throw new XmlPullParserException(\n                \"Invalid stream or encoding: \" + e.toString(),\n                this,\n                e);\n        }\n    }\n\n    public boolean getFeature(String feature) {\n        if (XmlPullParser.FEATURE_PROCESS_NAMESPACES.equals(feature))\n            return processNsp;\n        else if (isProp(feature, false, \"relaxed\"))\n            return relaxed;\n        else\n            return false;\n    }\n\n    public String getInputEncoding() {\n        return encoding;\n    }\n\n    public void defineEntityReplacementText(String entity, String value)\n        throws XmlPullParserException {\n        if (entityMap == null)\n            throw new RuntimeException(\"entity replacement text must be defined after setInput!\");\n        entityMap.put(entity, value);\n    }\n\n    public Object getProperty(String property) {\n        if (isProp(property, true, \"xmldecl-version\"))\n            return version;\n        if (isProp(property, true, \"xmldecl-standalone\"))\n            return standalone;\n\t\tif (isProp(property, true, \"location\"))            \n\t\t\treturn location != null ? location : reader.toString();\n        return null;\n    }\n\n    public int getNamespaceCount(int depth) {\n        if (depth > this.depth)\n            throw new IndexOutOfBoundsException();\n        return nspCounts[depth];\n    }\n\n    public String getNamespacePrefix(int pos) {\n        return nspStack[pos << 1];\n    }\n\n    public String getNamespaceUri(int pos) {\n        return nspStack[(pos << 1) + 1];\n    }\n\n    public String getNamespace(String prefix) {\n\n        if (\"xml\".equals(prefix))\n            return \"http://www.w3.org/XML/1998/namespace\";\n        if (\"xmlns\".equals(prefix))\n            return \"http://www.w3.org/2000/xmlns/\";\n\n        for (int i = (getNamespaceCount(depth) << 1) - 2; i >= 0; i -= 2) {\n            if (prefix == null) {\n                if (nspStack[i] == null)\n                    return nspStack[i + 1];\n            }\n            else if (prefix.equals(nspStack[i]))\n                return nspStack[i + 1];\n        }\n        return null;\n    }\n\n    public int getDepth() {\n        return depth;\n    }\n\n    public String getPositionDescription() {\n\n        StringBuffer buf =\n            new StringBuffer(type < TYPES.length ? TYPES[type] : \"unknown\");\n        buf.append(' ');\n\n        if (type == START_TAG || type == END_TAG) {\n            if (degenerated)\n                buf.append(\"(empty) \");\n            buf.append('<');\n            if (type == END_TAG)\n                buf.append('/');\n\n            if (prefix != null)\n                buf.append(\"{\" + namespace + \"}\" + prefix + \":\");\n            buf.append(name);\n\n            int cnt = attributeCount << 2;\n            for (int i = 0; i < cnt; i += 4) {\n                buf.append(' ');\n                if (attributes[i + 1] != null)\n                    buf.append(\n                        \"{\" + attributes[i] + \"}\" + attributes[i + 1] + \":\");\n                buf.append(attributes[i + 2] + \"='\" + attributes[i + 3] + \"'\");\n            }\n\n            buf.append('>');\n        }\n        else if (type == IGNORABLE_WHITESPACE);\n        else if (type != TEXT)\n            buf.append(getText());\n        else if (isWhitespace)\n            buf.append(\"(whitespace)\");\n        else {\n            String text = getText();\n            if (text.length() > 16)\n                text = text.substring(0, 16) + \"...\";\n            buf.append(text);\n        }\n\n\t\tbuf.append(\"@\"+line + \":\" + column);\n\t\tif(location != null){\n\t\t\tbuf.append(\" in \");\n\t\t\tbuf.append(location);\n\t\t}\n\t\telse if(reader != null){\n\t\t\tbuf.append(\" in \");\n\t\t\tbuf.append(reader.toString());\n\t\t}\n        return buf.toString();\n    }\n\n    public int getLineNumber() {\n        return line;\n    }\n\n    public int getColumnNumber() {\n        return column;\n    }\n\n    public boolean isWhitespace() throws XmlPullParserException {\n        if (type != TEXT && type != IGNORABLE_WHITESPACE && type != CDSECT)\n            exception(ILLEGAL_TYPE);\n        return isWhitespace;\n    }\n\n    public String getText() {\n        return type < TEXT\n            || (type == ENTITY_REF && unresolved) ? null : get(0);\n    }\n\n    public char[] getTextCharacters(int[] poslen) {\n        if (type >= TEXT) {\n            if (type == ENTITY_REF) {\n                poslen[0] = 0;\n                poslen[1] = name.length();\n                return name.toCharArray();\n            }\n            poslen[0] = 0;\n            poslen[1] = txtPos;\n            return txtBuf;\n        }\n\n        poslen[0] = -1;\n        poslen[1] = -1;\n        return null;\n    }\n\n    public String getNamespace() {\n        return namespace;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public String getPrefix() {\n        return prefix;\n    }\n\n    public boolean isEmptyElementTag() throws XmlPullParserException {\n        if (type != START_TAG)\n            exception(ILLEGAL_TYPE);\n        return degenerated;\n    }\n\n    public int getAttributeCount() {\n        return attributeCount;\n    }\n\n    public String getAttributeType(int index) {\n        return \"CDATA\";\n    }\n\n    public boolean isAttributeDefault(int index) {\n        return false;\n    }\n\n    public String getAttributeNamespace(int index) {\n        if (index >= attributeCount)\n            throw new IndexOutOfBoundsException();\n        return attributes[index << 2];\n    }\n\n    public String getAttributeName(int index) {\n        if (index >= attributeCount)\n            throw new IndexOutOfBoundsException();\n        return attributes[(index << 2) + 2];\n    }\n\n    public String getAttributePrefix(int index) {\n        if (index >= attributeCount)\n            throw new IndexOutOfBoundsException();\n        return attributes[(index << 2) + 1];\n    }\n\n    public String getAttributeValue(int index) {\n        if (index >= attributeCount)\n            throw new IndexOutOfBoundsException();\n        return attributes[(index << 2) + 3];\n    }\n\n    public String getAttributeValue(String namespace, String name) {\n\n        for (int i = (attributeCount << 2) - 4; i >= 0; i -= 4) {\n            if (attributes[i + 2].equals(name)\n                && (namespace == null || attributes[i].equals(namespace)))\n                return attributes[i + 3];\n        }\n\n        return null;\n    }\n\n    public int getEventType() throws XmlPullParserException {\n        return type;\n    }\n\n    public int next() throws XmlPullParserException, IOException {\n\n        txtPos = 0;\n        isWhitespace = true;\n        int minType = 9999;\n        token = false;\n\n        do {\n            nextImpl();\n            if (type < minType)\n                minType = type;\n            //\t    if (curr <= TEXT) type = curr; \n        }\n        while (minType > ENTITY_REF // ignorable\n            || (minType >= TEXT && peekType() >= TEXT));\n\n        type = minType;\n        if (type > TEXT)\n            type = TEXT;\n\n        return type;\n    }\n\n    public int nextToken() throws XmlPullParserException, IOException {\n\n        isWhitespace = true;\n        txtPos = 0;\n\n        token = true;\n        nextImpl();\n        return type;\n    }\n\n    //\n    // utility methods to make XML parsing easier ...\n\n    public int nextTag() throws XmlPullParserException, IOException {\n\n        next();\n        if (type == TEXT && isWhitespace)\n            next();\n\n        if (type != END_TAG && type != START_TAG)\n            exception(\"unexpected type\");\n\n        return type;\n    }\n\n    public void require(int type, String namespace, String name)\n        throws XmlPullParserException, IOException {\n\n        if (type != this.type\n            || (namespace != null && !namespace.equals(getNamespace()))\n            || (name != null && !name.equals(getName())))\n            exception(\n                \"expected: \" + TYPES[type] + \" {\" + namespace + \"}\" + name);\n    }\n\n    public String nextText() throws XmlPullParserException, IOException {\n        if (type != START_TAG)\n            exception(\"precondition: START_TAG\");\n\n        next();\n\n        String result;\n\n        if (type == TEXT) {\n            result = getText();\n            next();\n        }\n        else\n            result = \"\";\n\n        if (type != END_TAG)\n            exception(\"END_TAG expected\");\n\n        return result;\n    }\n\n    public void setFeature(String feature, boolean value)\n        throws XmlPullParserException {\n        if (XmlPullParser.FEATURE_PROCESS_NAMESPACES.equals(feature))\n            processNsp = value;\n        else if (isProp(feature, false, \"relaxed\"))\n            relaxed = value;\n        else\n            exception(\"unsupported feature: \" + feature);\n    }\n\n    public void setProperty(String property, Object value)\n        throws XmlPullParserException {\n        if(isProp(property, true, \"location\"))\n        \tlocation = value;\n        else\n\t        throw new XmlPullParserException(\"unsupported property: \" + property);\n    }\n\n    /**\n      * Skip sub tree that is currently porser positioned on.\n      * <br>NOTE: parser must be on START_TAG and when funtion returns\n      * parser will be positioned on corresponding END_TAG. \n      */\n\n    //\tImplementation copied from Alek's mail... \n\n    public void skipSubTree() throws XmlPullParserException, IOException {\n        require(START_TAG, null, null);\n        int level = 1;\n        while (level > 0) {\n            int eventType = next();\n            if (eventType == END_TAG) {\n                --level;\n            }\n            else if (eventType == START_TAG) {\n                ++level;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/kxml2/io/KXmlSerializer.java",
    "content": "/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The  above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE. */\n \n\npackage org.kxml2.io;\n\nimport java.io.*;\nimport org.xmlpull.v1.*;\n\npublic class KXmlSerializer implements XmlSerializer {\n\n    //    static final String UNDEFINED = \":\";\n\n    private Writer writer;\n\n    private boolean pending;\n    private int auto;\n    private int depth;\n\n    private String[] elementStack = new String[12];\n    //nsp/prefix/name\n    private int[] nspCounts = new int[4];\n    private String[] nspStack = new String[8];\n    //prefix/nsp; both empty are \"\"\n    private boolean[] indent = new boolean[4];\n    private boolean unicode;\n    private String encoding;\n\n    private final void check(boolean close) throws IOException {\n        if (!pending)\n            return;\n\n        depth++;\n        pending = false;\n\n        if (indent.length <= depth) {\n            boolean[] hlp = new boolean[depth + 4];\n            System.arraycopy(indent, 0, hlp, 0, depth);\n            indent = hlp;\n        }\n        indent[depth] = indent[depth - 1];\n\n        for (int i = nspCounts[depth - 1];\n            i < nspCounts[depth];\n            i++) {\n            writer.write(' ');\n            writer.write(\"xmlns\");\n            if (!\"\".equals(nspStack[i * 2])) {\n                writer.write(':');\n                writer.write(nspStack[i * 2]);\n            }\n            else if (\"\".equals(getNamespace()) && !\"\".equals(nspStack[i * 2 + 1]))\n                throw new IllegalStateException(\"Cannot set default namespace for elements in no namespace\");\n            writer.write(\"=\\\"\");\n            writeEscaped(nspStack[i * 2 + 1], '\"');\n            writer.write('\"');\n        }\n\n        if (nspCounts.length <= depth + 1) {\n            int[] hlp = new int[depth + 8];\n            System.arraycopy(nspCounts, 0, hlp, 0, depth + 1);\n            nspCounts = hlp;\n        }\n\n        nspCounts[depth + 1] = nspCounts[depth];\n        //   nspCounts[depth + 2] = nspCounts[depth];\n\n        writer.write(close ? \" />\" : \">\");\n    }\n\n    private final void writeEscaped(String s, int quot)\n        throws IOException {\n\n        for (int i = 0; i < s.length(); i++) {\n            char c = s.charAt(i);\n            switch (c) {\n            \tcase '\\n':\n            \tcase '\\r':\n            \tcase '\\t':\n            \t\tif(quot == -1) \n            \t\t\twriter.write(c);\n            \t\telse \n            \t\t\twriter.write(\"&#\"+((int) c)+';');\n            \t\tbreak;\n                case '&' :\n                    writer.write(\"&amp;\");\n                    break;\n                case '>' :\n                    writer.write(\"&gt;\");\n                    break;\n                case '<' :\n                    writer.write(\"&lt;\");\n                    break;\n                case '\"' :\n                case '\\'' :\n                    if (c == quot) {\n                        writer.write(\n                            c == '\"' ? \"&quot;\" : \"&apos;\");\n                        break;\n                    }\n                default :\n                \t//if(c < ' ')\n\t\t\t\t\t//\tthrow new IllegalArgumentException(\"Illegal control code:\"+((int) c));\n\n                    if (i < s.length() - 1) {\n                        char cLow = s.charAt(i + 1);\n                        // c is high surrogate and cLow is low surrogate\n                        if (c >= 0xd800 && c <= 0xdbff && cLow >= 0xdc00 && cLow <= 0xdfff) {\n                            // write surrogate pair as single code point\n                            int n = ((c - 0xd800) << 10) + (cLow - 0xdc00) + 0x010000;\n                            writer.write(\"&#\" + n + \";\");\n                            i++; // Skip the low surrogate\n                            break;\n                        }\n                        // Does nothing smart about orphan surrogates, just output them \"as is\"\n                    }\n                    if (c >= ' ' && c !='@' && (c < 127 || unicode)) {\n                        writer.write(c);\n                    } else {\n                        writer.write(\"&#\" + ((int) c) + \";\");\n                    }\n            }\n        }\n    }\n\n    /*\n    \tprivate final void writeIndent() throws IOException {\n    \t\twriter.write(\"\\r\\n\");\n    \t\tfor (int i = 0; i < depth; i++)\n    \t\t\twriter.write(' ');\n    \t}*/\n\n    public void docdecl(String dd) throws IOException {\n        writer.write(\"<!DOCTYPE\");\n        writer.write(dd);\n        writer.write(\">\");\n    }\n\n    public void endDocument() throws IOException {\n        while (depth > 0) {\n            endTag(\n                elementStack[depth * 3 - 3],\n                elementStack[depth * 3 - 1]);\n        }\n        flush();\n    }\n\n    public void entityRef(String name) throws IOException {\n        check(false);\n        writer.write('&');\n        writer.write(name);\n        writer.write(';');\n    }\n\n    public boolean getFeature(String name) {\n        //return false;\n        return (\n            \"http://xmlpull.org/v1/doc/features.html#indent-output\"\n                .equals(\n                name))\n            ? indent[depth]\n            : false;\n    }\n\n    public String getPrefix(String namespace, boolean create) {\n        try {\n            return getPrefix(namespace, false, create);\n        }\n        catch (IOException e) {\n            throw new RuntimeException(e.toString());\n        }\n    }\n\n    private final String getPrefix(\n        String namespace,\n        boolean includeDefault,\n        boolean create)\n        throws IOException {\n\n        for (int i = nspCounts[depth + 1] * 2 - 2;\n            i >= 0;\n            i -= 2) {\n            if (nspStack[i + 1].equals(namespace)\n                && (includeDefault || !nspStack[i].equals(\"\"))) {\n                String cand = nspStack[i];\n                for (int j = i + 2;\n                    j < nspCounts[depth + 1] * 2;\n                    j++) {\n                    if (nspStack[j].equals(cand)) {\n                        cand = null;\n                        break;\n                    }\n                }\n                if (cand != null)\n                    return cand;\n            }\n        }\n\n        if (!create)\n            return null;\n\n        String prefix;\n\n        if (\"\".equals(namespace))\n            prefix = \"\";\n        else {\n            do {\n                prefix = \"n\" + (auto++);\n                for (int i = nspCounts[depth + 1] * 2 - 2;\n                    i >= 0;\n                    i -= 2) {\n                    if (prefix.equals(nspStack[i])) {\n                        prefix = null;\n                        break;\n                    }\n                }\n            }\n            while (prefix == null);\n        }\n\n\t\tboolean p = pending;\n\t\tpending = false;\n        setPrefix(prefix, namespace);\n        pending = p;\n        return prefix;\n    }\n\n    public Object getProperty(String name) {\n        throw new RuntimeException(\"Unsupported property\");\n    }\n\n    public void ignorableWhitespace(String s)\n        throws IOException {\n        text(s);\n    }\n\n    public void setFeature(String name, boolean value) {\n        if (\"http://xmlpull.org/v1/doc/features.html#indent-output\"\n            .equals(name)) {\n            indent[depth] = value;\n        }\n        else\n            throw new RuntimeException(\"Unsupported Feature\");\n    }\n\n    public void setProperty(String name, Object value) {\n        throw new RuntimeException(\n            \"Unsupported Property:\" + value);\n    }\n\n    public void setPrefix(String prefix, String namespace)\n        throws IOException {\n\n        check(false);\n        if (prefix == null)\n            prefix = \"\";\n        if (namespace == null)\n            namespace = \"\";\n\n        String defined = getPrefix(namespace, true, false);\n\n        // boil out if already defined\n\n        if (prefix.equals(defined))\n            return;\n\n        int pos = (nspCounts[depth + 1]++) << 1;\n\n        if (nspStack.length < pos + 1) {\n            String[] hlp = new String[nspStack.length + 16];\n            System.arraycopy(nspStack, 0, hlp, 0, pos);\n            nspStack = hlp;\n        }\n\n        nspStack[pos++] = prefix;\n        nspStack[pos] = namespace;\n    }\n\n    public void setOutput(Writer writer) {\n        this.writer = writer;\n\n        // elementStack = new String[12]; //nsp/prefix/name\n        //nspCounts = new int[4];\n        //nspStack = new String[8]; //prefix/nsp\n        //indent = new boolean[4];\n\n        nspCounts[0] = 2;\n        nspCounts[1] = 2;\n        nspStack[0] = \"\";\n        nspStack[1] = \"\";\n        nspStack[2] = \"xml\";\n        nspStack[3] = \"http://www.w3.org/XML/1998/namespace\";\n        pending = false;\n        auto = 0;\n        depth = 0;\n\n        unicode = false;\n    }\n\n    public void setOutput(OutputStream os, String encoding)\n        throws IOException {\n        if (os == null)\n            throw new IllegalArgumentException();\n        setOutput(\n            encoding == null\n                ? new OutputStreamWriter(os)\n                : new OutputStreamWriter(os, encoding));\n        this.encoding = encoding;\n        if (encoding != null\n            && encoding.toLowerCase().startsWith(\"utf\"))\n            unicode = true;\n    }\n\n    public void startDocument(\n        String encoding,\n        Boolean standalone)\n        throws IOException {\n        writer.write(\"<?xml version='1.0' \");\n\n        if (encoding != null) {\n            this.encoding = encoding;\n            if (encoding.toLowerCase().startsWith(\"utf\"))\n                unicode = true;\n        }\n\n        if (this.encoding != null) {\n            writer.write(\"encoding='\");\n            writer.write(this.encoding);\n            writer.write(\"' \");\n        }\n\n        if (standalone != null) {\n            writer.write(\"standalone='\");\n            writer.write(\n                standalone.booleanValue() ? \"yes\" : \"no\");\n            writer.write(\"' \");\n        }\n        writer.write(\"?>\");\n    }\n\n    public XmlSerializer startTag(String namespace, String name)\n        throws IOException {\n        check(false);\n\n        //        if (namespace == null)\n        //            namespace = \"\";\n\n        if (indent[depth]) {\n            writer.write(\"\\r\\n\");\n            for (int i = 0; i < depth; i++)\n                writer.write(\"  \");\n        }\n\n        int esp = depth * 3;\n\n        if (elementStack.length < esp + 3) {\n            String[] hlp = new String[elementStack.length + 12];\n            System.arraycopy(elementStack, 0, hlp, 0, esp);\n            elementStack = hlp;\n        }\n\n        String prefix =\n            namespace == null\n                ? \"\"\n                : getPrefix(namespace, true, true);\n\n        if (\"\".equals(namespace)) {\n            for (int i = nspCounts[depth];\n                i < nspCounts[depth + 1];\n                i++) {\n                if (\"\".equals(nspStack[i * 2]) && !\"\".equals(nspStack[i * 2 + 1])) {\n                    throw new IllegalStateException(\"Cannot set default namespace for elements in no namespace\");\n                }\n            }\n        }\n\n        elementStack[esp++] = namespace;\n        elementStack[esp++] = prefix;\n        elementStack[esp] = name;\n\n        writer.write('<');\n        if (!\"\".equals(prefix)) {\n            writer.write(prefix);\n            writer.write(':');\n        }\n\n        writer.write(name);\n\n        pending = true;\n\n        return this;\n    }\n\n    public XmlSerializer attribute(\n        String namespace,\n        String name,\n        String value)\n        throws IOException {\n        if (!pending)\n            throw new IllegalStateException(\"illegal position for attribute\");\n\n        //        int cnt = nspCounts[depth];\n\n        if (namespace == null)\n            namespace = \"\";\n\n        //\t\tdepth--;\n        //\t\tpending = false;\n\n        String prefix =\n            \"\".equals(namespace)\n                ? \"\"\n                : getPrefix(namespace, false, true);\n\n        //\t\tpending = true;\n        //\t\tdepth++;\n\n        /*        if (cnt != nspCounts[depth]) {\n                    writer.write(' ');\n                    writer.write(\"xmlns\");\n                    if (nspStack[cnt * 2] != null) {\n                        writer.write(':');\n                        writer.write(nspStack[cnt * 2]);\n                    }\n                    writer.write(\"=\\\"\");\n                    writeEscaped(nspStack[cnt * 2 + 1], '\"');\n                    writer.write('\"');\n                }\n                */\n\n        writer.write(' ');\n        if (!\"\".equals(prefix)) {\n            writer.write(prefix);\n            writer.write(':');\n        }\n        writer.write(name);\n        writer.write('=');\n        char q = value.indexOf('\"') == -1 ? '\"' : '\\'';\n        writer.write(q);\n        writeEscaped(value, q);\n        writer.write(q);\n\n        return this;\n    }\n\n    public void flush() throws IOException {\n        check(false);\n        writer.flush();\n    }\n    /*\n    \tpublic void close() throws IOException {\n    \t\tcheck();\n    \t\twriter.close();\n    \t}\n    */\n    public XmlSerializer endTag(String namespace, String name)\n        throws IOException {\n\n        if (!pending)\n            depth--;\n        //        if (namespace == null)\n        //          namespace = \"\";\n\n        if ((namespace == null\n            && elementStack[depth * 3] != null)\n            || (namespace != null\n                && !namespace.equals(elementStack[depth * 3]))\n            || !elementStack[depth * 3 + 2].equals(name))\n            throw new IllegalArgumentException(\"</{\"+namespace+\"}\"+name+\"> does not match start\");\n\n        if (pending) {\n            check(true);\n            depth--;\n        }\n        else {\n            if (indent[depth + 1]) {\n                writer.write(\"\\r\\n\");\n                for (int i = 0; i < depth; i++)\n                    writer.write(\"  \");\n            }\n\n            writer.write(\"</\");\n            String prefix = elementStack[depth * 3 + 1];\n            if (!\"\".equals(prefix)) {\n                writer.write(prefix);\n                writer.write(':');\n            }\n            writer.write(name);\n            writer.write('>');\n        }\n\n        nspCounts[depth + 1] = nspCounts[depth];\n        return this;\n    }\n\n    public String getNamespace() {\n        return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 3];\n    }\n\n    public String getName() {\n        return getDepth() == 0 ? null : elementStack[getDepth() * 3 - 1];\n    }\n\n    public int getDepth() {\n        return pending ? depth + 1 : depth;\n    }\n\n    public XmlSerializer text(String text) throws IOException {\n        check(false);\n        indent[depth] = false;\n        writeEscaped(text, -1);\n        return this;\n    }\n\n    public XmlSerializer text(char[] text, int start, int len)\n        throws IOException {\n        text(new String(text, start, len));\n        return this;\n    }\n\n    public void cdsect(String data) throws IOException {\n        check(false);\n        writer.write(\"<![CDATA[\");\n        writer.write(data);\n        writer.write(\"]]>\");\n    }\n\n    public void comment(String comment) throws IOException {\n        check(false);\n        writer.write(\"<!--\");\n        writer.write(comment);\n        writer.write(\"-->\");\n    }\n\n    public void processingInstruction(String pi)\n        throws IOException {\n        check(false);\n        writer.write(\"<?\");\n        writer.write(pi);\n        writer.write(\"?>\");\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/kxml2/kdom/Document.java",
    "content": "/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The  above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE. */\n \n\npackage org.kxml2.kdom;\n\nimport java.io.*;\n\nimport org.xmlpull.v1.*;\n/** The document consists of some legacy events and a single root\n    element. This class basically adds some consistency checks to\n    Node. */\n\npublic class Document extends Node {\n\n    protected int rootIndex = -1;\n    String encoding;\n    Boolean standalone;\n\n    /** returns \"#document\" */\n\n    public String getEncoding () {\n        return encoding;\n    }\n    \n    public void setEncoding(String enc) {\n        this.encoding = enc;\n    }\n    \n    public void setStandalone (Boolean standalone) {\n        this.standalone = standalone;\n    }\n    \n    public Boolean getStandalone() {\n        return standalone;\n    }\n\n\n    public String getName() {\n        return \"#document\";\n    }\n\n    /** Adds a child at the given index position. Throws\n    an exception when a second root element is added */\n\n    public void addChild(int index, int type, Object child) {\n        if (type == ELEMENT) {\n         //   if (rootIndex != -1)\n           //     throw new RuntimeException(\"Only one document root element allowed\");\n\n            rootIndex = index;\n        }\n        else if (rootIndex >= index)\n            rootIndex++;\n\n        super.addChild(index, type, child);\n    }\n\n    /** reads the document and checks if the last event\n    is END_DOCUMENT. If not, an exception is thrown.\n    The end event is consumed. For parsing partial\n        XML structures, consider using Node.parse (). */\n\n    public void parse(XmlPullParser parser)\n        throws IOException, XmlPullParserException {\n\n\t\tparser.require(XmlPullParser.START_DOCUMENT, null, null);\n\t\tparser.nextToken ();        \t\n\n        encoding = parser.getInputEncoding();\n        standalone = (Boolean)parser.getProperty (\"http://xmlpull.org/v1/doc/properties.html#xmldecl-standalone\");\n        \n        super.parse(parser);\n\n        if (parser.getEventType() != XmlPullParser.END_DOCUMENT)\n            throw new RuntimeException(\"Document end expected!\");\n\n    }\n\n    public void removeChild(int index) {\n        if (index == rootIndex)\n            rootIndex = -1;\n        else if (index < rootIndex)\n            rootIndex--;\n\n        super.removeChild(index);\n    }\n\n    /** returns the root element of this document. */\n\n    public Element getRootElement() {\n        if (rootIndex == -1)\n            throw new RuntimeException(\"Document has no root element!\");\n\n        return (Element) getChild(rootIndex);\n    }\n    \n    \n    /** Writes this node to the given XmlWriter. For node and document,\n        this method is identical to writeChildren, except that the\n        stream is flushed automatically. */\n\n    public void write(XmlSerializer writer)\n        throws IOException {\n        \n        writer.startDocument(encoding, standalone);\n        writeChildren(writer);\n        writer.endDocument();\n    }\n    \n    \n}"
  },
  {
    "path": "src/main/java/org/kxml2/kdom/Element.java",
    "content": "/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The  above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE. */\n\npackage org.kxml2.kdom;\n\nimport java.io.*;\nimport java.util.*;\n\nimport org.xmlpull.v1.*;\n\n/** \n * In order to create an element, please use the createElement method\n * instead of invoking the constructor directly. The right place to\n * add user defined initialization code is the init method. */\n\npublic class Element extends Node {\n\n    protected String namespace;\n    protected String name;\n    protected Vector attributes;\n    protected Node parent;\n    protected Vector prefixes;\n\n    public Element() {\n    }\n\n    /** \n     * called when all properties are set, but before children\n     * are parsed. Please do not use setParent for initialization\n     * code any longer. */\n\n    public void init() {\n    }\n\n\n\n\n    /** \n     * removes all children and attributes */\n\n    public void clear() {\n        attributes = null;\n        children = null;\n    }\n\n    /** \n     * Forwards creation request to parent if any, otherwise\n     * calls super.createElement. */\n\n    public Element createElement(\n        String namespace,\n        String name) { \n\n        return (this.parent == null)\n            ? super.createElement(namespace, name)\n            : this.parent.createElement(namespace, name);\n    }\n\n    /** \n     * Returns the number of attributes of this element. */\n\n    public int getAttributeCount() {\n        return attributes == null ? 0 : attributes.size ();\n    }\n\n\tpublic String getAttributeNamespace (int index) {\n\t\treturn ((String []) attributes.elementAt (index)) [0];\n\t}\n\n/*\tpublic String getAttributePrefix (int index) {\n\t\treturn ((String []) attributes.elementAt (index)) [1];\n\t}*/\n\t\n\tpublic String getAttributeName (int index) {\n\t\treturn ((String []) attributes.elementAt (index)) [1];\n\t}\n\t\n\n\tpublic String getAttributeValue (int index) {\n\t\treturn ((String []) attributes.elementAt (index)) [2];\n\t}\n\t\n\t\n\tpublic String getAttributeValue (String namespace, String name) {\n\t\tfor (int i = 0; i < getAttributeCount (); i++) {\n\t\t\tif (name.equals (getAttributeName (i)) \n\t\t\t\t&& (namespace == null || namespace.equals (getAttributeNamespace(i)))) {\n\t\t\t\treturn getAttributeValue (i);\n\t\t\t}\n\t\t}\t\t\t\t\t\t\n\t\treturn null;\t\t\t\n\t}\n\n    /** \n     * Returns the root node, determined by ascending to the \n     * all parents un of the root element. */\n\n    public Node getRoot() {\n\n        Element current = this;\n        \n        while (current.parent != null) {\n            if (!(current.parent instanceof Element)) return current.parent;\n            current = (Element) current.parent;\n        }\n        \n        return current;\n    }\n\n    /** \n     * returns the (local) name of the element */\n\n    public String getName() {\n        return name;\n    }\n\n    /** \n     * returns the namespace of the element */\n\n    public String getNamespace() {\n        return namespace;\n    }\n\n\n    /** \n     * returns the namespace for the given prefix */\n    \n    public String getNamespaceUri (String prefix) {\n    \tint cnt = getNamespaceCount ();\n\t\tfor (int i = 0; i < cnt; i++) {\n\t\t\tif (prefix == getNamespacePrefix (i) ||\n\t\t\t\t(prefix != null && prefix.equals (getNamespacePrefix (i))))\n\t\t\t\treturn getNamespaceUri (i);\t\n\t\t}\n\t\treturn parent instanceof Element ? ((Element) parent).getNamespaceUri (prefix) : null;\n    }\n\n\n\t/** \n     * returns the number of declared namespaces, NOT including\n\t * parent elements */\n\n\tpublic int getNamespaceCount () {\n\t\treturn (prefixes == null ? 0 : prefixes.size ());\n\t}\n\n\n\tpublic String getNamespacePrefix (int i) {\n\t\treturn ((String []) prefixes.elementAt (i)) [0];\n\t}\n\n\tpublic String getNamespaceUri (int i) {\n\t\treturn ((String []) prefixes.elementAt (i)) [1];\n\t}\n\n\n    /** \n     * Returns the parent node of this element */\n\n    public Node getParent() {\n        return parent;\n    }\n\n    /* \n     * Returns the parent element if available, null otherwise \n\n    public Element getParentElement() {\n        return (parent instanceof Element)\n            ? ((Element) parent)\n            : null;\n    }\n*/\n\n    /** \n     * Builds the child elements from the given Parser. By overwriting \n     * parse, an element can take complete control over parsing its \n     * subtree. */\n\n    public void parse(XmlPullParser parser)\n        throws IOException, XmlPullParserException {\n\n        for (int i = parser.getNamespaceCount (parser.getDepth () - 1);\n        \ti < parser.getNamespaceCount (parser.getDepth ()); i++) {\n        \tsetPrefix (parser.getNamespacePrefix (i), parser.getNamespaceUri(i));\n        }\n        \n        \n        for (int i = 0; i < parser.getAttributeCount (); i++) \n\t        setAttribute (parser.getAttributeNamespace (i),\n//\t        \t\t\t  parser.getAttributePrefix (i),\n\t        \t\t\t  parser.getAttributeName (i),\n\t        \t\t\t  parser.getAttributeValue (i));\n\n\n        //        if (prefixMap == null) throw new RuntimeException (\"!!\");\n\n        init();\n\n\n\t\tif (parser.isEmptyElementTag()) \n\t\t\tparser.nextToken ();\n\t\telse {\n\t\t\tparser.nextToken ();\n\t        super.parse(parser);\n\n        \tif (getChildCount() == 0)\n            \taddChild(IGNORABLE_WHITESPACE, \"\");\n\t\t}\n\t\t\n        parser.require(\n            XmlPullParser.END_TAG,\n            getNamespace(),\n            getName());\n            \n        parser.nextToken ();\n    }\n\n\n    /** \n     * Sets the given attribute; a value of null removes the attribute */\n\n\tpublic void setAttribute (String namespace, String name, String value) {\n\t\tif (attributes == null) \n\t\t\tattributes = new Vector ();\n\n\t\tif (namespace == null) \n\t\t\tnamespace = \"\";\n\t\t\n        for (int i = attributes.size()-1; i >=0; i--){\n            String[] attribut = (String[]) attributes.elementAt(i);\n            if (attribut[0].equals(namespace) &&\n\t\t\t\tattribut[1].equals(name)){\n\t\t\t\t\t\n\t\t\t\tif (value == null) {\n\t                attributes.removeElementAt(i);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tattribut[2] = value;\n\t\t\t\t}\n\t            return; \n\t\t\t}\n        }\n\n\t\tattributes.addElement \n\t\t\t(new String [] {namespace, name, value});\n\t}\n\n\n\t/** \n     * Sets the given prefix; a namespace value of null removess the \n\t * prefix */\n\n\tpublic void setPrefix (String prefix, String namespace) {\n\t\tif (prefixes == null) prefixes = new Vector ();\n\t\tprefixes.addElement (new String [] {prefix, namespace});\t\t\n\t}\n\n\n    /** \n     * sets the name of the element */\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    /** \n     * sets the namespace of the element. Please note: For no\n     * namespace, please use Xml.NO_NAMESPACE, null is not a legal\n     * value. Currently, null is converted to Xml.NO_NAMESPACE, but\n     * future versions may throw an exception. */\n\n    public void setNamespace(String namespace) {\n        if (namespace == null) \n        \tthrow new NullPointerException (\"Use \\\"\\\" for empty namespace\");\n        this.namespace = namespace;\n    }\n\n    /** \n     * Sets the Parent of this element. Automatically called from the\n     * add method.  Please use with care, you can simply\n     * create inconsitencies in the document tree structure using\n     * this method!  */\n\n    protected void setParent(Node parent) {\n        this.parent = parent;\n    }\n\n\n    /** \n     * Writes this element and all children to the given XmlWriter. */\n\n    public void write(XmlSerializer writer)\n        throws IOException {\n\n\t\tif (prefixes != null) {\n\t\t\tfor (int i = 0; i < prefixes.size (); i++) {\n\t\t\t\twriter.setPrefix (getNamespacePrefix (i), getNamespaceUri (i));\n\t\t\t}\n\t\t}\n\n        writer.startTag(\n            getNamespace(),\n            getName());\n\n        int len = getAttributeCount();\n\n        for (int i = 0; i < len; i++) {\n            writer.attribute(\n                getAttributeNamespace(i),\n                getAttributeName(i),\n                getAttributeValue(i));\n        }\n\n        writeChildren(writer);\n\n        writer.endTag(getNamespace (), getName ());\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/kxml2/kdom/Node.java",
    "content": "/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The  above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE. */\n\npackage org.kxml2.kdom;\n\nimport java.util.*;\nimport java.io.*;\nimport org.xmlpull.v1.*;\n/** A common base class for Document and Element, also used for\n    storing XML fragments. */\n\npublic class Node { //implements XmlIO{\n\n    public static final int DOCUMENT = 0;\n    public static final int ELEMENT = 2;\n    public static final int TEXT = 4;\n    public static final int CDSECT = 5;\n    public static final int ENTITY_REF = 6;\n    public static final int IGNORABLE_WHITESPACE = 7;\n    public static final int PROCESSING_INSTRUCTION = 8;\n    public static final int COMMENT = 9;\n    public static final int DOCDECL = 10;\n\n    protected Vector children;\n    protected StringBuffer types;\n\n    /** inserts the given child object of the given type at the\n    given index. */\n\n    public void addChild(int index, int type, Object child) {\n\n        if (child == null)\n            throw new NullPointerException();\n\n        if (children == null) {\n            children = new Vector();\n            types = new StringBuffer();\n        }\n\n        if (type == ELEMENT) {\n            if (!(child instanceof Element))\n                throw new RuntimeException(\"Element obj expected)\");\n\n            ((Element) child).setParent(this);\n        }\n        else if (!(child instanceof String))\n            throw new RuntimeException(\"String expected\");\n\n        children.insertElementAt(child, index);\n        types.insert(index, (char) type);\n    }\n\n    /** convenience method for addChild (getChildCount (), child) */\n\n    public void addChild(int type, Object child) {\n        addChild(getChildCount(), type, child);\n    }\n\n    /** Builds a default element with the given properties. Elements\n    should always be created using this method instead of the\n    constructor in order to enable construction of specialized\n    subclasses by deriving custom Document classes. Please note:\n    For no namespace, please use Xml.NO_NAMESPACE, null is not a\n    legal value. Currently, null is converted to Xml.NO_NAMESPACE,\n    but future versions may throw an exception. */\n\n    public Element createElement(String namespace, String name) {\n\n        Element e = new Element();\n        e.namespace = namespace == null ? \"\" : namespace;\n        e.name = name;\n        return e;\n    }\n\n    /** Returns the child object at the given index.  For child\n        elements, an Element object is returned. For all other child\n        types, a String is returned. */\n\n    public Object getChild(int index) {\n        return children.elementAt(index);\n    }\n\n    /** Returns the number of child objects */\n\n    public int getChildCount() {\n        return children == null ? 0 : children.size();\n    }\n\n    /** returns the element at the given index. If the node at the\n    given index is a text node, null is returned */\n\n    public Element getElement(int index) {\n        Object child = getChild(index);\n        return (child instanceof Element) ? (Element) child : null;\n    }\n\n    /** Returns the element with the given namespace and name. If the\n        element is not found, or more than one matching elements are\n        found, an exception is thrown. */\n\n    public Element getElement(String namespace, String name) {\n\n        int i = indexOf(namespace, name, 0);\n        int j = indexOf(namespace, name, i + 1);\n\n        if (i == -1 || j != -1)\n            throw new RuntimeException(\n                \"Element {\"\n                    + namespace\n                    + \"}\"\n                    + name\n                    + (i == -1 ? \" not found in \" : \" more than once in \")\n                    + this);\n\n        return getElement(i);\n    }\n\n    /* returns \"#document-fragment\". For elements, the element name is returned \n    \n    public String getName() {\n        return \"#document-fragment\";\n    }\n    \n    /** Returns the namespace of the current element. For Node\n        and Document, Xml.NO_NAMESPACE is returned. \n    \n    public String getNamespace() {\n        return \"\";\n    }\n    \n    public int getNamespaceCount () {\n    \treturn 0;\n    }\n    \n    /** returns the text content if the element has text-only\n    content. Throws an exception for mixed content\n    \n    public String getText() {\n    \n        StringBuffer buf = new StringBuffer();\n        int len = getChildCount();\n    \n        for (int i = 0; i < len; i++) {\n            if (isText(i))\n                buf.append(getText(i));\n            else if (getType(i) == ELEMENT)\n                throw new RuntimeException(\"not text-only content!\");\n        }\n    \n        return buf.toString();\n    }\n    */\n\n    /** Returns the text node with the given index or null if the node\n        with the given index is not a text node. */\n\n    public String getText(int index) {\n        return (isText(index)) ? (String) getChild(index) : null;\n    }\n\n    /** Returns the type of the child at the given index. Possible \n    types are ELEMENT, TEXT, COMMENT, and PROCESSING_INSTRUCTION */\n\n    public int getType(int index) {\n        return types.charAt(index);\n    }\n\n    /** Convenience method for indexOf (getNamespace (), name,\n        startIndex). \n    \n    public int indexOf(String name, int startIndex) {\n        return indexOf(getNamespace(), name, startIndex);\n    }\n    */\n\n    /** Performs search for an element with the given namespace and\n    name, starting at the given start index. A null namespace\n    matches any namespace, please use Xml.NO_NAMESPACE for no\n    namespace).  returns -1 if no matching element was found. */\n\n    public int indexOf(String namespace, String name, int startIndex) {\n\n        int len = getChildCount();\n\n        for (int i = startIndex; i < len; i++) {\n\n            Element child = getElement(i);\n\n            if (child != null\n                && name.equals(child.getName())\n                && (namespace == null || namespace.equals(child.getNamespace())))\n                return i;\n        }\n        return -1;\n    }\n\n    public boolean isText(int i) {\n        int t = getType(i);\n        return t == TEXT || t == IGNORABLE_WHITESPACE || t == CDSECT;\n    }\n\n    /** Recursively builds the child elements from the given parser\n    until an end tag or end document is found. \n        The end tag is not consumed. */\n\n    public void parse(XmlPullParser parser)\n        throws IOException, XmlPullParserException {\n\n        boolean leave = false;\n\n        do {\n            int type = parser.getEventType();\n            \n   //         System.out.println(parser.getPositionDescription());\n            \n            switch (type) {\n\n                case XmlPullParser.START_TAG :\n                    {\n                        Element child =\n                            createElement(\n                                parser.getNamespace(),\n                                parser.getName());\n                        //    child.setAttributes (event.getAttributes ());\n                        addChild(ELEMENT, child);\n\n                        // order is important here since \n                        // setparent may perform some init code!\n\n                        child.parse(parser);\n                        break;\n                    }\n\n                case XmlPullParser.END_DOCUMENT :\n                case XmlPullParser.END_TAG :\n                    leave = true;\n                    break;\n\n                default :\n                    if (parser.getText() != null)\n                        addChild(\n                            type == XmlPullParser.ENTITY_REF ? TEXT : type,\n                            parser.getText());\n                    else if (\n                        type == XmlPullParser.ENTITY_REF\n                            && parser.getName() != null) {\n                        addChild(ENTITY_REF, parser.getName());\n                    }\n                    parser.nextToken();\n            }\n        }\n        while (!leave);\n    }\n\n    /** Removes the child object at the given index */\n\n    public void removeChild(int idx) {\n        children.removeElementAt(idx);\n\n        /***  Modification by HHS - start ***/\n        //      types.deleteCharAt (index);\n        /***/\n        int n = types.length() - 1;\n\n        for (int i = idx; i < n; i++)\n            types.setCharAt(i, types.charAt(i + 1));\n\n        types.setLength(n);\n\n        /***  Modification by HHS - end   ***/\n    }\n\n    /* returns a valid XML representation of this Element including\n    \tattributes and children. \n    public String toString() {\n        try {\n            ByteArrayOutputStream bos =\n                new ByteArrayOutputStream();\n            XmlWriter xw =\n                new XmlWriter(new OutputStreamWriter(bos));\n            write(xw);\n            xw.close();\n            return new String(bos.toByteArray());\n        }\n        catch (IOException e) {\n            throw new RuntimeException(e.toString());\n        }\n    }\n    */\n\n    /** Writes this node to the given XmlWriter. For node and document,\n        this method is identical to writeChildren, except that the\n        stream is flushed automatically. */\n\n    public void write(XmlSerializer writer) throws IOException {\n        writeChildren(writer);\n        writer.flush();\n    }\n\n    /** Writes the children of this node to the given XmlWriter. */\n\n    public void writeChildren(XmlSerializer writer) throws IOException {\n        if (children == null)\n            return;\n\n        int len = children.size();\n\n        for (int i = 0; i < len; i++) {\n            int type = getType(i);\n            Object child = children.elementAt(i);\n            switch (type) {\n                case ELEMENT :\n                     ((Element) child).write(writer);\n                    break;\n\n                case TEXT :\n                    writer.text((String) child);\n                    break;\n\n                case IGNORABLE_WHITESPACE :\n                    writer.ignorableWhitespace((String) child);\n                    break;\n\n                case CDSECT :\n                    writer.cdsect((String) child);\n                    break;\n\n                case COMMENT :\n                    writer.comment((String) child);\n                    break;\n\n                case ENTITY_REF :\n                    writer.entityRef((String) child);\n                    break;\n\n                case PROCESSING_INSTRUCTION :\n                    writer.processingInstruction((String) child);\n                    break;\n\n                case DOCDECL :\n                    writer.docdecl((String) child);\n                    break;\n\n                default :\n                    throw new RuntimeException(\"Illegal type: \" + type);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/org/kxml2/wap/Wbxml.java",
    "content": "/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The  above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE. */\n\npackage org.kxml2.wap;\n\n\n/** contains the WBXML constants  */\n\n\npublic interface Wbxml {\n\n    static public final int SWITCH_PAGE = 0;\n    static public final int END = 1;\n    static public final int ENTITY = 2;\n    static public final int STR_I = 3;\n    static public final int LITERAL = 4;\n    static public final int EXT_I_0 = 0x40;\n    static public final int EXT_I_1 = 0x41;\n    static public final int EXT_I_2 = 0x42;\n    static public final int PI = 0x43;\n    static public final int LITERAL_C = 0x44;\n    static public final int EXT_T_0 = 0x80;\n    static public final int EXT_T_1 = 0x81;\n    static public final int EXT_T_2 = 0x82;\n    static public final int STR_T = 0x83;\n    static public final int LITERAL_A = 0x084;\n    static public final int EXT_0 = 0x0c0;\n    static public final int EXT_1 = 0x0c1;\n    static public final int EXT_2 = 0x0c2;\n    static public final int OPAQUE = 0x0c3; \n    static public final int LITERAL_AC = 0x0c4;\n}\n"
  },
  {
    "path": "src/main/java/org/kxml2/wap/WbxmlParser.java",
    "content": "/* Copyright (c) 2002,2003,2004 Stefan Haustein, Oberhausen, Rhld., Germany\r\n *\r\n * Permission is hereby granted, free of charge, to any person obtaining a copy\r\n * of this software and associated documentation files (the \"Software\"), to deal\r\n * in the Software without restriction, including without limitation the rights\r\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or\r\n * sell copies of the Software, and to permit persons to whom the Software is\r\n * furnished to do so, subject to the following conditions:\r\n *\r\n * The  above copyright notice and this permission notice shall be included in\r\n * all copies or substantial portions of the Software.\r\n *\r\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\r\n * IN THE SOFTWARE. */\r\n\r\n// Contributors: Bjorn Aadland, Chris Bartley, Nicola Fankhauser,\r\n//               Victor Havin,  Christian Kurzke, Bogdan Onoiu,\r\n//                Elias Ross, Jain Sanjay, David Santoro.\r\n\r\npackage org.kxml2.wap;\r\n\r\nimport java.io.*;\r\nimport java.util.Vector;\r\nimport java.util.Hashtable;\r\n\r\nimport org.xmlpull.v1.*;\r\n\r\n\r\npublic class WbxmlParser implements XmlPullParser {\r\n\r\n\tstatic final String HEX_DIGITS = \"0123456789abcdef\";\r\n\t\r\n\t/** Parser event type for Wbxml-specific events. The Wbxml event code can be \r\n\t * accessed with getWapCode() */\r\n\t\r\n    public static final int WAP_EXTENSION = 64;\r\n    \r\n    static final private String UNEXPECTED_EOF =\r\n    \"Unexpected EOF\";\r\n    static final private String ILLEGAL_TYPE =\r\n    \"Wrong event type\";\r\n    \r\n    private InputStream in;\r\n    \r\n    private int TAG_TABLE = 0;\r\n    private int ATTR_START_TABLE = 1;\r\n    private int ATTR_VALUE_TABLE = 2;\r\n    \r\n    private String[] attrStartTable;\r\n    private String[] attrValueTable;\r\n    private String[] tagTable;\r\n    private byte[] stringTable;\r\n    private Hashtable cacheStringTable = null;\r\n    private boolean processNsp;\r\n    \r\n    private int depth;\r\n    private String[] elementStack = new String[16];\r\n    private String[] nspStack = new String[8];\r\n    private int[] nspCounts = new int[4];\r\n    \r\n    private int attributeCount;\r\n    private String[] attributes = new String[16];\r\n    private int nextId = -2;\r\n    \r\n    private Vector tables = new Vector();\r\n    \r\n    private int version;\r\n    private int publicIdentifierId;\r\n    \r\n    //    StartTag current;\r\n    //    ParseEvent next;\r\n    \r\n    private String prefix;\r\n    private String namespace;\r\n    private String name;\r\n    private String text;\r\n\r\n    private Object wapExtensionData;\r\n    private int wapCode;\r\n    \r\n    private int type;\r\n    \r\n    private boolean degenerated;\r\n    private boolean isWhitespace;\r\n    private String encoding;\r\n    \r\n    public boolean getFeature(String feature) {\r\n        if (XmlPullParser\r\n        .FEATURE_PROCESS_NAMESPACES\r\n        .equals(feature))\r\n            return processNsp;\r\n        else\r\n            return false;\r\n    }\r\n    \r\n    public String getInputEncoding() {\r\n        return encoding;\r\n    }\r\n    \r\n    public void defineEntityReplacementText(\r\n    String entity,\r\n    String value)\r\n    throws XmlPullParserException {\r\n        \r\n        // just ignore, has no effect\r\n    }\r\n    \r\n    public Object getProperty(String property) {\r\n        return null;\r\n    }\r\n    \r\n    public int getNamespaceCount(int depth) {\r\n        if (depth > this.depth)\r\n            throw new IndexOutOfBoundsException();\r\n        return nspCounts[depth];\r\n    }\r\n    \r\n    public String getNamespacePrefix(int pos) {\r\n        return nspStack[pos << 1];\r\n    }\r\n    \r\n    public String getNamespaceUri(int pos) {\r\n        return nspStack[(pos << 1) + 1];\r\n    }\r\n    \r\n    public String getNamespace(String prefix) {\r\n        \r\n        if (\"xml\".equals(prefix))\r\n            return \"http://www.w3.org/XML/1998/namespace\";\r\n        if (\"xmlns\".equals(prefix))\r\n            return \"http://www.w3.org/2000/xmlns/\";\r\n        \r\n        for (int i = (getNamespaceCount(depth) << 1) - 2;\r\n        i >= 0;\r\n        i -= 2) {\r\n            if (prefix == null) {\r\n                if (nspStack[i] == null)\r\n                    return nspStack[i + 1];\r\n            }\r\n            else if (prefix.equals(nspStack[i]))\r\n                return nspStack[i + 1];\r\n        }\r\n        return null;\r\n    }\r\n    \r\n    public int getDepth() {\r\n        return depth;\r\n    }\r\n    \r\n    public String getPositionDescription() {\r\n        \r\n        StringBuffer buf =\r\n        new StringBuffer(\r\n        type < TYPES.length ? TYPES[type] : \"unknown\");\r\n        buf.append(' ');\r\n        \r\n        if (type == START_TAG || type == END_TAG) {\r\n            if (degenerated)\r\n                buf.append(\"(empty) \");\r\n            buf.append('<');\r\n            if (type == END_TAG)\r\n                buf.append('/');\r\n            \r\n            if (prefix != null)\r\n                buf.append(\"{\" + namespace + \"}\" + prefix + \":\");\r\n            buf.append(name);\r\n            \r\n            int cnt = attributeCount << 2;\r\n            for (int i = 0; i < cnt; i += 4) {\r\n                buf.append(' ');\r\n                if (attributes[i + 1] != null)\r\n                    buf.append(\r\n                    \"{\"\r\n                    + attributes[i]\r\n                    + \"}\"\r\n                    + attributes[i\r\n                    + 1]\r\n                    + \":\");\r\n                buf.append(\r\n                attributes[i\r\n                + 2]\r\n                + \"='\"\r\n                + attributes[i\r\n                + 3]\r\n                + \"'\");\r\n            }\r\n            \r\n            buf.append('>');\r\n        }\r\n        else if (type == IGNORABLE_WHITESPACE);\r\n        else if (type != TEXT)\r\n            buf.append(getText());\r\n        else if (isWhitespace)\r\n            buf.append(\"(whitespace)\");\r\n        else {\r\n            String text = getText();\r\n            if (text.length() > 16)\r\n                text = text.substring(0, 16) + \"...\";\r\n            buf.append(text);\r\n        }\r\n        \r\n        return buf.toString();\r\n    }\r\n    \r\n    public int getLineNumber() {\r\n        return -1;\r\n    }\r\n    \r\n    public int getColumnNumber() {\r\n        return -1;\r\n    }\r\n    \r\n    public boolean isWhitespace()\r\n    throws XmlPullParserException {\r\n        if (type != TEXT\r\n        && type != IGNORABLE_WHITESPACE\r\n        && type != CDSECT)\r\n            exception(ILLEGAL_TYPE);\r\n        return isWhitespace;\r\n    }\r\n    \r\n    public String getText() {\r\n        return text;\r\n    }\r\n    \r\n    public char[] getTextCharacters(int[] poslen) {\r\n        if (type >= TEXT) {\r\n            poslen[0] = 0;\r\n            poslen[1] = text.length();\r\n            char[] buf = new char[text.length()];\r\n            text.getChars(0, text.length(), buf, 0);\r\n            return buf;\r\n        }\r\n        \r\n        poslen[0] = -1;\r\n        poslen[1] = -1;\r\n        return null;\r\n    }\r\n    \r\n    public String getNamespace() {\r\n        return namespace;\r\n    }\r\n    \r\n    public String getName() {\r\n        return name;\r\n    }\r\n    \r\n    public String getPrefix() {\r\n        return prefix;\r\n    }\r\n    \r\n    public boolean isEmptyElementTag()\r\n    throws XmlPullParserException {\r\n        if (type != START_TAG)\r\n            exception(ILLEGAL_TYPE);\r\n        return degenerated;\r\n    }\r\n    \r\n    public int getAttributeCount() {\r\n        return attributeCount;\r\n    }\r\n    \r\n    public String getAttributeType(int index) {\r\n        return \"CDATA\";\r\n    }\r\n    \r\n    public boolean isAttributeDefault(int index) {\r\n        return false;\r\n    }\r\n    \r\n    public String getAttributeNamespace(int index) {\r\n        if (index >= attributeCount)\r\n            throw new IndexOutOfBoundsException();\r\n        return attributes[index << 2];\r\n    }\r\n    \r\n    public String getAttributeName(int index) {\r\n        if (index >= attributeCount)\r\n            throw new IndexOutOfBoundsException();\r\n        return attributes[(index << 2) + 2];\r\n    }\r\n    \r\n    public String getAttributePrefix(int index) {\r\n        if (index >= attributeCount)\r\n            throw new IndexOutOfBoundsException();\r\n        return attributes[(index << 2) + 1];\r\n    }\r\n    \r\n    public String getAttributeValue(int index) {\r\n        if (index >= attributeCount)\r\n            throw new IndexOutOfBoundsException();\r\n        return attributes[(index << 2) + 3];\r\n    }\r\n    \r\n    public String getAttributeValue(\r\n    String namespace,\r\n    String name) {\r\n        \r\n        for (int i = (attributeCount << 2) - 4;\r\n        i >= 0;\r\n        i -= 4) {\r\n            if (attributes[i + 2].equals(name)\r\n            && (namespace == null\r\n            || attributes[i].equals(namespace)))\r\n                return attributes[i + 3];\r\n        }\r\n        \r\n        return null;\r\n    }\r\n    \r\n    public int getEventType() throws XmlPullParserException {\r\n        return type;\r\n    }\r\n    \r\n    \r\n    // TODO: Reuse resolveWapExtension here? Raw Wap extensions would still be accessible\r\n    // via nextToken();  ....?\r\n    \r\n    public int next() throws XmlPullParserException, IOException {\r\n        \r\n        isWhitespace = true;\r\n        int minType = 9999;\r\n        \r\n        while (true) {\r\n            \r\n            String save = text;\r\n            \r\n            nextImpl();\r\n            \r\n            if (type < minType)\r\n                minType = type;\r\n            \r\n            if (minType > CDSECT) continue; // no \"real\" event so far\r\n            \r\n            if (minType >= TEXT) {  // text, see if accumulate\r\n                \r\n                if (save != null) text = text == null ? save : save + text;\r\n                \r\n                switch(peekId()) {\r\n                    case Wbxml.ENTITY:\r\n                    case Wbxml.STR_I:\r\n                    case Wbxml.STR_T:\r\n                    case Wbxml.LITERAL:\r\n                    case Wbxml.LITERAL_C:\r\n                    case Wbxml.LITERAL_A:\r\n                    case Wbxml.LITERAL_AC: continue;\r\n                }\r\n            }\r\n            \r\n            break;\r\n        }\r\n        \r\n        type = minType;\r\n        \r\n        if (type > TEXT)\r\n            type = TEXT;\r\n        \r\n        return type;\r\n    }\r\n    \r\n    \r\n    public int nextToken() throws XmlPullParserException, IOException {\r\n        \r\n        isWhitespace = true;\r\n        nextImpl();\r\n        return type;\r\n    }\r\n    \r\n    \r\n    \r\n    public int nextTag() throws XmlPullParserException, IOException {\r\n        \r\n        next();\r\n        if (type == TEXT && isWhitespace)\r\n            next();\r\n        \r\n        if (type != END_TAG && type != START_TAG)\r\n            exception(\"unexpected type\");\r\n        \r\n        return type;\r\n    }\r\n    \r\n    \r\n    public String nextText() throws XmlPullParserException, IOException {\r\n        if (type != START_TAG)\r\n            exception(\"precondition: START_TAG\");\r\n        \r\n        next();\r\n        \r\n        String result;\r\n        \r\n        if (type == TEXT) {\r\n            result = getText();\r\n            next();\r\n        }\r\n        else\r\n            result = \"\";\r\n        \r\n        if (type != END_TAG)\r\n            exception(\"END_TAG expected\");\r\n        \r\n        return result;\r\n    }\r\n    \r\n    \r\n    public void require(int type, String namespace, String name)\r\n    throws XmlPullParserException, IOException {\r\n        \r\n        if (type != this.type\r\n        || (namespace != null && !namespace.equals(getNamespace()))\r\n        || (name != null && !name.equals(getName())))\r\n            exception(\r\n            \"expected: \" + (type == WAP_EXTENSION ? \"WAP Ext.\" : (TYPES[type] + \" {\" + namespace + \"}\" + name)));\r\n    }\r\n    \r\n    \r\n    public void setInput(Reader reader) throws XmlPullParserException {\r\n        exception(\"InputStream required\");\r\n    }\r\n    \r\n    public void setInput(InputStream in, String enc)\r\n    throws XmlPullParserException {\r\n        \r\n        this.in = in;\r\n        \r\n        try {\r\n            version = readByte();\r\n            publicIdentifierId = readInt();\r\n            \r\n            if (publicIdentifierId == 0)\r\n                readInt();\r\n            \r\n            int charset = readInt(); // skip charset\r\n            \r\n            if (null == enc){\r\n                switch (charset){\r\n                    case   4: encoding = \"ISO-8859-1\"; break;\r\n                    case 106: encoding = \"UTF-8\";      break;\r\n                    // add more if you need them\r\n                    // http://www.iana.org/assignments/character-sets\r\n                    // case MIBenum: encoding = Name  break;\r\n                    default:  throw new UnsupportedEncodingException(\"\"+charset);\r\n                } \r\n            }else{\r\n                encoding = enc;\r\n            }\r\n\r\n            int strTabSize = readInt();\r\n            stringTable = new byte[strTabSize];\r\n            \r\n            int ok = 0;\r\n            while(ok < strTabSize){\r\n            \tint cnt = in.read(stringTable, ok, strTabSize - ok);\r\n            \tif(cnt <= 0) break;\r\n            \tok += cnt;\r\n            }\r\n            \r\n            selectPage(0, true);\r\n            selectPage(0, false);\r\n        }\r\n        catch (IOException e) {\r\n            exception(\"Illegal input format\");\r\n        }\r\n    }\r\n    \r\n    public void setFeature(String feature, boolean value)\r\n    throws XmlPullParserException {\r\n        if (XmlPullParser.FEATURE_PROCESS_NAMESPACES.equals(feature))\r\n            processNsp = value;\r\n        else\r\n            exception(\"unsupported feature: \" + feature);\r\n    }\r\n    \r\n    public void setProperty(String property, Object value)\r\n    throws XmlPullParserException {\r\n        throw new XmlPullParserException(\"unsupported property: \" + property);\r\n    }\r\n    \r\n    // ---------------------- private / internal methods\r\n    \r\n    private final boolean adjustNsp()\r\n    throws XmlPullParserException {\r\n        \r\n        boolean any = false;\r\n        \r\n        for (int i = 0; i < attributeCount << 2; i += 4) {\r\n            // * 4 - 4; i >= 0; i -= 4) {\r\n            \r\n            String attrName = attributes[i + 2];\r\n            int cut = attrName.indexOf(':');\r\n            String prefix;\r\n            \r\n            if (cut != -1) {\r\n                prefix = attrName.substring(0, cut);\r\n                attrName = attrName.substring(cut + 1);\r\n            }\r\n            else if (attrName.equals(\"xmlns\")) {\r\n                prefix = attrName;\r\n                attrName = null;\r\n            }\r\n            else\r\n                continue;\r\n            \r\n            if (!prefix.equals(\"xmlns\")) {\r\n                any = true;\r\n            }\r\n            else {\r\n                int j = (nspCounts[depth]++) << 1;\r\n                \r\n                nspStack = ensureCapacity(nspStack, j + 2);\r\n                nspStack[j] = attrName;\r\n                nspStack[j + 1] = attributes[i + 3];\r\n                \r\n                if (attrName != null\r\n                && attributes[i + 3].equals(\"\"))\r\n                    exception(\"illegal empty namespace\");\r\n                \r\n                //  prefixMap = new PrefixMap (prefixMap, attrName, attr.getValue ());\r\n                \r\n                //System.out.println (prefixMap);\r\n                \r\n                System.arraycopy(\r\n                attributes,\r\n                i + 4,\r\n                attributes,\r\n                i,\r\n                ((--attributeCount) << 2) - i);\r\n                \r\n                i -= 4;\r\n            }\r\n        }\r\n        \r\n        if (any) {\r\n            for (int i = (attributeCount << 2) - 4;\r\n            i >= 0;\r\n            i -= 4) {\r\n                \r\n                String attrName = attributes[i + 2];\r\n                int cut = attrName.indexOf(':');\r\n                \r\n                if (cut == 0)\r\n                    throw new RuntimeException(\r\n                    \"illegal attribute name: \"\r\n                    + attrName\r\n                    + \" at \"\r\n                    + this);\r\n                \r\n                else if (cut != -1) {\r\n                    String attrPrefix =\r\n                    attrName.substring(0, cut);\r\n                    \r\n                    attrName = attrName.substring(cut + 1);\r\n                    \r\n                    String attrNs = getNamespace(attrPrefix);\r\n                    \r\n                    if (attrNs == null)\r\n                        throw new RuntimeException(\r\n                        \"Undefined Prefix: \"\r\n                        + attrPrefix\r\n                        + \" in \"\r\n                        + this);\r\n                    \r\n                    attributes[i] = attrNs;\r\n                    attributes[i + 1] = attrPrefix;\r\n                    attributes[i + 2] = attrName;\r\n                    \r\n                    for (int j = (attributeCount << 2) - 4;\r\n                    j > i;\r\n                    j -= 4)\r\n                        if (attrName.equals(attributes[j + 2])\r\n                        && attrNs.equals(attributes[j]))\r\n                            exception(\r\n                            \"Duplicate Attribute: {\"\r\n                            + attrNs\r\n                            + \"}\"\r\n                            + attrName);\r\n                }\r\n            }\r\n        }\r\n        \r\n        int cut = name.indexOf(':');\r\n        \r\n        if (cut == 0)\r\n            exception(\"illegal tag name: \" + name);\r\n        else if (cut != -1) {\r\n            prefix = name.substring(0, cut);\r\n            name = name.substring(cut + 1);\r\n        }\r\n        \r\n        this.namespace = getNamespace(prefix);\r\n        \r\n        if (this.namespace == null) {\r\n            if (prefix != null)\r\n                exception(\"undefined prefix: \" + prefix);\r\n            this.namespace = NO_NAMESPACE;\r\n        }\r\n        \r\n        return any;\r\n    }\r\n    \r\n    private final void setTable(int page, int type, String[] table) {\r\n        if(stringTable != null){\r\n            throw new RuntimeException(\"setXxxTable must be called before setInput!\");\r\n        }\r\n        while(tables.size() < 3*page +3){\r\n            tables.addElement(null);\r\n        }\r\n        tables.setElementAt(table, page*3+type);\r\n    }\r\n    \r\n    \r\n    \r\n    \r\n    \r\n    private final void exception(String desc)\r\n    throws XmlPullParserException {\r\n        throw new XmlPullParserException(desc, this, null);\r\n    }\r\n    \r\n    \r\n    private void selectPage(int nr, boolean tags) throws XmlPullParserException{\r\n        if(tables.size() == 0 && nr == 0) return;\r\n        \r\n        if(nr*3 > tables.size())\r\n            exception(\"Code Page \"+nr+\" undefined!\");\r\n        \r\n        if(tags)\r\n            tagTable = (String[]) tables.elementAt(nr * 3 + TAG_TABLE);\r\n        else {\r\n            attrStartTable = (String[]) tables.elementAt(nr * 3 + ATTR_START_TABLE);\r\n            attrValueTable = (String[]) tables.elementAt(nr * 3 + ATTR_VALUE_TABLE);\r\n        }\r\n    }\r\n    \r\n    private final void nextImpl()\r\n    throws IOException, XmlPullParserException {\r\n        \r\n        String s;\r\n        \r\n        if (type == END_TAG) {\r\n            depth--;\r\n        }\r\n        \r\n        if (degenerated) {\r\n            type = XmlPullParser.END_TAG;\r\n            degenerated = false;\r\n            return;\r\n        }\r\n        \r\n        text = null;\r\n        prefix = null;\r\n        name = null;\r\n        \r\n        int id = peekId ();\r\n        while(id == Wbxml.SWITCH_PAGE){\r\n            nextId = -2;\r\n            selectPage(readByte(), true);\r\n            id = peekId();\r\n        }\r\n        nextId = -2;\r\n        \r\n        switch (id) {\r\n            case -1 :\r\n                type = XmlPullParser.END_DOCUMENT;\r\n                break;\r\n                \r\n            case Wbxml.END : \r\n            {\r\n                int sp = (depth - 1) << 2;\r\n                \r\n                type = END_TAG;\r\n                namespace = elementStack[sp];\r\n                prefix = elementStack[sp + 1];\r\n                name = elementStack[sp + 2];\r\n            }\r\n            break;\r\n            \r\n            case Wbxml.ENTITY : \r\n            {\r\n                type = ENTITY_REF;\r\n                char c = (char) readInt();\r\n                text = \"\" + c;\r\n                name = \"#\" + ((int) c);\r\n            }\r\n            \r\n            break;\r\n            \r\n            case Wbxml.STR_I :\r\n                type = TEXT;\r\n                text = readStrI();\r\n                break;\r\n                \r\n            case Wbxml.EXT_I_0 :\r\n            case Wbxml.EXT_I_1 :\r\n            case Wbxml.EXT_I_2 :\r\n            case Wbxml.EXT_T_0 :\r\n            case Wbxml.EXT_T_1 :\r\n            case Wbxml.EXT_T_2 :\r\n            case Wbxml.EXT_0 :\r\n            case Wbxml.EXT_1 :\r\n            case Wbxml.EXT_2 :\r\n            case Wbxml.OPAQUE :\r\n            \t\r\n                type = WAP_EXTENSION;\r\n                wapCode = id;\r\n                wapExtensionData = parseWapExtension(id);\r\n                break;\r\n                \r\n            case Wbxml.PI :\r\n                throw new RuntimeException(\"PI curr. not supp.\");\r\n                // readPI;\r\n                // break;\r\n                \r\n            case Wbxml.STR_T : \r\n            {\r\n                type = TEXT;\r\n                text = readStrT();\r\n            }\r\n            break;\r\n            \r\n            default :\r\n                parseElement(id);\r\n        }\r\n        //        }\r\n        //      while (next == null);\r\n        \r\n        //        return next;\r\n    }\r\n    \r\n    /** Overwrite this method to intercept all wap events */\r\n    \r\n    public Object parseWapExtension(int id)  throws IOException, XmlPullParserException {\r\n        \r\n        switch (id) {\r\n            case Wbxml.EXT_I_0 :\r\n            case Wbxml.EXT_I_1 :\r\n            case Wbxml.EXT_I_2 :\r\n                return readStrI();\r\n                \r\n            case Wbxml.EXT_T_0 :\r\n            case Wbxml.EXT_T_1 :\r\n            case Wbxml.EXT_T_2 :\r\n                return new Integer(readInt());\r\n                \r\n            case Wbxml.EXT_0 :\r\n            case Wbxml.EXT_1 :\r\n            case Wbxml.EXT_2 :\r\n            \treturn null;\r\n                \r\n            case Wbxml.OPAQUE : \r\n            {\r\n                int count = readInt();\r\n                byte[] buf = new byte[count];\r\n                \r\n                while(count > 0){\r\n                \tcount -= in.read(buf, buf.length-count, count);\r\n                }\r\n                \r\n                return buf;\r\n            } // case OPAQUE\r\n    \r\n            \r\n            default:\r\n                exception(\"illegal id: \"+id);\r\n            \treturn null; // dead code\r\n        } // SWITCH\r\n    }\r\n    \r\n    public void readAttr() throws IOException, XmlPullParserException {\r\n        \r\n        int id = readByte();\r\n        int i = 0;\r\n        \r\n        while (id != 1) {\r\n            \r\n            while(id == Wbxml.SWITCH_PAGE){\r\n                selectPage(readByte(), false);\r\n                id = readByte();\r\n            }\r\n            \r\n            String name = resolveId(attrStartTable, id);\r\n            StringBuffer value;\r\n            \r\n            int cut = name.indexOf('=');\r\n            \r\n            if (cut == -1)\r\n                value = new StringBuffer();\r\n            else {\r\n                value =\r\n                new StringBuffer(name.substring(cut + 1));\r\n                name = name.substring(0, cut);\r\n            }\r\n            \r\n            id = readByte();\r\n            while (id > 128\r\n            || id == Wbxml.SWITCH_PAGE\r\n            || id == Wbxml.ENTITY\r\n            || id == Wbxml.STR_I\r\n            || id == Wbxml.STR_T\r\n            || (id >= Wbxml.EXT_I_0 && id <= Wbxml.EXT_I_2)\r\n            || (id >= Wbxml.EXT_T_0 && id <= Wbxml.EXT_T_2)) {\r\n                \r\n                switch (id) {\r\n                    case Wbxml.SWITCH_PAGE :\r\n                        selectPage(readByte(), false);\r\n                        break;\r\n                        \r\n                    case Wbxml.ENTITY :\r\n                        value.append((char) readInt());\r\n                        break;\r\n                        \r\n                    case Wbxml.STR_I :\r\n                        value.append(readStrI());\r\n                        break;\r\n                        \r\n                    case Wbxml.EXT_I_0 :\r\n                    case Wbxml.EXT_I_1 :\r\n                    case Wbxml.EXT_I_2 :\r\n                    case Wbxml.EXT_T_0 :\r\n                    case Wbxml.EXT_T_1 :\r\n                    case Wbxml.EXT_T_2 :\r\n                    case Wbxml.EXT_0 :\r\n                    case Wbxml.EXT_1 :\r\n                    case Wbxml.EXT_2 :\r\n                    case Wbxml.OPAQUE :\r\n                        value.append(resolveWapExtension(id, parseWapExtension(id)));\r\n                        break;\r\n                        \r\n                    case Wbxml.STR_T :\r\n                        value.append(readStrT());\r\n                        break;\r\n                        \r\n                    default :\r\n                        value.append(\r\n                        resolveId(attrValueTable, id));\r\n                }\r\n                \r\n                id = readByte();\r\n            }\r\n            \r\n            attributes = ensureCapacity(attributes, i + 4);\r\n            \r\n            attributes[i++] = \"\";\r\n            attributes[i++] = null;\r\n            attributes[i++] = name;\r\n            attributes[i++] = value.toString();\r\n            \r\n            attributeCount++;\r\n        }\r\n    }\r\n    \r\n    private int peekId () throws IOException {\r\n        if (nextId == -2) {\r\n            nextId = in.read ();\r\n        }\r\n        return nextId;\r\n    }\r\n    \r\n    /** overwrite for own WAP extension handling in attributes and high level parsing \r\n     * (above nextToken() level) */\r\n    \r\n    protected String resolveWapExtension(int id, Object data){\r\n    \t\r\n    \tif(data instanceof byte[]){\r\n    \t\tStringBuffer sb = new StringBuffer();\r\n    \t\tbyte[] b = (byte[]) data;\r\n    \t\t\r\n    \t\tfor (int i = 0; i < b.length; i++) {\r\n    \t\t\tsb.append(HEX_DIGITS.charAt((b[i] >> 4) & 0x0f));\r\n    \t\t\tsb.append(HEX_DIGITS.charAt(b[i] & 0x0f));\r\n    \t\t}\r\n    \t\treturn sb.toString();\r\n    \t}\r\n\r\n    \treturn \"$(\"+data+\")\";\r\n    }\r\n    \r\n    String resolveId(String[] tab, int id) throws IOException {\r\n        int idx = (id & 0x07f) - 5;\r\n        if (idx == -1){\r\n        \twapCode = -1;\r\n            return readStrT();\r\n        }\r\n        if (idx < 0\r\n        || tab == null\r\n        || idx >= tab.length\r\n        || tab[idx] == null)\r\n            throw new IOException(\"id \" + id + \" undef.\");\r\n        \r\n        wapCode = idx+5;\r\n        \r\n        return tab[idx];\r\n    }\r\n    \r\n    void parseElement(int id)\r\n    throws IOException, XmlPullParserException {\r\n        \r\n        type = START_TAG;\r\n        name = resolveId(tagTable, id & 0x03f);\r\n        \r\n        attributeCount = 0;\r\n        if ((id & 128) != 0) {\r\n            readAttr();\r\n        }\r\n        \r\n        degenerated = (id & 64) == 0;\r\n        \r\n        int sp = depth++ << 2;\r\n        \r\n        // transfer to element stack\r\n        \r\n        elementStack = ensureCapacity(elementStack, sp + 4);\r\n        elementStack[sp + 3] = name;\r\n        \r\n        if (depth >= nspCounts.length) {\r\n            int[] bigger = new int[depth + 4];\r\n            System.arraycopy(nspCounts, 0, bigger, 0, nspCounts.length);\r\n            nspCounts = bigger;\r\n        }\r\n        \r\n        nspCounts[depth] = nspCounts[depth - 1];\r\n        \r\n        for (int i = attributeCount - 1; i > 0; i--) {\r\n            for (int j = 0; j < i; j++) {\r\n                if (getAttributeName(i)\r\n                .equals(getAttributeName(j)))\r\n                    exception(\r\n                    \"Duplicate Attribute: \"\r\n                    + getAttributeName(i));\r\n            }\r\n        }\r\n        \r\n        if (processNsp)\r\n            adjustNsp();\r\n        else\r\n            namespace = \"\";\r\n        \r\n        elementStack[sp] = namespace;\r\n        elementStack[sp + 1] = prefix;\r\n        elementStack[sp + 2] = name;\r\n        \r\n    }\r\n    \r\n    private final String[] ensureCapacity(\r\n    String[] arr,\r\n    int required) {\r\n        if (arr.length >= required)\r\n            return arr;\r\n        String[] bigger = new String[required + 16];\r\n        System.arraycopy(arr, 0, bigger, 0, arr.length);\r\n        return bigger;\r\n    }\r\n    \r\n    int readByte() throws IOException {\r\n        int i = in.read();\r\n        if (i == -1)\r\n            throw new IOException(\"Unexpected EOF\");\r\n        return i;\r\n    }\r\n    \r\n    int readInt() throws IOException {\r\n        int result = 0;\r\n        int i;\r\n        \r\n        do {\r\n            i = readByte();\r\n            result = (result << 7) | (i & 0x7f);\r\n        }\r\n        while ((i & 0x80) != 0);\r\n        \r\n        return result;\r\n    }\r\n    \r\n    String readStrI() throws IOException {\r\n        ByteArrayOutputStream buf = new ByteArrayOutputStream();\r\n        boolean wsp = true;\r\n        while (true){\r\n            int i = in.read();\r\n            if (i == 0){\r\n            \tbreak;\r\n            }\r\n        \tif (i == -1){\r\n                throw new IOException(UNEXPECTED_EOF);\r\n        \t}\r\n            if (i > 32){\r\n                wsp = false;\r\n            }\r\n            buf.write(i);\r\n        }\r\n        isWhitespace = wsp;\r\n        String result = new String(buf.toByteArray(), encoding);\r\n        buf.close();\r\n        return result;\r\n    }\r\n    \r\n    String readStrT() throws IOException {\r\n        int pos = readInt();\r\n        // As the main reason of stringTable is compression we build a cache of Strings\r\n        // stringTable is supposed to help create Strings from parts which means some cache hit rate\r\n        // This will help to minimize the Strings created when invoking readStrT() repeatedly\r\n        if (cacheStringTable == null){\r\n            //Lazy init if device is not using StringTable but inline 0x03 strings\r\n            cacheStringTable = new Hashtable();\r\n        }\r\n        String forReturn = (String) cacheStringTable.get(new Integer(pos));\r\n        if (forReturn == null){\r\n\r\n            int end = pos;\r\n            while(end < stringTable.length && stringTable[end] != '\\0'){\r\n            \tend++;\r\n\t\t\t}\r\n            forReturn = new String(stringTable, pos, end-pos, encoding);\r\n            cacheStringTable.put(new Integer(pos), forReturn);\r\n        }\r\n        return forReturn;\r\n    }\r\n    \r\n    /**\r\n     * Sets the tag table for a given page.\r\n     * The first string in the array defines tag 5, the second tag 6 etc.\r\n     */\r\n    \r\n    public void setTagTable(int page, String[] table) {\r\n        setTable(page, TAG_TABLE, table);\r\n        \r\n        //        this.tagTable = tagTable;\r\n        //      if (page != 0)\r\n        //        throw new RuntimeException(\"code pages curr. not supp.\");\r\n    }\r\n    \r\n    /** Sets the attribute start Table for a given page.\r\n     *\tThe first string in the array defines attribute\r\n     *  5, the second attribute 6 etc. Please use the\r\n     *  character '=' (without quote!) as delimiter\r\n     *  between the attribute name and the (start of the) value\r\n     */\r\n    \r\n    public void setAttrStartTable(\r\n    int page,\r\n    String[] table) {\r\n        \r\n        setTable(page, ATTR_START_TABLE, table);\r\n    }\r\n    \r\n    /** Sets the attribute value Table for a given page.\r\n     *\tThe first string in the array defines attribute value 0x85,\r\n     *  the second attribute value 0x86 etc.\r\n     */\r\n    \r\n    public void setAttrValueTable(\r\n    int page,\r\n    String[] table) {\r\n        \r\n        setTable(page, ATTR_VALUE_TABLE, table);\r\n    }\r\n    \r\n    /** Returns the token ID for start tags or the event type for wap proprietary events\r\n     * such as OPAQUE.\r\n     */\r\n    \r\n    public int getWapCode(){\r\n    \treturn wapCode;\r\n    }\r\n    \r\n    public Object getWapExtensionData(){\r\n    \treturn wapExtensionData;\r\n    }\r\n    \r\n    \r\n}"
  },
  {
    "path": "src/main/java/org/kxml2/wap/WbxmlSerializer.java",
    "content": "/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany\r\n *\r\n * Permission is hereby granted, free of charge, to any person obtaining a copy\r\n * of this software and associated documentation files (the \"Software\"), to deal\r\n * in the Software without restriction, including without limitation the rights\r\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or\r\n * sell copies of the Software, and to permit persons to whom the Software is\r\n * furnished to do so, subject to the following conditions:\r\n *\r\n * The  above copyright notice and this permission notice shall be included in\r\n * all copies or substantial portions of the Software.\r\n *\r\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\r\n * IN THE SOFTWARE. */\r\n\r\n//Contributors: Jonathan Cox, Bogdan Onoiu, Jerry Tian\r\n\r\npackage org.kxml2.wap;\r\n\r\nimport java.io.*;\r\nimport java.util.*;\r\n\r\nimport org.xmlpull.v1.*;\r\n\r\n// TODO: make some of the \"direct\" WBXML token writing methods public??\r\n\r\n/**\r\n * A class for writing WBXML. Does not support namespaces yet.\r\n */\r\npublic class WbxmlSerializer implements XmlSerializer {\r\n    \r\n\t\r\n    Hashtable stringTable = new Hashtable();\r\n    \r\n    OutputStream out;\r\n    \r\n    ByteArrayOutputStream buf = new ByteArrayOutputStream();\r\n    ByteArrayOutputStream stringTableBuf = new ByteArrayOutputStream();\r\n    \r\n    String pending;\r\n    int depth;\r\n    String name;\r\n    String namespace;\r\n    Vector attributes = new Vector();\r\n    \r\n    Hashtable attrStartTable = new Hashtable();\r\n    Hashtable attrValueTable = new Hashtable();\r\n    Hashtable tagTable = new Hashtable();\r\n    \r\n    private int attrPage;\r\n    private int tagPage;\r\n    \r\n    private String encoding;\r\n    \r\n    private boolean headerSent = false;\n    \r\n    /**\n     * Write an attribute. \n     * Calls to attribute() MUST follow a call to startTag() immediately. \n     * If there is no prefix defined for the given namespace, \n     * a prefix will be defined automatically.\n     */\n    public XmlSerializer attribute(String namespace, String name, String value) {\r\n        attributes.addElement(name);\r\n        attributes.addElement(value);\r\n        return this;\r\n    }\r\n    \r\n    \r\n    public void cdsect (String cdsect) throws IOException{\r\n        text (cdsect);\r\n    }\r\n    \r\n    /**\n     * Add comment. Ignore for WBXML.\n     */\n    public void comment (String comment) {\r\n        // silently ignore comment\n    }\r\n    \r\n    /**\n     * Docdecl isn't supported for WBXML.\n     */\n    public void docdecl (String docdecl) {\r\n        throw new RuntimeException (\"Cannot write docdecl for WBXML\");\r\n    }\r\n    \r\n    /**\n     * EntityReference not supported for WBXML.\n     */\n    public void entityRef (String er) {\r\n        throw new RuntimeException (\"EntityReference not supported for WBXML\");\r\n    }\r\n    \r\n    /**\n     * Return current tag depth.\n     */\n    public int getDepth() {\r\n        return depth;\r\n    }\r\n    \r\n    /**\n     * Return the current value of the feature with given name.\n     */ \n    public boolean getFeature (String name) {\r\n        return false;\r\n    }\r\n    \r\n    /**\n     * Returns the namespace URI of the current element as set by startTag().\n     * Namespaces is not yet implemented.\n     */\n    public String getNamespace() {\r\n        // Namespaces is not yet implemented. So only null can be setted\n        return null;\n    }\r\n    \r\n    /**\n     * Returns the name of the current element as set by startTag(). \n     * It can only be null before first call to startTag() or when last endTag() \n     * is called to close first startTag().\n     */\n    public String getName() {\r\n        return pending;\n    }\r\n    \r\n    /**\n     * Prefix for namespace not supported for WBXML. Not yet implemented.\n     */\n    public String getPrefix(String nsp, boolean create) {\r\n        throw new RuntimeException (\"NYI\");\r\n    }\r\n    \r\n    /**\n     * Look up the value of a property. \n     * @param name The name of property. Name is any fully-qualified URI.\n     * @return The value of named property.\n     */\n    public Object getProperty (String name) {\r\n        return null;\r\n    }\r\n    \r\n    public void ignorableWhitespace (String sp) {\r\n    }\r\n    \r\n    /**\n     * Finish writing. \n     * All unclosed start tags will be closed and output will be flushed. \n     * After calling this method no more output can be serialized until \n     * next call to setOutput().\n     */\n    public void endDocument() throws IOException {\r\n        flush();\n    }\n        \r\n    /**\n     * Write all pending output to the stream. \n     * After first call string table willn't be used and you can't add tag\n     * which is not in tag table.\n     */\n    public void flush() throws IOException {\n        checkPending(false);\n        \r\n        if (!headerSent) {\n            writeInt(out, stringTableBuf.size());\n            out.write(stringTableBuf.toByteArray());\r\n            headerSent = true;\n        }\n        \r\n        out.write(buf.toByteArray());\r\n        buf.reset();\n    }\r\n    \r\n    public void checkPending(boolean degenerated) throws IOException {\r\n        if (pending == null)\r\n            return;\r\n        \r\n        int len = attributes.size();\r\n        \r\n        int[] idx = (int[]) tagTable.get(pending);\r\n        \r\n        // if no entry in known table, then add as literal\r\n        if (idx == null) {\r\n            buf.write(len == 0\r\n                ? (degenerated ? Wbxml.LITERAL : Wbxml.LITERAL_C)\r\n                    : (degenerated ? Wbxml.LITERAL_A : Wbxml.LITERAL_AC));\r\n            \r\n            writeStrT(pending, false);\r\n        } else {\n            if(idx[0] != tagPage){\r\n                tagPage=idx[0];\r\n\t\t\t\tbuf.write(Wbxml.SWITCH_PAGE);\n                buf.write(tagPage);\r\n            }\r\n            buf.write(len == 0\r\n                ? (degenerated ? idx[1] : idx[1] | 64)\r\n                    : (degenerated ? idx[1] | 128 : idx[1] | 192));\r\n        }\r\n        \r\n        for (int i = 0; i < len;) {\r\n            idx = (int[]) attrStartTable.get(attributes.elementAt(i));\r\n            \r\n            if (idx == null) {\r\n                buf.write(Wbxml.LITERAL);\r\n                writeStrT((String) attributes.elementAt(i), false);\r\n            }\r\n            else {\r\n                if(idx[0] != attrPage){\r\n                  attrPage = idx[0];\n                    buf.write(0);\r\n                    buf.write(attrPage);\r\n                }\r\n                buf.write(idx[1]);\r\n            }\r\n            idx = (int[]) attrValueTable.get(attributes.elementAt(++i));\r\n            if (idx == null) {\r\n                writeStr((String) attributes.elementAt(i));\r\n            }\r\n            else {\r\n                if(idx[0] != attrPage){\r\n                    attrPage = idx[0];\r\n                    buf.write(0);\r\n                    buf.write(attrPage);\t\t\t\t\t\r\n                }\r\n                buf.write(idx[1]);\r\n            }\r\n            ++i;\r\n        }\r\n        \r\n        if (len > 0)\r\n            buf.write(Wbxml.END);\r\n        \r\n        pending = null;\r\n        attributes.removeAllElements();\r\n    }\r\n    \r\n    /**\n     * Not Yet Implemented.\n     */\n    public void processingInstruction(String pi) {\r\n        throw new RuntimeException (\"PI NYI\");\r\n    }\r\n    \r\n    /**\n     * Set feature identified by name. There are no supported functions.\n     */\n    public void setFeature(String name, boolean value) {\r\n        throw new IllegalArgumentException (\"unknown feature \"+name);\r\n    }\r\n    \r\n    /**\n     * Set the output to the given writer. Wbxml requires an OutputStream.\n     */\n    public void setOutput (Writer writer) {\r\n        throw new RuntimeException (\"Wbxml requires an OutputStream!\");\r\n    }\r\n    \r\n    /**\n     * Set to use binary output stream with given encoding.\n     */\n    public void setOutput (OutputStream out, String encoding) throws IOException {\r\n        \r\n    \tthis.encoding = encoding == null ? \"UTF-8\" : encoding;\r\n        this.out = out;\r\n        \r\n        buf = new ByteArrayOutputStream();\r\n        stringTableBuf = new ByteArrayOutputStream();\r\n        headerSent = false;\n        \r\n        // ok, write header\r\n    }\r\n    \r\n    /**\n     * Binds the given prefix to the given namespace. Not yet implemented.\n     */\n    public void setPrefix(String prefix, String nsp) {\r\n        throw new RuntimeException(\"NYI\");\r\n    }\r\n    \r\n    /**\n     * Set the value of a property. There are no supported properties.\n     */\n    public void setProperty(String property, Object value) {\r\n        throw new IllegalArgumentException (\"unknown property \"+property);\r\n    }\r\n    \r\n    /**\n     * Write version and encoding information.\n     * This method can only be called just after setOutput.\n     * @param encoding Document encoding. Default is UTF-8.\n     * @param standalone Not used in WBXML.\n     */\n    public void startDocument(String encoding, Boolean standalone) throws IOException {\n        out.write(0x03); // version 1.3\r\n        // http://www.openmobilealliance.org/tech/omna/omna-wbxml-public-docid.htm\r\n        out.write(0x01); // unknown or missing public identifier\r\n\r\n        // default encoding is UTF-8\r\n        \r\n        if(encoding != null){\n            this.encoding = encoding;\n        }\r\n        \r\n        if (this.encoding.toUpperCase().equals(\"UTF-8\")){\r\n            out.write(106);\r\n        }else if (this.encoding.toUpperCase().equals(\"ISO-8859-1\")){\r\n            out.write(0x04);\r\n        }else{\r\n            throw new UnsupportedEncodingException(encoding);\r\n        }\r\n    }\r\n    \r\n    \r\n    public XmlSerializer startTag(String namespace, String name) throws IOException {\r\n        \r\n        if (namespace != null && !\"\".equals(namespace))\r\n            throw new RuntimeException (\"NSP NYI\");\r\n        \r\n        //current = new State(current, prefixMap, name);\r\n        \r\n        checkPending(false);\r\n        pending = name;\r\n        depth++;\r\n        \r\n        return this;\r\n    }\r\n    \r\n    public XmlSerializer text(char[] chars, int start, int len) throws IOException {\r\n    \tcheckPending(false);\r\n        writeStr(new String(chars, start, len));\r\n        return this;\r\n    }\r\n    \r\n    public XmlSerializer text(String text) throws IOException {\r\n        checkPending(false);\r\n        writeStr(text);\r\n        return this;\r\n    }\r\n    \r\n    /** \r\n     * Used in text() and attribute() to write text.\r\n     */ \r\n    private void writeStr(String text) throws IOException{\r\n        int p0 = 0;\r\n    \tint lastCut = 0;\r\n    \tint len = text.length();\r\n    \t\r\n        if (headerSent) {\r\n          writeStrI(buf, text);\r\n          return;\r\n        }\r\n         \r\n    \twhile(p0 < len){\r\n    \t    while(p0 < len && text.charAt(p0) < 'A' ){ // skip interpunctation\r\n    \t        p0++;\r\n    \t    }\r\n    \t    int p1 = p0;\r\n    \t    while (p1 < len && text.charAt(p1) >= 'A'){\r\n    \t        p1++;\r\n    \t    }\r\n    \t\t\r\n    \t    if (p1 - p0 > 10) {\r\n    \t        if (p0 > lastCut && text.charAt(p0-1) == ' ' \r\n    \t            && stringTable.get(text.substring(p0, p1)) == null){\r\n    \t            buf.write(Wbxml.STR_T);\r\n    \t            writeStrT(text.substring(lastCut, p1), false);\r\n    \t        }\r\n    \t        else {\r\n    \t            if(p0 > lastCut && text.charAt(p0-1) == ' '){\r\n    \t                p0--;\r\n    \t            }\r\n\r\n    \t            if(p0 > lastCut){\r\n    \t                buf.write(Wbxml.STR_T);\r\n    \t                writeStrT(text.substring(lastCut, p0), false);\r\n    \t            }\r\n    \t            buf.write(Wbxml.STR_T);\r\n    \t            writeStrT(text.substring(p0, p1), true);\r\n    \t        }\r\n    \t        lastCut = p1;\r\n    \t    }\r\n    \t    p0 = p1;\r\n    \t}\r\n\r\n    \tif(lastCut < len){\r\n    \t  buf.write(Wbxml.STR_T);\r\n    \t  writeStrT(text.substring(lastCut, len), false);\r\n    \t}\r\n    }\r\n    \r\n    \r\n\r\n    public XmlSerializer endTag(String namespace, String name) throws IOException {\r\n        //        current = current.prev;\r\n        if (pending != null) {\r\n            checkPending(true);\r\n        } else {\r\n            buf.write(Wbxml.END);\r\n        }\r\n        depth--;\r\n        return this;\r\n    }\r\n    \r\n    /** \r\n     * @throws IOException \r\n     */\r\n    public void writeWapExtension(int type, Object data) throws IOException {\r\n        checkPending(false);\r\n    \tbuf.write(type);\r\n    \tswitch(type){\r\n    \tcase Wbxml.EXT_0:\r\n    \tcase Wbxml.EXT_1:\r\n    \tcase Wbxml.EXT_2:\r\n    \t    break;\r\n    \t\r\n    \tcase Wbxml.OPAQUE:\r\n    \t    byte[] bytes = (byte[]) data;\r\n    \t    writeInt(buf, bytes.length);\r\n    \t    buf.write(bytes);\r\n    \t    break;\r\n    \t\t\r\n    \tcase Wbxml.EXT_I_0:\r\n    \tcase Wbxml.EXT_I_1:\r\n    \tcase Wbxml.EXT_I_2:\r\n    \t  writeStrI(buf, (String) data);\r\n    \t  break;\r\n\r\n    \tcase Wbxml.EXT_T_0:\r\n    \tcase Wbxml.EXT_T_1:\r\n    \tcase Wbxml.EXT_T_2:\r\n    \t  writeStrT((String) data, false);\r\n    \t  break;\r\n    \t\t\r\n    \tdefault: \r\n    \t  throw new IllegalArgumentException();\r\n    \t}\r\n    }\r\n    \r\n    // ------------- internal methods --------------------------\r\n    \r\n    static void writeInt(OutputStream out, int i) throws IOException {\r\n        byte[] buf = new byte[5];\r\n        int idx = 0;\r\n        \r\n        do {\r\n            buf[idx++] = (byte) (i & 0x7f);\r\n            i = i >> 7;\r\n        }\r\n        while (i != 0);\r\n        \r\n        while (idx > 1) {\r\n            out.write(buf[--idx] | 0x80);\r\n        }\r\n        out.write(buf[0]);\r\n    }\r\n    \r\n    void writeStrI(OutputStream out, String s) throws IOException {\r\n    \tbyte[] data = s.getBytes(encoding);\r\n    \tout.write(data);\r\n        out.write(0);\r\n    }\r\n    \r\n    private final void writeStrT(String s, boolean mayPrependSpace) throws IOException {\r\n        \r\n        Integer idx = (Integer) stringTable.get(s);        \r\n        writeInt(buf, idx == null \r\n            ? addToStringTable(s, mayPrependSpace)\r\n                : idx.intValue());\r\n    }\r\n    \r\n    \r\n    /**\r\n     * Add string to string table. Not permitted after string table has been flushed. \r\n     * \r\n     * @param s string to be added to the string table\r\n     * @param mayPrependSpace is set, a space is prepended to the string to archieve better compression results\r\n     * @return offset of s in the string table\r\n     */\r\n    public int addToStringTable(String s, boolean mayPrependSpace) throws IOException {\r\n        if (headerSent) {\r\n            throw new IOException(\"stringtable sent\");\r\n        }\r\n        \r\n        int i = stringTableBuf.size();\r\n        int offset = i;\r\n        if(s.charAt(0) >= '0' && mayPrependSpace){\r\n            s = ' ' + s;\r\n            offset++; \r\n        }\r\n        \r\n        stringTable.put(s, new Integer(i));\r\n        if(s.charAt(0) == ' '){\r\n            stringTable.put(s.substring(1), new Integer(i+1));\r\n        }\r\n        int j = s.lastIndexOf(' ');\r\n        if(j > 1){\r\n        \tString t = s.substring(j);\r\n        \tint k = t.getBytes(\"utf-8\").length;\r\n        \tstringTable.put(t, new Integer(i+k));\r\n        \tstringTable.put(s.substring(j+1), new Integer(i+k+1));\r\n        }\r\n                \r\n        writeStrI(stringTableBuf, s);\r\n        stringTableBuf.flush();\r\n        return offset;\r\n    }\r\n    \r\n    /**\r\n     * Sets the tag table for a given page.\r\n     * The first string in the array defines tag 5, the second tag 6 etc.\r\n     */\r\n    public void setTagTable(int page, String[] tagTable) {\r\n        // TODO: clear entries in tagTable?\r\n        \r\n        for (int i = 0; i < tagTable.length; i++) {\r\n            if (tagTable[i] != null) {\r\n                Object idx = new int[]{page, i+5};\r\n                this.tagTable.put(tagTable[i], idx);\r\n            }\r\n        }\r\n    }\r\n    \r\n    /**\r\n     * Sets the attribute start Table for a given page.\r\n     * The first string in the array defines attribute\r\n     * 5, the second attribute 6 etc.\r\n     *  Please use the\r\n     *  character '=' (without quote!) as delimiter\r\n     *  between the attribute name and the (start of the) value\r\n     */\r\n    public void setAttrStartTable(int page, String[] attrStartTable) {\r\n        \r\n        for (int i = 0; i < attrStartTable.length; i++) {\r\n            if (attrStartTable[i] != null) {\r\n                Object idx = new int[] {page, i + 5};\r\n                this.attrStartTable.put(attrStartTable[i], idx);\r\n            }\r\n        }\r\n    }\r\n    \r\n    /**\r\n     * Sets the attribute value Table for a given page.\r\n     * The first string in the array defines attribute value 0x85,\r\n     * the second attribute value 0x86 etc.\r\n     * Must be called BEFORE use attribute(), flush() etc.\n     */\r\n    public void setAttrValueTable(int page, String[] attrValueTable) {\r\n        // clear entries in this.table!\r\n        for (int i = 0; i < attrValueTable.length; i++) {\r\n            if (attrValueTable[i] != null) {\r\n                Object idx = new int[]{page, i + 0x085};\r\n                this.attrValueTable.put(attrValueTable[i], idx);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/org/kxml2/wap/syncml/SyncML.java",
    "content": "package org.kxml2.wap.syncml;\r\n\r\nimport org.kxml2.wap.*;\r\n\r\npublic abstract class SyncML {\r\n\t\r\n\t\r\n\t// SyncML-Common (-//SYNCML//DTD SyncML 1.2//EN and -//SYNCML//DTD MetInf 1.2//EN) support\r\n\t\r\n\tpublic static WbxmlParser createParser() {\r\n\t\tWbxmlParser p = new WbxmlParser();\r\n\t\tp.setTagTable(0, TAG_TABLE_0);\r\n\t\tp.setTagTable(1, TAG_TABLE_1);\r\n\t\treturn p;\r\n\t}\r\n\r\n\tpublic static WbxmlSerializer createSerializer() {\r\n\t\tWbxmlSerializer s = new WbxmlSerializer();\r\n\t\ts.setTagTable(0, TAG_TABLE_0);\r\n\t\ts.setTagTable(1, TAG_TABLE_1);\r\n\t\treturn s;\r\n\t}\r\n\t\r\n\t\r\n\t// SyncML-Common + DMDDF (-//OMA//DTD-DM-DDF 1.2//EN) support\r\n\t\r\n\tpublic static WbxmlParser createDMParser() {\r\n\t\tWbxmlParser p = createParser();\r\n\t\tp.setTagTable(2, TAG_TABLE_2_DM);\r\n\t\treturn p;\r\n\t}\r\n\r\n\tpublic static WbxmlSerializer createDMSerializer() {\r\n\t\tWbxmlSerializer s = createSerializer();\r\n\t\ts.setTagTable(2, TAG_TABLE_2_DM);\r\n\t\treturn s;\r\n\t}\r\n\r\n\t// Tables\r\n\t\r\n    public static final String [] TAG_TABLE_0 = {\r\n    \t\r\n    \t //  -//SYNCML//DTD SyncML 1.2//EN\r\n    \t\r\n         \"Add\",            // 0x05 \r\n         \"Alert\",          // 0x06 \r\n         \"Archive\",        // 0x07 \r\n         \"Atomic\",         // 0x08 \r\n         \"Chal\",           // 0x09 \r\n         \"Cmd\",            // 0x0a \r\n         \"CmdID\",          // 0x0b \r\n         \"CmdRef\",         // 0x0c \r\n         \"Copy\",           // 0x0d \r\n         \"Cred\",           // 0x0e \r\n         \"Data\",           // 0x0f \r\n         \"Delete\",         // 0x10 \r\n         \"Exec\",           // 0x11 \r\n         \"Final\",          // 0x12 \r\n         \"Get\",            // 0x13 \r\n         \"Item\",           // 0x14 \r\n         \"Lang\",           // 0x15 \r\n         \"LocName\",        // 0x16 \r\n         \"LocURI\",         // 0x17 \r\n         \"Map\",            // 0x18 \r\n         \"MapItem\",        // 0x19 \r\n         \"Meta\",           // 0x1a \r\n         \"MsgID\",          // 0x1b \r\n         \"MsgRef\",         // 0x1c \r\n         \"NoResp\",         // 0x1d \r\n         \"NoResults\",      // 0x1e \r\n         \"Put\",            // 0x1f \r\n         \"Replace\",        // 0x20 \r\n         \"RespURI\",        // 0x21 \r\n         \"Results\",        // 0x22 \r\n         \"Search\",         // 0x23 \r\n         \"Sequence\",       // 0x24 \r\n         \"SessionID\",      // 0x25 \r\n         \"SftDel\",         // 0x26 \r\n         \"Source\",         // 0x27 \r\n         \"SourceRef\",      // 0x28 \r\n         \"Status\",         // 0x29 \r\n         \"Sync\",           // 0x2a \r\n         \"SyncBody\",       // 0x2b \r\n         \"SyncHdr\",        // 0x2c \r\n         \"SyncML\",         // 0x2d \r\n         \"Target\",         // 0x2e \r\n         \"TargetRef\",      // 0x2f \r\n         \"Reserved for future use\",    // 0x30 \r\n         \"VerDTD\",         // 0x31 \r\n         \"VerProto\",       // 0x32 \r\n         \"NumberOfChanged\",// 0x33 \r\n         \"MoreData\",       // 0x34 \r\n         \"Field\",          // 0x35\r\n         \"Filter\",         // 0x36\r\n         \"Record\",         // 0x37\r\n         \"FilterType\",     // 0x38\r\n         \"SourceParent\",   // 0x39\r\n         \"TargetParent\",   // 0x3a\r\n         \"Move\",           // 0x3b\r\n         \"Correlator\"      // 0x3c\r\n    };  \r\n    \r\n    public static final String [] TAG_TABLE_1 = {\r\n\t   \r\n         //  -//SYNCML//DTD MetInf 1.2//EN \r\n    \t\r\n         \"Anchor\",         // 0x05 \r\n         \"EMI\",            // 0x06 \r\n         \"Format\",         // 0x07 \r\n         \"FreeID\",         // 0x08 \r\n         \"FreeMem\",        // 0x09 \r\n         \"Last\",           // 0x0a \r\n         \"Mark\",           // 0x0b \r\n         \"MaxMsgSize\",     // 0x0c \r\n         \"Mem\",            // 0x0d \r\n         \"MetInf\",         // 0x0e \r\n         \"Next\",           // 0x0f \r\n         \"NextNonce\",      // 0x10 \r\n         \"SharedMem\",      // 0x11 \r\n         \"Size\",           // 0x12 \r\n         \"Type\",           // 0x13 \r\n         \"Version\",        // 0x14 \r\n         \"MaxObjSize\",     // 0x15\r\n         \"FieldLevel\"      // 0x16\r\n         \r\n    };\r\n\r\n    public static final String [] TAG_TABLE_2_DM = {\r\n \t   \r\n        //  -//OMA//DTD-DM-DDF 1.2//EN \r\n   \t\r\n        \"AccessType\",         // 0x05 \r\n        \"ACL\",                // 0x06 \r\n        \"Add\",                // 0x07 \r\n        \"b64\",                // 0x08 \r\n        \"bin\",                // 0x09 \r\n        \"bool\",               // 0x0a \r\n        \"chr\",                // 0x0b \r\n        \"CaseSense\",          // 0x0c \r\n        \"CIS\",                // 0x0d \r\n        \"Copy\",               // 0x0e \r\n        \"CS\",                 // 0x0f \r\n        \"date\",               // 0x10 \r\n        \"DDFName\",            // 0x11 \r\n        \"DefaultValue\",       // 0x12 \r\n        \"Delete\",             // 0x13 \r\n        \"Description\",        // 0x14 \r\n        \"DDFFormat\",          // 0x15 \r\n        \"DFProperties\",       // 0x16 \r\n        \"DFTitle\",            // 0x17 \r\n        \"DFType\",             // 0x18 \r\n        \"Dynamic\",            // 0x19 \r\n        \"Exec\",               // 0x1a \r\n        \"float\",              // 0x1b \r\n        \"Format\",             // 0x1c \r\n        \"Get\",                // 0x1d \r\n        \"int\",                // 0x1e \r\n        \"Man\",                // 0x1f \r\n        \"MgmtTree\",           // 0x20 \r\n        \"MIME\",               // 0x21 \r\n        \"Mod\",                // 0x22 \r\n        \"Name\",               // 0x23 \r\n        \"Node\",               // 0x24 \r\n        \"node\",               // 0x25 \r\n        \"NodeName\",           // 0x26 \r\n        \"null\",               // 0x27 \r\n        \"Occurence\",          // 0x28 \r\n        \"One\",                // 0x29 \r\n        \"OneOrMore\",          // 0x2a \r\n        \"OneOrN\",             // 0x2b \r\n        \"Path\",               // 0x2c \r\n        \"Permanent\",          // 0x2d \r\n        \"Replace\",            // 0x2e \r\n        \"RTProperties\",       // 0x2f \r\n        \"Scope\",              // 0x30 \r\n        \"Size\",               // 0x31 \r\n        \"time\",               // 0x32 \r\n        \"Title\",              // 0x33 \r\n        \"TStamp\",             // 0x34 \r\n        \"Type\",               // 0x35\r\n        \"Value\",              // 0x36\r\n        \"VerDTD\",             // 0x37\r\n        \"VerNo\",              // 0x38\r\n        \"xml\",                // 0x39\r\n        \"ZeroOrMore\",         // 0x3a\r\n        \"ZeroOrN\",            // 0x3b\r\n        \"ZeroOrOne\"           // 0x3c\r\n        \r\n   };\r\n    \r\n}\r\n\r\n"
  },
  {
    "path": "src/main/java/org/kxml2/wap/wml/Wml.java",
    "content": "package org.kxml2.wap.wml;\n\nimport org.kxml2.wap.*;\n\n\n/** This class contains the wml coding tables for elements \n *  and attributes needed by the WmlParser. \n */\n\n\npublic abstract class Wml {\n\n\t/** Creates a WbxmlParser with the WML code pages set */\n\n\tpublic static WbxmlParser createParser() {\n\t\tWbxmlParser p = new WbxmlParser();\n\t\tp.setTagTable(0, TAG_TABLE);\n\t\tp.setAttrStartTable(0, ATTR_START_TABLE);\n\t\tp.setAttrValueTable(0, ATTR_VALUE_TABLE);\n\t\treturn p;\n\t}\n\n\tpublic static WbxmlSerializer createSerializer() {\n\t\tWbxmlSerializer s = new WbxmlSerializer();\n\t\ts.setTagTable(0, TAG_TABLE);\n\t\ts.setAttrStartTable(0, ATTR_START_TABLE);\n\t\ts.setAttrValueTable(0, ATTR_VALUE_TABLE);\n\t\treturn s;\n\t}\n\n\n    public static final String [] TAG_TABLE = {\n\n\tnull, // 05\n\tnull, // 06\n\tnull, // 07\n\tnull, // 08\n\tnull, // 09\n\tnull, // 0A\n\tnull, // 0B\n\tnull, // 0C\n\tnull, // 0D\n\tnull, // 0E\n\tnull, // 0F\n\n\tnull, // 10\n\tnull, // 11\n\tnull, // 12\n\tnull, // 13\n\tnull, // 14\n\tnull, // 15\n\tnull, // 16\n\tnull, // 17\n\tnull, // 18\n\tnull, // 19\n\tnull, // 1A\n\tnull, // 1B\n\t\"a\",  // 1C\n\t\"td\", // 1D\n\t\"tr\", // 1E\n\t\"table\", // 1F\n\n\t\"p\", // 20\n\t\"postfield\", // 21\n\t\"anchor\", // 22\n\t\"access\", // 23\n\t\"b\",  // 24\n\t\"big\", // 25\n\t\"br\", // 26\n\t\"card\", // 27\n\t\"do\", // 28\n\t\"em\", // 29\n\t\"fieldset\", // 2A\n\t\"go\", // 2B\n\t\"head\", // 2C\n\t\"i\", // 2D\n\t\"img\", // 2E\n\t\"input\", // 2F\n\n\t\"meta\", // 30\n\t\"noop\", // 31\n\t\"prev\", // 32\n\t\"onevent\", // 33\n\t\"optgroup\", // 34\n\t\"option\", // 35\n\t\"refresh\", // 36\n\t\"select\", // 37\n\t\"small\", // 38\n\t\"strong\", // 39\n\tnull, // 3A\n\t\"template\", // 3B\n\t\"timer\", // 3C\n\t\"u\", // 3D\n\t\"setvar\", // 3E\n\t\"wml\", // 3F\n    };\n\n    \n    public static final String [] ATTR_START_TABLE = { \n\t\"accept-charset\", // 05\n\t\"align=bottom\", // 06\n\t\"align=center\", // 07\n\t\"align=left\", // 08\n\t\"align=middle\", // 09\n\t\"align=right\", // 0A\n\t\"align=top\", // 0B\n\t\"alt\", // 0C\n\t\"content\", // 0D\n\tnull, // 0E\n\t\"domain\", // 0F\n\t\n\t\"emptyok=false\", // 10\n\t\"emptyok=true\", // 11\n\t\"format\", // 12\n\t\"height\", // 13\n\t\"hspace\", // 14\n\t\"ivalue\", // 15\n\t\"iname\", // 16\n\tnull, // 17\n\t\"label\", // 18\n\t\"localsrc\", // 19\n\t\"maxlength\", // 1A\n\t\"method=get\", // 1B\n\t\"method=post\", // 1C\n\t\"mode=nowrap\", // 1D\n\t\"mode=wrap\", // 1E\n\t\"multiple=false\", // 1F\n\n\t\"multiple=true\", // 20\n\t\"name\", // 21\n\t\"newcontext=false\", // 22\n\t\"newcontext=true\", // 23\n\t\"onpick\", // 24\n\t\"onenterbackward\", // 25\n\t\"onenterforward\", // 26\n\t\"ontimer\", // 27\n\t\"optimal=false\", // 28\n\t\"optimal=true\", // 29\n\t\"path\", // 2A\n\tnull, // 2B\n\tnull, // 2C\n\tnull, // 2D\n\t\"scheme\", // 2E\n\t\"sendreferer=false\", // 2F\n\t\n\t\"sendreferer=true\", // 30\n\t\"size\", // 31\n\t\"src\", // 32\n\t\"ordered=true\", // 33\n\t\"ordered=false\", // 34\n\t\"tabindex\", // 35\n\t\"title\", // 36\n\t\"type\", // 37\n\t\"type=accept\", // 38\n\t\"type=delete\", // 39\n\t\"type=help\", // 3A\n\t\"type=password\", // 3B\n\t\"type=onpick\", // 3C\n\t\"type=onenterbackward\", // 3D\n\t\"type=onenterforward\", // 3E\n\t\"type=ontimer\", // 3F\n\n\tnull, // 40\n\tnull, // 41\n\tnull, // 42\n\tnull, // 43\n\tnull, // 44\n\t\"type=options\", // 45\n\t\"type=prev\", // 46\n\t\"type=reset\", // 47\n\t\"type=text\", // 48\n\t\"type=vnd.\", // 49\n\t\"href\", // 4A\n\t\"href=http://\", // 4B\n\t\"href=https://\", // 4C\n\t\"value\", // 4D\n\t\"vspace\", // 4E\n\t\"width\", // 4F\n\n\t\"xml:lang\", // 50\n\tnull, // 51\n\t\"align\", // 52\n\t\"columns\", // 53\n\t\"class\", // 54\n\t\"id\", // 55\n\t\"forua=false\", // 56\n\t\"forua=true\", // 57\n\t\"src=http://\", // 58\n\t\"src=https://\", // 59\n\t\"http-equiv\", // 5A\n\t\"http-equiv=Content-Type\", // 5B\n\t\"content=application/vnd.wap.wmlc;charset=\", // 5C\n\t\"http-equiv=Expires\", // 5D\n\tnull, // 5E\n\tnull, // 5F\n    };\n\n\n    public static final String [] ATTR_VALUE_TABLE = {\n\t\".com/\", // 85\n\t\".edu/\", // 86\n\t\".net/\", // 87\n\t\".org/\", // 88\n\t\"accept\", // 89\n\t\"bottom\", // 8A\n\t\"clear\", // 8B\n\t\"delete\", // 8C\n\t\"help\", // 8D\n\t\"http://\", // 8E\n\t\"http://www.\", // 8F\n\t\n\t\"https://\", // 90\n\t\"https://www.\", // 91\n\tnull, // 92\n\t\"middle\", // 93\n\t\"nowrap\", // 94\n\t\"onpick\", // 95\n\t\"onenterbackward\", // 96\n\t\"onenterforward\", // 97\n\t\"ontimer\", // 98\n\t\"options\", // 99\n\t\"password\", // 9A\n\t\"reset\", // 9B\n\tnull, // 9C\n\t\"text\", // 9D\n\t\"top\", // 9E\n\t\"unknown\", // 9F\n\t\n\t\"wrap\", // A0\n\t\"www.\", // A1\n    };\n}    \n\n"
  },
  {
    "path": "src/main/java/org/kxml2/wap/wv/WV.java",
    "content": "package org.kxml2.wap.wv;\r\n\r\nimport java.io.IOException;\r\n\r\nimport org.kxml2.wap.*;\r\n\r\n/*\r\n\r\n * WV.java\r\n\r\n *\r\n\r\n * Created on 25 September 2003, 10:40\r\n\r\n */\r\n\r\n\r\n\r\n\r\n\r\n   /** \r\n\t *    Wireless Village CSP 1.1 (\"OMA-WV-CSP-V1_1-20021001-A.pdf\")\r\n\t *    Wireless Village CSP 1.2 (\"OMA-IMPS-WV-CSP_WBXML-v1_2-20030221-C.PDF\")\r\n\t *    There are some bugs in the 1.2 spec but this is Ok. 1.2 is candidate  \r\n *\r\n\r\n * @author  Bogdan Onoiu\r\n\r\n */\r\n\r\npublic abstract class WV {\r\n\r\n    \r\n\r\n    \r\n    \r\n\tpublic static WbxmlParser createParser () throws IOException {\r\n\t\t\r\n\t\tWbxmlParser parser = new WbxmlParser();\r\n\r\n\t\tparser.setTagTable (0, WV.tagTablePage0);\r\n\t\tparser.setTagTable (1, WV.tagTablePage1);\r\n\t\tparser.setTagTable (2, WV.tagTablePage2);\r\n\t\tparser.setTagTable (3, WV.tagTablePage3);\r\n\t\tparser.setTagTable (4, WV.tagTablePage4);\r\n\t\tparser.setTagTable (5, WV.tagTablePage5);\r\n\t\tparser.setTagTable (6, WV.tagTablePage6);\r\n\t\tparser.setTagTable (7, WV.tagTablePage7);\r\n\t\tparser.setTagTable (8, WV.tagTablePage8);\r\n\t\tparser.setTagTable (9, WV.tagTablePage9);\r\n\t\tparser.setTagTable (10, WV.tagTablePageA);\r\n\r\n\t\tparser.setAttrStartTable (0, WV.attrStartTable);\r\n        \r\n\t\tparser.setAttrValueTable (0, WV.attrValueTable);\r\n\r\n\t\treturn parser;\r\n\t}\r\n    \r\n   \r\n    \r\n    public static final String [] tagTablePage0 = {\r\n        /* Common ... continue on Page 0x09 */\r\n        \"Acceptance\",     //0x00, 0x05\r\n        \"AddList\",        //0x00, 0x06\r\n        \"AddNickList\",    //0x00, 0x07\r\n        \"SName\",          //0x00, 0x08\r\n        \"WV-CSP-Message\", //0x00, 0x09\r\n        \"ClientID\",       //0x00, 0x0A\r\n        \"Code\",           //0x00, 0x0B\r\n        \"ContactList\",    //0x00, 0x0C\r\n        \"ContentData\",    //0x00, 0x0D\r\n        \"ContentEncoding\",//0x00, 0x0E\r\n        \"ContentSize\",    //0x00, 0x0F\r\n        \"ContentType\",    //0x00, 0x10\r\n        \"DateTime\",       //0x00, 0x11\r\n        \"Description\",    //0x00, 0x12\r\n        \"DetailedResult\", //0x00, 0x13\r\n        \"EntityList\",     //0x00, 0x14\r\n        \"Group\",          //0x00, 0x15\r\n        \"GroupID\",        //0x00, 0x16\r\n        \"GroupList\",      //0x00, 0x17\r\n        \"InUse\",          //0x00, 0x18\r\n        \"Logo\",           //0x00, 0x19\r\n        \"MessageCount\",   //0x00, 0x1A\r\n        \"MessageID\",      //0x00, 0x1B\r\n        \"MessageURI\",     //0x00, 0x1C\r\n        \"MSISDN\",         //0x00, 0x1D\r\n        \"Name\",           //0x00, 0x1E\r\n        \"NickList\",       //0x00, 0x1F\r\n        \"NickName\",       //0x00, 0x20\r\n        \"Poll\",           //0x00, 0x21\r\n        \"Presence\",       //0x00, 0x22\r\n        \"PresenceSubList\",//0x00, 0x23\r\n        \"PresenceValue\",  //0x00, 0x24\r\n        \"Property\",       //0x00, 0x25\r\n        \"Qualifier\",      //0x00, 0x26\r\n        \"Recipient\",      //0x00, 0x27\r\n        \"RemoveList\",     //0x00, 0x28\r\n        \"RemoveNickList\", //0x00, 0x29\r\n        \"Result\",         //0x00, 0x2A\r\n        \"ScreenName\",     //0x00, 0x2B\r\n        \"Sender\",         //0x00, 0x2C\r\n        \"Session\",        //0x00, 0x2D\r\n        \"SessionDescriptor\",//0x00, 0x2E\r\n        \"SessionID\",      //0x00, 0x2F\r\n        \"SessionType\",    //0x00, 0x30\r\n        \"Status\",         //0x00, 0x31\r\n        \"Transaction\",    //0x00, 0x32\r\n        \"TransactionContent\",//0x00, 0x33\r\n        \"TransactionDescriptor\",//0x00, 0x34\r\n        \"TransactionID\",  //0x00, 0x35\r\n        \"TransactionMode\",//0x00, 0x36\r\n        \"URL\",            //0x00, 0x37\r\n        \"URLList\",        //0x00, 0x38\r\n        \"User\",           //0x00, 0x39\r\n        \"UserID\",         //0x00, 0x3A\r\n        \"UserList\",       //0x00, 0x3B\r\n        \"Validity\",       //0x00, 0x3C\r\n        \"Value\",          //0x00, 0x3D\r\n    };\r\n    \r\n    public static final String [] tagTablePage1 = {\r\n        /* Access ... continue on Page 0x0A */\r\n        \"AllFunctions\",             //  0x01, 0x05\r\n        \"AllFunctionsRequest\",      //  0x01, 0x06\r\n        \"CancelInvite-Request\",     //  0x01, 0x07\r\n        \"CancelInviteUser-Request\", //  0x01, 0x08\r\n        \"Capability\",               //  0x01, 0x09\r\n        \"CapabilityList\",           //  0x01, 0x0A\r\n        \"CapabilityRequest\",        //  0x01, 0x0B\r\n        \"ClientCapability-Request\", //  0x01, 0x0C\r\n        \"ClientCapability-Response\",//  0x01, 0x0D\r\n        \"DigestBytes\",          //  0x01, 0x0E\r\n        \"DigestSchema\",         //  0x01, 0x0F\r\n        \"Disconnect\",           //  0x01, 0x10\r\n        \"Functions\",            //  0x01, 0x11\r\n        \"GetSPInfo-Request\",    //  0x01, 0x12\r\n        \"GetSPInfo-Response\",   //  0x01, 0x13\r\n        \"InviteID\",             //  0x01, 0x14\r\n        \"InviteNote\",           //  0x01, 0x15\r\n        \"Invite-Request\",       //  0x01, 0x16\r\n        \"Invite-Response\",      //  0x01, 0x17\r\n        \"InviteType\",           //  0x01, 0x18\r\n        \"InviteUser-Request\",   //  0x01, 0x19\r\n        \"InviteUser-Response\",  //  0x01, 0x1A\r\n        \"KeepAlive-Request\",    //  0x01, 0x1B\r\n        \"KeepAliveTime\",        //  0x01, 0x1C\r\n        \"Login-Request\",        //  0x01, 0x1D\r\n        \"Login-Response\",       //  0x01, 0x1E\r\n        \"Logout-Request\",       //  0x01, 0x1F\r\n        \"Nonce\",                //  0x01, 0x20\r\n        \"Password\",             //  0x01, 0x21\r\n        \"Polling-Request\",      //  0x01, 0x22\r\n        \"ResponseNote\",         //  0x01, 0x23\r\n        \"SearchElement\",        //  0x01, 0x24\r\n        \"SearchFindings\",       //  0x01, 0x25\r\n        \"SearchID\",             //  0x01, 0x26\r\n        \"SearchIndex\",          //  0x01, 0x27\r\n        \"SearchLimit\",          //  0x01, 0x28\r\n        \"KeepAlive-Response\",   //  0x01, 0x29\r\n        \"SearchPairList\",       //  0x01, 0x2A\r\n        \"Search-Request\",       //  0x01, 0x2B\r\n        \"Search-Response\",      //  0x01, 0x2C\r\n        \"SearchResult\",         //  0x01, 0x2D\r\n        \"Service-Request\",      //  0x01, 0x2E\r\n        \"Service-Response\",     //  0x01, 0x2F\r\n        \"SessionCookie\",        //  0x01, 0x30\r\n        \"StopSearch-Request\",   //  0x01, 0x31\r\n        \"TimeToLive\",           //  0x01, 0x32\r\n        \"SearchString\",         //  0x01, 0x33\r\n        \"CompletionFlag\",       //  0x01, 0x34\r\n        null,                   //  0x01, 0x35\r\n        \"ReceiveList\",          //  0x01, 0x36 /* WV 1.2 */\r\n        \"VerifyID-Request\",     //  0x01, 0x37 /* WV 1.2 */\r\n        \"Extended-Request\",     //  0x01, 0x38 /* WV 1.2 */\r\n        \"Extended-Response\",    //  0x01, 0x39 /* WV 1.2 */\r\n        \"AgreedCapabilityList\", //  0x01, 0x3A /* WV 1.2 */\r\n        \"Extended-Data\",        //  0x01, 0x3B /* WV 1.2 */\r\n        \"OtherServer\",          //  0x01, 0x3C /* WV 1.2 */\r\n        \"PresenceAttributeNSName\",//0x01, 0x3D /* WV 1.2 */\r\n        \"SessionNSName\",        //  0x01, 0x3E /* WV 1.2 */\r\n        \"TransactionNSName\",    //  0x01, 0x3F /* WV 1.2 */\r\n    };\r\n    \r\n    public static final String [] tagTablePage2 = {\r\n        /* Service ... continue on Page 0x08 */\r\n        \"ADDGM\",        //  0x02, 0x05\r\n        \"AttListFunc\",  //  0x02, 0x06\r\n        \"BLENT\",        //  0x02, 0x07\r\n        \"CAAUT\",        //  0x02, 0x08\r\n        \"CAINV\",        //  0x02, 0x09\r\n        \"CALI\",         //  0x02, 0x0A\r\n        \"CCLI\",         //  0x02, 0x0B\r\n        \"ContListFunc\", //  0x02, 0x0C\r\n        \"CREAG\",        //  0x02, 0x0D\r\n        \"DALI\",         //  0x02, 0x0E\r\n        \"DCLI\",         //  0x02, 0x0F\r\n        \"DELGR\",        //  0x02, 0x10\r\n        \"FundamentalFeat\",//0x02, 0x11\r\n        \"FWMSG\",        //  0x02, 0x12\r\n        \"GALS\",         //  0x02, 0x13\r\n        \"GCLI\",         //  0x02, 0x14\r\n        \"GETGM\",        //  0x02, 0x15\r\n        \"GETGP\",        //  0x02, 0x16\r\n        \"GETLM\",        //  0x02, 0x17\r\n        \"GETM\",         //  0x02, 0x18\r\n        \"GETPR\",        //  0x02, 0x19\r\n        \"GETSPI\",       //  0x02, 0x1A\r\n        \"GETWL\",        //  0x02, 0x1B\r\n        \"GLBLU\",        //  0x02, 0x1C\r\n        \"GRCHN\",        //  0x02, 0x1D\r\n        \"GroupAuthFunc\",//  0x02, 0x1E\r\n        \"GroupFeat\",    //  0x02, 0x1F\r\n        \"GroupMgmtFunc\",//  0x02, 0x20\r\n        \"GroupUseFunc\", //  0x02, 0x21\r\n        \"IMAuthFunc\",   //  0x02, 0x22\r\n        \"IMFeat\",       //  0x02, 0x23\r\n        \"IMReceiveFunc\",//  0x02, 0x24\r\n        \"IMSendFunc\",   //  0x02, 0x25\r\n        \"INVIT\",        //  0x02, 0x26\r\n        \"InviteFunc\",   //  0x02, 0x27\r\n        \"MBRAC\",        //  0x02, 0x28\r\n        \"MCLS\",         //  0x02, 0x29\r\n        \"MDELIV\",       //  0x02, 0x2A\r\n        \"NEWM\",         //  0x02, 0x2B\r\n        \"NOTIF\",        //  0x02, 0x2C\r\n        \"PresenceAuthFunc\",//0x02, 0x2D\r\n        \"PresenceDeliverFunc\",//0x02, 0x2E\r\n        \"PresenceFeat\", //  0x02, 0x2F\r\n        \"REACT\",        //  0x02, 0x30\r\n        \"REJCM\",        //  0x02, 0x31\r\n        \"REJEC\",        //  0x02, 0x32\r\n        \"RMVGM\",        //  0x02, 0x33\r\n        \"SearchFunc\",   //  0x02, 0x34\r\n        \"ServiceFunc\",  //  0x02, 0x35\r\n        \"SETD\",         //  0x02, 0x36\r\n        \"SETGP\",        //  0x02, 0x37\r\n        \"SRCH\",         //  0x02, 0x38\r\n        \"STSRC\",        //  0x02, 0x39\r\n        \"SUBGCN\",       //  0x02, 0x3A\r\n        \"UPDPR\",        //  0x02, 0x3B\r\n        \"WVCSPFeat\",    //  0x02, 0x3C\r\n        \"MF\",           //  0x02, 0x3D /* WV 1.2 */\r\n        \"MG\",           //  0x02, 0x3E /* WV 1.2 */\r\n        \"MM\"            //  0x02, 0x3F /* WV 1.2 */\r\n    };\r\n    \r\n    public static final String [] tagTablePage3 = {\r\n        /* Client Capability */\r\n        \"AcceptedCharset\",          //  0x03, 0x05\r\n        \"AcceptedContentLength\",    //  0x03, 0x06\r\n        \"AcceptedContentType\",      //  0x03, 0x07\r\n        \"AcceptedTransferEncoding\", //  0x03, 0x08\r\n        \"AnyContent\",               //  0x03, 0x09\r\n        \"DefaultLanguage\",          //  0x03, 0x0A\r\n        \"InitialDeliveryMethod\",    //  0x03, 0x0B\r\n        \"MultiTrans\",               //  0x03, 0x0C\r\n        \"ParserSize\",               //  0x03, 0x0D\r\n        \"ServerPollMin\",            //  0x03, 0x0E\r\n        \"SupportedBearer\",          //  0x03, 0x0F\r\n        \"SupportedCIRMethod\",       //  0x03, 0x10\r\n        \"TCPAddress\",               //  0x03, 0x11\r\n        \"TCPPort\",                  //  0x03, 0x12\r\n        \"UDPPort\"                  //  0x03, 0x13\r\n    };\r\n    \r\n    public static final String [] tagTablePage4 = {\r\n        /* Presence Primitive */\r\n        \"CancelAuth-Request\",           //  0x04, 0x05\r\n        \"ContactListProperties\",        //  0x04, 0x06\r\n        \"CreateAttributeList-Request\",  //  0x04, 0x07\r\n        \"CreateList-Request\",           //  0x04, 0x08\r\n        \"DefaultAttributeList\",         //  0x04, 0x09\r\n        \"DefaultContactList\",           //  0x04, 0x0A\r\n        \"DefaultList\",                  //  0x04, 0x0B\r\n        \"DeleteAttributeList-Request\",  //  0x04, 0x0C\r\n        \"DeleteList-Request\",           //  0x04, 0x0D\r\n        \"GetAttributeList-Request\",     //  0x04, 0x0E\r\n        \"GetAttributeList-Response\",    //  0x04, 0x0F\r\n        \"GetList-Request\",              //  0x04, 0x10\r\n        \"GetList-Response\",             //  0x04, 0x11\r\n        \"GetPresence-Request\",          //  0x04, 0x12\r\n        \"GetPresence-Response\",         //  0x04, 0x13\r\n        \"GetWatcherList-Request\",       //  0x04, 0x14\r\n        \"GetWatcherList-Response\",      //  0x04, 0x15\r\n        \"ListManage-Request\",           //  0x04, 0x16\r\n        \"ListManage-Response\",          //  0x04, 0x17\r\n        \"UnsubscribePresence-Request\",  //  0x04, 0x18\r\n        \"PresenceAuth-Request\",         //  0x04, 0x19\r\n        \"PresenceAuth-User\",            //  0x04, 0x1A\r\n        \"PresenceNotification-Request\", //  0x04, 0x1B\r\n        \"UpdatePresence-Request\",       //  0x04, 0x1C\r\n        \"SubscribePresence-Request\",    //  0x04, 0x1D\r\n        \"Auto-Subscribe\",               //  0x04, 0x1E /* WV 1.2 */\r\n        \"GetReactiveAuthStatus-Request\",//  0x04, 0x1F /* WV 1.2 */\r\n        \"GetReactiveAuthStatus-Response\",// 0x04, 0x20 /* WV 1.2 */\r\n    };\r\n    \r\n    public static final String [] tagTablePage5 = {\r\n        /* Presence Attribute */\r\n        \"Accuracy\",         //  0x05, 0x05\r\n        \"Address\",          //  0x05, 0x06\r\n        \"AddrPref\",         //  0x05, 0x07\r\n        \"Alias\",            //  0x05, 0x08\r\n        \"Altitude\",         //  0x05, 0x09\r\n        \"Building\",         //  0x05, 0x0A\r\n        \"Caddr\",            //  0x05, 0x0B\r\n        \"City\",             //  0x05, 0x0C\r\n        \"ClientInfo\",       //  0x05, 0x0D\r\n        \"ClientProducer\",   //  0x05, 0x0E\r\n        \"ClientType\",       //  0x05, 0x0F\r\n        \"ClientVersion\",    //  0x05, 0x10\r\n        \"CommC\",            //  0x05, 0x11\r\n        \"CommCap\",          //  0x05, 0x12\r\n        \"ContactInfo\",      //  0x05, 0x13\r\n        \"ContainedvCard\",   //  0x05, 0x14\r\n        \"Country\",          //  0x05, 0x15\r\n        \"Crossing1\",        //  0x05, 0x16\r\n        \"Crossing2\",        //  0x05, 0x17\r\n        \"DevManufacturer\",  //  0x05, 0x18\r\n        \"DirectContent\",    //  0x05, 0x19\r\n        \"FreeTextLocation\", //  0x05, 0x1A\r\n        \"GeoLocation\",      //  0x05, 0x1B\r\n        \"Language\",         //  0x05, 0x1C\r\n        \"Latitude\",         //  0x05, 0x1D\r\n        \"Longitude\",        //  0x05, 0x1E\r\n        \"Model\",            //  0x05, 0x1F\r\n        \"NamedArea\",        //  0x05, 0x20\r\n        \"OnlineStatus\",     //  0x05, 0x21\r\n        \"PLMN\",             //  0x05, 0x22\r\n        \"PrefC\",            //  0x05, 0x23\r\n        \"PreferredContacts\",//  0x05, 0x24\r\n        \"PreferredLanguage\",//  0x05, 0x25\r\n        \"PreferredContent\", //  0x05, 0x26\r\n        \"PreferredvCard\",   //  0x05, 0x27\r\n        \"Registration\",     //  0x05, 0x28\r\n        \"StatusContent\",    //  0x05, 0x29\r\n        \"StatusMood\",       //  0x05, 0x2A\r\n        \"StatusText\",       //  0x05, 0x2B\r\n        \"Street\",           //  0x05, 0x2C\r\n        \"TimeZone\",         //  0x05, 0x2D\r\n        \"UserAvailability\", //  0x05, 0x2E\r\n        \"Cap\",              //  0x05, 0x2F\r\n        \"Cname\",            //  0x05, 0x30\r\n        \"Contact\",          //  0x05, 0x31\r\n        \"Cpriority\",        //  0x05, 0x32\r\n        \"Cstatus\",          //  0x05, 0x33\r\n        \"Note\",             //  0x05, 0x34 /* WV 1.2 */\r\n        \"Zone\",             //  0x05, 0x35\r\n        null,\r\n        \"Inf_link\",         //  0x05, 0x37 /* WV 1.2 */\r\n        \"InfoLink\",         //  0x05, 0x38 /* WV 1.2 */\r\n        \"Link\",             //  0x05, 0x39 /* WV 1.2 */\r\n        \"Text\",             //  0x05, 0x3A /* WV 1.2 */\r\n    };\r\n    \r\n    public static final String [] tagTablePage6 = {\r\n        /* Messaging */\r\n        \"BlockList\",                //  0x06, 0x05\r\n//      \"BlockUser-Request\",        //  0x06, 0x06  //This is a bug in the spec\r\n        \"BlockEntity-Request\",        //  0x06, 0x06  \r\n        \"DeliveryMethod\",           //  0x06, 0x07\r\n        \"DeliveryReport\",           //  0x06, 0x08\r\n        \"DeliveryReport-Request\",   //  0x06, 0x09\r\n        \"ForwardMessage-Request\",   //  0x06, 0x0A\r\n        \"GetBlockedList-Request\",   //  0x06, 0x0B\r\n        \"GetBlockedList-Response\",  //  0x06, 0x0C\r\n        \"GetMessageList-Request\",   //  0x06, 0x0D\r\n        \"GetMessageList-Response\",  //  0x06, 0x0E\r\n        \"GetMessage-Request\",       //  0x06, 0x0F\r\n        \"GetMessage-Response\",      //  0x06, 0x10\r\n        \"GrantList\",                //  0x06, 0x11\r\n        \"MessageDelivered\",         //  0x06, 0x12\r\n        \"MessageInfo\",              //  0x06, 0x13\r\n        \"MessageNotification\",      //  0x06, 0x14\r\n        \"NewMessage\",               //  0x06, 0x15\r\n        \"RejectMessage-Request\",    //  0x06, 0x16\r\n        \"SendMessage-Request\",      //  0x06, 0x17\r\n        \"SendMessage-Response\",     //  0x06, 0x18\r\n        \"SetDeliveryMethod-Request\",//  0x06, 0x19\r\n        \"DeliveryTime\",             //  0x06, 0x1A\r\n    };\r\n    \r\n    public static final String [] tagTablePage7 = {\r\n        /* Group */\r\n        \"AddGroupMembers-Request\",  //  0x07, 0x05\r\n        \"Admin\",                    //  0x07, 0x06\r\n        \"CreateGroup-Request\",      //  0x07, 0x07\r\n        \"DeleteGroup-Request\",      //  0x07, 0x08\r\n        \"GetGroupMembers-Request\",  //  0x07, 0x09\r\n        \"GetGroupMembers-Response\", //  0x07, 0x0A\r\n        \"GetGroupProps-Request\",    //  0x07, 0x0B\r\n        \"GetGroupProps-Response\",   //  0x07, 0x0C\r\n        \"GroupChangeNotice\",        //  0x07, 0x0D\r\n        \"GroupProperties\",          //  0x07, 0x0E\r\n        \"Joined\",                   //  0x07, 0x0F\r\n        \"JoinedRequest\",            //  0x07, 0x10\r\n        \"JoinGroup-Request\",        //  0x07, 0x11\r\n        \"JoinGroup-Response\",       //  0x07, 0x12\r\n        \"LeaveGroup-Request\",       //  0x07, 0x13\r\n        \"LeaveGroup-Response\",      //  0x07, 0x14\r\n        \"Left\",                     //  0x07, 0x15\r\n        \"MemberAccess-Request\",     //  0x07, 0x16\r\n        \"Mod\",                      //  0x07, 0x17\r\n        \"OwnProperties\",            //  0x07, 0x18\r\n        \"RejectList-Request\",       //  0x07, 0x19\r\n        \"RejectList-Response\",      //  0x07, 0x1A\r\n        \"RemoveGroupMembers-Request\",// 0x07, 0x1B\r\n        \"SetGroupProps-Request\",    //  0x07, 0x1C\r\n        \"SubscribeGroupNotice-Request\", //  0x07, 0x1D\r\n        \"SubscribeGroupNotice-Response\",//  0x07, 0x1E\r\n        \"Users\",                    //  0x07, 0x1F\r\n        \"WelcomeNote\",              //  0x07, 0x20\r\n        \"JoinGroup\",                //  0x07, 0x21\r\n        \"SubscribeNotification\",    //  0x07, 0x22\r\n        \"SubscribeType\",            //  0x07, 0x23\r\n        \"GetJoinedUsers-Request\",   //  0x07, 0x24 /* WV 1.2 */\r\n        \"GetJoinedUsers-Response\",  //  0x07, 0x25 /* WV 1.2 */\r\n        \"AdminMapList\",             //  0x07, 0x26 /* WV 1.2 */\r\n        \"AdminMapping\",             //  0x07, 0x27 /* WV 1.2 */\r\n        \"Mapping\",                  //  0x07, 0x28 /* WV 1.2 */\r\n        \"ModMapping\",               //  0x07, 0x29 /* WV 1.2 */\r\n        \"UserMapList\",              //  0x07, 0x2A /* WV 1.2 */\r\n        \"UserMapping\",              //  0x07, 0x2B /* WV 1.2 */\r\n    };\r\n    \r\n    public static final String [] tagTablePage8 = {\r\n        /* Service ... continued */\r\n        \"MP\",                       //  0x08, 0x05 /* WV 1.2 */\r\n        \"GETAUT\",                   //  0x08, 0x06 /* WV 1.2 */\r\n        \"GETJU\",                    //  0x08, 0x07 /* WV 1.2 */\r\n        \"VRID\",                     //  0x08, 0x08 /* WV 1.2 */\r\n        \"VerifyIDFunc\",             //  0x08, 0x09 /* WV 1.2 */\r\n    };\r\n    \r\n    public static final String [] tagTablePage9 = {\r\n        /* Common ... continued */\r\n        \"CIR\",                      //  0x09, 0x05 /* WV 1.2 */\r\n        \"Domain\",                   //  0x09, 0x06 /* WV 1.2 */\r\n        \"ExtBlock\",                 //  0x09, 0x07 /* WV 1.2 */\r\n        \"HistoryPeriod\",            //  0x09, 0x08 /* WV 1.2 */\r\n        \"IDList\",                   //  0x09, 0x09 /* WV 1.2 */\r\n        \"MaxWatcherList\",           //  0x09, 0x0A /* WV 1.2 */\r\n        \"ReactiveAuthState\",        //  0x09, 0x0B /* WV 1.2 */\r\n        \"ReactiveAuthStatus\",       //  0x09, 0x0C /* WV 1.2 */\r\n        \"ReactiveAuthStatusList\",   //  0x09, 0x0D /* WV 1.2 */\r\n        \"Watcher\",                  //  0x09, 0x0E /* WV 1.2 */\r\n        \"WatcherStatus\"             //  0x09, 0x0F /* WV 1.2 */\r\n    };\r\n    \r\n    public static final String [] tagTablePageA = {\r\n        /* Access ... continued */\r\n        \"WV-CSP-NSDiscovery-Request\",  //0x0A, 0x05 /* WV 1.2 */\r\n        \"WV-CSP-NSDiscovery-Response\", //0x0A, 0x06 /* WV 1.2 */\r\n        \"VersionList\"                  //0x0A, 0x07 /* WV 1.2 */\r\n    };\r\n    \r\n    public static final String [] attrStartTable = {\r\n        \"xmlns=http://www.wireless-village.org/CSP\",//  0x00, 0x05\r\n        \"xmlns=http://www.wireless-village.org/PA\", //  0x00, 0x06\r\n        \"xmlns=http://www.wireless-village.org/TRC\",//  0x00, 0x07\r\n        \"xmlns=http://www.openmobilealliance.org/DTD/WV-CSP\",   //  0x00, 0x08\r\n        \"xmlns=http://www.openmobilealliance.org/DTD/WV-PA\",    //  0x00, 0x09\r\n        \"xmlns=http://www.openmobilealliance.org/DTD/WV-TRC\",   //  0x00, 0x0A\r\n    };\r\n    \r\n    public static final String [] attrValueTable = {\r\n      \r\n        \"AccessType\",                           // 0x00 /* Common value token */\r\n        \"ActiveUsers\",                          // 0x01 /* Common value token */\r\n        \"Admin\",                                // 0x02 /* Common value token */\r\n        \"application/\",                         // 0x03 /* Common value token */\r\n        \"application/vnd.wap.mms-message\",      // 0x04 /* Common value token */\r\n        \"application/x-sms\",                    // 0x05 /* Common value token */\r\n        \"AutoJoin\",                             // 0x06 /* Common value token */\r\n        \"BASE64\",                               // 0x07 /* Common value token */\r\n        \"Closed\",                               // 0x08 /* Common value token */\r\n        \"Default\",                              // 0x09 /* Common value token */\r\n        \"DisplayName\",                          // 0x0a /* Common value token */\r\n        \"F\",                                    // 0x0b /* Common value token */\r\n        \"G\",                                    // 0x0c /* Common value token */\r\n        \"GR\",                                   // 0x0d /* Common value token */\r\n        \"http://\",                              // 0x0e /* Common value token */\r\n        \"https://\",                             // 0x0f /* Common value token */\r\n        \"image/\",                               // 0x10 /* Common value token */\r\n        \"Inband\",                               // 0x11 /* Common value token */\r\n        \"IM\",                                   // 0x12 /* Common value token */\r\n        \"MaxActiveUsers\",                       // 0x13 /* Common value token */\r\n        \"Mod\",                                  // 0x14 /* Common value token */\r\n        \"Name\",                                 // 0x15 /* Common value token */\r\n        \"None\",                                 // 0x16 /* Common value token */\r\n        \"N\",                                    // 0x17 /* Common value token */\r\n        \"Open\",                                 // 0x18 /* Common value token */\r\n        \"Outband\",                              // 0x19 /* Common value token */\r\n        \"PR\",                                   // 0x1a /* Common value token */\r\n        \"Private\",                              // 0x1b /* Common value token */\r\n        \"PrivateMessaging\",                     // 0x1c /* Common value token */\r\n        \"PrivilegeLevel\",                       // 0x1d /* Common value token */\r\n        \"Public\",                               // 0x1e /* Common value token */\r\n        \"P\",                                    // 0x1f /* Common value token */\r\n        \"Request\",                              // 0x20 /* Common value token */\r\n        \"Response\",                             // 0x21 /* Common value token */\r\n        \"Restricted\",                           // 0x22 /* Common value token */\r\n        \"ScreenName\",                           // 0x23 /* Common value token */\r\n        \"Searchable\",                           // 0x24 /* Common value token */\r\n        \"S\",                                    // 0x25 /* Common value token */\r\n        \"SC\",                                   // 0x26 /* Common value token */\r\n        \"text/\",                                // 0x27 /* Common value token */\r\n        \"text/plain\",                           // 0x28 /* Common value token */\r\n        \"text/x-vCalendar\",                     // 0x29 /* Common value token */\r\n        \"text/x-vCard\",                         // 0x2a /* Common value token */\r\n        \"Topic\",                                // 0x2b /* Common value token */\r\n        \"T\",                                    // 0x2c /* Common value token */\r\n        \"Type\",                                 // 0x2d /* Common value token */\r\n        \"U\",                                    // 0x2e /* Common value token */\r\n        \"US\",                                   // 0x2f /* Common value token */\r\n        \"www.wireless-village.org\",             // 0x30 /* Common value token */\r\n        \"AutoDelete\",                           // 0x31 /* Common value token */ /* WV 1.2 */\r\n        \"GM\",                                   // 0x32 /* Common value token */ /* WV 1.2 */\r\n        \"Validity\",                             // 0x33 /* Common value token */ /* WV 1.2 */\r\n        \"ShowID\",                               // 0x34 /* Common value token */ /* WV 1.2 */\r\n        \"GRANTED\",                              // 0x35 /* Common value token */ /* WV 1.2 */\r\n        \"PENDING\",                              // 0x36 /* Common value token */ /* WV 1.2 */\r\n        null,                                   // 0x37\r\n        null,                                   // 0x38\r\n        null,                                   // 0x39\r\n        null,                                   // 0x3a\r\n        null,                                   // 0x3b\r\n        null,                                   // 0x3c\r\n        \"GROUP_ID\",                             // 0x3d /* Access value token */\r\n        \"GROUP_NAME\",                           // 0x3e /* Access value token */\r\n        \"GROUP_TOPIC\",                          // 0x3f /* Access value token */\r\n        \"GROUP_USER_ID_JOINED\",                 // 0x40 /* Access value token */\r\n        \"GROUP_USER_ID_OWNER\",                  // 0x41 /* Access value token */\r\n        \"HTTP\",                                 // 0x42 /* Access value token */\r\n        \"SMS\",                                  // 0x43 /* Access value token */\r\n        \"STCP\",                                 // 0x44 /* Access value token */\r\n        \"SUDP\",                                 // 0x45 /* Access value token */\r\n        \"USER_ALIAS\",                           // 0x46 /* Access value token */\r\n        \"USER_EMAIL_ADDRESS\",                   // 0x47 /* Access value token */\r\n        \"USER_FIRST_NAME\",                      // 0x48 /* Access value token */\r\n        \"USER_ID\",                              // 0x49 /* Access value token */\r\n        \"USER_LAST_NAME\",                       // 0x4a /* Access value token */\r\n        \"USER_MOBILE_NUMBER\",                   // 0x4b /* Access value token */\r\n        \"USER_ONLINE_STATUS\",                   // 0x4c /* Access value token */\r\n        \"WAPSMS\",                               // 0x4d /* Access value token */\r\n        \"WAPUDP\",                               // 0x4e /* Access value token */\r\n        \"WSP\",                                  // 0x4f /* Access value token */\r\n        \"GROUP_USER_ID_AUTOJOIN\",               // 0x50 /* Access value token */ /* WV 1.2 */\r\n        null,                                   // 0x51\r\n        null,                                   // 0x52\r\n        null,                                   // 0x53\r\n        null,                                   // 0x54\r\n        null,                                   // 0x55\r\n        null,                                   // 0x56\r\n        null,                                   // 0x57\r\n        null,                                   // 0x58\r\n        null,                                   // 0x59\r\n        null,                                   // 0x5a\r\n        \"ANGRY\",                                // 0x5b /* Presence value token */\r\n        \"ANXIOUS\",                              // 0x5c /* Presence value token */\r\n        \"ASHAMED\",                              // 0x5d /* Presence value token */\r\n        \"AUDIO_CALL\",                           // 0x5e /* Presence value token */\r\n        \"AVAILABLE\",                            // 0x5f /* Presence value token */\r\n        \"BORED\",                                // 0x60 /* Presence value token */\r\n        \"CALL\",                                 // 0x61 /* Presence value token */\r\n        \"CLI\",                                  // 0x62 /* Presence value token */\r\n        \"COMPUTER\",                             // 0x63 /* Presence value token */\r\n        \"DISCREET\",                             // 0x64 /* Presence value token */\r\n        \"EMAIL\",                                // 0x65 /* Presence value token */\r\n        \"EXCITED\",                              // 0x66 /* Presence value token */\r\n        \"HAPPY\",                                // 0x67 /* Presence value token */\r\n        \"IM\",                                   // 0x68 /* Presence value token */\r\n        \"IM_OFFLINE\",                           // 0x69 /* Presence value token */\r\n        \"IM_ONLINE\",                            // 0x6a /* Presence value token */\r\n        \"IN_LOVE\",                              // 0x6b /* Presence value token */\r\n        \"INVINCIBLE\",                           // 0x6c /* Presence value token */\r\n        \"JEALOUS\",                              // 0x6d /* Presence value token */\r\n        \"MMS\",                                  // 0x6e /* Presence value token */\r\n        \"MOBILE_PHONE\",                         // 0x6f /* Presence value token */\r\n        \"NOT_AVAILABLE\",                        // 0x70 /* Presence value token */\r\n        \"OTHER\",                                // 0x71 /* Presence value token */\r\n        \"PDA\",                                  // 0x72 /* Presence value token */\r\n        \"SAD\",                                  // 0x73 /* Presence value token */\r\n        \"SLEEPY\",                               // 0x74 /* Presence value token */\r\n        \"SMS\",                                  // 0x75 /* Presence value token */\r\n        \"VIDEO_CALL\",                           // 0x76 /* Presence value token */\r\n        \"VIDEO_STREAM\",                         // 0x77 /* Presence value token */\r\n    };\r\n    \r\n    \r\n}"
  },
  {
    "path": "src/main/resources/META-INF/services/org.xmlpull.v1.XmlPullParserFactory",
    "content": "org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer\r\n"
  },
  {
    "path": "src/main/resources/application-prod.yml",
    "content": "spring:\n  profiles:\n    active: prod\n\nreader:\n  app:\n    storagePath: 'storage'\n    showUI: false\n    debug: false\n    packaged: false\n    secure: false\n    inviteCode: \"\"\n    secureKey: \"\"\n    proxy: false\n    proxyType: \"HTTP\"\n    proxyHost: \"\"\n    proxyPort: \"\"\n    proxyUsername: \"\"\n    proxyPassword: \"\"\n    cacheChapterContent: true\n    userLimit: 50\n    userBookLimit: 200\n    debugLog: false\n    autoClearInactiveUser: 0\n\n  server:\n    port: 8080\n    webUrl: http://localhost:${reader.server.port}\n\nlogging:\n  path: \"./logs\""
  },
  {
    "path": "src/main/resources/application.yml",
    "content": "reader:\n  app:\n    storagePath: storage\n    showUI: false\n    debug: false\n    packaged: false\n    secure: false\n    inviteCode: \"\"\n    secureKey: \"\"\n    proxy: false\n    proxyType: \"HTTP\"\n    proxyHost: \"\"\n    proxyPort: \"\"\n    proxyUsername: \"\"\n    proxyPassword: \"\"\n    cacheChapterContent: true\n    userLimit: 50\n    userBookLimit: 200\n    debugLog: false\n    autoClearInactiveUser: 0\n\n  server:\n    port: 8080\n    webUrl: http://localhost:${reader.server.port}\n\nlogging:\n  path: \"./logs\""
  },
  {
    "path": "src/main/resources/banner.txt",
    "content": "██████  ███████  █████  ██████  ███████ ██████  \r\n██   ██ ██      ██   ██ ██   ██ ██      ██   ██ \r\n██████  █████   ███████ ██   ██ █████   ██████  \r\n██   ██ ██      ██   ██ ██   ██ ██      ██   ██ \r\n██   ██ ███████ ██   ██ ██████  ███████ ██   ██ \r\n                                                \r\n"
  },
  {
    "path": "src/main/resources/defaultData/txtTocRule.json",
    "content": "[\n    {\n        \"id\": -1,\n        \"enable\": true,\n        \"name\": \"目录(去空白)\",\n        \"rule\": \"(?<=[　\\\\s])(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\\\s{0,4}[\\\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$\",\n        \"serialNumber\": 0\n    },\n    {\n        \"id\": -2,\n        \"enable\": true,\n        \"name\": \"目录\",\n        \"rule\": \"^[ 　\\\\t]{0,4}(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\\\s{0,4}[\\\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$\",\n        \"serialNumber\": 1\n    },\n    {\n        \"id\": -3,\n        \"enable\": false,\n        \"name\": \"目录(匹配简介)\",\n        \"rule\": \"(?<=[　\\\\s])(?:(?:内容|文章)?简介|文案|前言|序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\\\s{0,4}[\\\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$\",\n        \"serialNumber\": 2\n    },\n    {\n        \"id\": -4,\n        \"enable\": false,\n        \"name\": \"目录(古典、轻小说备用)\",\n        \"rule\": \"^[ 　\\\\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\\\s{0,4}[\\\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|话|篇(?!张))).{0,30}$\",\n        \"serialNumber\": 3\n    },\n    {\n        \"id\": -5,\n        \"enable\": false,\n        \"name\": \"数字(纯数字标题)\",\n        \"rule\": \"(?<=[　\\\\s])\\\\d+\\\\.?[ 　\\\\t]{0,4}$\",\n        \"serialNumber\": 4\n    },\n    {\n        \"id\": -6,\n        \"enable\": true,\n        \"name\": \"数字 分隔符 标题名称\",\n        \"rule\": \"^[ 　\\\\t]{0,4}\\\\d{1,5}[：:,.， 、_—\\\\-].{1,30}$\",\n        \"serialNumber\": 5\n    },\n    {\n        \"id\": -7,\n        \"enable\": true,\n        \"name\": \"大写数字 分隔符 标题名称\",\n        \"rule\": \"^[ 　\\\\t]{0,4}(?:序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|[〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[ 、_—\\\\-].{1,30}$\",\n        \"serialNumber\": 6\n    },\n    {\n        \"id\": -8,\n        \"enable\": true,\n        \"name\": \"正文 标题/序号\",\n        \"rule\": \"^[ 　\\\\t]{0,4}正文[ 　]{1,4}.{0,20}$\",\n        \"serialNumber\": 7\n    },\n    {\n        \"id\": -9,\n        \"enable\": true,\n        \"name\": \"Chapter/Section/Part/Episode 序号 标题\",\n        \"rule\": \"^[ 　\\\\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|ＰＡＲＴ|[Nn][oO]\\\\.|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\\\s{0,4}\\\\d{1,4}.{0,30}$\",\n        \"serialNumber\": 8\n    },\n    {\n        \"id\": -10,\n        \"enable\": false,\n        \"name\": \"Chapter(去简介)\",\n        \"rule\": \"^[ 　\\\\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|ＰＡＲＴ|[Nn][Oo]\\\\.|[Ee]pisode)\\\\s{0,4}\\\\d{1,4}.{0,30}$\",\n        \"serialNumber\": 9\n    },\n    {\n        \"id\": -11,\n        \"enable\": true,\n        \"name\": \"特殊符号 序号 标题\",\n        \"rule\": \"(?<=[\\\\s　])[【〔〖「『〈［\\\\[](?:第|[Cc]hapter)[\\\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$\",\n        \"serialNumber\": 10\n    },\n    {\n        \"id\": -12,\n        \"enable\": false,\n        \"name\": \"特殊符号 标题(成对)\",\n        \"rule\": \"(?<=[\\\\s　]{0,4})(?:[\\\\[〈「『〖〔《（【\\\\(].{1,30}[\\\\)】）》〕〗』」〉\\\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[ 　]{0,4}$\",\n        \"serialNumber\": 11\n    },\n    {\n        \"id\": -13,\n        \"enable\":true,\n        \"name\": \"特殊符号 标题(单个)\",\n        \"rule\": \"(?<=[\\\\s　]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[ 　]{0,4}$\",\n        \"serialNumber\": 12\n    },\n    {\n        \"id\": -14,\n        \"enable\": true,\n        \"name\": \"章/卷 序号 标题\",\n        \"rule\": \"^[ \\\\t　]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|序言|卷首语|扉页|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[ 　]{0,4}.{0,30}$\",\n        \"serialNumber\": 13\n    },\n    {\n        \"id\": -15,\n        \"enable\":false,\n        \"name\": \"顶格标题\",\n        \"rule\": \"^\\\\S.{1,20}$\",\n        \"serialNumber\": 14\n    },\n    {\n        \"id\": -16,\n        \"enable\":false,\n        \"name\": \"双标题(前向)\",\n        \"rule\": \"(?m)(?<=[ \\\\t　]{0,4})第[\\\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\\\s　]{0,8}第[\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)\",\n        \"serialNumber\": 15\n    },\n    {\n        \"id\": -17,\n        \"enable\":false,\n        \"name\": \"双标题(后向)\",\n        \"rule\": \"(?m)(?<=[ \\\\t　]{0,4}第[\\\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\\\s　]{0,8})第[\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$\",\n        \"serialNumber\": 16\n    },\n    {\n        \"id\":-18,\n        \"enable\": true,\n        \"name\": \"标题 特殊符号 序号\",\n        \"rule\": \"^.{1,20}[(（][\\\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[)）][ 　\\t]{0,4}$\",\n        \"serialNumber\": 17\n    }\n]"
  },
  {
    "path": "src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oeb12.ent",
    "content": "<!--\n\nTitle:\n\n     Mnemonic Character Entities For the Open eBook Publication\n     Structure Version 1.2\n\n\nVersion:\n\n     1.2\n\n\nRevision:\n\n     20020930-x  (supercedes 20020424-x)\n\n\nRevision History Note:\n\n     This revision, 20020930-x, which supercedes the prior revision\n     20020424-x, updates: 1) an email address within this comment\n     prologue, and 2) the Unicode version number referenced in various\n     comments throughout this document. No changes whatsoever were\n     made to the parsed content of this DTD fragment.\n\n\nPrevious Version:\n\n     1.0.1 (Revision of 22-November-2000, \"Character Entities for\n            the Open eBook Publication Structure Version 1.0.1\")\n\n\nAuthors:\n\n     Version 1.0; 1.0.1\n\n          Gunter Hille <hille@abc.de>\n          Ben Trafford <ben@legendary.org>\n          Garret Wilson <garret@globalmentor.com>\n\n\n     This Version 1.2 updated and edited by:\n\n          Jon Noring <jon@noring.name>\n\n\nUsage:\n\n     <!ENTITY % OEBEntities\n              PUBLIC \"+//ISBN 0-9673008-1-9//DTD OEB 1.2 Entities//EN\"\n              \"http://openebook.org/dtds/oeb-1.2/oeb12.ent\">\n\n     %OEBEntities;\n\n\nSummary:\n\n     This DTD fragment exactly duplicates, with some reorganization,\n     correction, and reformatting of the descriptive text, the 253\n     character entity declarations in the XHTML 1.1 DTD. Refer to:\n\n          http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent\n          http://www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent\n          http://www.w3.org/TR/xhtml1/DTD/xhtml-special.ent\n\n\nRelation to OEBPS Version 1.0.1:\n\n     The 253 character entities declared herein include all 249 from\n     Version 1.0.1 plus four of the five pre-defined XML 1.0 character\n     entities of &amp;, &lt;, &gt;, &quot; (the fifth pre-defined XML\n     character entity, &apos;, is one of the 249 character entities\n     already declared in Version 1.0.1.)\n\n     The five pre-defined XML 1.0 character entities are included for\n     completeness and interoperability as recommended by W3C, and to\n     follow XHTML practice. (Further information on the purpose and\n     usage of these five pre-defined XML character entities, and the\n     normative reference, is given in the Usage Note below.)\n\n\nRelation to Unicode 3.2.0 and ISO/IEC 10646:\n\n     The mnemonic character entities declared herein substitute for\n     numeric character references, the numeric values for the\n     associated characters specified by Unicode (in turn, the Unicode\n     Character Data Set conforms with the ISO/IEC 10646 character set\n     which XML 1.0 specifies.) The current version of Unicode is\n     3.2.0. General information on Unicode, including information on\n     the latest version, is found at\n\n          http://www.unicode.org/\n\n     In addition, Unicode has categorized the massive number of\n     characters in its Character Database using two different systems:\n     Character Blocks and Script Names. These two systems are used\n     herein for general categorization of the 253 character entities.\n     The text files listing the code points for these two systems are:\n\n          http://www.unicode.org/Public/UNIDATA/Blocks.txt\n          http://www.unicode.org/Public/UNIDATA/Scripts.txt\n\n\nTutorial Note to Document Authors: Character Entity Usage\n\n     To insert the desired special character into the content of an\n     OEBPS Document or Package file (which are XML documents), prefix\n     the associated mnemonic character entity with the '&' character\n     and terminate with the ';' character.\n\n     Example: to insert the \"em dash\" character (which has the\n     mnemonic 'mdash'), use &mdash; .\n\n     If preferred, the character can instead be inserted using the\n     direct (Unicode) numerical character reference, the codes of\n     which are given herein (see the above note on Unicode.) So, for\n     the \"em dash\" character one can use, instead of &mdash;, either\n     the decimal &#8212; or the hexadecimal equivalent &#x2014; .\n\n     Importantly note that within the content (PCDATA) of all OEBPS\n     documents and package files, the special XML characters '&' and\n     '<', when intended to be used literally, MUST be represented with\n     the mnemonic character entities of &amp; and &lt; (or the numerical\n     character entity equivalents), respectively. In addition, it is\n     considered good practice to use the &gt; (or numerical equivalent)\n     for the '>' symbol, although it is not necessary except in very\n     unusual and rare circumstances. The two other special XML character\n     entities, apostrophe (&apos;) and quote (&quot;), are only\n     necessary within element attribute values to literally represent\n     these characters, and for similar non-content purposes.\n\n     (The normative reference on the five XML pre-defined mnemonic\n     character entities is given in Sections 2.4 and 4.6 of the XML\n     1.0 Specification, Second Edition:\n\n          http://www.w3.org/TR/2000/REC-xml-20001006\n\n     )\n\n\n     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n       Portions (C) International Organization for\n       Standardization 1986. Permission to copy in any\n       form is granted for use with conforming SGML\n       systems and applications as defined in ISO 8879,\n       provided this notice is included in all copies.\n     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\n-->\n\n<!--\n\n     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n       XML 1.0 Pre-Defined Character Entities\n     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n\n       Drawn From Unicode 3.2.0 Character Sets:\n\n             Block Name(s):  Basic Latin              (U+0000 to U+007F)\n            Script Name(s):  (none)\n\n-->\n\n\n<!ENTITY quot       \"&#34;\" ><!-- quotation mark\n                                  APL quote\n                                  ==================== U+0022 ISOnum -->\n\n<!ENTITY amp    \"&#38;#38;\" ><!-- ampersand\n                                  ==================== U+0026 ISOnum -->\n\n<!ENTITY apos       \"&#39;\" ><!-- apostrophe mark\n                                  ==================== U+0027 ISOnum -->\n\n<!ENTITY lt     \"&#38;#60;\" ><!-- less-than sign\n                                  ==================== U+003C ISOnum -->\n\n<!ENTITY gt         \"&#62;\" ><!-- greater-than sign\n                                  ==================== U+003E ISOnum -->\n\n\n<!--\n\n     +-+-+-+-+-+-+-+-+-+-+-+-+\n       Extended Latin Script\n     +-+-+-+-+-+-+-+-+-+-+-+-+\n\n       Drawn From Unicode 3.2.0 Character Sets:\n\n             Block Name(s):  Latin-1 Supplement       (U+0080 to U+00FF)\n                             Latin Extended-A         (U+0100 to U+017F)\n                             Latin Extended-B         (U+0180 to U+024F)\n            Script Name(s):  Latin\n\n-->\n\n\n<!ENTITY ordf      \"&#170;\" ><!-- feminine ordinal indicator\n                                  ==================== U+00AA ISOnum -->\n\n<!ENTITY ordm      \"&#186;\" ><!-- masculine ordinal indicator\n                                  ==================== U+00BA ISOnum -->\n\n<!ENTITY Agrave    \"&#192;\" ><!-- Latin capital letter A with grave\n                                  Latin capital letter A grave\n                                  =================== U+00C0 ISOlat1 -->\n\n<!ENTITY Aacute    \"&#193;\" ><!-- Latin capital letter A with acute\n                                  =================== U+00C1 ISOlat1 -->\n\n<!ENTITY Acirc     \"&#194;\" ><!-- Latin capital letter A with circumflex\n                                  =================== U+00C2 ISOlat1 -->\n\n<!ENTITY Atilde    \"&#195;\" ><!-- Latin capital letter A with tilde\n                                  =================== U+00C3 ISOlat1 -->\n\n<!ENTITY Auml      \"&#196;\" ><!-- Latin capital letter A with diaeresis\n                                  =================== U+00C4 ISOlat1 -->\n\n<!ENTITY Aring     \"&#197;\" ><!-- Latin capital letter A with ring above\n                                  Latin capital letter A ring\n                                  =================== U+00C5 ISOlat1 -->\n\n<!ENTITY AElig     \"&#198;\" ><!-- Latin capital letter AE\n                                  Latin capital ligature AE\n                                  =================== U+00C6 ISOlat1 -->\n\n<!ENTITY Ccedil    \"&#199;\" ><!-- Latin capital letter C with cedilla\n                                  =================== U+00C7 ISOlat1 -->\n\n<!ENTITY Egrave    \"&#200;\" ><!-- Latin capital letter E with grave\n                                  =================== U+00C8 ISOlat1 -->\n\n<!ENTITY Eacute    \"&#201;\" ><!-- Latin capital letter E with acute\n                                  =================== U+00C9 ISOlat1 -->\n\n<!ENTITY Ecirc     \"&#202;\" ><!-- Latin capital letter E with circumflex\n                                  =================== U+00CA ISOlat1 -->\n\n<!ENTITY Euml      \"&#203;\" ><!-- Latin capital letter E with diaeresis\n                                  =================== U+00CB ISOlat1 -->\n\n<!ENTITY Igrave    \"&#204;\" ><!-- Latin capital letter I with grave\n                                  =================== U+00CC ISOlat1 -->\n\n<!ENTITY Iacute    \"&#205;\" ><!-- Latin capital letter I with acute\n                                  =================== U+00CD ISOlat1 -->\n\n<!ENTITY Icirc     \"&#206;\" ><!-- Latin capital letter I with circumflex\n                                  =================== U+00CE ISOlat1 -->\n\n<!ENTITY Iuml      \"&#207;\" ><!-- Latin capital letter I with diaeresis\n                                  =================== U+00CF ISOlat1 -->\n\n<!ENTITY ETH       \"&#208;\" ><!-- Latin capital letter ETH\n                                  =================== U+00D0 ISOlat1 -->\n\n<!ENTITY Ntilde    \"&#209;\" ><!-- Latin capital letter N with tilde\n                                  =================== U+00D1 ISOlat1 -->\n\n<!ENTITY Ograve    \"&#210;\" ><!-- Latin capital letter O with grave\n                                  =================== U+00D2 ISOlat1 -->\n\n<!ENTITY Oacute    \"&#211;\" ><!-- Latin capital letter O with acute\n                                  =================== U+00D3 ISOlat1 -->\n\n<!ENTITY Ocirc     \"&#212;\" ><!-- Latin capital letter O with circumflex\n                                  =================== U+00D4 ISOlat1 -->\n\n<!ENTITY Otilde    \"&#213;\" ><!-- Latin capital letter O with tilde\n                                  =================== U+00D5 ISOlat1 -->\n\n<!ENTITY Ouml      \"&#214;\" ><!-- Latin capital letter O with diaeresis\n                                  =================== U+00D6 ISOlat1 -->\n\n<!ENTITY Oslash    \"&#216;\" ><!-- Latin capital letter O with stroke\n                                  Latin capital letter O slash\n                                  =================== U+00D8 ISOlat1 -->\n\n<!ENTITY Ugrave    \"&#217;\" ><!-- Latin capital letter U with grave\n                                  =================== U+00D9 ISOlat1 -->\n\n<!ENTITY Uacute    \"&#218;\" ><!-- Latin capital letter U with acute\n                                  =================== U+00DA ISOlat1 -->\n\n<!ENTITY Ucirc     \"&#219;\" ><!-- Latin capital letter U with circumflex\n                                  =================== U+00DB ISOlat1 -->\n\n<!ENTITY Uuml      \"&#220;\" ><!-- Latin capital letter U with diaeresis\n                                  =================== U+00DC ISOlat1 -->\n\n<!ENTITY Yacute    \"&#221;\" ><!-- Latin capital letter Y with acute\n                                  =================== U+00DD ISOlat1 -->\n\n<!ENTITY THORN     \"&#222;\" ><!-- Latin capital letter THORN\n                                  =================== U+00DE ISOlat1 -->\n\n<!ENTITY szlig     \"&#223;\" ><!-- Latin small letter sharp s\n                                  ess-zed\n                                  =================== U+00DF ISOlat1 -->\n\n<!ENTITY agrave    \"&#224;\" ><!-- Latin small letter a with grave\n                                  Latin small letter a grave\n                                  =================== U+00E0 ISOlat1 -->\n\n<!ENTITY aacute    \"&#225;\" ><!-- Latin small letter a with acute\n                                  =================== U+00E1 ISOlat1 -->\n\n<!ENTITY acirc     \"&#226;\" ><!-- Latin small letter a with circumflex\n                                  =================== U+00E2 ISOlat1 -->\n\n<!ENTITY atilde    \"&#227;\" ><!-- Latin small letter a with tilde\n                                  =================== U+00E3 ISOlat1 -->\n\n<!ENTITY auml      \"&#228;\" ><!-- Latin small letter a with diaeresis\n                                  =================== U+00E4 ISOlat1 -->\n\n<!ENTITY aring     \"&#229;\" ><!-- Latin small letter a with ring above\n                                  Latin small letter a ring\n                                  =================== U+00E5 ISOlat1 -->\n\n<!ENTITY aelig     \"&#230;\" ><!-- Latin small letter ae\n                                  Latin small ligature ae\n                                  =================== U+00E6 ISOlat1 -->\n\n<!ENTITY ccedil    \"&#231;\" ><!-- Latin small letter c with cedilla\n                                  =================== U+00E7 ISOlat1 -->\n\n<!ENTITY egrave    \"&#232;\" ><!-- Latin small letter e with grave\n                                  =================== U+00E8 ISOlat1 -->\n\n<!ENTITY eacute    \"&#233;\" ><!-- Latin small letter e with acute\n                                  =================== U+00E9 ISOlat1 -->\n\n<!ENTITY ecirc     \"&#234;\" ><!-- Latin small letter e with circumflex\n                                  =================== U+00EA ISOlat1 -->\n\n<!ENTITY euml      \"&#235;\" ><!-- Latin small letter e with diaeresis\n                                  =================== U+00EB ISOlat1 -->\n\n<!ENTITY igrave    \"&#236;\" ><!-- Latin small letter i with grave\n                                  =================== U+00EC ISOlat1 -->\n\n<!ENTITY iacute    \"&#237;\" ><!-- Latin small letter i with acute\n                                  =================== U+00ED ISOlat1 -->\n\n<!ENTITY icirc     \"&#238;\" ><!-- Latin small letter i with circumflex\n                                  =================== U+00EE ISOlat1 -->\n\n<!ENTITY iuml      \"&#239;\" ><!-- Latin small letter i with diaeresis\n                                  =================== U+00EF ISOlat1 -->\n\n<!ENTITY eth       \"&#240;\" ><!-- Latin small letter eth\n                                  =================== U+00F0 ISOlat1 -->\n\n<!ENTITY ntilde    \"&#241;\" ><!-- Latin small letter n with tilde\n                                  =================== U+00F1 ISOlat1 -->\n\n<!ENTITY ograve    \"&#242;\" ><!-- Latin small letter o with grave\n                                  =================== U+00F2 ISOlat1 -->\n\n<!ENTITY oacute    \"&#243;\" ><!-- Latin small letter o with acute\n                                  =================== U+00F3 ISOlat1 -->\n\n<!ENTITY ocirc     \"&#244;\" ><!-- Latin small letter o with circumflex\n                                  =================== U+00F4 ISOlat1 -->\n\n<!ENTITY otilde    \"&#245;\" ><!-- Latin small letter o with tilde\n                                  =================== U+00F5 ISOlat1 -->\n\n<!ENTITY ouml      \"&#246;\" ><!-- Latin small letter o with diaeresis\n                                  =================== U+00F6 ISOlat1 -->\n\n<!ENTITY oslash    \"&#248;\" ><!-- Latin small letter o with stroke\n                                  Latin small letter o slash\n                                  =================== U+00F8 ISOlat1 -->\n\n<!ENTITY ugrave    \"&#249;\" ><!-- Latin small letter u with grave\n                                  =================== U+00F9 ISOlat1 -->\n\n<!ENTITY uacute    \"&#250;\" ><!-- Latin small letter u with acute\n                                  =================== U+00FA ISOlat1 -->\n\n<!ENTITY ucirc     \"&#251;\" ><!-- Latin small letter u with circumflex\n                                  =================== U+00FB ISOlat1 -->\n\n<!ENTITY uuml      \"&#252;\" ><!-- Latin small letter u with diaeresis\n                                  =================== U+00FC ISOlat1 -->\n\n<!ENTITY yacute    \"&#253;\" ><!-- Latin small letter y with acute\n                                  =================== U+00FD ISOlat1 -->\n\n<!ENTITY thorn     \"&#254;\" ><!-- Latin small letter thorn with\n                                  =================== U+00FE ISOlat1 -->\n\n<!ENTITY yuml      \"&#255;\" ><!-- Latin small letter y with diaeresis\n                                  =================== U+00FF ISOlat1 -->\n\n<!ENTITY OElig     \"&#338;\" ><!-- Latin capital ligature OE\n                                  =================== U+0152 ISOlat2 -->\n\n<!ENTITY oelig     \"&#339;\" ><!-- Latin small ligature oe\n                                  =================== U+0153 ISOlat2 -->\n\n<!ENTITY Scaron    \"&#352;\" ><!-- Latin capital letter S with caron\n                                  =================== U+0160 ISOlat2 -->\n\n<!ENTITY scaron    \"&#353;\" ><!-- Latin small letter s with caron\n                                  =================== U+0161 ISOlat2 -->\n\n<!ENTITY Yuml      \"&#376;\" ><!-- Latin capital letter Y with diaeresis\n                                  =================== U+0178 ISOlat2 -->\n\n<!ENTITY fnof      \"&#402;\" ><!-- Latin small f with hook\n                                  function\n                                  florin\n                                  =================== U+0192 ISOtech -->\n\n\n<!--\n\n     +-+-+-+-+-+-+-+\n       Greek Script\n     +-+-+-+-+-+-+-+\n\n       Drawn From Unicode 3.2.0 Character Sets:\n\n             Block Name(s):  Greek                    (U+0370 to U+03FF)\n            Script Name(s):  Greek\n\n-->\n\n\n<!ENTITY Alpha     \"&#913;\" ><!-- Greek capital letter alpha\n                                  =========================== U+0391 -->\n\n<!ENTITY Beta      \"&#914;\" ><!-- Greek capital letter beta\n                                  =========================== U+0392 -->\n\n<!ENTITY Gamma     \"&#915;\" ><!-- Greek capital letter gamma\n                                  =================== U+0393 ISOgrk3 -->\n\n<!ENTITY Delta     \"&#916;\" ><!-- Greek capital letter delta\n                                  =================== U+0394 ISOgrk3 -->\n\n<!ENTITY Epsilon   \"&#917;\" ><!-- Greek capital letter epsilon\n                                  =========================== U+0395 -->\n\n<!ENTITY Zeta      \"&#918;\" ><!-- Greek capital letter zeta\n                                  =========================== U+0396 -->\n\n<!ENTITY Eta       \"&#919;\" ><!-- Greek capital letter eta\n                                  =========================== U+0397 -->\n\n<!ENTITY Theta     \"&#920;\" ><!-- Greek capital letter theta\n                                  =================== U+0398 ISOgrk3 -->\n\n<!ENTITY Iota      \"&#921;\" ><!-- Greek capital letter iota\n                                  =========================== U+0399 -->\n\n<!ENTITY Kappa     \"&#922;\" ><!-- Greek capital letter kappa\n                                  =========================== U+039A -->\n\n<!ENTITY Lambda    \"&#923;\" ><!-- Greek capital letter lambda\n                                  =================== U+039B ISOgrk3 -->\n\n<!ENTITY Mu        \"&#924;\" ><!-- Greek capital letter mu\n                                  =========================== U+039C -->\n\n<!ENTITY Nu        \"&#925;\" ><!-- Greek capital letter nu\n                                  =========================== U+039D -->\n\n<!ENTITY Xi        \"&#926;\" ><!-- Greek capital letter xi\n                                  =================== U+039E ISOgrk3 -->\n\n<!ENTITY Omicron   \"&#927;\" ><!-- Greek capital letter omicron\n                                  =========================== U+039F -->\n\n<!ENTITY Pi        \"&#928;\" ><!-- Greek capital letter pi\n                                  =================== U+03A0 ISOgrk3 -->\n\n<!ENTITY Rho       \"&#929;\" ><!-- Greek capital letter rho\n                                  =========================== U+03A1 -->\n\n<!ENTITY Sigma     \"&#931;\" ><!-- Greek capital letter sigma\n                                  =================== U+03A3 ISOgrk3 -->\n\n<!ENTITY Tau       \"&#932;\" ><!-- Greek capital letter tau\n                                  =========================== U+03A4 -->\n\n<!ENTITY Upsilon   \"&#933;\" ><!-- Greek capital letter upsilon\n                                  =================== U+03A5 ISOgrk3 -->\n\n<!ENTITY Phi       \"&#934;\" ><!-- Greek capital letter phi\n                                  =================== U+03A6 ISOgrk3 -->\n\n<!ENTITY Chi       \"&#935;\" ><!-- Greek capital letter chi\n                                  =========================== U+03A7 -->\n\n<!ENTITY Psi       \"&#936;\" ><!-- Greek capital letter psi\n                                  =================== U+03A8 ISOgrk3 -->\n\n<!ENTITY Omega     \"&#937;\" ><!-- Greek capital letter omega\n                                  =================== U+03A9 ISOgrk3 -->\n\n<!ENTITY alpha     \"&#945;\" ><!-- Greek small letter alpha\n                                  =================== U+03B1 ISOgrk3 -->\n\n<!ENTITY beta      \"&#946;\" ><!-- Greek small letter beta\n                                  =================== U+03B2 ISOgrk3 -->\n\n<!ENTITY gamma     \"&#947;\" ><!-- Greek small letter gamma\n                                  =================== U+03B3 ISOgrk3 -->\n\n<!ENTITY delta     \"&#948;\" ><!-- Greek small letter delta\n                                  =================== U+03B4 ISOgrk3 -->\n\n<!ENTITY epsilon   \"&#949;\" ><!-- Greek small letter epsilon\n                                  =================== U+03B5 ISOgrk3 -->\n\n<!ENTITY zeta      \"&#950;\" ><!-- Greek small letter zeta\n                                  =================== U+03B6 ISOgrk3 -->\n\n<!ENTITY eta       \"&#951;\" ><!-- Greek small letter eta\n                                  =================== U+03B7 ISOgrk3 -->\n\n<!ENTITY theta     \"&#952;\" ><!-- Greek small letter theta\n                                  =================== U+03B8 ISOgrk3 -->\n\n<!ENTITY iota      \"&#953;\" ><!-- Greek small letter iota\n                                  =================== U+03B9 ISOgrk3 -->\n\n<!ENTITY kappa     \"&#954;\" ><!-- Greek small letter kappa\n                                  =================== U+03BA ISOgrk3 -->\n\n<!ENTITY lambda    \"&#955;\" ><!-- Greek small letter lambda\n                                  =================== U+03BB ISOgrk3 -->\n\n<!ENTITY mu        \"&#956;\" ><!-- Greek small letter mu\n                                  =================== U+03BC ISOgrk3 -->\n\n<!ENTITY nu        \"&#957;\" ><!-- Greek small letter nu\n                                  =================== U+03BD ISOgrk3 -->\n\n<!ENTITY xi        \"&#958;\" ><!-- Greek small letter xi\n                                  =================== U+03BE ISOgrk3 -->\n\n<!ENTITY omicron   \"&#959;\" ><!-- Greek small letter omicron\n                                  ======================= U+03BF NEW -->\n\n<!ENTITY pi        \"&#960;\" ><!-- Greek small letter pi\n                                  =================== U+03C0 ISOgrk3 -->\n\n<!ENTITY rho       \"&#961;\" ><!-- Greek small letter rho\n                                  =================== U+03C1 ISOgrk3 -->\n\n<!ENTITY sigmaf    \"&#962;\" ><!-- Greek small letter final sigma\n                                  =================== U+03C2 ISOgrk3 -->\n\n<!ENTITY sigma     \"&#963;\" ><!-- Greek small letter sigma\n                                  =================== U+03C3 ISOgrk3 -->\n\n<!ENTITY tau       \"&#964;\" ><!-- Greek small letter tau\n                                  =================== U+03C4 ISOgrk3 -->\n\n<!ENTITY upsilon   \"&#965;\" ><!-- Greek small letter upsilon\n                                  =================== U+03C5 ISOgrk3 -->\n\n<!ENTITY phi       \"&#966;\" ><!-- Greek small letter phi\n                                  =================== U+03C6 ISOgrk3 -->\n\n<!ENTITY chi       \"&#967;\" ><!-- Greek small letter chi\n                                  =================== U+03C7 ISOgrk3 -->\n\n<!ENTITY psi       \"&#968;\" ><!-- Greek small letter psi\n                                  =================== U+03C8 ISOgrk3 -->\n\n<!ENTITY omega     \"&#969;\" ><!-- Greek small letter omega\n                                  =================== U+03C9 ISOgrk3 -->\n\n<!ENTITY thetasym  \"&#977;\" ><!-- Greek small letter theta symbol\n                                  ======================= U+03D1 NEW -->\n\n<!ENTITY upsih     \"&#978;\" ><!-- Greek upsilon with hook symbol\n                                  ======================= U+03D2 NEW -->\n\n<!ENTITY piv       \"&#982;\" ><!-- Greek pi symbol\n                                  =================== U+03D6 ISOgrk3 -->\n\n\n<!--\n\n     +-+-+-+-+-+-+-+-+-+-+-+\n       General Punctuation\n     +-+-+-+-+-+-+-+-+-+-+-+\n\n       Drawn From Unicode 3.2.0 Character Sets:\n\n             Block Name(s):  General Punctuation      (U+2000 to U+206F)\n            Script Name(s):  (none)\n\n-->\n\n\n<!ENTITY ensp     \"&#8194;\" ><!-- en space\n                                  ==================== U+2002 ISOpub -->\n\n<!ENTITY emsp     \"&#8195;\" ><!-- em space\n                                  ==================== U+2003 ISOpub -->\n\n<!ENTITY thinsp   \"&#8201;\" ><!-- thin space\n                                  ==================== U+2009 ISOpub -->\n\n<!ENTITY zwnj     \"&#8204;\" ><!-- zero width non-joiner\n                                  ============== U+200C NEW RFC 2070 -->\n\n<!ENTITY zwj      \"&#8205;\" ><!-- zero width joiner\n                                  ============== U+200D NEW RFC 2070 -->\n\n<!ENTITY lrm      \"&#8206;\" ><!-- left-to-right mark\n                                  ============== U+200E NEW RFC 2070 -->\n\n<!ENTITY rlm      \"&#8207;\" ><!-- right-to-left mark\n                                  ============== U+200F NEW RFC 2070 -->\n\n<!ENTITY ndash    \"&#8211;\" ><!-- en dash\n                                  ==================== U+2013 ISOpub -->\n\n<!ENTITY mdash    \"&#8212;\" ><!-- em dash\n                                  ==================== U+2014 ISOpub -->\n\n<!ENTITY lsquo    \"&#8216;\" ><!-- left single quotation mark\n                                  ==================== U+2018 ISOnum -->\n\n<!ENTITY rsquo    \"&#8217;\" ><!-- right single quotation mark\n                                  ==================== U+2019 ISOnum -->\n\n<!ENTITY sbquo    \"&#8218;\" ><!-- single low-9 quotation mark\n                                  ======================= U+201A NEW -->\n\n<!ENTITY ldquo    \"&#8220;\" ><!-- left double quotation mark\n                                  ==================== U+201C ISOnum -->\n\n<!ENTITY rdquo    \"&#8221;\" ><!-- right double quotation mark\n                                  ==================== U+201D ISOnum -->\n\n<!ENTITY bdquo    \"&#8222;\" ><!-- double low-9 quotation mark\n                                  ======================= U+201E NEW -->\n\n<!ENTITY dagger   \"&#8224;\" ><!-- dagger\n                                  ==================== U+2020 ISOpub -->\n\n<!ENTITY Dagger   \"&#8225;\" ><!-- double dagger\n                                  ==================== U+2021 ISOpub -->\n\n<!ENTITY bull     \"&#8226;\" ><!-- bullet\n                                  black small circle\n                                  ==================== U+2022 ISOpub -->\n                             <!-- bullet is NOT the same as U+2219,\n                                 'bullet operator' -->\n\n<!ENTITY hellip   \"&#8230;\" ><!-- horizontal ellipsis\n                                  three dot leader\n                                  ==================== U+2026 ISOpub -->\n\n<!ENTITY permil   \"&#8240;\" ><!-- per mille sign\n                                  =================== U+2030 ISOtech -->\n\n<!ENTITY prime    \"&#8242;\" ><!-- prime\n                                  minutes\n                                  feet\n                                  =================== U+2032 ISOtech -->\n\n<!ENTITY Prime    \"&#8243;\" ><!-- double prime\n                                  seconds\n                                  inches\n                                  =================== U+2033 ISOtech -->\n\n<!ENTITY lsaquo   \"&#8249;\" ><!-- single left-pointing angle quotation\n                                       mark\n                                  ============== U+2039 ISO proposed -->\n\n<!ENTITY rsaquo   \"&#8250;\" ><!-- single right-pointing angle quotation\n                                  ============== U+203A ISO proposed -->\n\n<!ENTITY oline    \"&#8254;\" ><!-- overline\n                                  spacing overscore\n                                  ======================= U+203E NEW -->\n\n<!ENTITY frasl    \"&#8260;\" ><!-- fraction slash\n                                  ======================= U+2044 NEW -->\n\n\n<!--\n\n     +-+-+-+-+-+-+-+-+-+-+\n       Spacing Modifiers\n     +-+-+-+-+-+-+-+-+-+-+\n\n       Drawn From Unicode 3.2.0 Character Sets:\n\n             Block Name(s):  Spacing Modifier Letters (U+0280 to U+02FF)\n            Script Name(s):  (none)\n\n       Note: The Spacing Modifier Letters are an unusual class of\n             characters. They are an assorted collection of small signs\n             used to indicate modifications of the preceding or\n             following character, and sometimes to be an independent\n             character. They differ from diacritical marks in that they\n             are treated as free-standing, independent characters, which\n             form part of the word and do not break up the word. They\n             have the \"letter\" property. Most of the characters are\n             phonetic modifiers. For further information, refer to\n             Section 7.8 of the Unicode 3.2 manual, an online version is\n             at http://www.unicode.org/unicode/uni2book/ch07.pdf .\n\n-->\n\n\n<!ENTITY circ      \"&#710;\" ><!-- modifier letter circumflex accent\n                                  ==================== U+02C6 ISOpub -->\n\n<!ENTITY tilde     \"&#732;\" ><!-- small tilde\n                                  ==================== U+02DC ISOdia -->\n\n\n<!--\n\n     +-+-+-+-+-+-+-+-+-+\n       Various Symbols\n     +-+-+-+-+-+-+-+-+-+\n\n       Drawn From Unicode 3.2.0 Character Sets:\n\n             Block Name(s):  Latin-1 Supplement       (U+0080 to U+00FF)\n                             Currency Symbols         (U+20A0 to U+20CF)\n                             Letterlike Symbols       (U+2100 to U+214F)\n                             Arrows                   (U+2190 to U+21FF)\n                             Mathematical Operators   (U+2200 to U+22FF)\n                             Miscellaneous Technical  (U+2300 to U+23FF)\n                             Geometric Shapes         (U+25A0 to U+25FF)\n                             Miscellaneous Symbols    (U+2600 to U+26FF)\n            Script Name(s):  (none, except Greek for \"micro\", U+00B5)\n\n-->\n\n\n<!ENTITY nbsp      \"&#160;\" ><!-- no-break space\n                                  non-breaking space\n                                  ==================== U+00A0 ISOnum -->\n\n<!ENTITY iexcl     \"&#161;\" ><!-- inverted exclamation mark\n                                  ==================== U+00A1 ISOnum -->\n\n<!ENTITY cent      \"&#162;\" ><!-- cent sign\n                                  ==================== U+00A2 ISOnum -->\n\n<!ENTITY pound     \"&#163;\" ><!-- pound sign\n                                  ==================== U+00A3 ISOnum -->\n\n<!ENTITY curren    \"&#164;\" ><!-- currency sign\n                                  ==================== U+00A4 ISOnum -->\n\n<!ENTITY yen       \"&#165;\" ><!-- yen sign\n                                  yuan sign\n                                  ==================== U+00A5 ISOnum -->\n\n<!ENTITY brvbar    \"&#166;\" ><!-- broken bar\n                                  broken vertical bar\n                                  ==================== U+00A6 ISOnum -->\n\n<!ENTITY sect      \"&#167;\" ><!-- section sign\n                                  ==================== U+00A7 ISOnum -->\n\n<!ENTITY uml       \"&#168;\" ><!-- diaeresis\n                                  spacing diaeresis\n                                  ==================== U+00A8 ISOdia -->\n\n<!ENTITY copy      \"&#169;\" ><!-- copyright sign\n                                  ==================== U+00A9 ISOnum -->\n\n<!ENTITY laquo     \"&#171;\" ><!-- left-pointing double angle quotation\n                                       mark\n                                  left pointing guillemet\n                                  ==================== U+00AB ISOnum -->\n\n<!ENTITY not       \"&#172;\" ><!-- not sign\n                                  ==================== U+00AC ISOnum -->\n\n<!ENTITY shy       \"&#173;\" ><!-- soft hyphen\n                                  discretionary hyphen\n                                  ==================== U+00AD ISOnum -->\n\n<!ENTITY reg       \"&#174;\" ><!-- registered sign\n                                  registered trade mark sign\n                                  ==================== U+00AE ISOnum -->\n\n<!ENTITY macr      \"&#175;\" ><!-- macron\n                                  spacing macron\n                                  overline\n                                  APL overbar\n                                  ==================== U+00AF ISOdia -->\n\n<!ENTITY deg       \"&#176;\" ><!-- degree sign\n                                  ==================== U+00B0 ISOnum -->\n\n<!ENTITY plusmn    \"&#177;\" ><!-- plus-minus sign\n                                  plus-or-minus sign\n                                  ==================== U+00B1 ISOnum -->\n\n<!ENTITY sup2      \"&#178;\" ><!-- superscript two\n                                  superscript digit two\n                                  squared\n                                  ==================== U+00B2 ISOnum -->\n\n<!ENTITY sup3      \"&#179;\" ><!-- superscript three\n                                  superscript digit three\n                                  cubed\n                                  ==================== U+00B3 ISOnum -->\n\n<!ENTITY acute     \"&#180;\" ><!-- acute accent\n                                  spacing acute\n                                  ==================== U+00B4 ISOdia -->\n\n<!ENTITY micro     \"&#181;\" ><!-- micro sign\n                                  ==================== U+00B5 ISOnum -->\n\n<!ENTITY para      \"&#182;\" ><!-- pilcrow sign\n                                  paragraph sign\n                                  ==================== U+00B6 ISOnum -->\n\n<!ENTITY middot    \"&#183;\" ><!-- middle dot\n                                  Georgian comma\n                                  Greek middle dot\n                                  ==================== U+00B7 ISOnum -->\n\n<!ENTITY cedil     \"&#184;\" ><!-- cedilla\n                                  spacing cedilla\n                                  ==================== U+00B8 ISOdia -->\n\n<!ENTITY sup1      \"&#185;\" ><!-- superscript one\n                                  superscript digit one\n                                  ==================== U+00B9 ISOnum -->\n\n<!ENTITY raquo     \"&#187;\" ><!-- right-pointing double angle quotation\n                                       mark\n                                  right pointing guillemet\n                                  ==================== U+00BB ISOnum -->\n\n<!ENTITY frac14    \"&#188;\" ><!-- vulgar fraction one quarter\n                                  fraction one quarter\n                                  ==================== U+00BC ISOnum -->\n\n<!ENTITY frac12    \"&#189;\" ><!-- vulgar fraction one half\n                                  fraction one half\n                                  ==================== U+00BD ISOnum -->\n\n<!ENTITY frac34    \"&#190;\" ><!-- vulgar fraction three quarters\n                                  fraction three quarters\n                                  ==================== U+00BE ISOnum -->\n\n<!ENTITY iquest    \"&#191;\" ><!-- inverted question mark\n                                  turned question mark\n                                  ==================== U+00BF ISOnum -->\n\n<!ENTITY times     \"&#215;\" ><!-- multiplication sign\n                                  ==================== U+00D7 ISOnum -->\n\n<!ENTITY divide    \"&#247;\" ><!-- division sign\n                                  ==================== U+00F7 ISOnum -->\n\n<!ENTITY euro     \"&#8364;\" ><!-- euro sign\n                                  ======================= U+20AC NEW -->\n\n<!ENTITY image    \"&#8465;\" ><!-- blackletter capital I\n                                  imaginary part\n                                  =================== U+2111 ISOamso -->\n\n<!ENTITY weierp   \"&#8472;\" ><!-- script capital P\n                                  power set\n                                  Weierstrass p\n                                  =================== U+2118 ISOamso -->\n\n<!ENTITY real     \"&#8476;\" ><!-- blackletter capital R\n                                  real part symbol\n                                  =================== U+211C ISOamso -->\n\n<!ENTITY trade    \"&#8482;\" ><!-- trade mark sign\n                                  ==================== U+2122 ISOnum -->\n\n<!ENTITY alefsym  \"&#8501;\" ><!-- alef symbol\n                                  first transfinite cardinal\n                                  ======================= U+2135 NEW -->\n                             <!-- alef symbol is NOT the same as\n                                  U+05D0, 'Hebrew letter alef',\n                                  although the same glyph could be\n                                  used to represent both -->\n\n<!ENTITY larr     \"&#8592;\" ><!-- leftwards arrow\n                                  ==================== U+2190 ISOnum -->\n\n<!ENTITY uarr     \"&#8593;\" ><!-- upwards arrow\n                                  ==================== U+2191 ISOnum -->\n\n<!ENTITY rarr     \"&#8594;\" ><!-- rightwards arrow\n                                  ==================== U+2192 ISOnum -->\n\n<!ENTITY darr     \"&#8595;\" ><!-- downwards arrow\n                                  ==================== U+2193 ISOnum -->\n\n<!ENTITY harr     \"&#8596;\" ><!-- left right arrow\n                                  =================== U+2194 ISOamsa -->\n\n<!ENTITY crarr    \"&#8629;\" ><!-- downwards arrow with corner leftwards\n                                  carriage return\n                                  ======================= U+21B5 NEW -->\n\n<!ENTITY lArr     \"&#8656;\" ><!-- leftwards double arrow\n                                  =================== U+21D0 ISOtech -->\n                             <!-- Unicode does not say that lArr is\n                                  the same as the 'is implied by'\n                                  arrow, but also does not have any\n                                  other character for that function.\n                                  As ISOtech suggests, lArr can be\n                                  used for 'is implied by'. -->\n\n<!ENTITY uArr     \"&#8657;\" ><!-- upwards double arrow\n                                  =================== U+21D1 ISOamsa -->\n\n<!ENTITY rArr     \"&#8658;\" ><!-- rightwards double arrow\n                                  =================== U+21D2 ISOtech -->\n                             <!-- Unicode does not say that rArr is\n                                  the same as the 'implies' arrow,\n                                  but also does not have any other\n                                  character for that function. As\n                                  ISOtech suggests, rArr can be used\n                                  for 'implies'. -->\n\n<!ENTITY dArr     \"&#8659;\" ><!-- downwards double arrow\n                                  =================== U+21D3 ISOamsa -->\n\n<!ENTITY hArr     \"&#8660;\" ><!-- left right double arrow\n                                  =================== U+21D4 ISOamsa -->\n\n<!ENTITY forall   \"&#8704;\" ><!-- for all\n                                  =================== U+2200 ISOtech -->\n\n<!ENTITY part     \"&#8706;\" ><!-- partial differential\n                                  =================== U+2202 ISOtech -->\n\n<!ENTITY exist    \"&#8707;\" ><!-- there exists\n                                  =================== U+2203 ISOtech -->\n\n<!ENTITY empty    \"&#8709;\" ><!-- empty set\n                                  null set\n                                  diameter\n                                  =================== U+2205 ISOamso -->\n\n<!ENTITY nabla    \"&#8711;\" ><!-- nabla\n                                  backward difference\n                                  =================== U+2207 ISOtech -->\n\n<!ENTITY isin     \"&#8712;\" ><!-- element of\n                                  =================== U+2208 ISOtech -->\n\n<!ENTITY notin    \"&#8713;\" ><!-- not an element of\n                                  =================== U+2209 ISOtech -->\n\n<!ENTITY ni       \"&#8715;\" ><!-- contains as member\n                                  =================== U+220B ISOtech -->\n\n<!ENTITY prod     \"&#8719;\" ><!-- n-ary product\n                                  product sign\n                                  =================== U+220F ISOamsb -->\n                             <!-- prod is NOT the same character as\n                                  U+03A0, 'Greek capital letter pi',\n                                  although the same glyph could be\n                                  used to represent both -->\n\n<!ENTITY sum      \"&#8721;\" ><!-- n-ary summation\n                                  =================== U+2211 ISOamsb -->\n                             <!-- sum is NOT the same character as\n                                  U+03A3, 'Greek capital letter sigma',\n                                  although the same glyph could be\n                                  used to represent both -->\n\n<!ENTITY minus    \"&#8722;\" ><!-- minus sign\n                                  =================== U+2212 ISOtech -->\n\n<!ENTITY lowast   \"&#8727;\" ><!-- asterisk operator\n                                  =================== U+2217 ISOtech -->\n\n<!ENTITY radic    \"&#8730;\" ><!-- square root\n                                  radical sign\n                                  =================== U+221A ISOtech -->\n\n<!ENTITY prop     \"&#8733;\" ><!-- proportional to\n                                  =================== U+221D ISOtech -->\n\n<!ENTITY infin    \"&#8734;\" ><!-- infinity\n                                  =================== U+221E ISOtech -->\n\n<!ENTITY ang      \"&#8736;\" ><!-- angle\n                                  =================== U+2220 ISOamso -->\n\n<!ENTITY and      \"&#8743;\" ><!-- logical and\n                                  wedge\n                                  =================== U+2227 ISOtech -->\n\n<!ENTITY or       \"&#8744;\" ><!-- logical or\n                                  vee\n                                  =================== U+2228 ISOtech -->\n\n<!ENTITY cap      \"&#8745;\" ><!-- intersection\n                                  cap\n                                  =================== U+2229 ISOtech -->\n\n<!ENTITY cup      \"&#8746;\" ><!-- union\n                                  cup\n                                  =================== U+222A ISOtech -->\n\n<!ENTITY int      \"&#8747;\" ><!-- integral\n                                  =================== U+222B ISOtech -->\n\n<!ENTITY there4   \"&#8756;\" ><!-- therefore\n                                  =================== U+2234 ISOtech -->\n\n<!ENTITY sim      \"&#8764;\" ><!-- tilde operator\n                                  varies with\n                                  similar to\n                                  =================== U+223C ISOtech -->\n                             <!-- tilde operator is NOT the same\n                                  character as U+007E, 'tilde',\n                                  although the same glyph could be\n                                  used to represent both -->\n\n<!ENTITY cong     \"&#8773;\" ><!-- approximately equal to\n                                  =================== U+2245 ISOtech -->\n\n<!ENTITY asymp    \"&#8776;\" ><!-- almost equal to\n                                  asymptotic to\n                                  =================== U+2248 ISOamsr -->\n\n<!ENTITY ne       \"&#8800;\" ><!-- not equal to\n                                  =================== U+2260 ISOtech -->\n\n<!ENTITY equiv    \"&#8801;\" ><!-- identical to\n                                  =================== U+2261 ISOtech -->\n\n<!ENTITY le       \"&#8804;\" ><!-- less-than or equal to\n                                  =================== U+2264 ISOtech -->\n\n<!ENTITY ge       \"&#8805;\" ><!-- greater-than or equal to\n                                  =================== U+2265 ISOtech -->\n\n<!ENTITY sub      \"&#8834;\" ><!-- subset of\n                                  =================== U+2282 ISOtech -->\n\n<!ENTITY sup      \"&#8835;\" ><!-- superset of\n                                  =================== U+2283 ISOtech -->\n\n<!ENTITY nsub     \"&#8836;\" ><!-- not a subset of\n                                  =================== U+2284 ISOamsn -->\n\n<!ENTITY sube     \"&#8838;\" ><!-- subset of or equal to\n                                  =================== U+2286 ISOtech -->\n\n<!ENTITY supe     \"&#8839;\" ><!-- superset of or equal to\n                                  =================== U+2287 ISOtech -->\n\n<!ENTITY oplus    \"&#8853;\" ><!-- circled plus\n                                  direct sum\n                                  =================== U+2295 ISOamsb -->\n\n<!ENTITY otimes   \"&#8855;\" ><!-- circled times\n                                  vector product\n                                  =================== U+2297 ISOamsb -->\n\n<!ENTITY perp     \"&#8869;\" ><!-- up tack\n                                  orthogonal to\n                                  perpendicular\n                                  =================== U+22A5 ISOtech -->\n\n<!ENTITY sdot     \"&#8901;\" ><!-- dot operator\n                                  =================== U+22C5 ISOamsb -->\n                             <!-- dot operator is NOT the same\n                                  character as U+00B7, 'middle dot' -->\n\n<!ENTITY lceil    \"&#8968;\" ><!-- left ceiling\n                                  APL upstile\n                                  =================== U+2308 ISOamsc -->\n\n<!ENTITY rceil    \"&#8969;\" ><!-- right ceiling\n                                  =================== U+2309 ISOamsc -->\n\n<!ENTITY lfloor   \"&#8970;\" ><!-- left floor\n                                  APL downstile\n                                  =================== U+230A ISOamsc -->\n\n<!ENTITY rfloor   \"&#8971;\" ><!-- right floor\n                                  =================== U+230B ISOamsc -->\n\n<!ENTITY lang     \"&#9001;\" ><!-- left-pointing angle bracket\n                                  bra\n                                  =================== U+2329 ISOtech -->\n                             <!-- lang is NOT the same character as\n                                  U+003C, 'less than', or U+2039,\n                                 'single left-pointing angle quotation\n                                  mark' -->\n\n<!ENTITY rang     \"&#9002;\" ><!-- right-pointing angle bracket\n                                  ket\n                                  =================== U+232A ISOtech -->\n                             <!-- rang is NOT the same character as\n                                  U+003E, 'greater than', or U+203A,\n                                 'single right-pointing angle quotation\n                                  mark' -->\n\n<!ENTITY loz      \"&#9674;\" ><!-- lozenge\n                                  ==================== U+25CA ISOpub -->\n\n<!ENTITY spades   \"&#9824;\" ><!-- black spade suit\n                                  ==================== U+2660 ISOpub -->\n\n<!ENTITY clubs    \"&#9827;\" ><!-- black club suit\n                                  shamrock\n                                  ==================== U+2663 ISOpub -->\n\n<!ENTITY hearts   \"&#9829;\" ><!-- black heart suit\n                                  valentine\n                                  ==================== U+2665 ISOpub -->\n\n<!ENTITY diams    \"&#9830;\" ><!-- black diamond suit\n                                  ==================== U+2666 ISOpub -->\n"
  },
  {
    "path": "src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oebpkg12.dtd",
    "content": "<!--\n\nTitle:\n\n     The Package Document Type Definition (DTD) for the Open\n     eBook Publication Structure Version 1.2\n\n\nVersion:\n\n     1.2\n\n\nRevision:\n\n     20020930-x  (supercedes 20020605-x)\n\n\nRevision History Note:\n\n     This revision, 20020930-x, which supercedes the prior revision\n     20020605-x, solely updates the email addresses within this\n     comment prologue. No changes whatsover were made to the parsed\n     content of this DTD.\n\n\nPrevious Released Version:\n\n     1.0.1 (Revision of 01-February-2001, \"Document Type\n            Definition for the Open eBook package version\n            1.0.1\")\n\n\nAuthors:\n\n     Version 1.0; 1.0.1\n\n          Steve DeRose <sjd@stg.brown.edu>\n          Gunter Hille <hille@abc.de>\n          Ben Trafford <ben@legendary.org>\n          Garret Wilson <garret@globalmentor.com>\n\n     This Version 1.2 updated and edited by:\n\n          Jon Noring <jon@noring.name>\n          Benjamin Jung <benjamin.jung@deepx.com>\n\n\nUsage:\n\n     <?xml version=\"1.0\"?>\n     <!DOCTYPE package\n               PUBLIC \"+//ISBN 0-9673008-1-9//DTD OEB 1.2 Package//EN\"\n               \"http://openebook.org/dtds/oeb-1.2/oebpkg12.dtd\">\n     <package unique-identifier=\"foo\">\n          metadata\n          manifest\n          spine\n          tours\n          guide\n     </package>\n\n\nSummary Description:\n\n     This is the Package Document Type Definition (DTD) for\n     the Open eBook Publication Structure version 1.2.\n\n     Changes to this DTD from version 1.0.1 include:\n\n          a. Upgrading the <dc-metadata> content model to\n             fully conform with the OEBPS 1.2 specification\n             requirements. Specifically, <dc:Language> is\n             now required, while in OEBPS 1.0.1 it was\n             optional.\n\n          b. Updating the mnemonic character entity\n             declaration to refer to version 1.2.\n\n          c. Updating the xmlns:dc namespace to refer to\n             version 1.1 of the Dublin Core Metadata\n             Initiative.\n\n          d. Editing and updating the various non-parsed\n             comments.\n\n          e. Revising the layout (e.g., white space\n             alteration) to aid in readability.\n\n-->\n\n\n<!-- *************************************************** -->\n\n<!-- XHTML MNEMONIC CHARACTER ENTITIES ................. -->\n\n<!ENTITY % OEBEntities\n     PUBLIC \"+//ISBN 0-9673008-1-9//DTD OEB 1.2 Entities//EN\"\n     \"http://openebook.org/dtds/oeb-1.2/oeb12.ent\">\n\n%OEBEntities;\n\n<!-- *************************************************** -->\n\n<!-- DATATYPE ENTITIES ................................. -->\n\n<!-- Uniform Resource Identifier (URI), per [RFC2396] -->\n\n<!ENTITY % URI \"CDATA\">\n\n<!-- Language code, per [RFC3066] -->\n\n<!ENTITY % LanguageCode \"NMTOKEN\">\n\n<!-- *************************************************** -->\n\n<!-- NAMESPACE ENTITIES ................................ -->\n\n<!ENTITY % dc.xmlns\n     \"'http://purl.org/dc/elements/1.1/'\">\n\n<!ENTITY % oebpk.xmlns\n     \"'http://openebook.org/namespaces/oeb-package/1.0/'\">\n\n<!-- *************************************************** -->\n\n<!-- ELEMENT ENTITIES .................................. -->\n\n<!-- The entity 'DCMetadataOpt' includes the 12 optional\n     <dc:Xxx> children elements of <dc-metadata>. It will\n     be used in the <dc-metadata> content model. -->\n\n<!ENTITY % DCMetadataOpt\n     \"dc:Contributor |\n      dc:Coverage    |\n      dc:Creator     |\n      dc:Date        |\n      dc:Description |\n      dc:Format      |\n      dc:Publisher   |\n      dc:Relation    |\n      dc:Rights      |\n      dc:Source      |\n      dc:Subject     |\n      dc:Type        \">\n\n<!-- *************************************************** -->\n\n<!-- ATTRIBUTE ENTITIES ................................ -->\n\n<!ENTITY % CoreAttributes\n     \"id                 ID              #IMPLIED\">\n\n<!ENTITY % InternationalAttributes\n     \"xml:lang           %LanguageCode;  #IMPLIED\">\n\n<!ENTITY % CommonAttributes\n     \"%CoreAttributes;\n      %InternationalAttributes;\">\n\n<!-- 'DCNamespaceAttribute' is an attribute entity declaring\n     the Dublin Core namespace. Used on each <dc:Xxx> element\n     to accommodate XML parsers which unnecessarily require\n     this. -->\n\n<!ENTITY % DCNamespaceAttribute\n     \"xmlns:dc           %URI;           #FIXED %dc.xmlns;\">\n\n<!-- *************************************************** -->\n\n<!-- ELEMENTS AND ATTRIBUTES ........................... -->\n\n<!-- <package> must have as children elements, in this order:\n     <metadata>, <manifest>, and <spine>, and optionally may\n     include <tours> and/or <guide>. The 'unique-identifier'\n     attribute is required for <package> (see comment for\n     <dc:Identifier>.) -->\n\n<!ELEMENT package (metadata, manifest, spine, tours?, guide?)>\n<!ATTLIST package\n      %CommonAttributes;\n      unique-identifier  IDREF           #REQUIRED\n      xmlns              %URI;           #FIXED %oebpk.xmlns;>\n\n<!-- <metadata> must contain one <dc-metadata>, and\n     optionally contain one <x-metadata>. There are no\n     attributes for <metadata>. -->\n\n<!ELEMENT metadata (dc-metadata, x-metadata?)>\n\n<!-- <dc-metadata> must contain at least one <dc:Title>,\n     one <dc:Identifier>, and one <dc:Language>, and may\n     contain one or more of each of the other twelve\n     optional <dc:XXX> elements, all in any order. -->\n\n<!ELEMENT dc-metadata\n( (%DCMetadataOpt;)*,\n  ( (dc:Title, (%DCMetadataOpt; | dc:Title)*,\n      ( (dc:Identifier, (%DCMetadataOpt; | dc:Title | dc:Identifier)*,\n         dc:Language) |\n        (dc:Language, (%DCMetadataOpt; | dc:Title | dc:Language)*,\n         dc:Identifier) ) ) |\n    (dc:Identifier, (%DCMetadataOpt; | dc:Identifier)*,\n      ( (dc:Title, (%DCMetadataOpt; | dc:Identifier | dc:Title)*,\n         dc:Language) |\n\n        (dc:Language, (%DCMetadataOpt; | dc:Identifier | dc:Language)*,\n         dc:Title) ) ) |\n    (dc:Language, (%DCMetadataOpt; | dc:Language)*,\n      ( (dc:Identifier, (%DCMetadataOpt; | dc:Language | dc:Identifier)*,\n         dc:Title) |\n        (dc:Title, (%DCMetadataOpt; | dc:Language | dc:Title)*,\n         dc:Identifier) ) ) ),\n  (%DCMetadataOpt; | dc:Title | dc:Identifier | dc:Language)* )>\n\n<!ATTLIST dc-metadata\n      %CommonAttributes;\n      %DCNamespaceAttribute;\n      xmlns:oebpackage   %URI;           #FIXED %oebpk.xmlns;>\n\n<!-- Required elements for <dc-metadata>. -->\n\n<!ELEMENT dc:Title (#PCDATA)>\n<!ATTLIST dc:Title\n      %CommonAttributes;\n      %DCNamespaceAttribute;>\n\n<!-- One <dc:Identifier> must specify an 'id' identical to\n     the value of the required <package> 'unique-identifier'\n     attribute. -->\n\n<!ELEMENT dc:Identifier (#PCDATA)>\n<!ATTLIST dc:Identifier\n      %CommonAttributes;\n      %DCNamespaceAttribute;\n      scheme             NMTOKEN         #IMPLIED>\n\n<!ELEMENT dc:Language (#PCDATA)>\n<!ATTLIST dc:Language\n      %CommonAttributes;\n      %DCNamespaceAttribute;>\n\n<!-- Optional elements for <dc-metadata>. -->\n\n<!ELEMENT dc:Contributor (#PCDATA)>\n<!ATTLIST dc:Contributor\n      %CommonAttributes;\n      %DCNamespaceAttribute;\n      file-as            CDATA           #IMPLIED\n      role               NMTOKEN         #IMPLIED>\n\n<!ELEMENT dc:Coverage (#PCDATA)>\n<!ATTLIST dc:Coverage\n      %CommonAttributes;\n      %DCNamespaceAttribute;>\n\n<!ELEMENT dc:Creator (#PCDATA)>\n<!ATTLIST dc:Creator\n      %CommonAttributes;\n      %DCNamespaceAttribute;\n      file-as            CDATA           #IMPLIED\n      role               NMTOKEN         #IMPLIED>\n\n<!ELEMENT dc:Date (#PCDATA)>\n<!ATTLIST dc:Date\n      %CommonAttributes;\n      %DCNamespaceAttribute;\n      event              NMTOKEN         #IMPLIED>\n\n<!ELEMENT dc:Description (#PCDATA)>\n<!ATTLIST dc:Description\n      %CommonAttributes;\n      %DCNamespaceAttribute;>\n\n<!ELEMENT dc:Format (#PCDATA)>\n<!ATTLIST dc:Format\n      %CommonAttributes;\n      %DCNamespaceAttribute;>\n\n<!ELEMENT dc:Publisher (#PCDATA)>\n<!ATTLIST dc:Publisher\n      %CommonAttributes;\n      %DCNamespaceAttribute;>\n\n<!ELEMENT dc:Relation (#PCDATA)>\n<!ATTLIST dc:Relation\n      %CommonAttributes;\n      %DCNamespaceAttribute;>\n\n<!ELEMENT dc:Rights (#PCDATA)>\n<!ATTLIST dc:Rights\n      %CommonAttributes;\n      %DCNamespaceAttribute;>\n\n<!ELEMENT dc:Source (#PCDATA)>\n<!ATTLIST dc:Source\n      %CommonAttributes;\n      %DCNamespaceAttribute;>\n\n<!ELEMENT dc:Subject (#PCDATA)>\n<!ATTLIST dc:Subject\n      %CommonAttributes;\n      %DCNamespaceAttribute;>\n\n<!ELEMENT dc:Type (#PCDATA)>\n\n<!ATTLIST dc:Type\n      %CommonAttributes;\n      %DCNamespaceAttribute;>\n\n<!-- <x-metadata> must contain at least one <meta>. -->\n\n<!ELEMENT x-metadata (meta+)>\n<!ATTLIST x-metadata %CommonAttributes;>\n\n<!-- Note that 'content' and 'name' are required attributes\n     for <meta>. -->\n\n<!ELEMENT meta EMPTY>\n<!ATTLIST meta\n      %CommonAttributes;\n      content            CDATA           #REQUIRED\n      name               NMTOKEN         #REQUIRED\n      scheme             CDATA           #IMPLIED>\n\n<!-- <manifest> must contain at least one <item>. -->\n\n<!ELEMENT manifest (item+)>\n<!ATTLIST manifest %CommonAttributes;>\n\n<!-- Note that 'href', 'id' and 'media-type' are required\n     attributes for <item>. -->\n\n<!ELEMENT item EMPTY>\n<!ATTLIST item\n      %InternationalAttributes;\n      fallback           IDREF           #IMPLIED\n      href               %URI;           #REQUIRED\n      id                 ID              #REQUIRED\n      media-type         CDATA           #REQUIRED>\n\n<!-- <spine> must contain at least one <itemref>. -->\n\n<!ELEMENT spine (itemref+)>\n<!ATTLIST spine %CommonAttributes;>\n\n<!-- Note that 'idref' is a required attribute for\n     <itemref>. -->\n\n<!ELEMENT itemref EMPTY>\n<!ATTLIST itemref\n      %CommonAttributes;\n      idref              IDREF           #REQUIRED>\n\n<!-- <tours> must contain at least one <tour>. -->\n\n<!ELEMENT tours (tour+)>\n<!ATTLIST tours %CommonAttributes;>\n\n<!-- <tour> must contain at least one <site>. Note that\n     'title' is a required attribute for <tour>. -->\n\n<!ELEMENT tour (site+)>\n<!ATTLIST tour\n      %CommonAttributes;\n      title              CDATA           #REQUIRED>\n\n<!-- Note that 'href' and 'title' are required attributes\n     for <site>. -->\n\n<!ELEMENT site EMPTY>\n<!ATTLIST site\n      %CommonAttributes;\n      href               %URI;           #REQUIRED\n      title              CDATA           #REQUIRED>\n\n<!-- <guide> must contain at least one <reference>. -->\n\n<!ELEMENT guide (reference+)>\n<!ATTLIST guide %CommonAttributes;>\n\n<!-- Note that 'href', 'title' and 'type' are required\n     attributes for <reference>. -->\n\n<!ELEMENT reference EMPTY>\n<!ATTLIST reference\n      %CommonAttributes;\n      href               %URI;           #REQUIRED\n      title              CDATA           #REQUIRED\n      type               NMTOKEN         #REQUIRED>\n"
  },
  {
    "path": "src/main/resources/dtd/www.daisy.org/z3986/2005/ncx-2005-1.dtd",
    "content": "<!-- NCX 2005-1 DTD  2005-06-26\nfile: ncx-2005-1.dtd                                 \n\n  Authors: Mark Hakkinen, George Kerscher, Tom McLaughlin, James Pritchett, and Michael Moodie\n  Change list:\n  2002-02-12 M. Moodie. Changed content model of navLabel element to eliminate ambiguity.\n  2002-02-27 M. Moodie. Grammatical changes suggested by editor.\n  2004-03-31 J. Pritchett.  Various changes per the 2004 change list:\n            - Changed internal version numbers from 1.1.0 to 1.2.0\n            - Made audio clipBegin/clipEnd mandatory (change #10)\n            - Dropped value attribute from navPoint (change #11)\n            - Replaced lang attribute with xml:lang (change #12)\n            - Added <pageList> and <pageTarget> elements (change #48)\n            - Dropped onFocus and onBlur attributes from navPoint and navTarget (change #49)\n            - Added <img> to content models of docTitle and docAuthor (change #50)\n            - Removed reference to pages in description of navList (change #52)\n            - Added <navInfo> element (change #53)\n            - Added default namespace attribute to description of <ncx> (change #L8)\n            - Removed pageRef and mapRef attributes\n  2004-04-05 J. Pritchett.  Changes after feedback from MM and MG to 2004-03-31 version\n            - Changed internal version numbers from 1.2.0 to 1.1.2 (per MM e-mail of 3/31)\n            - Changed system identifier to use z3986/2004 as path instead of z3986/v100 (per 3/31 con call)\n            - Added class attribute to both pageTarget and pageList (per MG e-mail of 4/1)\n            - Added comment text describing value attribute for pageTarget and navTarget (per MM e-mail of 3/31)\n            - Changed declaration of type attribute on pageTarget to enumerate allowed values \n            - Added playOrder attribute to navPoint, navTarget, and pageTarget (per Lloyd's proposal)\n2004-04-05 T. McLaughlin. In description of smilCustomTest, added id and defaultState are to be copied. \nVersion update to 1.1.3.\n2004-05-14 T. McLaughlin. Reinstated override attribute to be copied also. Added bookStruct attribute \nand enum list to smilCustomTest. Update to 1.1.4.\nRevised, 4/5/2004:  Changed version to 1.1.2 \nRevised, 4/5/2004:  Changed system identifier to use '2004' path \nRevised, 4/5/2004:  TM, Changed version to 1.1.3 \nRevised, 5/14/2004:  TM, Changed version to 1.1.4 \n2004-07-07 M. Moodie Updated version to 1.2.0 everywhere but at top, where version was set to 1.1.5.\n2004-09-15 M. Moodie.  Changed uri to URI throughout.  Set version to 1.1.6.\n2004-09-16 M. Moodie.  Changed version to 1.2.0\n2005-06-26 M. Gylling. Changed pid, sid, ns uri, and filename for Z3986-2005\n            \n  Description:\n                                                  \n  NCX (Navigation Control for XML applications) is a generalized navigation definition DTD for application\nto Digital Talking Books, eBooks, and general web content models.                                                \nThis DTD is an XML application that layers navigation functionality on top of SMIL 2.0  content.                                       \n  \n  The NCX defines a navigation path/model that may be applied upon existing publications,\nwithout modification of the existing publication source, so long as the navigation targets within\nthe source publication can be directly referenced via a URI.                      \n             \nThe following identifiers apply to this DTD:\n  \"-//NISO//DTD ncx 2005-1//EN\"\n  \"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\"\n-->\n\n<!-- Basic Entities -->\n\n<!ENTITY % i18n \n  \"xml:lang    NMTOKEN    #IMPLIED\n  dir    (ltr|rtl)  #IMPLIED\" >\n\n<!ENTITY % SMILtimeVal  \"CDATA\" >\n<!ENTITY % URI    \"CDATA\" >\n<!ENTITY % script  \"CDATA\" >\n\n<!-- ELEMENTS -->\n\n<!-- Top Level NCX Container. -->\n<!-- Revised, 3/31/2004:  Added pageList to content model -->\n<!ELEMENT ncx (head, docTitle, docAuthor*, navMap, pageList?, navList*)>\n<!-- Revised, 4/5/2004:  Changed version to 1.1.2 -->\n<!-- Revised 3/29/2004:  Added xmlns -->\n<!-- Revised, 4/5/2004:  TM, Changed version to 1.1.3 -->\n<!-- Revised, 5/14/2004:  TM, Changed version to 1.1.4 -->\n<!-- Revised, 7/7/2004:  MM, Changed version to 1.2.0 -->\n<!ATTLIST ncx \n  version     CDATA     #FIXED \"2005-1\"\n  xmlns       %URI;     #FIXED \"http://www.daisy.org/z3986/2005/ncx/\"\n  %i18n;\n>\n\n<!-- Document Head - Contains all NCX metadata.  \n-->\n\n<!ELEMENT head (smilCustomTest | meta)+>\n\n<!-- 2004-04-05 TM - only id and defaultState are copied -->\n<!-- 2004-05-14 TM - revert to override copied too; added bookStruct attribute -->\n<!-- smilCustomTest - Duplicates customTest data found in SMIL files.  Each unique customTest \nelement that appears in one or more SMIL files must have its id, defaultState and override \nattributes duplicated in a smilCustomTest element in the NCX.  The NCX thus gathers in one \nplace all customTest elements used in the SMIL files, for presentation to the user.\n-->\n<!ELEMENT smilCustomTest EMPTY>\n<!ATTLIST smilCustomTest\nid    ID    #REQUIRED\ndefaultState  (true|false)   'false'\noverride  (visible|hidden) 'hidden'\nbookStruct  (PAGE_NUMBER|NOTE|NOTE_REFERENCE|ANNOTATION|LINE_NUMBER|OPTIONAL_SIDEBAR|OPTIONAL_PRODUCER_NOTE)  #IMPLIED\n>\n\n<!-- Meta Element - metadata about this NCX -->\n<!ELEMENT meta EMPTY>\n<!ATTLIST meta\n  name    CDATA    #REQUIRED\n  content  CDATA    #REQUIRED\n  scheme  CDATA    #IMPLIED\n>\n\n<!-- DocTitle - the title of the document, required and must immediately follow head. \n-->\n\n<!-- Revised, 3/31/2004:  Added img to content model -->\n<!ELEMENT docTitle (text, audio?, img?)>\n<!ATTLIST docTitle\n  id    ID    #IMPLIED\n  %i18n;\n>\n\n<!-- DocAuthor - the author of the document, immediately follows docTitle.\n-->\n\n<!-- Revised, 3/31/2004:  Added img to content model -->\n<!ELEMENT docAuthor (text, audio?, img?)>\n<!ATTLIST docAuthor\n  id    ID    #IMPLIED\n  %i18n;\n>\n\n<!-- Navigation Structure - container for all of the NCX objects that are part of the \nhierarchical structure of the document.\n-->\n\n<!-- Revised, 3/31/2004:  Added navInfo to content model -->\n<!ELEMENT navMap (navInfo*, navLabel*, navPoint+)>\n<!ATTLIST navMap\n  id    ID    #IMPLIED\n>\n\n<!-- Navigation Point - contains description(s) of target, as well as a pointer to \nentire content of target.\nHierarchy is represented by nesting navPoints.  \"class\" attribute describes the kind \nof structural unit this object represents (e.g., \"chapter\", \"section\").\n-->\n<!ELEMENT navPoint (navLabel+, content, navPoint*)>\n<!-- Revised, 3/29/2004:  Removed onFocus/onBlur -->\n<!-- Revised, 3/29/2004:  Removed value -->\n<!-- Revised, 3/31/2004:  Removed pageRef -->\n<!-- Revised, 4/5/2004:  Added playOrder -->\n<!ATTLIST navPoint\n  id    ID      #REQUIRED\n  class    CDATA    #IMPLIED\n  playOrder CDATA       #REQUIRED\n>\n\n<!-- Revised, 3/31/2004:  Added pageList element -->\n<!-- Page List -  Container for pagination information.\n  -->\n<!ELEMENT pageList (navInfo*, navLabel*, pageTarget+)>\n<!-- Revised, 4/5/2004:  Added class attribute -->\n<!ATTLIST pageList\n   id       ID          #IMPLIED\n   class    CDATA       #IMPLIED\n>\n\n<!-- Revised, 3/31/2004:  Added pageTarget element -->\n<!-- Revised, 4/5/2004:  Added description of value attribute to comment -->\n<!-- Page Target -  Container for \n  text, audio, image, and content elements containing navigational \n  information for pages.  The \"value\" attribute is a positive integer representing \nthe numeric value associated with a page. Combination of values of type and \nvalue attributes must be unique, when value attribute is present. \n-->\n<!ELEMENT pageTarget (navLabel+, content)>\n<!-- Revised, 4/5/2004:  Added class attribute -->\n<!-- Revised, 4/5/2004:  Changed declaration of type attribute to enumerate values -->\n<!-- Revised, 4/5/2004:  Added playOrder -->\n<!ATTLIST pageTarget\n   id       ID          #IMPLIED\n   value    CDATA       #IMPLIED\n   type     (front | normal | special)       #REQUIRED\n   class    CDATA       #IMPLIED\n   playOrder CDATA      #REQUIRED\n>\n\n<!-- Navigation List - container for distinct, flat sets of navigable elements, e.g.  \nnotes, figures, tables, etc.  Essentially a flat version of navMap.  The \"class\" attribute \ndescribes the type of object contained in this navList, using dtbook element names, e.g., note.\n-->\n\n<!-- Revised, 3/31/2004:  Added navInfo to content model -->\n<!ELEMENT navList   (navInfo*, navLabel+, navTarget+) >\n<!ATTLIST navList\n  id    ID    #IMPLIED\n  class    CDATA    #IMPLIED\n>\n\n<!-- Revised, 4/5/2004:  Added description of value attribute to comment -->\n<!-- Navigation Target - contains description(s) of target, as well as a pointer to \nentire content of target.\nnavTargets are the equivalent of navPoints for use in navLists.  \"class\" attribute \ndescribes the kind of structure this target represents, using its dtbook element \nname, e.g., note.  The \"value\" attribute is a positive integer representing the \nnumeric value associated with the navTarget.\n-->\n\n<!ELEMENT navTarget  (navLabel+, content) >\n<!-- Revised, 3/29/2004:  Removed onFocus/onBlur -->\n<!-- Revised, 3/31/2004:  Removed mapRef -->\n<!-- Revised, 4/5/2004:  Added playOrder -->\n<!ATTLIST navTarget\n  id    ID    #REQUIRED\n  class    CDATA    #IMPLIED\n  value    CDATA    #IMPLIED\n  playOrder CDATA       #REQUIRED\n>\n\n\n<!-- Revised, 3/31/2004:  Added navInfo element -->\n<!-- Navigation Information - Contains an informative comment\n  about a navMap, pageList, or navList in various media for presentation to the user.\n-->\n<!ELEMENT navInfo (((text, audio?) | audio), img?)>\n<!ATTLIST navInfo\n  %i18n; \n>\n\n\n<!-- Navigation Label - Contains a description of a given <navMap>, <navPoint>, \n<navList>, or <navTarget> in various media for presentation to the user. Can be \nrepeated so descriptions can be provided in multiple languages. -->\n<!ELEMENT navLabel (((text, audio?) | audio), img?)>\n<!ATTLIST navLabel\n  %i18n; \n>\n\n\n<!-- Content Element - pointer into SMIL to beginning of navPoint. -->\n<!ELEMENT content EMPTY>\n<!ATTLIST content\n  id    ID    #IMPLIED\n  src    %URI;    #REQUIRED\n>\n\n<!-- Text Element - Contains text of docTitle, navPoint heading, navTarget (e.g., page number), \nor label for navMap or navList. -->\n<!ELEMENT text (#PCDATA)>\n<!ATTLIST text\n  id    ID        #IMPLIED\n  class  CDATA      #IMPLIED\n>\n\n<!-- Audio Element - audio clip of navPoint heading. -->\n<!ELEMENT audio EMPTY>\n<!-- Revised, 3/29/2004:  clipBegin/clipEnd now REQUIRED -->\n<!ATTLIST audio\n  id    ID        #IMPLIED\n  class  CDATA      #IMPLIED\n  src    %URI;      #REQUIRED\n  clipBegin %SMILtimeVal;  #REQUIRED\n  clipEnd  %SMILtimeVal;  #REQUIRED\n>\n\n<!-- Image Element - image that may accompany heading. -->\n<!ELEMENT img EMPTY>\n<!ATTLIST img\n  id    ID      #IMPLIED\n  class  CDATA    #IMPLIED\n  src    %URI;    #REQUIRED\n>\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/ruby/xhtml-ruby-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Ruby Module .................................................... -->\n<!-- file: xhtml-ruby-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1999-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-ruby-1.mod,v 4.0 2001/04/03 23:14:33 altheim Exp $\n\n     This module is based on the W3C Ruby Annotation Specification:\n\n        http://www.w3.org/TR/ruby\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Ruby 1.0//EN\"\n       SYSTEM \"http://www.w3.org/TR/ruby/xhtml-ruby-1.mod\"\n\n     ...................................................................... -->\n\n<!-- Ruby Elements\n\n        ruby, rbc, rtc, rb, rt, rp\n\n     This module declares the elements and their attributes used to\n     support ruby annotation markup.\n-->\n\n<!-- declare qualified element type names:\n-->\n<!ENTITY % ruby.qname  \"ruby\" >\n<!ENTITY % rbc.qname  \"rbc\" >\n<!ENTITY % rtc.qname  \"rtc\" >\n<!ENTITY % rb.qname  \"rb\" >\n<!ENTITY % rt.qname  \"rt\" >\n<!ENTITY % rp.qname  \"rp\" >\n\n<!-- rp fallback is included by default.\n-->\n<!ENTITY % Ruby.fallback \"INCLUDE\" >\n<!ENTITY % Ruby.fallback.mandatory \"IGNORE\" >\n\n<!-- Complex ruby is included by default; it may be \n     overridden by other modules to ignore it.\n-->\n<!ENTITY % Ruby.complex \"INCLUDE\" >\n\n<!-- Fragments for the content model of the ruby element -->\n<![%Ruby.fallback;[\n<![%Ruby.fallback.mandatory;[\n<!ENTITY % Ruby.content.simple \n     \"( %rb.qname;, %rp.qname;, %rt.qname;, %rp.qname; )\"\n>\n]]>\n<!ENTITY % Ruby.content.simple \n     \"( %rb.qname;, ( %rt.qname; | ( %rp.qname;, %rt.qname;, %rp.qname; ) ) )\"\n>\n]]>\n<!ENTITY % Ruby.content.simple \"( %rb.qname;, %rt.qname; )\" >\n\n<![%Ruby.complex;[\n<!ENTITY % Ruby.content.complex \n     \"| ( %rbc.qname;, %rtc.qname;, %rtc.qname;? )\"\n>\n]]>\n<!ENTITY % Ruby.content.complex \"\" >\n\n<!-- Content models of the rb and the rt elements are intended to\n     allow other inline-level elements of its parent markup language,\n     but it should not include ruby descendent elements. The following\n     parameter entity %NoRuby.content; can be used to redefine\n     those content models with minimum effort.  It's defined as\n     '( #PCDATA )' by default.\n-->\n<!ENTITY % NoRuby.content \"( #PCDATA )\" >\n\n<!-- one or more digits (NUMBER) -->\n<!ENTITY % Number.datatype \"CDATA\" >\n\n<!-- ruby element ...................................... -->\n\n<!ENTITY % ruby.element  \"INCLUDE\" >\n<![%ruby.element;[\n<!ENTITY % ruby.content\n     \"( %Ruby.content.simple; %Ruby.content.complex; )\"\n>\n<!ELEMENT %ruby.qname;  %ruby.content; >\n<!-- end of ruby.element -->]]>\n\n<![%Ruby.complex;[\n<!-- rbc (ruby base component) element ................. -->\n\n<!ENTITY % rbc.element  \"INCLUDE\" >\n<![%rbc.element;[\n<!ENTITY % rbc.content\n     \"(%rb.qname;)+\"\n>\n<!ELEMENT %rbc.qname;  %rbc.content; >\n<!-- end of rbc.element -->]]>\n\n<!-- rtc (ruby text component) element ................. -->\n\n<!ENTITY % rtc.element  \"INCLUDE\" >\n<![%rtc.element;[\n<!ENTITY % rtc.content\n     \"(%rt.qname;)+\"\n>\n<!ELEMENT %rtc.qname;  %rtc.content; >\n<!-- end of rtc.element -->]]>\n]]>\n\n<!-- rb (ruby base) element ............................ -->\n\n<!ENTITY % rb.element  \"INCLUDE\" >\n<![%rb.element;[\n<!-- %rb.content; uses %NoRuby.content; as its content model,\n     which is '( #PCDATA )' by default. It may be overridden\n     by other modules to allow other inline-level elements\n     of its parent markup language, but it should not include\n     ruby descendent elements.\n-->\n<!ENTITY % rb.content \"%NoRuby.content;\" >\n<!ELEMENT %rb.qname;  %rb.content; >\n<!-- end of rb.element -->]]>\n\n<!-- rt (ruby text) element ............................ -->\n\n<!ENTITY % rt.element  \"INCLUDE\" >\n<![%rt.element;[\n<!-- %rt.content; uses %NoRuby.content; as its content model,\n     which is '( #PCDATA )' by default. It may be overridden\n     by other modules to allow other inline-level elements\n     of its parent markup language, but it should not include\n     ruby descendent elements.\n-->\n<!ENTITY % rt.content \"%NoRuby.content;\" >\n\n<!ELEMENT %rt.qname;  %rt.content; >\n<!-- end of rt.element -->]]>\n\n<!-- rbspan attribute is used for complex ruby only ...... -->\n<![%Ruby.complex;[\n<!ENTITY % rt.attlist  \"INCLUDE\" >\n<![%rt.attlist;[\n<!ATTLIST %rt.qname;\n      rbspan         %Number.datatype;      \"1\"\n>\n<!-- end of rt.attlist -->]]>\n]]>\n\n<!-- rp (ruby parenthesis) element ..................... -->\n\n<![%Ruby.fallback;[\n<!ENTITY % rp.element  \"INCLUDE\" >\n<![%rp.element;[\n<!ENTITY % rp.content\n     \"( #PCDATA )\"\n>\n<!ELEMENT %rp.qname;  %rp.content; >\n<!-- end of rp.element -->]]>\n]]>\n\n<!-- Ruby Common Attributes\n\n     The following optional ATTLIST declarations provide an easy way\n     to define common attributes for ruby elements.  These declarations\n     are ignored by default.\n\n     Ruby elements are intended to have common attributes of its\n     parent markup language.  For example, if a markup language defines\n     common attributes as a parameter entity %attrs;, you may add\n     those attributes by just declaring the following parameter entities\n\n         <!ENTITY % Ruby.common.attlists  \"INCLUDE\" >\n         <!ENTITY % Ruby.common.attrib  \"%attrs;\" >\n\n     before including the Ruby module.\n-->\n\n<!ENTITY % Ruby.common.attlists  \"IGNORE\" >\n<![%Ruby.common.attlists;[\n<!ENTITY % Ruby.common.attrib  \"\" >\n\n<!-- common attributes for ruby ........................ -->\n\n<!ENTITY % Ruby.common.attlist  \"INCLUDE\" >\n<![%Ruby.common.attlist;[\n<!ATTLIST %ruby.qname;\n      %Ruby.common.attrib;\n>\n<!-- end of Ruby.common.attlist -->]]>\n\n<![%Ruby.complex;[\n<!-- common attributes for rbc ......................... -->\n\n<!ENTITY % Rbc.common.attlist  \"INCLUDE\" >\n<![%Rbc.common.attlist;[\n<!ATTLIST %rbc.qname;\n      %Ruby.common.attrib;\n>\n<!-- end of Rbc.common.attlist -->]]>\n\n<!-- common attributes for rtc ......................... -->\n\n<!ENTITY % Rtc.common.attlist  \"INCLUDE\" >\n<![%Rtc.common.attlist;[\n<!ATTLIST %rtc.qname;\n      %Ruby.common.attrib;\n>\n<!-- end of Rtc.common.attlist -->]]>\n]]>\n\n<!-- common attributes for rb .......................... -->\n\n<!ENTITY % Rb.common.attlist  \"INCLUDE\" >\n<![%Rb.common.attlist;[\n<!ATTLIST %rb.qname;\n      %Ruby.common.attrib;\n>\n<!-- end of Rb.common.attlist -->]]>\n\n<!-- common attributes for rt .......................... -->\n\n<!ENTITY % Rt.common.attlist  \"INCLUDE\" >\n<![%Rt.common.attlist;[\n<!ATTLIST %rt.qname;\n      %Ruby.common.attrib;\n>\n<!-- end of Rt.common.attlist -->]]>\n\n<![%Ruby.fallback;[\n<!-- common attributes for rp .......................... -->\n\n<!ENTITY % Rp.common.attlist  \"INCLUDE\" >\n<![%Rp.common.attlist;[\n<!ATTLIST %rp.qname;\n      %Ruby.common.attrib;\n>\n<!-- end of Rp.common.attlist -->]]>\n]]>\n]]>\n\n<!-- end of xhtml-ruby-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-arch-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Base Architecture Module  ...................................... -->\n<!-- file: xhtml-arch-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-arch-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Base Architecture 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-arch-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- This optional module includes declarations that enable XHTML to be used\n     as a base architecture according to the 'Architectural Forms Definition\n     Requirements' (Annex A.3, ISO/IEC 10744, 2nd edition). For more information\n     on use of architectural forms, see the HyTime web site at:\n\n         http://www.hytime.org/\n-->\n\n<?IS10744 ArcBase xhtml ?>\n\n<!NOTATION xhtml PUBLIC \"-//W3C//NOTATION AFDR ARCBASE XHTML 1.1//EN\" >\n\n<!-- Entity declaration for associated Architectural DTD\n-->\n<!ENTITY xhtml-arch.dtd\n      PUBLIC \"-//W3C//DTD XHTML Architecture 1.1//EN\"\n             \"xhtml11-arch.dtd\" >\n\n<?IS10744:arch xhtml\n    public-id       =  \"-//W3C//NOTATION AFDR ARCBASE XHTML 1.1//EN\"\n    dtd-public-id   =  \"-//W3C//DTD XHTML 1.1//EN\"\n    dtd-system-id   =  \"xhtml11.dtd\"\n    doc-elem-form   =  \"html\"\n    form-att        =  \"html\"\n    renamer-att     =  \"htnames\"\n    suppressor-att  =  \"htsupp\"\n    data-ignore-att =  \"htign\"\n    auto            =  \"ArcAuto\"\n    options         =  \"HtModReq HtModOpt\"\n    HtModReq        =  \"Framework Text Hypertext Lists Structure\"\n    HtModOpt        =  \"Standard\"\n?>\n\n<!-- end of xhtml-arch-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-attribs-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Common Attributes Module  ...................................... -->\n<!-- file: xhtml-attribs-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-attribs-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ENTITIES XHTML Common Attributes 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-attribs-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Common Attributes\n\n     This module declares many of the common attributes for the XHTML DTD.\n     %NS.decl.attrib; is declared in the XHTML Qname module.\n\n\t Note that this file was extended in XHTML Modularization 1.1 to \n\t include declarations of \"global\" versions of the attribute collections.\n\t The global versions of the attributes are for use on elements in other \n\t namespaces.  The global version of \"common\" includes the xmlns declaration\n\t for the prefixed version of the xhtml namespace.  If you are only using a\n\t specific attribute or an individual attribute collection, you must also\n\t include the XHTML.xmlns.attrib.prefixed PE on your elements.\n-->\n\n<!ENTITY % id.attrib\n     \"id           ID                       #IMPLIED\"\n>\n\n<![%XHTML.global.attrs.prefixed;[\n<!ENTITY % XHTML.global.id.attrib\n     \"%XHTML.prefix;:id           ID        #IMPLIED\"\n>\n]]>\n\n<!ENTITY % class.attrib\n     \"class        CDATA                 #IMPLIED\"\n>\n\n<![%XHTML.global.attrs.prefixed;[\n<!ENTITY % XHTML.global.class.attrib\n     \"%XHTML.prefix;:class        CDATA                 #IMPLIED\"\n>\n]]>\n\n<!ENTITY % title.attrib\n     \"title        %Text.datatype;          #IMPLIED\"\n>\n\n<![%XHTML.global.attrs.prefixed;[\n<!ENTITY % XHTML.global.title.attrib\n     \"%XHTML.prefix;:title        %Text.datatype;          #IMPLIED\"\n>\n]]>\n\n<!ENTITY % Core.extra.attrib \"\" >\n\n<!ENTITY % Core.attrib\n     \"%XHTML.xmlns.attrib;\n      %id.attrib;\n      %class.attrib;\n      %title.attrib;\n      xml:space    ( preserve )             #FIXED 'preserve'\n      %Core.extra.attrib;\"\n>\n\n<!ENTITY % XHTML.global.core.extra.attrib \"\" >\n\n<![%XHTML.global.attrs.prefixed;[\n\n<!ENTITY % XHTML.global.core.attrib\n     \"%XHTML.global.id.attrib;\n      %XHTML.global.class.attrib;\n      %XHTML.global.title.attrib;\n      %XHTML.global.core.extra.attrib;\"\n>\n]]>\n\n<!ENTITY % XHTML.global.core.attrib \"\" >\n\n\n<!ENTITY % lang.attrib\n     \"xml:lang     %LanguageCode.datatype;  #IMPLIED\"\n>\n\n<![%XHTML.bidi;[\n<!ENTITY % dir.attrib\n     \"dir          ( ltr | rtl )            #IMPLIED\"\n>\n\n<!ENTITY % I18n.attrib\n     \"%dir.attrib;\n      %lang.attrib;\"\n>\n\n<![%XHTML.global.attrs.prefixed;[\n<!ENTITY XHTML.global.i18n.attrib\n     \"%XHTML.prefix;:dir          ( ltr | rtl )            #IMPLIED\n      %lang.attrib;\"\n>\n]]>\n<!ENTITY XHTML.global.i18n.attrib \"\" >\n\n]]>\n<!ENTITY % I18n.attrib\n     \"%lang.attrib;\"\n>\n<!ENTITY % XHTML.global.i18n.attrib\n     \"%lang.attrib;\"\n>\n\n<!ENTITY % Common.extra.attrib \"\" >\n<!ENTITY % XHTML.global.common.extra.attrib \"\" >\n\n<!-- intrinsic event attributes declared previously\n-->\n<!ENTITY % Events.attrib \"\" >\n\n<!ENTITY % XHTML.global.events.attrib \"\" >\n\n<!ENTITY % Common.attrib\n     \"%Core.attrib;\n      %I18n.attrib;\n      %Events.attrib;\n      %Common.extra.attrib;\"\n>\n\n<!ENTITY % XHTML.global.common.attrib\n     \"%XHTML.xmlns.attrib.prefixed;\n      %XHTML.global.core.attrib;\n\t  %XHTML.global.i18n.attrib;\n\t  %XHTML.global.events.attrib;\n\t  %XHTML.global.common.extra.attrib;\"\n>\n\n<!-- end of xhtml-attribs-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-base-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Base Element Module  ........................................... -->\n<!-- file: xhtml-base-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-base-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Base Element 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-base-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Base element\n\n        base\n\n     This module declares the base element type and its attributes,\n     used to define a base URI against which relative URIs in the\n     document will be resolved.\n\n     Note that this module also redeclares the content model for\n     the head element to include the base element.\n-->\n\n<!-- base: Document Base URI ........................... -->\n\n<!ENTITY % base.element  \"INCLUDE\" >\n<![%base.element;[\n<!ENTITY % base.content  \"EMPTY\" >\n<!ENTITY % base.qname  \"base\" >\n<!ELEMENT %base.qname;  %base.content; >\n<!-- end of base.element -->]]>\n\n<!ENTITY % base.attlist  \"INCLUDE\" >\n<![%base.attlist;[\n<!ATTLIST %base.qname;\n      %XHTML.xmlns.attrib;\n      href         %URI.datatype;           #REQUIRED\n>\n<!-- end of base.attlist -->]]>\n\n<!ENTITY % head.content\n    \"( %HeadOpts.mix;,\n     ( ( %title.qname;, %HeadOpts.mix;, ( %base.qname;, %HeadOpts.mix; )? )\n     | ( %base.qname;, %HeadOpts.mix;, ( %title.qname;, %HeadOpts.mix; ))))\"\n>\n\n<!-- end of xhtml-base-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-bdo-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML BDO Element Module ............................................. -->\n<!-- file: xhtml-bdo-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-bdo-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML BDO Element 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-bdo-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Bidirectional Override (bdo) Element\n\n     This modules declares the element 'bdo', used to override the\n     Unicode bidirectional algorithm for selected fragments of text.\n\n     DEPENDENCIES:\n     Relies on the conditional section keyword %XHTML.bidi; declared\n     as \"INCLUDE\". Bidirectional text support includes both the bdo\n     element and the 'dir' attribute.\n-->\n\n<!ENTITY % bdo.element  \"INCLUDE\" >\n<![%bdo.element;[\n<!ENTITY % bdo.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % bdo.qname  \"bdo\" >\n<!ELEMENT %bdo.qname;  %bdo.content; >\n<!-- end of bdo.element -->]]>\n\n<!ENTITY % bdo.attlist  \"INCLUDE\" >\n<![%bdo.attlist;[\n<!ATTLIST %bdo.qname;\n      %Core.attrib;\n\t  %lang.attrib;\n      dir          ( ltr | rtl )            #REQUIRED\n>\n]]>\n\n<!-- end of xhtml-bdo-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkphras-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Block Phrasal Module  .......................................... -->\n<!-- file: xhtml-blkphras-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-blkphras-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Block Phrasal 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-blkphras-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Block Phrasal\n\n        address, blockquote, pre, h1, h2, h3, h4, h5, h6\n\n     This module declares the elements and their attributes used to\n     support block-level phrasal markup.\n-->\n\n<!ENTITY % address.element  \"INCLUDE\" >\n<![%address.element;[\n<!ENTITY % address.content\n     \"( #PCDATA | %Inline.mix; )*\" >\n<!ENTITY % address.qname  \"address\" >\n<!ELEMENT %address.qname;  %address.content; >\n<!-- end of address.element -->]]>\n\n<!ENTITY % address.attlist  \"INCLUDE\" >\n<![%address.attlist;[\n<!ATTLIST %address.qname;\n      %Common.attrib;\n>\n<!-- end of address.attlist -->]]>\n\n<!ENTITY % blockquote.element  \"INCLUDE\" >\n<![%blockquote.element;[\n<!ENTITY % blockquote.content\n     \"( %Block.mix; )*\"\n>\n<!ENTITY % blockquote.qname  \"blockquote\" >\n<!ELEMENT %blockquote.qname;  %blockquote.content; >\n<!-- end of blockquote.element -->]]>\n\n<!ENTITY % blockquote.attlist  \"INCLUDE\" >\n<![%blockquote.attlist;[\n<!ATTLIST %blockquote.qname;\n      %Common.attrib;\n      cite         %URI.datatype;           #IMPLIED\n>\n<!-- end of blockquote.attlist -->]]>\n\n<!ENTITY % pre.element  \"INCLUDE\" >\n<![%pre.element;[\n<!ENTITY % pre.content\n     \"( #PCDATA\n      | %InlStruct.class;\n      %InlPhras.class;\n      | %tt.qname; | %i.qname; | %b.qname;\n      %I18n.class;\n      %Anchor.class;\n      | %map.qname;\n      %Misc.class;\n      %Inline.extra; )*\"\n>\n<!ENTITY % pre.qname  \"pre\" >\n<!ELEMENT %pre.qname;  %pre.content; >\n<!-- end of pre.element -->]]>\n\n<!ENTITY % pre.attlist  \"INCLUDE\" >\n<![%pre.attlist;[\n<!ATTLIST %pre.qname;\n      %Common.attrib;\n>\n<!-- end of pre.attlist -->]]>\n\n<!-- ...................  Heading Elements  ................... -->\n\n<!ENTITY % Heading.content  \"( #PCDATA | %Inline.mix; )*\" >\n\n<!ENTITY % h1.element  \"INCLUDE\" >\n<![%h1.element;[\n<!ENTITY % h1.qname  \"h1\" >\n<!ELEMENT %h1.qname;  %Heading.content; >\n<!-- end of h1.element -->]]>\n\n<!ENTITY % h1.attlist  \"INCLUDE\" >\n<![%h1.attlist;[\n<!ATTLIST %h1.qname;\n      %Common.attrib;\n>\n<!-- end of h1.attlist -->]]>\n\n<!ENTITY % h2.element  \"INCLUDE\" >\n<![%h2.element;[\n<!ENTITY % h2.qname  \"h2\" >\n<!ELEMENT %h2.qname;  %Heading.content; >\n<!-- end of h2.element -->]]>\n\n<!ENTITY % h2.attlist  \"INCLUDE\" >\n<![%h2.attlist;[\n<!ATTLIST %h2.qname;\n      %Common.attrib;\n>\n<!-- end of h2.attlist -->]]>\n\n<!ENTITY % h3.element  \"INCLUDE\" >\n<![%h3.element;[\n<!ENTITY % h3.qname  \"h3\" >\n<!ELEMENT %h3.qname;  %Heading.content; >\n<!-- end of h3.element -->]]>\n\n<!ENTITY % h3.attlist  \"INCLUDE\" >\n<![%h3.attlist;[\n<!ATTLIST %h3.qname;\n      %Common.attrib;\n>\n<!-- end of h3.attlist -->]]>\n\n<!ENTITY % h4.element  \"INCLUDE\" >\n<![%h4.element;[\n<!ENTITY % h4.qname  \"h4\" >\n<!ELEMENT %h4.qname;  %Heading.content; >\n<!-- end of h4.element -->]]>\n\n<!ENTITY % h4.attlist  \"INCLUDE\" >\n<![%h4.attlist;[\n<!ATTLIST %h4.qname;\n      %Common.attrib;\n>\n<!-- end of h4.attlist -->]]>\n\n<!ENTITY % h5.element  \"INCLUDE\" >\n<![%h5.element;[\n<!ENTITY % h5.qname  \"h5\" >\n<!ELEMENT %h5.qname;  %Heading.content; >\n<!-- end of h5.element -->]]>\n\n<!ENTITY % h5.attlist  \"INCLUDE\" >\n<![%h5.attlist;[\n<!ATTLIST %h5.qname;\n      %Common.attrib;\n>\n<!-- end of h5.attlist -->]]>\n\n<!ENTITY % h6.element  \"INCLUDE\" >\n<![%h6.element;[\n<!ENTITY % h6.qname  \"h6\" >\n<!ELEMENT %h6.qname;  %Heading.content; >\n<!-- end of h6.element -->]]>\n\n<!ENTITY % h6.attlist  \"INCLUDE\" >\n<![%h6.attlist;[\n<!ATTLIST %h6.qname;\n      %Common.attrib;\n>\n<!-- end of h6.attlist -->]]>\n\n<!-- end of xhtml-blkphras-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkpres-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Block Presentation Module  ..................................... -->\n<!-- file: xhtml-blkpres-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-blkpres-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Block Presentation 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-blkpres-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Block Presentational Elements\n\n        hr\n\n     This module declares the elements and their attributes used to\n     support block-level presentational markup.\n-->\n\n<!ENTITY % hr.element  \"INCLUDE\" >\n<![%hr.element;[\n<!ENTITY % hr.content  \"EMPTY\" >\n<!ENTITY % hr.qname  \"hr\" >\n<!ELEMENT %hr.qname;  %hr.content; >\n<!-- end of hr.element -->]]>\n\n<!ENTITY % hr.attlist  \"INCLUDE\" >\n<![%hr.attlist;[\n<!ATTLIST %hr.qname;\n      %Common.attrib;\n>\n<!-- end of hr.attlist -->]]>\n\n<!-- end of xhtml-blkpres-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkstruct-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Block Structural Module  ....................................... -->\n<!-- file: xhtml-blkstruct-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-blkstruct-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Block Structural 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-blkstruct-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Block Structural\n\n        div, p\n\n     This module declares the elements and their attributes used to\n     support block-level structural markup.\n-->\n\n<!ENTITY % div.element  \"INCLUDE\" >\n<![%div.element;[\n<!ENTITY % div.content\n     \"( #PCDATA | %Flow.mix; )*\"\n>\n<!ENTITY % div.qname  \"div\" >\n<!ELEMENT %div.qname;  %div.content; >\n<!-- end of div.element -->]]>\n\n<!ENTITY % div.attlist  \"INCLUDE\" >\n<![%div.attlist;[\n<!ATTLIST %div.qname;\n      %Common.attrib;\n>\n<!-- end of div.attlist -->]]>\n\n<!ENTITY % p.element  \"INCLUDE\" >\n<![%p.element;[\n<!ENTITY % p.content\n     \"( #PCDATA | %Inline.mix; )*\" >\n<!ENTITY % p.qname  \"p\" >\n<!ELEMENT %p.qname;  %p.content; >\n<!-- end of p.element -->]]>\n\n<!ENTITY % p.attlist  \"INCLUDE\" >\n<![%p.attlist;[\n<!ATTLIST %p.qname;\n      %Common.attrib;\n>\n<!-- end of p.attlist -->]]>\n\n<!-- end of xhtml-blkstruct-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-charent-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Character Entities Module  ......................................... -->\n<!-- file: xhtml-charent-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-charent-1.mod,v 1.1 2010/07/29 13:42:46 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ENTITIES XHTML Character Entities 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-charent-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Character Entities for XHTML\n\n     This module declares the set of character entities for XHTML,\n     including the Latin 1, Symbol and Special character collections.\n-->\n\n<!ENTITY % xhtml-lat1\n    PUBLIC \"-//W3C//ENTITIES Latin 1 for XHTML//EN\"\n           \"xhtml-lat1.ent\" >\n%xhtml-lat1;\n\n<!ENTITY % xhtml-symbol\n    PUBLIC \"-//W3C//ENTITIES Symbols for XHTML//EN\"\n           \"xhtml-symbol.ent\" >\n%xhtml-symbol;\n\n<!ENTITY % xhtml-special\n    PUBLIC \"-//W3C//ENTITIES Special for XHTML//EN\"\n           \"xhtml-special.ent\" >\n%xhtml-special;\n\n<!-- end of xhtml-charent-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-csismap-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Client-side Image Map Module  .................................. -->\n<!-- file: xhtml-csismap-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-csismap-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Client-side Image Maps 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-csismap-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Client-side Image Maps\n\n        area, map\n\n     This module declares elements and attributes to support client-side\n     image maps. This requires that the Image Module (or a module\n     declaring the img element type) be included in the DTD.\n\n     These can be placed in the same document or grouped in a\n     separate document, although the latter isn't widely supported\n-->\n\n<!ENTITY % area.element  \"INCLUDE\" >\n<![%area.element;[\n<!ENTITY % area.content  \"EMPTY\" >\n<!ENTITY % area.qname  \"area\" >\n<!ELEMENT %area.qname;  %area.content; >\n<!-- end of area.element -->]]>\n\n<!ENTITY % Shape.datatype \"( rect | circle | poly | default )\">\n<!ENTITY % Coords.datatype \"CDATA\" >\n\n<!ENTITY % area.attlist  \"INCLUDE\" >\n<![%area.attlist;[\n<!ATTLIST %area.qname;\n      %Common.attrib;\n      href         %URI.datatype;           #IMPLIED\n      shape        %Shape.datatype;         'rect'\n      coords       %Coords.datatype;        #IMPLIED\n      nohref       ( nohref )               #IMPLIED\n      alt          %Text.datatype;          #REQUIRED\n      tabindex     %Number.datatype;        #IMPLIED\n      accesskey    %Character.datatype;     #IMPLIED\n>\n<!-- end of area.attlist -->]]>\n\n<!-- modify anchor attribute definition list\n     to allow for client-side image maps\n-->\n<!ATTLIST %a.qname;\n      shape        %Shape.datatype;         'rect'\n      coords       %Coords.datatype;        #IMPLIED\n>\n\n<!-- modify img attribute definition list\n     to allow for client-side image maps\n-->\n<!ATTLIST %img.qname;\n      usemap       %URIREF.datatype;        #IMPLIED\n>\n\n<!-- modify form input attribute definition list\n     to allow for client-side image maps\n-->\n<!ATTLIST %input.qname;\n      usemap       %URIREF.datatype;        #IMPLIED\n>\n\n<!-- modify object attribute definition list\n     to allow for client-side image maps\n-->\n<!ATTLIST %object.qname;\n      usemap       %URIREF.datatype;        #IMPLIED\n>\n\n<!-- 'usemap' points to the 'id' attribute of a <map> element,\n     which must be in the same document; support for external\n     document maps was not widely supported in HTML and is\n     eliminated in XHTML.\n\n     It is considered an error for the element pointed to by\n     a usemap URIREF to occur in anything but a <map> element.\n-->\n\n<!ENTITY % map.element  \"INCLUDE\" >\n<![%map.element;[\n<!ENTITY % map.content\n     \"(( %Block.mix; ) | %area.qname; )+\"\n>\n<!ENTITY % map.qname  \"map\" >\n<!ELEMENT %map.qname;  %map.content; >\n<!-- end of map.element -->]]>\n\n<!ENTITY % map.attlist  \"INCLUDE\" >\n<![%map.attlist;[\n<!ATTLIST %map.qname;\n      %XHTML.xmlns.attrib;\n      id           ID                       #REQUIRED\n      %class.attrib;\n      %title.attrib;\n      %Core.extra.attrib;\n      %I18n.attrib;\n      %Events.attrib;\n>\n<!-- end of map.attlist -->]]>\n\n<!-- end of xhtml-csismap-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Datatypes Module  .............................................. -->\n<!-- file: xhtml-datatypes-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-datatypes-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ENTITIES XHTML Datatypes 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-datatypes-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Datatypes\n\n     defines containers for the following datatypes, many of\n     these imported from other specifications and standards.\n-->\n\n<!-- Length defined for cellpadding/cellspacing -->\n\n<!-- nn for pixels or nn% for percentage length -->\n<!ENTITY % Length.datatype \"CDATA\" >\n\n<!-- space-separated list of link types -->\n<!ENTITY % LinkTypes.datatype \"NMTOKENS\" >\n\n<!-- single or comma-separated list of media descriptors -->\n<!ENTITY % MediaDesc.datatype \"CDATA\" >\n\n<!-- pixel, percentage, or relative -->\n<!ENTITY % MultiLength.datatype \"CDATA\" >\n\n<!-- one or more digits (NUMBER) -->\n<!ENTITY % Number.datatype \"CDATA\" >\n\n<!-- integer representing length in pixels -->\n<!ENTITY % Pixels.datatype \"CDATA\" >\n\n<!-- script expression -->\n<!ENTITY % Script.datatype \"CDATA\" >\n\n<!-- textual content -->\n<!ENTITY % Text.datatype \"CDATA\" >\n\n<!-- Placeholder Compact URI-related types -->\n<!ENTITY % CURIE.datatype \"CDATA\" >\n<!ENTITY % CURIEs.datatype \"CDATA\" >\n<!ENTITY % SafeCURIE.datatype \"CDATA\" >\n<!ENTITY % SafeCURIEs.datatype \"CDATA\" >\n<!ENTITY % URIorSafeCURIE.datatype \"CDATA\" >\n<!ENTITY % URIorSafeCURIEs.datatype \"CDATA\" >\n\n<!-- Imported Datatypes ................................ -->\n\n<!-- a single character from [ISO10646] -->\n<!ENTITY % Character.datatype \"CDATA\" >\n\n<!-- a character encoding, as per [RFC2045] -->\n<!ENTITY % Charset.datatype \"CDATA\" >\n\n<!-- a space separated list of character encodings, as per [RFC2045] -->\n<!ENTITY % Charsets.datatype \"CDATA\" >\n\n<!-- Color specification using color name or sRGB (#RRGGBB) values -->\n<!ENTITY % Color.datatype \"CDATA\" >\n\n<!-- media type, as per [RFC2045] -->\n<!ENTITY % ContentType.datatype \"CDATA\" >\n\n<!-- comma-separated list of media types, as per [RFC2045] -->\n<!ENTITY % ContentTypes.datatype \"CDATA\" >\n\n<!-- date and time information. ISO date format -->\n<!ENTITY % Datetime.datatype \"CDATA\" >\n\n<!-- formal public identifier, as per [ISO8879] -->\n<!ENTITY % FPI.datatype \"CDATA\" >\n\n<!-- a language code, as per [RFC3066] or its successor -->\n<!ENTITY % LanguageCode.datatype \"CDATA\" >\n\n<!-- a comma separated list of language code ranges -->\n<!ENTITY % LanguageCodes.datatype \"CDATA\" >\n\n<!-- a qualified name , as per [XMLNS] or its successor -->\n<!ENTITY % QName.datatype \"CDATA\" >\n<!ENTITY % QNames.datatype \"CDATA\" >\n\n<!-- a Uniform Resource Identifier, see [URI] -->\n<!ENTITY % URI.datatype \"CDATA\" >\n\n<!-- a space-separated list of Uniform Resource Identifiers, see [URI] -->\n<!ENTITY % URIs.datatype \"CDATA\" >\n\n<!-- a relative URI reference consisting of an initial '#' and a fragment ID -->\n<!ENTITY % URIREF.datatype \"CDATA\" >\n\n<!-- end of xhtml-datatypes-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod.1",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Datatypes Module  .............................................. -->\n<!-- file: xhtml-datatypes-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-datatypes-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ENTITIES XHTML Datatypes 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-datatypes-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Datatypes\n\n     defines containers for the following datatypes, many of\n     these imported from other specifications and standards.\n-->\n\n<!-- Length defined for cellpadding/cellspacing -->\n\n<!-- nn for pixels or nn% for percentage length -->\n<!ENTITY % Length.datatype \"CDATA\" >\n\n<!-- space-separated list of link types -->\n<!ENTITY % LinkTypes.datatype \"NMTOKENS\" >\n\n<!-- single or comma-separated list of media descriptors -->\n<!ENTITY % MediaDesc.datatype \"CDATA\" >\n\n<!-- pixel, percentage, or relative -->\n<!ENTITY % MultiLength.datatype \"CDATA\" >\n\n<!-- one or more digits (NUMBER) -->\n<!ENTITY % Number.datatype \"CDATA\" >\n\n<!-- integer representing length in pixels -->\n<!ENTITY % Pixels.datatype \"CDATA\" >\n\n<!-- script expression -->\n<!ENTITY % Script.datatype \"CDATA\" >\n\n<!-- textual content -->\n<!ENTITY % Text.datatype \"CDATA\" >\n\n<!-- Placeholder Compact URI-related types -->\n<!ENTITY % CURIE.datatype \"CDATA\" >\n<!ENTITY % CURIEs.datatype \"CDATA\" >\n<!ENTITY % SafeCURIE.datatype \"CDATA\" >\n<!ENTITY % SafeCURIEs.datatype \"CDATA\" >\n<!ENTITY % URIorSafeCURIE.datatype \"CDATA\" >\n<!ENTITY % URIorSafeCURIEs.datatype \"CDATA\" >\n\n<!-- Imported Datatypes ................................ -->\n\n<!-- a single character from [ISO10646] -->\n<!ENTITY % Character.datatype \"CDATA\" >\n\n<!-- a character encoding, as per [RFC2045] -->\n<!ENTITY % Charset.datatype \"CDATA\" >\n\n<!-- a space separated list of character encodings, as per [RFC2045] -->\n<!ENTITY % Charsets.datatype \"CDATA\" >\n\n<!-- Color specification using color name or sRGB (#RRGGBB) values -->\n<!ENTITY % Color.datatype \"CDATA\" >\n\n<!-- media type, as per [RFC2045] -->\n<!ENTITY % ContentType.datatype \"CDATA\" >\n\n<!-- comma-separated list of media types, as per [RFC2045] -->\n<!ENTITY % ContentTypes.datatype \"CDATA\" >\n\n<!-- date and time information. ISO date format -->\n<!ENTITY % Datetime.datatype \"CDATA\" >\n\n<!-- formal public identifier, as per [ISO8879] -->\n<!ENTITY % FPI.datatype \"CDATA\" >\n\n<!-- a language code, as per [RFC3066] or its successor -->\n<!ENTITY % LanguageCode.datatype \"CDATA\" >\n\n<!-- a comma separated list of language code ranges -->\n<!ENTITY % LanguageCodes.datatype \"CDATA\" >\n\n<!-- a qualified name , as per [XMLNS] or its successor -->\n<!ENTITY % QName.datatype \"CDATA\" >\n<!ENTITY % QNames.datatype \"CDATA\" >\n\n<!-- a Uniform Resource Identifier, see [URI] -->\n<!ENTITY % URI.datatype \"CDATA\" >\n\n<!-- a space-separated list of Uniform Resource Identifiers, see [URI] -->\n<!ENTITY % URIs.datatype \"CDATA\" >\n\n<!-- a relative URI reference consisting of an initial '#' and a fragment ID -->\n<!ENTITY % URIREF.datatype \"CDATA\" >\n\n<!-- end of xhtml-datatypes-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-edit-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Editing Elements Module  ....................................... -->\n<!-- file: xhtml-edit-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-edit-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Editing Markup 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-edit-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Editing Elements\n\n        ins, del\n\n     This module declares element types and attributes used to indicate\n     inserted and deleted content while editing a document.\n-->\n\n<!-- ins: Inserted Text  ............................... -->\n\n<!ENTITY % ins.element  \"INCLUDE\" >\n<![%ins.element;[\n<!ENTITY % ins.content\n     \"( #PCDATA | %Flow.mix; )*\"\n>\n<!ENTITY % ins.qname  \"ins\" >\n<!ELEMENT %ins.qname;  %ins.content; >\n<!-- end of ins.element -->]]>\n\n<!ENTITY % ins.attlist  \"INCLUDE\" >\n<![%ins.attlist;[\n<!ATTLIST %ins.qname;\n      %Common.attrib;\n      cite         %URI.datatype;           #IMPLIED\n      datetime     %Datetime.datatype;      #IMPLIED\n>\n<!-- end of ins.attlist -->]]>\n\n<!-- del: Deleted Text  ................................ -->\n\n<!ENTITY % del.element  \"INCLUDE\" >\n<![%del.element;[\n<!ENTITY % del.content\n     \"( #PCDATA | %Flow.mix; )*\"\n>\n<!ENTITY % del.qname  \"del\" >\n<!ELEMENT %del.qname;  %del.content; >\n<!-- end of del.element -->]]>\n\n<!ENTITY % del.attlist  \"INCLUDE\" >\n<![%del.attlist;[\n<!ATTLIST %del.qname;\n      %Common.attrib;\n      cite         %URI.datatype;           #IMPLIED\n      datetime     %Datetime.datatype;      #IMPLIED\n>\n<!-- end of del.attlist -->]]>\n\n<!-- end of xhtml-edit-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-events-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Intrinsic Events Module  ....................................... -->\n<!-- file: xhtml-events-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-events-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ENTITIES XHTML Intrinsic Events 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-events-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Intrinsic Event Attributes\n\n     These are the event attributes defined in HTML 4,\n     Section 18.2.3 \"Intrinsic Events\". This module must be\n     instantiated prior to the Attributes Module but after\n     the Datatype Module in the Modular Framework module.\n\n    \"Note: Authors of HTML documents are advised that changes\n     are likely to occur in the realm of intrinsic events\n     (e.g., how scripts are bound to events). Research in\n     this realm is carried on by members of the W3C Document\n     Object Model Working Group (see the W3C Web site at\n     http://www.w3.org/ for more information).\"\n-->\n<!-- NOTE: Because the ATTLIST declarations in this module occur\n     before their respective ELEMENT declarations in other\n     modules, there may be a dependency on this module that\n     should be considered if any of the parameter entities used\n     for element type names (eg., %a.qname;) are redeclared.\n-->\n\n<!ENTITY % Events.attrib\n     \"onclick      %Script.datatype;        #IMPLIED\n      ondblclick   %Script.datatype;        #IMPLIED\n      onmousedown  %Script.datatype;        #IMPLIED\n      onmouseup    %Script.datatype;        #IMPLIED\n      onmouseover  %Script.datatype;        #IMPLIED\n      onmousemove  %Script.datatype;        #IMPLIED\n      onmouseout   %Script.datatype;        #IMPLIED\n      onkeypress   %Script.datatype;        #IMPLIED\n      onkeydown    %Script.datatype;        #IMPLIED\n      onkeyup      %Script.datatype;        #IMPLIED\"\n>\n\n<![%XHTML.global.attrs.prefixed;[\n<!ENTITY % XHTML.global.events.attrib\n     \"%XHTML.prefix;:onclick      %Script.datatype;        #IMPLIED\n      %XHTML.prefix;:ondblclick   %Script.datatype;        #IMPLIED\n      %XHTML.prefix;:onmousedown  %Script.datatype;        #IMPLIED\n      %XHTML.prefix;:onmouseup    %Script.datatype;        #IMPLIED\n      %XHTML.prefix;:onmouseover  %Script.datatype;        #IMPLIED\n      %XHTML.prefix;:onmousemove  %Script.datatype;        #IMPLIED\n      %XHTML.prefix;:onmouseout   %Script.datatype;        #IMPLIED\n      %XHTML.prefix;:onkeypress   %Script.datatype;        #IMPLIED\n      %XHTML.prefix;:onkeydown    %Script.datatype;        #IMPLIED\n      %XHTML.prefix;:onkeyup      %Script.datatype;        #IMPLIED\"\n>\n]]>\n\n<!-- additional attributes on anchor element\n-->\n<!ATTLIST %a.qname;\n     onfocus      %Script.datatype;         #IMPLIED\n     onblur       %Script.datatype;         #IMPLIED\n>\n\n<!-- additional attributes on form element\n-->\n<!ATTLIST %form.qname;\n      onsubmit     %Script.datatype;        #IMPLIED\n      onreset      %Script.datatype;        #IMPLIED\n>\n\n<!-- additional attributes on label element\n-->\n<!ATTLIST %label.qname;\n      onfocus      %Script.datatype;        #IMPLIED\n      onblur       %Script.datatype;        #IMPLIED\n>\n\n<!-- additional attributes on input element\n-->\n<!ATTLIST %input.qname;\n      onfocus      %Script.datatype;        #IMPLIED\n      onblur       %Script.datatype;        #IMPLIED\n      onselect     %Script.datatype;        #IMPLIED\n      onchange     %Script.datatype;        #IMPLIED\n>\n\n<!-- additional attributes on select element\n-->\n<!ATTLIST %select.qname;\n      onfocus      %Script.datatype;        #IMPLIED\n      onblur       %Script.datatype;        #IMPLIED\n      onchange     %Script.datatype;        #IMPLIED\n>\n\n<!-- additional attributes on textarea element\n-->\n<!ATTLIST %textarea.qname;\n      onfocus      %Script.datatype;        #IMPLIED\n      onblur       %Script.datatype;        #IMPLIED\n      onselect     %Script.datatype;        #IMPLIED\n      onchange     %Script.datatype;        #IMPLIED\n>\n\n<!-- additional attributes on button element\n-->\n<!ATTLIST %button.qname;\n      onfocus      %Script.datatype;        #IMPLIED\n      onblur       %Script.datatype;        #IMPLIED\n>\n\n<!-- additional attributes on body element\n-->\n<!ATTLIST %body.qname;\n      onload       %Script.datatype;        #IMPLIED\n      onunload     %Script.datatype;        #IMPLIED\n>\n\n<!-- additional attributes on area element\n-->\n<!ATTLIST %area.qname;\n      onfocus      %Script.datatype;        #IMPLIED\n      onblur       %Script.datatype;        #IMPLIED\n>\n\n<!-- end of xhtml-events-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-form-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Forms Module  .................................................. -->\n<!-- file: xhtml-form-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-form-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Forms 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-form-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Forms\n\n        form, label, input, select, optgroup, option,\n        textarea, fieldset, legend, button\n\n     This module declares markup to provide support for online\n     forms, based on the features found in HTML 4 forms.\n-->\n\n<!-- declare qualified element type names:\n-->\n<!ENTITY % form.qname  \"form\" >\n<!ENTITY % label.qname  \"label\" >\n<!ENTITY % input.qname  \"input\" >\n<!ENTITY % select.qname  \"select\" >\n<!ENTITY % optgroup.qname  \"optgroup\" >\n<!ENTITY % option.qname  \"option\" >\n<!ENTITY % textarea.qname  \"textarea\" >\n<!ENTITY % fieldset.qname  \"fieldset\" >\n<!ENTITY % legend.qname  \"legend\" >\n<!ENTITY % button.qname  \"button\" >\n\n<!-- %BlkNoForm.mix; includes all non-form block elements,\n     plus %Misc.class;\n-->\n<!ENTITY % BlkNoForm.mix\n     \"%Heading.class;\n      | %List.class;\n      | %BlkStruct.class;\n      %BlkPhras.class;\n      %BlkPres.class;\n      %Table.class;\n      %Block.extra;\n      %Misc.class;\"\n>\n\n<!-- form: Form Element ................................ -->\n\n<!ENTITY % form.element  \"INCLUDE\" >\n<![%form.element;[\n<!ENTITY % form.content\n     \"( %BlkNoForm.mix;\n      | %fieldset.qname; )+\"\n>\n<!ELEMENT %form.qname;  %form.content; >\n<!-- end of form.element -->]]>\n\n<!ENTITY % form.attlist  \"INCLUDE\" >\n<![%form.attlist;[\n<!ATTLIST %form.qname;\n      %Common.attrib;\n      action       %URI.datatype;           #REQUIRED\n      method       ( get | post )           'get'\n      name         CDATA                    #IMPLIED\n      enctype      %ContentType.datatype;   'application/x-www-form-urlencoded'\n      accept-charset %Charsets.datatype;    #IMPLIED\n      accept       %ContentTypes.datatype;  #IMPLIED\n>\n<!-- end of form.attlist -->]]>\n\n<!-- label: Form Field Label Text ...................... -->\n\n<!-- Each label must not contain more than ONE field\n-->\n\n<!ENTITY % label.element  \"INCLUDE\" >\n<![%label.element;[\n<!ENTITY % label.content\n     \"( #PCDATA\n      | %input.qname; | %select.qname; | %textarea.qname; | %button.qname;\n      | %InlStruct.class;\n      %InlPhras.class;\n      %I18n.class;\n      %InlPres.class;\n      %Anchor.class;\n      %InlSpecial.class;\n      %Inline.extra;\n      %Misc.class; )*\"\n>\n<!ELEMENT %label.qname;  %label.content; >\n<!-- end of label.element -->]]>\n\n<!ENTITY % label.attlist  \"INCLUDE\" >\n<![%label.attlist;[\n<!ATTLIST %label.qname;\n      %Common.attrib;\n      for          IDREF                    #IMPLIED\n      accesskey    %Character.datatype;     #IMPLIED\n>\n<!-- end of label.attlist -->]]>\n\n<!-- input: Form Control ............................... -->\n\n<!ENTITY % input.element  \"INCLUDE\" >\n<![%input.element;[\n<!ENTITY % input.content  \"EMPTY\" >\n<!ELEMENT %input.qname;  %input.content; >\n<!-- end of input.element -->]]>\n\n<!ENTITY % input.attlist  \"INCLUDE\" >\n<![%input.attlist;[\n<!ENTITY % InputType.class\n     \"( text | password | checkbox | radio | submit\n      | reset | file | hidden | image | button )\"\n>\n<!-- attribute 'name' required for all but submit & reset\n-->\n<!ATTLIST %input.qname;\n      %Common.attrib;\n      type         %InputType.class;        'text'\n      name         CDATA                    #IMPLIED\n      value        CDATA                    #IMPLIED\n      checked      ( checked )              #IMPLIED\n      disabled     ( disabled )             #IMPLIED\n      readonly     ( readonly )             #IMPLIED\n      size         %Number.datatype;        #IMPLIED\n      maxlength    %Number.datatype;        #IMPLIED\n      src          %URI.datatype;           #IMPLIED\n      alt          %Text.datatype;          #IMPLIED\n      tabindex     %Number.datatype;        #IMPLIED\n      accesskey    %Character.datatype;     #IMPLIED\n      accept       %ContentTypes.datatype;  #IMPLIED\n>\n<!-- end of input.attlist -->]]>\n\n<!-- select: Option Selector ........................... -->\n\n<!ENTITY % select.element  \"INCLUDE\" >\n<![%select.element;[\n<!ENTITY % select.content\n     \"( %optgroup.qname; | %option.qname; )+\"\n>\n<!ELEMENT %select.qname;  %select.content; >\n<!-- end of select.element -->]]>\n\n<!ENTITY % select.attlist  \"INCLUDE\" >\n<![%select.attlist;[\n<!ATTLIST %select.qname;\n      %Common.attrib;\n      name         CDATA                    #IMPLIED\n      size         %Number.datatype;        #IMPLIED\n      multiple     ( multiple )             #IMPLIED\n      disabled     ( disabled )             #IMPLIED\n      tabindex     %Number.datatype;        #IMPLIED\n>\n<!-- end of select.attlist -->]]>\n\n<!-- optgroup: Option Group ............................ -->\n\n<!ENTITY % optgroup.element  \"INCLUDE\" >\n<![%optgroup.element;[\n<!ENTITY % optgroup.content  \"( %option.qname; )+\" >\n<!ELEMENT %optgroup.qname;  %optgroup.content; >\n<!-- end of optgroup.element -->]]>\n\n<!ENTITY % optgroup.attlist  \"INCLUDE\" >\n<![%optgroup.attlist;[\n<!ATTLIST %optgroup.qname;\n      %Common.attrib;\n      disabled     ( disabled )             #IMPLIED\n      label        %Text.datatype;          #REQUIRED\n>\n<!-- end of optgroup.attlist -->]]>\n\n<!-- option: Selectable Choice ......................... -->\n\n<!ENTITY % option.element  \"INCLUDE\" >\n<![%option.element;[\n<!ENTITY % option.content  \"( #PCDATA )\" >\n<!ELEMENT %option.qname;  %option.content; >\n<!-- end of option.element -->]]>\n\n<!ENTITY % option.attlist  \"INCLUDE\" >\n<![%option.attlist;[\n<!ATTLIST %option.qname;\n      %Common.attrib;\n      selected     ( selected )             #IMPLIED\n      disabled     ( disabled )             #IMPLIED\n      label        %Text.datatype;          #IMPLIED\n      value        CDATA                    #IMPLIED\n>\n<!-- end of option.attlist -->]]>\n\n<!-- textarea: Multi-Line Text Field ................... -->\n\n<!ENTITY % textarea.element  \"INCLUDE\" >\n<![%textarea.element;[\n<!ENTITY % textarea.content  \"( #PCDATA )\" >\n<!ELEMENT %textarea.qname;  %textarea.content; >\n<!-- end of textarea.element -->]]>\n\n<!ENTITY % textarea.attlist  \"INCLUDE\" >\n<![%textarea.attlist;[\n<!ATTLIST %textarea.qname;\n      %Common.attrib;\n      name         CDATA                    #IMPLIED\n      rows         %Number.datatype;        #REQUIRED\n      cols         %Number.datatype;        #REQUIRED\n      disabled     ( disabled )             #IMPLIED\n      readonly     ( readonly )             #IMPLIED\n      tabindex     %Number.datatype;        #IMPLIED\n      accesskey    %Character.datatype;     #IMPLIED\n>\n<!-- end of textarea.attlist -->]]>\n\n<!-- fieldset: Form Control Group ...................... -->\n\n<!-- #PCDATA is to solve the mixed content problem,\n     per specification only whitespace is allowed\n-->\n\n<!ENTITY % fieldset.element  \"INCLUDE\" >\n<![%fieldset.element;[\n<!ENTITY % fieldset.content\n     \"( #PCDATA | %legend.qname; | %Flow.mix; )*\"\n>\n<!ELEMENT %fieldset.qname;  %fieldset.content; >\n<!-- end of fieldset.element -->]]>\n\n<!ENTITY % fieldset.attlist  \"INCLUDE\" >\n<![%fieldset.attlist;[\n<!ATTLIST %fieldset.qname;\n      %Common.attrib;\n>\n<!-- end of fieldset.attlist -->]]>\n\n<!-- legend: Fieldset Legend ........................... -->\n\n<!ENTITY % legend.element  \"INCLUDE\" >\n<![%legend.element;[\n<!ENTITY % legend.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ELEMENT %legend.qname;  %legend.content; >\n<!-- end of legend.element -->]]>\n\n<!ENTITY % legend.attlist  \"INCLUDE\" >\n<![%legend.attlist;[\n<!ATTLIST %legend.qname;\n      %Common.attrib;\n      accesskey    %Character.datatype;     #IMPLIED\n>\n<!-- end of legend.attlist -->]]>\n\n<!-- button: Push Button ............................... -->\n\n<!ENTITY % button.element  \"INCLUDE\" >\n<![%button.element;[\n<!ENTITY % button.content\n     \"( #PCDATA\n      | %BlkNoForm.mix;\n      | %InlStruct.class;\n      %InlPhras.class;\n      %InlPres.class;\n      %I18n.class;\n      %InlSpecial.class;\n      %Inline.extra; )*\"\n>\n<!ELEMENT %button.qname;  %button.content; >\n<!-- end of button.element -->]]>\n\n<!ENTITY % button.attlist  \"INCLUDE\" >\n<![%button.attlist;[\n<!ATTLIST %button.qname;\n      %Common.attrib;\n      name         CDATA                    #IMPLIED\n      value        CDATA                    #IMPLIED\n      type         ( button | submit | reset ) 'submit'\n      disabled     ( disabled )             #IMPLIED\n      tabindex     %Number.datatype;        #IMPLIED\n      accesskey    %Character.datatype;     #IMPLIED\n>\n<!-- end of button.attlist -->]]>\n\n<!-- end of xhtml-form-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-framework-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Modular Framework Module  ...................................... -->\n<!-- file: xhtml-framework-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-framework-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ENTITIES XHTML Modular Framework 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-framework-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Modular Framework\n\n     This required module instantiates the modules needed\n     to support the XHTML modularization model, including:\n\n        +  datatypes\n        +  namespace-qualified names\n        +  common attributes\n        +  document model\n        +  character entities\n\n     The Intrinsic Events module is ignored by default but\n     occurs in this module because it must be instantiated\n     prior to Attributes but after Datatypes.\n-->\n\n<!ENTITY % xhtml-arch.module \"IGNORE\" >\n<![%xhtml-arch.module;[\n<!ENTITY % xhtml-arch.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Base Architecture 1.0//EN\"\n            \"xhtml-arch-1.mod\" >\n%xhtml-arch.mod;]]>\n\n<!ENTITY % xhtml-notations.module \"IGNORE\" >\n<![%xhtml-notations.module;[\n<!ENTITY % xhtml-notations.mod\n     PUBLIC \"-//W3C//NOTATIONS XHTML Notations 1.0//EN\"\n            \"xhtml-notations-1.mod\" >\n%xhtml-notations.mod;]]>\n\n<!ENTITY % xhtml-datatypes.module \"INCLUDE\" >\n<![%xhtml-datatypes.module;[\n<!ENTITY % xhtml-datatypes.mod\n     PUBLIC \"-//W3C//ENTITIES XHTML Datatypes 1.0//EN\"\n            \"xhtml-datatypes-1.mod\" >\n%xhtml-datatypes.mod;]]>\n\n<!-- placeholder for XLink support module -->\n<!ENTITY % xhtml-xlink.mod \"\" >\n%xhtml-xlink.mod;\n\n<!ENTITY % xhtml-qname.module \"INCLUDE\" >\n<![%xhtml-qname.module;[\n<!ENTITY % xhtml-qname.mod\n     PUBLIC \"-//W3C//ENTITIES XHTML Qualified Names 1.0//EN\"\n            \"xhtml-qname-1.mod\" >\n%xhtml-qname.mod;]]>\n\n<!ENTITY % xhtml-events.module \"IGNORE\" >\n<![%xhtml-events.module;[\n<!ENTITY % xhtml-events.mod\n     PUBLIC \"-//W3C//ENTITIES XHTML Intrinsic Events 1.0//EN\"\n            \"xhtml-events-1.mod\" >\n%xhtml-events.mod;]]>\n\n<!ENTITY % xhtml-attribs.module \"INCLUDE\" >\n<![%xhtml-attribs.module;[\n<!ENTITY % xhtml-attribs.mod\n     PUBLIC \"-//W3C//ENTITIES XHTML Common Attributes 1.0//EN\"\n            \"xhtml-attribs-1.mod\" >\n%xhtml-attribs.mod;]]>\n\n<!-- placeholder for content model redeclarations -->\n<!ENTITY % xhtml-model.redecl \"\" >\n%xhtml-model.redecl;\n\n<!ENTITY % xhtml-model.module \"INCLUDE\" >\n<![%xhtml-model.module;[\n<!-- instantiate the Document Model module declared in the DTD driver\n-->\n%xhtml-model.mod;]]>\n\n<!ENTITY % xhtml-charent.module \"INCLUDE\" >\n<![%xhtml-charent.module;[\n<!ENTITY % xhtml-charent.mod\n     PUBLIC \"-//W3C//ENTITIES XHTML Character Entities 1.0//EN\"\n            \"xhtml-charent-1.mod\" >\n%xhtml-charent.mod;]]>\n\n<!-- end of xhtml-framework-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-hypertext-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Hypertext Module  .............................................. -->\n<!-- file: xhtml-hypertext-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-hypertext-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Hypertext 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-hypertext-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Hypertext\n\n        a\n\n     This module declares the anchor ('a') element type, which\n     defines the source of a hypertext link. The destination\n     (or link 'target') is identified via its 'id' attribute\n     rather than the 'name' attribute as was used in HTML.\n-->\n\n<!-- ............  Anchor Element  ............ -->\n\n<!ENTITY % a.element  \"INCLUDE\" >\n<![%a.element;[\n<!ENTITY % a.content\n     \"( #PCDATA | %InlNoAnchor.mix; )*\"\n>\n<!ENTITY % a.qname  \"a\" >\n<!ELEMENT %a.qname;  %a.content; >\n<!-- end of a.element -->]]>\n\n<!ENTITY % a.attlist  \"INCLUDE\" >\n<![%a.attlist;[\n<!ATTLIST %a.qname;\n      %Common.attrib;\n      href         %URI.datatype;           #IMPLIED\n      charset      %Charset.datatype;       #IMPLIED\n      type         %ContentType.datatype;   #IMPLIED\n      hreflang     %LanguageCode.datatype;  #IMPLIED\n      rel          %LinkTypes.datatype;     #IMPLIED\n      rev          %LinkTypes.datatype;     #IMPLIED\n      accesskey    %Character.datatype;     #IMPLIED\n      tabindex     %Number.datatype;        #IMPLIED\n>\n<!-- end of a.attlist -->]]>\n\n<!-- end of xhtml-hypertext-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-image-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Images Module  ................................................. -->\n<!-- file: xhtml-image-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Rovision: $Id: xhtml-image-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Images 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-image-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Images\n\n        img\n\n     This module provides markup to support basic image embedding.\n-->\n\n<!-- To avoid problems with text-only UAs as well as to make\n     image content understandable and navigable to users of\n     non-visual UAs, you need to provide a description with\n     the 'alt' attribute, and avoid server-side image maps.\n-->\n\n<!ENTITY % img.element  \"INCLUDE\" >\n<![%img.element;[\n<!ENTITY % img.content  \"EMPTY\" >\n<!ENTITY % img.qname  \"img\" >\n<!ELEMENT %img.qname;  %img.content; >\n<!-- end of img.element -->]]>\n\n<!ENTITY % img.attlist  \"INCLUDE\" >\n<![%img.attlist;[\n<!ATTLIST %img.qname;\n      %Common.attrib;\n      src          %URI.datatype;           #REQUIRED\n      alt          %Text.datatype;          #REQUIRED\n      longdesc     %URI.datatype;           #IMPLIED\n      name         CDATA                    #IMPLIED\n      height       %Length.datatype;        #IMPLIED\n      width        %Length.datatype;        #IMPLIED\n>\n<!-- end of img.attlist -->]]>\n\n<!-- end of xhtml-image-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlphras-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Inline Phrasal Module  ......................................... -->\n<!-- file: xhtml-inlphras-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-inlphras-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Inline Phrasal 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-inlphras-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Inline Phrasal\n\n        abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var\n\n     This module declares the elements and their attributes used to\n     support inline-level phrasal markup.\n-->\n\n<!ENTITY % abbr.element  \"INCLUDE\" >\n<![%abbr.element;[\n<!ENTITY % abbr.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % abbr.qname  \"abbr\" >\n<!ELEMENT %abbr.qname;  %abbr.content; >\n<!-- end of abbr.element -->]]>\n\n<!ENTITY % abbr.attlist  \"INCLUDE\" >\n<![%abbr.attlist;[\n<!ATTLIST %abbr.qname;\n      %Common.attrib;\n>\n<!-- end of abbr.attlist -->]]>\n\n<!ENTITY % acronym.element  \"INCLUDE\" >\n<![%acronym.element;[\n<!ENTITY % acronym.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % acronym.qname  \"acronym\" >\n<!ELEMENT %acronym.qname;  %acronym.content; >\n<!-- end of acronym.element -->]]>\n\n<!ENTITY % acronym.attlist  \"INCLUDE\" >\n<![%acronym.attlist;[\n<!ATTLIST %acronym.qname;\n      %Common.attrib;\n>\n<!-- end of acronym.attlist -->]]>\n\n<!ENTITY % cite.element  \"INCLUDE\" >\n<![%cite.element;[\n<!ENTITY % cite.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % cite.qname  \"cite\" >\n<!ELEMENT %cite.qname;  %cite.content; >\n<!-- end of cite.element -->]]>\n\n<!ENTITY % cite.attlist  \"INCLUDE\" >\n<![%cite.attlist;[\n<!ATTLIST %cite.qname;\n      %Common.attrib;\n>\n<!-- end of cite.attlist -->]]>\n\n<!ENTITY % code.element  \"INCLUDE\" >\n<![%code.element;[\n<!ENTITY % code.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % code.qname  \"code\" >\n<!ELEMENT %code.qname;  %code.content; >\n<!-- end of code.element -->]]>\n\n<!ENTITY % code.attlist  \"INCLUDE\" >\n<![%code.attlist;[\n<!ATTLIST %code.qname;\n      %Common.attrib;\n>\n<!-- end of code.attlist -->]]>\n\n<!ENTITY % dfn.element  \"INCLUDE\" >\n<![%dfn.element;[\n<!ENTITY % dfn.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % dfn.qname  \"dfn\" >\n<!ELEMENT %dfn.qname;  %dfn.content; >\n<!-- end of dfn.element -->]]>\n\n<!ENTITY % dfn.attlist  \"INCLUDE\" >\n<![%dfn.attlist;[\n<!ATTLIST %dfn.qname;\n      %Common.attrib;\n>\n<!-- end of dfn.attlist -->]]>\n\n<!ENTITY % em.element  \"INCLUDE\" >\n<![%em.element;[\n<!ENTITY % em.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % em.qname  \"em\" >\n<!ELEMENT %em.qname;  %em.content; >\n<!-- end of em.element -->]]>\n\n<!ENTITY % em.attlist  \"INCLUDE\" >\n<![%em.attlist;[\n<!ATTLIST %em.qname;\n      %Common.attrib;\n>\n<!-- end of em.attlist -->]]>\n\n<!ENTITY % kbd.element  \"INCLUDE\" >\n<![%kbd.element;[\n<!ENTITY % kbd.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % kbd.qname  \"kbd\" >\n<!ELEMENT %kbd.qname;  %kbd.content; >\n<!-- end of kbd.element -->]]>\n\n<!ENTITY % kbd.attlist  \"INCLUDE\" >\n<![%kbd.attlist;[\n<!ATTLIST %kbd.qname;\n      %Common.attrib;\n>\n<!-- end of kbd.attlist -->]]>\n\n<!ENTITY % q.element  \"INCLUDE\" >\n<![%q.element;[\n<!ENTITY % q.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % q.qname  \"q\" >\n<!ELEMENT %q.qname;  %q.content; >\n<!-- end of q.element -->]]>\n\n<!ENTITY % q.attlist  \"INCLUDE\" >\n<![%q.attlist;[\n<!ATTLIST %q.qname;\n      %Common.attrib;\n      cite         %URI.datatype;           #IMPLIED\n>\n<!-- end of q.attlist -->]]>\n\n<!ENTITY % samp.element  \"INCLUDE\" >\n<![%samp.element;[\n<!ENTITY % samp.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % samp.qname  \"samp\" >\n<!ELEMENT %samp.qname;  %samp.content; >\n<!-- end of samp.element -->]]>\n\n<!ENTITY % samp.attlist  \"INCLUDE\" >\n<![%samp.attlist;[\n<!ATTLIST %samp.qname;\n      %Common.attrib;\n>\n<!-- end of samp.attlist -->]]>\n\n<!ENTITY % strong.element  \"INCLUDE\" >\n<![%strong.element;[\n<!ENTITY % strong.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % strong.qname  \"strong\" >\n<!ELEMENT %strong.qname;  %strong.content; >\n<!-- end of strong.element -->]]>\n\n<!ENTITY % strong.attlist  \"INCLUDE\" >\n<![%strong.attlist;[\n<!ATTLIST %strong.qname;\n      %Common.attrib;\n>\n<!-- end of strong.attlist -->]]>\n\n<!ENTITY % var.element  \"INCLUDE\" >\n<![%var.element;[\n<!ENTITY % var.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % var.qname  \"var\" >\n<!ELEMENT %var.qname;  %var.content; >\n<!-- end of var.element -->]]>\n\n<!ENTITY % var.attlist  \"INCLUDE\" >\n<![%var.attlist;[\n<!ATTLIST %var.qname;\n      %Common.attrib;\n>\n<!-- end of var.attlist -->]]>\n\n<!-- end of xhtml-inlphras-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlpres-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Inline Presentation Module  .................................... -->\n<!-- file: xhtml-inlpres-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-inlpres-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Inline Presentation 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-inlpres-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Inline Presentational Elements\n\n        b, big, i, small, sub, sup, tt\n\n     This module declares the elements and their attributes used to\n     support inline-level presentational markup.\n-->\n\n<!ENTITY % b.element  \"INCLUDE\" >\n<![%b.element;[\n<!ENTITY % b.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % b.qname  \"b\" >\n<!ELEMENT %b.qname;  %b.content; >\n<!-- end of b.element -->]]>\n\n<!ENTITY % b.attlist  \"INCLUDE\" >\n<![%b.attlist;[\n<!ATTLIST %b.qname;\n      %Common.attrib;\n>\n<!-- end of b.attlist -->]]>\n\n<!ENTITY % big.element  \"INCLUDE\" >\n<![%big.element;[\n<!ENTITY % big.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % big.qname  \"big\" >\n<!ELEMENT %big.qname;  %big.content; >\n<!-- end of big.element -->]]>\n\n<!ENTITY % big.attlist  \"INCLUDE\" >\n<![%big.attlist;[\n<!ATTLIST %big.qname;\n      %Common.attrib;\n>\n<!-- end of big.attlist -->]]>\n\n<!ENTITY % i.element  \"INCLUDE\" >\n<![%i.element;[\n<!ENTITY % i.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % i.qname  \"i\" >\n<!ELEMENT %i.qname;  %i.content; >\n<!-- end of i.element -->]]>\n\n<!ENTITY % i.attlist  \"INCLUDE\" >\n<![%i.attlist;[\n<!ATTLIST %i.qname;\n      %Common.attrib;\n>\n<!-- end of i.attlist -->]]>\n\n<!ENTITY % small.element  \"INCLUDE\" >\n<![%small.element;[\n<!ENTITY % small.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % small.qname  \"small\" >\n<!ELEMENT %small.qname;  %small.content; >\n<!-- end of small.element -->]]>\n\n<!ENTITY % small.attlist  \"INCLUDE\" >\n<![%small.attlist;[\n<!ATTLIST %small.qname;\n      %Common.attrib;\n>\n<!-- end of small.attlist -->]]>\n\n<!ENTITY % sub.element  \"INCLUDE\" >\n<![%sub.element;[\n<!ENTITY % sub.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % sub.qname  \"sub\" >\n<!ELEMENT %sub.qname;  %sub.content; >\n<!-- end of sub.element -->]]>\n\n<!ENTITY % sub.attlist  \"INCLUDE\" >\n<![%sub.attlist;[\n<!ATTLIST %sub.qname;\n      %Common.attrib;\n>\n<!-- end of sub.attlist -->]]>\n\n<!ENTITY % sup.element  \"INCLUDE\" >\n<![%sup.element;[\n<!ENTITY % sup.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % sup.qname  \"sup\" >\n<!ELEMENT %sup.qname;  %sup.content; >\n<!-- end of sup.element -->]]>\n\n<!ENTITY % sup.attlist  \"INCLUDE\" >\n<![%sup.attlist;[\n<!ATTLIST %sup.qname;\n      %Common.attrib;\n>\n<!-- end of sup.attlist -->]]>\n\n<!ENTITY % tt.element  \"INCLUDE\" >\n<![%tt.element;[\n<!ENTITY % tt.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % tt.qname  \"tt\" >\n<!ELEMENT %tt.qname;  %tt.content; >\n<!-- end of tt.element -->]]>\n\n<!ENTITY % tt.attlist  \"INCLUDE\" >\n<![%tt.attlist;[\n<!ATTLIST %tt.qname;\n      %Common.attrib;\n>\n<!-- end of tt.attlist -->]]>\n\n<!-- end of xhtml-inlpres-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstruct-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Inline Structural Module  ...................................... -->\n<!-- file: xhtml-inlstruct-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-inlstruct-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Inline Structural 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-inlstruct-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Inline Structural\n\n        br, span\n\n     This module declares the elements and their attributes\n     used to support inline-level structural markup.\n-->\n\n<!-- br: forced line break ............................. -->\n\n<!ENTITY % br.element  \"INCLUDE\" >\n<![%br.element;[\n\n<!ENTITY % br.content  \"EMPTY\" >\n<!ENTITY % br.qname  \"br\" >\n<!ELEMENT %br.qname;  %br.content; >\n\n<!-- end of br.element -->]]>\n\n<!ENTITY % br.attlist  \"INCLUDE\" >\n<![%br.attlist;[\n<!ATTLIST %br.qname;\n      %Core.attrib;\n>\n<!-- end of br.attlist -->]]>\n\n<!-- span: generic inline container .................... -->\n\n<!ENTITY % span.element  \"INCLUDE\" >\n<![%span.element;[\n<!ENTITY % span.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ENTITY % span.qname  \"span\" >\n<!ELEMENT %span.qname;  %span.content; >\n<!-- end of span.element -->]]>\n\n<!ENTITY % span.attlist  \"INCLUDE\" >\n<![%span.attlist;[\n<!ATTLIST %span.qname;\n      %Common.attrib;\n>\n<!-- end of span.attlist -->]]>\n\n<!-- end of xhtml-inlstruct-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstyle-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Inline Style Module  ........................................... -->\n<!-- file: xhtml-inlstyle-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-inlstyle-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ENTITIES XHTML Inline Style 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-inlstyle-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Inline Style\n\n     This module declares the 'style' attribute, used to support inline\n     style markup. This module must be instantiated prior to the XHTML\n     Common Attributes module in order to be included in %Core.attrib;.\n-->\n\n<!ENTITY % style.attrib\n     \"style        CDATA                    #IMPLIED\"\n>\n\n\n<!ENTITY % Core.extra.attrib\n     \"%style.attrib;\"\n>\n\n<!-- end of xhtml-inlstyle-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-lat1.ent",
    "content": "<!-- Portions (C) International Organization for Standardization 1986\n     Permission to copy in any form is granted for use with\n     conforming SGML systems and applications as defined in\n     ISO 8879, provided this notice is included in all copies.\n-->\n<!-- Character entity set. Typical invocation:\n    <!ENTITY % HTMLlat1 PUBLIC\n       \"-//W3C//ENTITIES Latin 1 for XHTML//EN\"\n       \"http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent\">\n    %HTMLlat1;\n-->\n\n<!ENTITY nbsp   \"&#160;\"> <!-- no-break space = non-breaking space,\n                                  U+00A0 ISOnum -->\n<!ENTITY iexcl  \"&#161;\"> <!-- inverted exclamation mark, U+00A1 ISOnum -->\n<!ENTITY cent   \"&#162;\"> <!-- cent sign, U+00A2 ISOnum -->\n<!ENTITY pound  \"&#163;\"> <!-- pound sign, U+00A3 ISOnum -->\n<!ENTITY curren \"&#164;\"> <!-- currency sign, U+00A4 ISOnum -->\n<!ENTITY yen    \"&#165;\"> <!-- yen sign = yuan sign, U+00A5 ISOnum -->\n<!ENTITY brvbar \"&#166;\"> <!-- broken bar = broken vertical bar,\n                                  U+00A6 ISOnum -->\n<!ENTITY sect   \"&#167;\"> <!-- section sign, U+00A7 ISOnum -->\n<!ENTITY uml    \"&#168;\"> <!-- diaeresis = spacing diaeresis,\n                                  U+00A8 ISOdia -->\n<!ENTITY copy   \"&#169;\"> <!-- copyright sign, U+00A9 ISOnum -->\n<!ENTITY ordf   \"&#170;\"> <!-- feminine ordinal indicator, U+00AA ISOnum -->\n<!ENTITY laquo  \"&#171;\"> <!-- left-pointing double angle quotation mark\n                                  = left pointing guillemet, U+00AB ISOnum -->\n<!ENTITY not    \"&#172;\"> <!-- not sign = angled dash,\n                                  U+00AC ISOnum -->\n<!ENTITY shy    \"&#173;\"> <!-- soft hyphen = discretionary hyphen,\n                                  U+00AD ISOnum -->\n<!ENTITY reg    \"&#174;\"> <!-- registered sign = registered trade mark sign,\n                                  U+00AE ISOnum -->\n<!ENTITY macr   \"&#175;\"> <!-- macron = spacing macron = overline\n                                  = APL overbar, U+00AF ISOdia -->\n<!ENTITY deg    \"&#176;\"> <!-- degree sign, U+00B0 ISOnum -->\n<!ENTITY plusmn \"&#177;\"> <!-- plus-minus sign = plus-or-minus sign,\n                                  U+00B1 ISOnum -->\n<!ENTITY sup2   \"&#178;\"> <!-- superscript two = superscript digit two\n                                  = squared, U+00B2 ISOnum -->\n<!ENTITY sup3   \"&#179;\"> <!-- superscript three = superscript digit three\n                                  = cubed, U+00B3 ISOnum -->\n<!ENTITY acute  \"&#180;\"> <!-- acute accent = spacing acute,\n                                  U+00B4 ISOdia -->\n<!ENTITY micro  \"&#181;\"> <!-- micro sign, U+00B5 ISOnum -->\n<!ENTITY para   \"&#182;\"> <!-- pilcrow sign = paragraph sign,\n                                  U+00B6 ISOnum -->\n<!ENTITY middot \"&#183;\"> <!-- middle dot = Georgian comma\n                                  = Greek middle dot, U+00B7 ISOnum -->\n<!ENTITY cedil  \"&#184;\"> <!-- cedilla = spacing cedilla, U+00B8 ISOdia -->\n<!ENTITY sup1   \"&#185;\"> <!-- superscript one = superscript digit one,\n                                  U+00B9 ISOnum -->\n<!ENTITY ordm   \"&#186;\"> <!-- masculine ordinal indicator,\n                                  U+00BA ISOnum -->\n<!ENTITY raquo  \"&#187;\"> <!-- right-pointing double angle quotation mark\n                                  = right pointing guillemet, U+00BB ISOnum -->\n<!ENTITY frac14 \"&#188;\"> <!-- vulgar fraction one quarter\n                                  = fraction one quarter, U+00BC ISOnum -->\n<!ENTITY frac12 \"&#189;\"> <!-- vulgar fraction one half\n                                  = fraction one half, U+00BD ISOnum -->\n<!ENTITY frac34 \"&#190;\"> <!-- vulgar fraction three quarters\n                                  = fraction three quarters, U+00BE ISOnum -->\n<!ENTITY iquest \"&#191;\"> <!-- inverted question mark\n                                  = turned question mark, U+00BF ISOnum -->\n<!ENTITY Agrave \"&#192;\"> <!-- latin capital letter A with grave\n                                  = latin capital letter A grave,\n                                  U+00C0 ISOlat1 -->\n<!ENTITY Aacute \"&#193;\"> <!-- latin capital letter A with acute,\n                                  U+00C1 ISOlat1 -->\n<!ENTITY Acirc  \"&#194;\"> <!-- latin capital letter A with circumflex,\n                                  U+00C2 ISOlat1 -->\n<!ENTITY Atilde \"&#195;\"> <!-- latin capital letter A with tilde,\n                                  U+00C3 ISOlat1 -->\n<!ENTITY Auml   \"&#196;\"> <!-- latin capital letter A with diaeresis,\n                                  U+00C4 ISOlat1 -->\n<!ENTITY Aring  \"&#197;\"> <!-- latin capital letter A with ring above\n                                  = latin capital letter A ring,\n                                  U+00C5 ISOlat1 -->\n<!ENTITY AElig  \"&#198;\"> <!-- latin capital letter AE\n                                  = latin capital ligature AE,\n                                  U+00C6 ISOlat1 -->\n<!ENTITY Ccedil \"&#199;\"> <!-- latin capital letter C with cedilla,\n                                  U+00C7 ISOlat1 -->\n<!ENTITY Egrave \"&#200;\"> <!-- latin capital letter E with grave,\n                                  U+00C8 ISOlat1 -->\n<!ENTITY Eacute \"&#201;\"> <!-- latin capital letter E with acute,\n                                  U+00C9 ISOlat1 -->\n<!ENTITY Ecirc  \"&#202;\"> <!-- latin capital letter E with circumflex,\n                                  U+00CA ISOlat1 -->\n<!ENTITY Euml   \"&#203;\"> <!-- latin capital letter E with diaeresis,\n                                  U+00CB ISOlat1 -->\n<!ENTITY Igrave \"&#204;\"> <!-- latin capital letter I with grave,\n                                  U+00CC ISOlat1 -->\n<!ENTITY Iacute \"&#205;\"> <!-- latin capital letter I with acute,\n                                  U+00CD ISOlat1 -->\n<!ENTITY Icirc  \"&#206;\"> <!-- latin capital letter I with circumflex,\n                                  U+00CE ISOlat1 -->\n<!ENTITY Iuml   \"&#207;\"> <!-- latin capital letter I with diaeresis,\n                                  U+00CF ISOlat1 -->\n<!ENTITY ETH    \"&#208;\"> <!-- latin capital letter ETH, U+00D0 ISOlat1 -->\n<!ENTITY Ntilde \"&#209;\"> <!-- latin capital letter N with tilde,\n                                  U+00D1 ISOlat1 -->\n<!ENTITY Ograve \"&#210;\"> <!-- latin capital letter O with grave,\n                                  U+00D2 ISOlat1 -->\n<!ENTITY Oacute \"&#211;\"> <!-- latin capital letter O with acute,\n                                  U+00D3 ISOlat1 -->\n<!ENTITY Ocirc  \"&#212;\"> <!-- latin capital letter O with circumflex,\n                                  U+00D4 ISOlat1 -->\n<!ENTITY Otilde \"&#213;\"> <!-- latin capital letter O with tilde,\n                                  U+00D5 ISOlat1 -->\n<!ENTITY Ouml   \"&#214;\"> <!-- latin capital letter O with diaeresis,\n                                  U+00D6 ISOlat1 -->\n<!ENTITY times  \"&#215;\"> <!-- multiplication sign, U+00D7 ISOnum -->\n<!ENTITY Oslash \"&#216;\"> <!-- latin capital letter O with stroke\n                                  = latin capital letter O slash,\n                                  U+00D8 ISOlat1 -->\n<!ENTITY Ugrave \"&#217;\"> <!-- latin capital letter U with grave,\n                                  U+00D9 ISOlat1 -->\n<!ENTITY Uacute \"&#218;\"> <!-- latin capital letter U with acute,\n                                  U+00DA ISOlat1 -->\n<!ENTITY Ucirc  \"&#219;\"> <!-- latin capital letter U with circumflex,\n                                  U+00DB ISOlat1 -->\n<!ENTITY Uuml   \"&#220;\"> <!-- latin capital letter U with diaeresis,\n                                  U+00DC ISOlat1 -->\n<!ENTITY Yacute \"&#221;\"> <!-- latin capital letter Y with acute,\n                                  U+00DD ISOlat1 -->\n<!ENTITY THORN  \"&#222;\"> <!-- latin capital letter THORN,\n                                  U+00DE ISOlat1 -->\n<!ENTITY szlig  \"&#223;\"> <!-- latin small letter sharp s = ess-zed,\n                                  U+00DF ISOlat1 -->\n<!ENTITY agrave \"&#224;\"> <!-- latin small letter a with grave\n                                  = latin small letter a grave,\n                                  U+00E0 ISOlat1 -->\n<!ENTITY aacute \"&#225;\"> <!-- latin small letter a with acute,\n                                  U+00E1 ISOlat1 -->\n<!ENTITY acirc  \"&#226;\"> <!-- latin small letter a with circumflex,\n                                  U+00E2 ISOlat1 -->\n<!ENTITY atilde \"&#227;\"> <!-- latin small letter a with tilde,\n                                  U+00E3 ISOlat1 -->\n<!ENTITY auml   \"&#228;\"> <!-- latin small letter a with diaeresis,\n                                  U+00E4 ISOlat1 -->\n<!ENTITY aring  \"&#229;\"> <!-- latin small letter a with ring above\n                                  = latin small letter a ring,\n                                  U+00E5 ISOlat1 -->\n<!ENTITY aelig  \"&#230;\"> <!-- latin small letter ae\n                                  = latin small ligature ae, U+00E6 ISOlat1 -->\n<!ENTITY ccedil \"&#231;\"> <!-- latin small letter c with cedilla,\n                                  U+00E7 ISOlat1 -->\n<!ENTITY egrave \"&#232;\"> <!-- latin small letter e with grave,\n                                  U+00E8 ISOlat1 -->\n<!ENTITY eacute \"&#233;\"> <!-- latin small letter e with acute,\n                                  U+00E9 ISOlat1 -->\n<!ENTITY ecirc  \"&#234;\"> <!-- latin small letter e with circumflex,\n                                  U+00EA ISOlat1 -->\n<!ENTITY euml   \"&#235;\"> <!-- latin small letter e with diaeresis,\n                                  U+00EB ISOlat1 -->\n<!ENTITY igrave \"&#236;\"> <!-- latin small letter i with grave,\n                                  U+00EC ISOlat1 -->\n<!ENTITY iacute \"&#237;\"> <!-- latin small letter i with acute,\n                                  U+00ED ISOlat1 -->\n<!ENTITY icirc  \"&#238;\"> <!-- latin small letter i with circumflex,\n                                  U+00EE ISOlat1 -->\n<!ENTITY iuml   \"&#239;\"> <!-- latin small letter i with diaeresis,\n                                  U+00EF ISOlat1 -->\n<!ENTITY eth    \"&#240;\"> <!-- latin small letter eth, U+00F0 ISOlat1 -->\n<!ENTITY ntilde \"&#241;\"> <!-- latin small letter n with tilde,\n                                  U+00F1 ISOlat1 -->\n<!ENTITY ograve \"&#242;\"> <!-- latin small letter o with grave,\n                                  U+00F2 ISOlat1 -->\n<!ENTITY oacute \"&#243;\"> <!-- latin small letter o with acute,\n                                  U+00F3 ISOlat1 -->\n<!ENTITY ocirc  \"&#244;\"> <!-- latin small letter o with circumflex,\n                                  U+00F4 ISOlat1 -->\n<!ENTITY otilde \"&#245;\"> <!-- latin small letter o with tilde,\n                                  U+00F5 ISOlat1 -->\n<!ENTITY ouml   \"&#246;\"> <!-- latin small letter o with diaeresis,\n                                  U+00F6 ISOlat1 -->\n<!ENTITY divide \"&#247;\"> <!-- division sign, U+00F7 ISOnum -->\n<!ENTITY oslash \"&#248;\"> <!-- latin small letter o with stroke,\n                                  = latin small letter o slash,\n                                  U+00F8 ISOlat1 -->\n<!ENTITY ugrave \"&#249;\"> <!-- latin small letter u with grave,\n                                  U+00F9 ISOlat1 -->\n<!ENTITY uacute \"&#250;\"> <!-- latin small letter u with acute,\n                                  U+00FA ISOlat1 -->\n<!ENTITY ucirc  \"&#251;\"> <!-- latin small letter u with circumflex,\n                                  U+00FB ISOlat1 -->\n<!ENTITY uuml   \"&#252;\"> <!-- latin small letter u with diaeresis,\n                                  U+00FC ISOlat1 -->\n<!ENTITY yacute \"&#253;\"> <!-- latin small letter y with acute,\n                                  U+00FD ISOlat1 -->\n<!ENTITY thorn  \"&#254;\"> <!-- latin small letter thorn,\n                                  U+00FE ISOlat1 -->\n<!ENTITY yuml   \"&#255;\"> <!-- latin small letter y with diaeresis,\n                                  U+00FF ISOlat1 -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-link-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Link Element Module  ........................................... -->\n<!-- file: xhtml-link-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-link-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Link Element 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-link-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Link element\n\n        link\n\n     This module declares the link element type and its attributes,\n     which could (in principle) be used to define document-level links\n     to external resources such as:\n\n     a) for document specific toolbars/menus, e.g. start, contents,\n        previous, next, index, end, help\n     b) to link to a separate style sheet (rel=\"stylesheet\")\n     c) to make a link to a script (rel=\"script\")\n     d) by style sheets to control how collections of html nodes are\n        rendered into printed documents\n     e) to make a link to a printable version of this document\n        e.g. a postscript or pdf version (rel=\"alternate\" media=\"print\")\n-->\n\n<!-- link: Media-Independent Link ...................... -->\n\n<!ENTITY % link.element  \"INCLUDE\" >\n<![%link.element;[\n<!ENTITY % link.content  \"EMPTY\" >\n<!ENTITY % link.qname  \"link\" >\n<!ELEMENT %link.qname;  %link.content; >\n<!-- end of link.element -->]]>\n\n<!ENTITY % link.attlist  \"INCLUDE\" >\n<![%link.attlist;[\n<!ATTLIST %link.qname;\n      %Common.attrib;\n      charset      %Charset.datatype;       #IMPLIED\n      href         %URI.datatype;           #IMPLIED\n      hreflang     %LanguageCode.datatype;  #IMPLIED\n      type         %ContentType.datatype;   #IMPLIED\n      rel          %LinkTypes.datatype;     #IMPLIED\n      rev          %LinkTypes.datatype;     #IMPLIED\n      media        %MediaDesc.datatype;     #IMPLIED\n>\n<!-- end of link.attlist -->]]>\n\n<!-- end of xhtml-link-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-list-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Lists Module  .................................................. -->\n<!-- file: xhtml-list-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-list-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Lists 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-list-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Lists\n\n        dl, dt, dd, ol, ul, li\n\n     This module declares the list-oriented element types\n     and their attributes.\n-->\n\n<!ENTITY % dl.qname  \"dl\" >\n<!ENTITY % dt.qname  \"dt\" >\n<!ENTITY % dd.qname  \"dd\" >\n<!ENTITY % ol.qname  \"ol\" >\n<!ENTITY % ul.qname  \"ul\" >\n<!ENTITY % li.qname  \"li\" >\n\n<!-- dl: Definition List ............................... -->\n\n<!ENTITY % dl.element  \"INCLUDE\" >\n<![%dl.element;[\n<!ENTITY % dl.content  \"( %dt.qname; | %dd.qname; )+\" >\n<!ELEMENT %dl.qname;  %dl.content; >\n<!-- end of dl.element -->]]>\n\n<!ENTITY % dl.attlist  \"INCLUDE\" >\n<![%dl.attlist;[\n<!ATTLIST %dl.qname;\n      %Common.attrib;\n>\n<!-- end of dl.attlist -->]]>\n\n<!-- dt: Definition Term ............................... -->\n\n<!ENTITY % dt.element  \"INCLUDE\" >\n<![%dt.element;[\n<!ENTITY % dt.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ELEMENT %dt.qname;  %dt.content; >\n<!-- end of dt.element -->]]>\n\n<!ENTITY % dt.attlist  \"INCLUDE\" >\n<![%dt.attlist;[\n<!ATTLIST %dt.qname;\n      %Common.attrib;\n>\n<!-- end of dt.attlist -->]]>\n\n<!-- dd: Definition Description ........................ -->\n\n<!ENTITY % dd.element  \"INCLUDE\" >\n<![%dd.element;[\n<!ENTITY % dd.content\n     \"( #PCDATA | %Flow.mix; )*\"\n>\n<!ELEMENT %dd.qname;  %dd.content; >\n<!-- end of dd.element -->]]>\n\n<!ENTITY % dd.attlist  \"INCLUDE\" >\n<![%dd.attlist;[\n<!ATTLIST %dd.qname;\n      %Common.attrib;\n>\n<!-- end of dd.attlist -->]]>\n\n<!-- ol: Ordered List (numbered styles) ................ -->\n\n<!ENTITY % ol.element  \"INCLUDE\" >\n<![%ol.element;[\n<!ENTITY % ol.content  \"( %li.qname; )+\" >\n<!ELEMENT %ol.qname;  %ol.content; >\n<!-- end of ol.element -->]]>\n\n<!ENTITY % ol.attlist  \"INCLUDE\" >\n<![%ol.attlist;[\n<!ATTLIST %ol.qname;\n      %Common.attrib;\n>\n<!-- end of ol.attlist -->]]>\n\n<!-- ul: Unordered List (bullet styles) ................ -->\n\n<!ENTITY % ul.element  \"INCLUDE\" >\n<![%ul.element;[\n<!ENTITY % ul.content  \"( %li.qname; )+\" >\n<!ELEMENT %ul.qname;  %ul.content; >\n<!-- end of ul.element -->]]>\n\n<!ENTITY % ul.attlist  \"INCLUDE\" >\n<![%ul.attlist;[\n<!ATTLIST %ul.qname;\n      %Common.attrib;\n>\n<!-- end of ul.attlist -->]]>\n\n<!-- li: List Item ..................................... -->\n\n<!ENTITY % li.element  \"INCLUDE\" >\n<![%li.element;[\n<!ENTITY % li.content\n     \"( #PCDATA | %Flow.mix; )*\"\n>\n<!ELEMENT %li.qname;  %li.content; >\n<!-- end of li.element -->]]>\n\n<!ENTITY % li.attlist  \"INCLUDE\" >\n<![%li.attlist;[\n<!ATTLIST %li.qname;\n      %Common.attrib;\n>\n<!-- end of li.attlist -->]]>\n\n<!-- end of xhtml-list-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-meta-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Document Metainformation Module  ............................... -->\n<!-- file: xhtml-meta-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-meta-1.mod,v 1.1 2010/07/29 13:42:47 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Metainformation 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-meta-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Meta Information\n\n        meta\n\n     This module declares the meta element type and its attributes,\n     used to provide declarative document metainformation.\n-->\n\n<!-- meta: Generic Metainformation ..................... -->\n\n<!ENTITY % meta.element  \"INCLUDE\" >\n<![%meta.element;[\n<!ENTITY % meta.content  \"EMPTY\" >\n<!ENTITY % meta.qname  \"meta\" >\n<!ELEMENT %meta.qname;  %meta.content; >\n<!-- end of meta.element -->]]>\n\n<!ENTITY % meta.attlist  \"INCLUDE\" >\n<![%meta.attlist;[\n<!ATTLIST %meta.qname;\n      %XHTML.xmlns.attrib;\n      %I18n.attrib;\n      http-equiv   NMTOKEN                  #IMPLIED\n      name         NMTOKEN                  #IMPLIED\n      content      CDATA                    #REQUIRED\n      scheme       CDATA                    #IMPLIED\n>\n<!-- end of meta.attlist -->]]>\n\n<!-- end of xhtml-meta-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-notations-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Notations Module  .............................................. -->\n<!-- file: xhtml-notations-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-notations-1.mod,v 1.1 2010/07/29 13:42:48 bertails Exp $\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//NOTATIONS XHTML Notations 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-notations-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Notations\n\n     defines the following notations, many of these imported from\n     other specifications and standards. When an existing FPI is\n     known, it is incorporated here.\n-->\n\n<!-- XML Notations ..................................... -->\n<!-- SGML and XML Notations ............................ -->\n\n<!-- W3C XML 1.0 Recommendation -->\n<!NOTATION w3c-xml\n     PUBLIC \"ISO 8879//NOTATION Extensible Markup Language (XML) 1.0//EN\" >\n\n<!-- XML 1.0 CDATA -->\n<!NOTATION cdata\n     PUBLIC \"-//W3C//NOTATION XML 1.0: CDATA//EN\" >\n\n<!-- SGML Formal Public Identifiers -->\n<!NOTATION fpi\n     PUBLIC \"ISO 8879:1986//NOTATION Formal Public Identifier//EN\" >\n\n<!-- XHTML Notations ................................... -->\n\n<!-- Length defined for cellpadding/cellspacing -->\n\n<!-- nn for pixels or nn% for percentage length -->\n<!NOTATION length\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: Length//EN\" >\n\n<!-- space-separated list of link types -->\n<!NOTATION linkTypes\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: LinkTypes//EN\" >\n\n<!-- single or comma-separated list of media descriptors -->\n<!NOTATION mediaDesc\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: MediaDesc//EN\" >\n\n<!-- pixel, percentage, or relative -->\n<!NOTATION multiLength\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: MultiLength//EN\" >\n\n<!-- one or more digits (NUMBER) -->\n<!NOTATION number\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: Number//EN\" >\n\n<!-- integer representing length in pixels -->\n<!NOTATION pixels\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: Pixels//EN\" >\n\n<!-- script expression -->\n<!NOTATION script\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: Script//EN\" >\n\n<!-- textual content -->\n<!NOTATION text\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: Text//EN\" >\n\n<!-- Imported Notations ................................ -->\n\n<!-- a single character from [ISO10646] -->\n<!NOTATION character\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: Character//EN\" >\n\n<!-- a character encoding, as per [RFC2045] -->\n<!NOTATION charset\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: Charset//EN\" >\n\n<!-- a space separated list of character encodings, as per [RFC2045] -->\n<!NOTATION charsets\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: Charsets//EN\" >\n\n<!-- media type, as per [RFC2045] -->\n<!NOTATION contentType\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: ContentType//EN\" >\n\n<!-- comma-separated list of media types, as per [RFC2045] -->\n<!NOTATION contentTypes\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: ContentTypes//EN\" >\n\n<!-- date and time information. ISO date format -->\n<!NOTATION datetime\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: Datetime//EN\" >\n\n<!-- a language code, as per [RFC3066] -->\n<!NOTATION languageCode\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: LanguageCode//EN\" >\n\n<!-- a Uniform Resource Identifier, see [URI] -->\n<!NOTATION uri\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: URI//EN\" >\n\n<!-- a space-separated list of Uniform Resource Identifiers, see [URI] -->\n<!NOTATION uris\n    PUBLIC \"-//W3C//NOTATION XHTML Datatype: URIs//EN\" >\n\n<!-- end of xhtml-notations-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-object-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Embedded Object Module  ........................................ -->\n<!-- file: xhtml-object-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-object-1.mod,v 1.1 2010/07/29 13:42:48 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Embedded Object 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-object-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Embedded Objects\n\n        object\n\n     This module declares the object element type and its attributes, used\n     to embed external objects as part of XHTML pages. In the document,\n     place param elements prior to other content within the object element.\n\n     Note that use of this module requires instantiation of the Param\n     Element Module.\n-->\n\n<!-- object: Generic Embedded Object ................... -->\n\n<!ENTITY % object.element  \"INCLUDE\" >\n<![%object.element;[\n<!ENTITY % object.content\n     \"( #PCDATA | %Flow.mix; | %param.qname; )*\"\n>\n<!ENTITY % object.qname  \"object\" >\n<!ELEMENT %object.qname;  %object.content; >\n<!-- end of object.element -->]]>\n\n<!ENTITY % object.attlist  \"INCLUDE\" >\n<![%object.attlist;[\n<!ATTLIST %object.qname;\n      %Common.attrib;\n      declare      ( declare )              #IMPLIED\n      classid      %URI.datatype;           #IMPLIED\n      codebase     %URI.datatype;           #IMPLIED\n      data         %URI.datatype;           #IMPLIED\n      type         %ContentType.datatype;   #IMPLIED\n      codetype     %ContentType.datatype;   #IMPLIED\n      archive      %URIs.datatype;          #IMPLIED\n      standby      %Text.datatype;          #IMPLIED\n      height       %Length.datatype;        #IMPLIED\n      width        %Length.datatype;        #IMPLIED\n      name         CDATA                    #IMPLIED\n      tabindex     %Number.datatype;        #IMPLIED\n>\n<!-- end of object.attlist -->]]>\n\n<!-- end of xhtml-object-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-param-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Param Element Module  ..................................... -->\n<!-- file: xhtml-param-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-param-1.mod,v 1.1 2010/07/29 13:42:48 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Param Element 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-param-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Parameters for Java Applets and Embedded Objects\n\n        param\n\n     This module provides declarations for the param element,\n     used to provide named property values for the applet\n     and object elements.\n-->\n\n<!-- param: Named Property Value ....................... -->\n\n<!ENTITY % param.element  \"INCLUDE\" >\n<![%param.element;[\n<!ENTITY % param.content  \"EMPTY\" >\n<!ENTITY % param.qname  \"param\" >\n<!ELEMENT %param.qname;  %param.content; >\n<!-- end of param.element -->]]>\n\n<!ENTITY % param.attlist  \"INCLUDE\" >\n<![%param.attlist;[\n<!ATTLIST %param.qname;\n      %XHTML.xmlns.attrib;\n      %id.attrib;\n      name         CDATA                    #REQUIRED\n      value        CDATA                    #IMPLIED\n      valuetype    ( data | ref | object )  'data'\n      type         %ContentType.datatype;   #IMPLIED\n>\n<!-- end of param.attlist -->]]>\n\n<!-- end of xhtml-param-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-pres-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Presentation Module ............................................ -->\n<!-- file: xhtml-pres-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-pres-1.mod,v 1.1 2010/07/29 13:42:48 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Presentation 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-pres-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Presentational Elements\n\n     This module defines elements and their attributes for\n     simple presentation-related markup.\n-->\n\n<!ENTITY % xhtml-inlpres.module \"INCLUDE\" >\n<![%xhtml-inlpres.module;[\n<!ENTITY % xhtml-inlpres.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Inline Presentation 1.0//EN\"\n            \"xhtml-inlpres-1.mod\" >\n%xhtml-inlpres.mod;]]>\n\n<!ENTITY % xhtml-blkpres.module \"INCLUDE\" >\n<![%xhtml-blkpres.module;[\n<!ENTITY % xhtml-blkpres.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Block Presentation 1.0//EN\"\n            \"xhtml-blkpres-1.mod\" >\n%xhtml-blkpres.mod;]]>\n\n<!-- end of xhtml-pres-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-qname-1.mod",
    "content": "<!-- ....................................................................... -->\n<!-- XHTML Qname Module  ................................................... -->\n<!-- file: xhtml-qname-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-qname-1.mod,v 1.1 2010/07/29 13:42:48 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ENTITIES XHTML Qualified Names 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-qname-1.mod\"\n\n     Revisions:\n\t   #2000-10-22: added qname declarations for ruby elements\n     ....................................................................... -->\n\n<!-- XHTML Qname (Qualified Name) Module\n\n     This module is contained in two parts, labeled Section 'A' and 'B':\n\n       Section A declares parameter entities to support namespace-\n       qualified names, namespace declarations, and name prefixing\n       for XHTML and extensions.\n\n       Section B declares parameter entities used to provide\n       namespace-qualified names for all XHTML element types:\n\n         %applet.qname;   the xmlns-qualified name for <applet>\n         %base.qname;     the xmlns-qualified name for <base>\n         ...\n\n     XHTML extensions would create a module similar to this one.\n     Included in the XHTML distribution is a template module\n     ('template-qname-1.mod') suitable for this purpose.\n-->\n\n<!-- Section A: XHTML XML Namespace Framework :::::::::::::::::::: -->\n\n<!-- 1. Declare a %XHTML.prefixed; conditional section keyword, used\n        to activate namespace prefixing. The default value should\n        inherit '%NS.prefixed;' from the DTD driver, so that unless\n        overridden, the default behaviour follows the overall DTD\n        prefixing scheme.\n-->\n<!ENTITY % NS.prefixed \"IGNORE\" >\n<!ENTITY % XHTML.prefixed \"%NS.prefixed;\" >\n\n<!-- By default, we always permit XHTML attribute collections to have\n     namespace-qualified prefixes as well.\n-->\n<!ENTITY % XHTML.global.attrs.prefixed \"INCLUDE\" >\n<!-- By default, we allow the XML Schema attributes on the root\n     element.\n-->\n<!ENTITY % XHTML.xsi.attrs \"INCLUDE\" >\n\n<!-- 2. Declare a parameter entity (eg., %XHTML.xmlns;) containing\n        the URI reference used to identify the XHTML namespace:\n-->\n<!ENTITY % XHTML.xmlns  \"http://www.w3.org/1999/xhtml\" >\n\n<!-- 3. Declare parameter entities (eg., %XHTML.prefix;) containing\n        the default namespace prefix string(s) to use when prefixing\n        is enabled. This may be overridden in the DTD driver or the\n        internal subset of an document instance. If no default prefix\n        is desired, this may be declared as an empty string.\n\n     NOTE: As specified in [XMLNAMES], the namespace prefix serves\n     as a proxy for the URI reference, and is not in itself significant.\n-->\n<!ENTITY % XHTML.prefix  \"xhtml\" >\n\n<!-- 4. Declare parameter entities (eg., %XHTML.pfx;) containing the\n        colonized prefix(es) (eg., '%XHTML.prefix;:') used when\n        prefixing is active, an empty string when it is not.\n-->\n<![%XHTML.prefixed;[\n<!ENTITY % XHTML.pfx  \"%XHTML.prefix;:\" >\n]]>\n<!ENTITY % XHTML.pfx  \"\" >\n\n<!-- declare qualified name extensions here ............ -->\n<!ENTITY % xhtml-qname-extra.mod \"\" >\n%xhtml-qname-extra.mod;\n\n<!-- 5. The parameter entity %XHTML.xmlns.extra.attrib; may be\n        redeclared to contain any non-XHTML namespace declaration\n        attributes for namespaces embedded in XHTML. The default\n        is an empty string.  XLink should be included here if used\n        in the DTD.\n-->\n<!ENTITY % XHTML.xmlns.extra.attrib \"\" >\n\n<!-- The remainder of Section A is only followed in XHTML, not extensions. -->\n\n<!-- Declare a parameter entity %NS.decl.attrib; containing\n     all XML Namespace declarations used in the DTD, plus the\n     xmlns declaration for XHTML, its form dependent on whether\n     prefixing is active.\n-->\n<!ENTITY % XHTML.xmlns.attrib.prefixed\n     \"xmlns:%XHTML.prefix;  %URI.datatype;   #FIXED '%XHTML.xmlns;'\"\n>\n<![%XHTML.prefixed;[\n<!ENTITY % NS.decl.attrib\n     \"%XHTML.xmlns.attrib.prefixed;\n      %XHTML.xmlns.extra.attrib;\"\n>\n]]>\n<!ENTITY % NS.decl.attrib\n     \"%XHTML.xmlns.extra.attrib;\"\n>\n\n<!-- Declare a parameter entity %XSI.prefix as a prefix to use for XML\n     Schema Instance attributes.\n-->\n<!ENTITY % XSI.prefix \"xsi\" >\n\n<!ENTITY % XSI.xmlns \"http://www.w3.org/2001/XMLSchema-instance\" >\n\n<!-- Declare a parameter entity %XSI.xmlns.attrib as support for the\n     schemaLocation attribute, since this is legal throughout the DTD.\n-->\n<!ENTITY % XSI.xmlns.attrib\n     \"xmlns:%XSI.prefix;  %URI.datatype;   #FIXED '%XSI.xmlns;'\" >\n\n<!-- This is a placeholder for future XLink support.\n-->\n<!ENTITY % XLINK.xmlns.attrib \"\" >\n\n<!-- This is the attribute for the XML Schema namespace - XHTML\n     Modularization is also expressed in XML Schema, and it needs to\n\t be legal to declare the XML Schema namespace and the\n\t schemaLocation attribute on the root element of XHTML family\n\t documents.\n-->\n<![%XHTML.xsi.attrs;[\n<!ENTITY % XSI.prefix \"xsi\" >\n<!ENTITY % XSI.pfx \"%XSI.prefix;:\" >\n<!ENTITY % XSI.xmlns \"http://www.w3.org/2001/XMLSchema-instance\" >\n\n<!ENTITY % XSI.xmlns.attrib\n     \"xmlns:%XSI.prefix;  %URI.datatype;    #FIXED '%XSI.xmlns;'\"\n>\n]]>\n<!ENTITY % XSI.prefix \"\" >\n<!ENTITY % XSI.pfx \"\" >\n<!ENTITY % XSI.xmlns.attrib \"\" >\n\n\n<!-- Declare a parameter entity %NS.decl.attrib; containing all\n     XML namespace declaration attributes used by XHTML, including\n     a default xmlns attribute when prefixing is inactive.\n-->\n<![%XHTML.prefixed;[\n<!ENTITY % XHTML.xmlns.attrib\n     \"%NS.decl.attrib;\n      %XSI.xmlns.attrib;\n      %XLINK.xmlns.attrib;\"\n>\n]]>\n<!ENTITY % XHTML.xmlns.attrib\n     \"xmlns        %URI.datatype;           #FIXED '%XHTML.xmlns;'\n      %NS.decl.attrib;\n      %XSI.xmlns.attrib;\n      %XLINK.xmlns.attrib;\"\n>\n\n<!-- placeholder for qualified name redeclarations -->\n<!ENTITY % xhtml-qname.redecl \"\" >\n%xhtml-qname.redecl;\n\n<!-- Section B: XHTML Qualified Names ::::::::::::::::::::::::::::: -->\n\n<!-- 6. This section declares parameter entities used to provide\n        namespace-qualified names for all XHTML element types.\n-->\n\n<!-- module:  xhtml-applet-1.mod -->\n<!ENTITY % applet.qname  \"%XHTML.pfx;applet\" >\n\n<!-- module:  xhtml-base-1.mod -->\n<!ENTITY % base.qname    \"%XHTML.pfx;base\" >\n\n<!-- module:  xhtml-bdo-1.mod -->\n<!ENTITY % bdo.qname     \"%XHTML.pfx;bdo\" >\n\n<!-- module:  xhtml-blkphras-1.mod -->\n<!ENTITY % address.qname \"%XHTML.pfx;address\" >\n<!ENTITY % blockquote.qname  \"%XHTML.pfx;blockquote\" >\n<!ENTITY % pre.qname     \"%XHTML.pfx;pre\" >\n<!ENTITY % h1.qname      \"%XHTML.pfx;h1\" >\n<!ENTITY % h2.qname      \"%XHTML.pfx;h2\" >\n<!ENTITY % h3.qname      \"%XHTML.pfx;h3\" >\n<!ENTITY % h4.qname      \"%XHTML.pfx;h4\" >\n<!ENTITY % h5.qname      \"%XHTML.pfx;h5\" >\n<!ENTITY % h6.qname      \"%XHTML.pfx;h6\" >\n\n<!-- module:  xhtml-blkpres-1.mod -->\n<!ENTITY % hr.qname      \"%XHTML.pfx;hr\" >\n\n<!-- module:  xhtml-blkstruct-1.mod -->\n<!ENTITY % div.qname     \"%XHTML.pfx;div\" >\n<!ENTITY % p.qname       \"%XHTML.pfx;p\" >\n\n<!-- module:  xhtml-edit-1.mod -->\n<!ENTITY % ins.qname     \"%XHTML.pfx;ins\" >\n<!ENTITY % del.qname     \"%XHTML.pfx;del\" >\n\n<!-- module:  xhtml-form-1.mod -->\n<!ENTITY % form.qname    \"%XHTML.pfx;form\" >\n<!ENTITY % label.qname   \"%XHTML.pfx;label\" >\n<!ENTITY % input.qname   \"%XHTML.pfx;input\" >\n<!ENTITY % select.qname  \"%XHTML.pfx;select\" >\n<!ENTITY % optgroup.qname  \"%XHTML.pfx;optgroup\" >\n<!ENTITY % option.qname  \"%XHTML.pfx;option\" >\n<!ENTITY % textarea.qname  \"%XHTML.pfx;textarea\" >\n<!ENTITY % fieldset.qname  \"%XHTML.pfx;fieldset\" >\n<!ENTITY % legend.qname  \"%XHTML.pfx;legend\" >\n<!ENTITY % button.qname  \"%XHTML.pfx;button\" >\n\n<!-- module:  xhtml-hypertext-1.mod -->\n<!ENTITY % a.qname       \"%XHTML.pfx;a\" >\n\n<!-- module:  xhtml-image-1.mod -->\n<!ENTITY % img.qname     \"%XHTML.pfx;img\" >\n\n<!-- module:  xhtml-inlphras-1.mod -->\n<!ENTITY % abbr.qname    \"%XHTML.pfx;abbr\" >\n<!ENTITY % acronym.qname \"%XHTML.pfx;acronym\" >\n<!ENTITY % cite.qname    \"%XHTML.pfx;cite\" >\n<!ENTITY % code.qname    \"%XHTML.pfx;code\" >\n<!ENTITY % dfn.qname     \"%XHTML.pfx;dfn\" >\n<!ENTITY % em.qname      \"%XHTML.pfx;em\" >\n<!ENTITY % kbd.qname     \"%XHTML.pfx;kbd\" >\n<!ENTITY % q.qname       \"%XHTML.pfx;q\" >\n<!ENTITY % samp.qname    \"%XHTML.pfx;samp\" >\n<!ENTITY % strong.qname  \"%XHTML.pfx;strong\" >\n<!ENTITY % var.qname     \"%XHTML.pfx;var\" >\n\n<!-- module:  xhtml-inlpres-1.mod -->\n<!ENTITY % b.qname       \"%XHTML.pfx;b\" >\n<!ENTITY % big.qname     \"%XHTML.pfx;big\" >\n<!ENTITY % i.qname       \"%XHTML.pfx;i\" >\n<!ENTITY % small.qname   \"%XHTML.pfx;small\" >\n<!ENTITY % sub.qname     \"%XHTML.pfx;sub\" >\n<!ENTITY % sup.qname     \"%XHTML.pfx;sup\" >\n<!ENTITY % tt.qname      \"%XHTML.pfx;tt\" >\n\n<!-- module:  xhtml-inlstruct-1.mod -->\n<!ENTITY % br.qname      \"%XHTML.pfx;br\" >\n<!ENTITY % span.qname    \"%XHTML.pfx;span\" >\n\n<!-- module:  xhtml-ismap-1.mod (also csismap, ssismap) -->\n<!ENTITY % map.qname     \"%XHTML.pfx;map\" >\n<!ENTITY % area.qname    \"%XHTML.pfx;area\" >\n\n<!-- module:  xhtml-link-1.mod -->\n<!ENTITY % link.qname    \"%XHTML.pfx;link\" >\n\n<!-- module:  xhtml-list-1.mod -->\n<!ENTITY % dl.qname      \"%XHTML.pfx;dl\" >\n<!ENTITY % dt.qname      \"%XHTML.pfx;dt\" >\n<!ENTITY % dd.qname      \"%XHTML.pfx;dd\" >\n<!ENTITY % ol.qname      \"%XHTML.pfx;ol\" >\n<!ENTITY % ul.qname      \"%XHTML.pfx;ul\" >\n<!ENTITY % li.qname      \"%XHTML.pfx;li\" >\n\n<!-- module:  xhtml-meta-1.mod -->\n<!ENTITY % meta.qname    \"%XHTML.pfx;meta\" >\n\n<!-- module:  xhtml-param-1.mod -->\n<!ENTITY % param.qname   \"%XHTML.pfx;param\" >\n\n<!-- module:  xhtml-object-1.mod -->\n<!ENTITY % object.qname  \"%XHTML.pfx;object\" >\n\n<!-- module:  xhtml-script-1.mod -->\n<!ENTITY % script.qname  \"%XHTML.pfx;script\" >\n<!ENTITY % noscript.qname  \"%XHTML.pfx;noscript\" >\n\n<!-- module:  xhtml-struct-1.mod -->\n<!ENTITY % html.qname    \"%XHTML.pfx;html\" >\n<!ENTITY % head.qname    \"%XHTML.pfx;head\" >\n<!ENTITY % title.qname   \"%XHTML.pfx;title\" >\n<!ENTITY % body.qname    \"%XHTML.pfx;body\" >\n\n<!-- module:  xhtml-style-1.mod -->\n<!ENTITY % style.qname   \"%XHTML.pfx;style\" >\n\n<!-- module:  xhtml-table-1.mod -->\n<!ENTITY % table.qname   \"%XHTML.pfx;table\" >\n<!ENTITY % caption.qname \"%XHTML.pfx;caption\" >\n<!ENTITY % thead.qname   \"%XHTML.pfx;thead\" >\n<!ENTITY % tfoot.qname   \"%XHTML.pfx;tfoot\" >\n<!ENTITY % tbody.qname   \"%XHTML.pfx;tbody\" >\n<!ENTITY % colgroup.qname  \"%XHTML.pfx;colgroup\" >\n<!ENTITY % col.qname     \"%XHTML.pfx;col\" >\n<!ENTITY % tr.qname      \"%XHTML.pfx;tr\" >\n<!ENTITY % th.qname      \"%XHTML.pfx;th\" >\n<!ENTITY % td.qname      \"%XHTML.pfx;td\" >\n\n<!-- module:  xhtml-ruby-1.mod -->\n\n<!ENTITY % ruby.qname    \"%XHTML.pfx;ruby\" >\n<!ENTITY % rbc.qname     \"%XHTML.pfx;rbc\" >\n<!ENTITY % rtc.qname     \"%XHTML.pfx;rtc\" >\n<!ENTITY % rb.qname      \"%XHTML.pfx;rb\" >\n<!ENTITY % rt.qname      \"%XHTML.pfx;rt\" >\n<!ENTITY % rp.qname      \"%XHTML.pfx;rp\" >\n\n<!-- Provisional XHTML 2.0 Qualified Names  ...................... -->\n\n<!-- module:  xhtml-image-2.mod -->\n<!ENTITY % alt.qname     \"%XHTML.pfx;alt\" >\n\n<!-- end of xhtml-qname-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-script-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Document Scripting Module  ..................................... -->\n<!-- file: xhtml-script-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-script-1.mod,v 1.1 2010/07/29 13:42:48 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Scripting 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-script-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Scripting\n\n        script, noscript\n\n     This module declares element types and attributes used to provide\n     support for executable scripts as well as an alternate content\n     container where scripts are not supported.\n-->\n\n<!-- script: Scripting Statement ....................... -->\n\n<!ENTITY % script.element  \"INCLUDE\" >\n<![%script.element;[\n<!ENTITY % script.content  \"( #PCDATA )\" >\n<!ENTITY % script.qname  \"script\" >\n<!ELEMENT %script.qname;  %script.content; >\n<!-- end of script.element -->]]>\n\n<!ENTITY % script.attlist  \"INCLUDE\" >\n<![%script.attlist;[\n<!ATTLIST %script.qname;\n      %XHTML.xmlns.attrib;\n\t  %id.attrib;\n      xml:space    ( preserve )             #FIXED 'preserve'\n      charset      %Charset.datatype;       #IMPLIED\n      type         %ContentType.datatype;   #REQUIRED\n      src          %URI.datatype;           #IMPLIED\n      defer        ( defer )                #IMPLIED\n>\n<!-- end of script.attlist -->]]>\n\n<!-- noscript: No-Script Alternate Content ............. -->\n\n<!ENTITY % noscript.element  \"INCLUDE\" >\n<![%noscript.element;[\n<!ENTITY % noscript.content\n     \"( %Block.mix; )+\"\n>\n<!ENTITY % noscript.qname  \"noscript\" >\n<!ELEMENT %noscript.qname;  %noscript.content; >\n<!-- end of noscript.element -->]]>\n\n<!ENTITY % noscript.attlist  \"INCLUDE\" >\n<![%noscript.attlist;[\n<!ATTLIST %noscript.qname;\n      %Common.attrib;\n>\n<!-- end of noscript.attlist -->]]>\n\n<!-- end of xhtml-script-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-special.ent",
    "content": "<!-- Special characters for XHTML -->\n\n<!-- Character entity set. Typical invocation:\n     <!ENTITY % HTMLspecial PUBLIC\n        \"-//W3C//ENTITIES Special for XHTML//EN\"\n        \"http://www.w3.org/TR/xhtml1/DTD/xhtml-special.ent\">\n     %HTMLspecial;\n-->\n\n<!-- Portions (C) International Organization for Standardization 1986:\n     Permission to copy in any form is granted for use with\n     conforming SGML systems and applications as defined in\n     ISO 8879, provided this notice is included in all copies.\n-->\n\n<!-- Relevant ISO entity set is given unless names are newly introduced.\n     New names (i.e., not in ISO 8879 list) do not clash with any\n     existing ISO 8879 entity names. ISO 10646 character numbers\n     are given for each character, in hex. values are decimal\n     conversions of the ISO 10646 values and refer to the document\n     character set. Names are Unicode names. \n-->\n\n<!-- C0 Controls and Basic Latin -->\n<!ENTITY quot    \"&#34;\"> <!--  quotation mark, U+0022 ISOnum -->\n<!ENTITY amp     \"&#38;#38;\"> <!--  ampersand, U+0026 ISOnum -->\n<!ENTITY lt      \"&#38;#60;\"> <!--  less-than sign, U+003C ISOnum -->\n<!ENTITY gt      \"&#62;\"> <!--  greater-than sign, U+003E ISOnum -->\n<!ENTITY apos\t \"&#39;\"> <!--  apostrophe = APL quote, U+0027 ISOnum -->\n\n<!-- Latin Extended-A -->\n<!ENTITY OElig   \"&#338;\"> <!--  latin capital ligature OE,\n                                    U+0152 ISOlat2 -->\n<!ENTITY oelig   \"&#339;\"> <!--  latin small ligature oe, U+0153 ISOlat2 -->\n<!-- ligature is a misnomer, this is a separate character in some languages -->\n<!ENTITY Scaron  \"&#352;\"> <!--  latin capital letter S with caron,\n                                    U+0160 ISOlat2 -->\n<!ENTITY scaron  \"&#353;\"> <!--  latin small letter s with caron,\n                                    U+0161 ISOlat2 -->\n<!ENTITY Yuml    \"&#376;\"> <!--  latin capital letter Y with diaeresis,\n                                    U+0178 ISOlat2 -->\n\n<!-- Spacing Modifier Letters -->\n<!ENTITY circ    \"&#710;\"> <!--  modifier letter circumflex accent,\n                                    U+02C6 ISOpub -->\n<!ENTITY tilde   \"&#732;\"> <!--  small tilde, U+02DC ISOdia -->\n\n<!-- General Punctuation -->\n<!ENTITY ensp    \"&#8194;\"> <!-- en space, U+2002 ISOpub -->\n<!ENTITY emsp    \"&#8195;\"> <!-- em space, U+2003 ISOpub -->\n<!ENTITY thinsp  \"&#8201;\"> <!-- thin space, U+2009 ISOpub -->\n<!ENTITY zwnj    \"&#8204;\"> <!-- zero width non-joiner,\n                                    U+200C NEW RFC 2070 -->\n<!ENTITY zwj     \"&#8205;\"> <!-- zero width joiner, U+200D NEW RFC 2070 -->\n<!ENTITY lrm     \"&#8206;\"> <!-- left-to-right mark, U+200E NEW RFC 2070 -->\n<!ENTITY rlm     \"&#8207;\"> <!-- right-to-left mark, U+200F NEW RFC 2070 -->\n<!ENTITY ndash   \"&#8211;\"> <!-- en dash, U+2013 ISOpub -->\n<!ENTITY mdash   \"&#8212;\"> <!-- em dash, U+2014 ISOpub -->\n<!ENTITY lsquo   \"&#8216;\"> <!-- left single quotation mark,\n                                    U+2018 ISOnum -->\n<!ENTITY rsquo   \"&#8217;\"> <!-- right single quotation mark,\n                                    U+2019 ISOnum -->\n<!ENTITY sbquo   \"&#8218;\"> <!-- single low-9 quotation mark, U+201A NEW -->\n<!ENTITY ldquo   \"&#8220;\"> <!-- left double quotation mark,\n                                    U+201C ISOnum -->\n<!ENTITY rdquo   \"&#8221;\"> <!-- right double quotation mark,\n                                    U+201D ISOnum -->\n<!ENTITY bdquo   \"&#8222;\"> <!-- double low-9 quotation mark, U+201E NEW -->\n<!ENTITY dagger  \"&#8224;\"> <!-- dagger, U+2020 ISOpub -->\n<!ENTITY Dagger  \"&#8225;\"> <!-- double dagger, U+2021 ISOpub -->\n<!ENTITY permil  \"&#8240;\"> <!-- per mille sign, U+2030 ISOtech -->\n<!ENTITY lsaquo  \"&#8249;\"> <!-- single left-pointing angle quotation mark,\n                                    U+2039 ISO proposed -->\n<!-- lsaquo is proposed but not yet ISO standardized -->\n<!ENTITY rsaquo  \"&#8250;\"> <!-- single right-pointing angle quotation mark,\n                                    U+203A ISO proposed -->\n<!-- rsaquo is proposed but not yet ISO standardized -->\n\n<!-- Currency Symbols -->\n<!ENTITY euro   \"&#8364;\"> <!--  euro sign, U+20AC NEW -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-ssismap-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Server-side Image Map Module  .................................. -->\n<!-- file: xhtml-ssismap-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-ssismap-1.mod,v 1.1 2010/07/29 13:42:48 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Server-side Image Maps 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-ssismap-1.mod\"\n\n     Revisions:\n#2000-10-22: added declaration for 'ismap' on <input>\n     ....................................................................... -->\n\n<!-- Server-side Image Maps\n\n     This adds the 'ismap' attribute to the img and input elements\n     to support server-side processing of a user selection.\n-->\n\n<!ATTLIST %img.qname;\n      ismap        ( ismap )                #IMPLIED\n>\n\n<!ATTLIST %input.qname;\n      ismap        ( ismap )                #IMPLIED\n>\n\n<!-- end of xhtml-ssismap-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-struct-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Structure Module  .............................................. -->\n<!-- file: xhtml-struct-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-struct-1.mod,v 1.1 2010/07/29 13:42:48 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Document Structure 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-struct-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Document Structure\n\n        title, head, body, html\n\n     The Structure Module defines the major structural elements and\n     their attributes.\n\n     Note that the content model of the head element type is redeclared\n     when the Base Module is included in the DTD.\n\n     The parameter entity containing the XML namespace URI value used\n     for XHTML is '%XHTML.xmlns;', defined in the Qualified Names module.\n-->\n\n<!-- title: Document Title ............................. -->\n\n<!-- The title element is not considered part of the flow of text.\n     It should be displayed, for example as the page header or\n     window title. Exactly one title is required per document.\n-->\n\n<!ENTITY % title.element  \"INCLUDE\" >\n<![%title.element;[\n<!ENTITY % title.content  \"( #PCDATA )\" >\n<!ENTITY % title.qname  \"title\" >\n<!ELEMENT %title.qname;  %title.content; >\n<!-- end of title.element -->]]>\n\n<!ENTITY % title.attlist  \"INCLUDE\" >\n<![%title.attlist;[\n<!ATTLIST %title.qname;\n      %XHTML.xmlns.attrib;\n      %I18n.attrib;\n>\n<!-- end of title.attlist -->]]>\n\n<!-- head: Document Head ............................... -->\n\n<!ENTITY % head.element  \"INCLUDE\" >\n<![%head.element;[\n<!ENTITY % head.content\n    \"( %HeadOpts.mix;, %title.qname;, %HeadOpts.mix; )\"\n>\n<!ENTITY % head.qname  \"head\" >\n<!ELEMENT %head.qname;  %head.content; >\n<!-- end of head.element -->]]>\n\n<!ENTITY % head.attlist  \"INCLUDE\" >\n<![%head.attlist;[\n<!-- reserved for future use with document profiles\n-->\n<!ENTITY % profile.attrib\n     \"profile      %URIs.datatype;           #IMPLIED\"\n>\n\n<!ATTLIST %head.qname;\n      %XHTML.xmlns.attrib;\n      %I18n.attrib;\n      %profile.attrib;\n      %id.attrib;\n>\n<!-- end of head.attlist -->]]>\n\n<!-- body: Document Body ............................... -->\n\n<!ENTITY % body.element  \"INCLUDE\" >\n<![%body.element;[\n<!ENTITY % body.content\n     \"( %Block.mix; )*\"\n>\n<!ENTITY % body.qname  \"body\" >\n<!ELEMENT %body.qname;  %body.content; >\n<!-- end of body.element -->]]>\n\n<!ENTITY % body.attlist  \"INCLUDE\" >\n<![%body.attlist;[\n<!ATTLIST %body.qname;\n      %Common.attrib;\n>\n<!-- end of body.attlist -->]]>\n\n<!-- html: XHTML Document Element ...................... -->\n\n<!ENTITY % html.element  \"INCLUDE\" >\n<![%html.element;[\n<!ENTITY % html.content  \"( %head.qname;, %body.qname; )\" >\n<!ENTITY % html.qname  \"html\" >\n<!ELEMENT %html.qname;  %html.content; >\n<!-- end of html.element -->]]>\n\n<![%XHTML.xsi.attrs;[\n<!-- define a parameter for the XSI schemaLocation attribute -->\n<!ENTITY % XSI.schemaLocation.attrib\n     \"%XSI.pfx;schemaLocation  %URIs.datatype;    #IMPLIED\"\n>\n]]>\n<!ENTITY % XSI.schemaLocation.attrib \"\">\n\n<!ENTITY % html.attlist  \"INCLUDE\" >\n<![%html.attlist;[\n<!-- version attribute value defined in driver\n-->\n<!ENTITY % XHTML.version.attrib\n     \"version      CDATA           #FIXED '%XHTML.version;'\"\n>\n\n<!-- see the Qualified Names module for information\n     on how to extend XHTML using XML namespaces\n-->\n<!ATTLIST %html.qname;\n      %XHTML.xmlns.attrib;\n      %XSI.schemaLocation.attrib;\n      %XHTML.version.attrib;\n      %I18n.attrib;\n      %id.attrib;\n>\n<!-- end of html.attlist -->]]>\n\n<!-- end of xhtml-struct-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-style-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Document Style Sheet Module  ................................... -->\n<!-- file: xhtml-style-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-style-1.mod,v 1.1 2010/07/29 13:42:48 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//DTD XHTML Style Sheets 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-style-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Style Sheets\n\n        style\n\n     This module declares the style element type and its attributes,\n     used to embed style sheet information in the document head element.\n-->\n\n<!-- style: Style Sheet Information .................... -->\n\n<!ENTITY % style.element  \"INCLUDE\" >\n<![%style.element;[\n<!ENTITY % style.content  \"( #PCDATA )\" >\n<!ENTITY % style.qname  \"style\" >\n<!ELEMENT %style.qname;  %style.content; >\n<!-- end of style.element -->]]>\n\n<!ENTITY % style.attlist  \"INCLUDE\" >\n<![%style.attlist;[\n<!ATTLIST %style.qname;\n      %XHTML.xmlns.attrib;\n      %id.attrib;\n      %title.attrib;\n      %I18n.attrib;\n      xml:space    ( preserve )             #FIXED 'preserve'\n      type         %ContentType.datatype;   #REQUIRED\n      media        %MediaDesc.datatype;     #IMPLIED\n>\n<!-- end of style.attlist -->]]>\n\n<!-- end of xhtml-style-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent",
    "content": "<!-- Mathematical, Greek and Symbolic characters for XHTML -->\n\n<!-- Character entity set. Typical invocation:\n     <!ENTITY % HTMLsymbol PUBLIC\n        \"-//W3C//ENTITIES Symbols for XHTML//EN\"\n        \"http://www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent\">\n     %HTMLsymbol;\n-->\n\n<!-- Portions (C) International Organization for Standardization 1986:\n     Permission to copy in any form is granted for use with\n     conforming SGML systems and applications as defined in\n     ISO 8879, provided this notice is included in all copies.\n-->\n\n<!-- Relevant ISO entity set is given unless names are newly introduced.\n     New names (i.e., not in ISO 8879 list) do not clash with any\n     existing ISO 8879 entity names. ISO 10646 character numbers\n     are given for each character, in hex. values are decimal\n     conversions of the ISO 10646 values and refer to the document\n     character set. Names are Unicode names. \n-->\n\n<!-- Latin Extended-B -->\n<!ENTITY fnof     \"&#402;\"> <!-- latin small letter f with hook = function\n                                    = florin, U+0192 ISOtech -->\n\n<!-- Greek -->\n<!ENTITY Alpha    \"&#913;\"> <!-- greek capital letter alpha, U+0391 -->\n<!ENTITY Beta     \"&#914;\"> <!-- greek capital letter beta, U+0392 -->\n<!ENTITY Gamma    \"&#915;\"> <!-- greek capital letter gamma,\n                                    U+0393 ISOgrk3 -->\n<!ENTITY Delta    \"&#916;\"> <!-- greek capital letter delta,\n                                    U+0394 ISOgrk3 -->\n<!ENTITY Epsilon  \"&#917;\"> <!-- greek capital letter epsilon, U+0395 -->\n<!ENTITY Zeta     \"&#918;\"> <!-- greek capital letter zeta, U+0396 -->\n<!ENTITY Eta      \"&#919;\"> <!-- greek capital letter eta, U+0397 -->\n<!ENTITY Theta    \"&#920;\"> <!-- greek capital letter theta,\n                                    U+0398 ISOgrk3 -->\n<!ENTITY Iota     \"&#921;\"> <!-- greek capital letter iota, U+0399 -->\n<!ENTITY Kappa    \"&#922;\"> <!-- greek capital letter kappa, U+039A -->\n<!ENTITY Lambda   \"&#923;\"> <!-- greek capital letter lamda,\n                                    U+039B ISOgrk3 -->\n<!ENTITY Mu       \"&#924;\"> <!-- greek capital letter mu, U+039C -->\n<!ENTITY Nu       \"&#925;\"> <!-- greek capital letter nu, U+039D -->\n<!ENTITY Xi       \"&#926;\"> <!-- greek capital letter xi, U+039E ISOgrk3 -->\n<!ENTITY Omicron  \"&#927;\"> <!-- greek capital letter omicron, U+039F -->\n<!ENTITY Pi       \"&#928;\"> <!-- greek capital letter pi, U+03A0 ISOgrk3 -->\n<!ENTITY Rho      \"&#929;\"> <!-- greek capital letter rho, U+03A1 -->\n<!-- there is no Sigmaf, and no U+03A2 character either -->\n<!ENTITY Sigma    \"&#931;\"> <!-- greek capital letter sigma,\n                                    U+03A3 ISOgrk3 -->\n<!ENTITY Tau      \"&#932;\"> <!-- greek capital letter tau, U+03A4 -->\n<!ENTITY Upsilon  \"&#933;\"> <!-- greek capital letter upsilon,\n                                    U+03A5 ISOgrk3 -->\n<!ENTITY Phi      \"&#934;\"> <!-- greek capital letter phi,\n                                    U+03A6 ISOgrk3 -->\n<!ENTITY Chi      \"&#935;\"> <!-- greek capital letter chi, U+03A7 -->\n<!ENTITY Psi      \"&#936;\"> <!-- greek capital letter psi,\n                                    U+03A8 ISOgrk3 -->\n<!ENTITY Omega    \"&#937;\"> <!-- greek capital letter omega,\n                                    U+03A9 ISOgrk3 -->\n\n<!ENTITY alpha    \"&#945;\"> <!-- greek small letter alpha,\n                                    U+03B1 ISOgrk3 -->\n<!ENTITY beta     \"&#946;\"> <!-- greek small letter beta, U+03B2 ISOgrk3 -->\n<!ENTITY gamma    \"&#947;\"> <!-- greek small letter gamma,\n                                    U+03B3 ISOgrk3 -->\n<!ENTITY delta    \"&#948;\"> <!-- greek small letter delta,\n                                    U+03B4 ISOgrk3 -->\n<!ENTITY epsilon  \"&#949;\"> <!-- greek small letter epsilon,\n                                    U+03B5 ISOgrk3 -->\n<!ENTITY zeta     \"&#950;\"> <!-- greek small letter zeta, U+03B6 ISOgrk3 -->\n<!ENTITY eta      \"&#951;\"> <!-- greek small letter eta, U+03B7 ISOgrk3 -->\n<!ENTITY theta    \"&#952;\"> <!-- greek small letter theta,\n                                    U+03B8 ISOgrk3 -->\n<!ENTITY iota     \"&#953;\"> <!-- greek small letter iota, U+03B9 ISOgrk3 -->\n<!ENTITY kappa    \"&#954;\"> <!-- greek small letter kappa,\n                                    U+03BA ISOgrk3 -->\n<!ENTITY lambda   \"&#955;\"> <!-- greek small letter lamda,\n                                    U+03BB ISOgrk3 -->\n<!ENTITY mu       \"&#956;\"> <!-- greek small letter mu, U+03BC ISOgrk3 -->\n<!ENTITY nu       \"&#957;\"> <!-- greek small letter nu, U+03BD ISOgrk3 -->\n<!ENTITY xi       \"&#958;\"> <!-- greek small letter xi, U+03BE ISOgrk3 -->\n<!ENTITY omicron  \"&#959;\"> <!-- greek small letter omicron, U+03BF NEW -->\n<!ENTITY pi       \"&#960;\"> <!-- greek small letter pi, U+03C0 ISOgrk3 -->\n<!ENTITY rho      \"&#961;\"> <!-- greek small letter rho, U+03C1 ISOgrk3 -->\n<!ENTITY sigmaf   \"&#962;\"> <!-- greek small letter final sigma,\n                                    U+03C2 ISOgrk3 -->\n<!ENTITY sigma    \"&#963;\"> <!-- greek small letter sigma,\n                                    U+03C3 ISOgrk3 -->\n<!ENTITY tau      \"&#964;\"> <!-- greek small letter tau, U+03C4 ISOgrk3 -->\n<!ENTITY upsilon  \"&#965;\"> <!-- greek small letter upsilon,\n                                    U+03C5 ISOgrk3 -->\n<!ENTITY phi      \"&#966;\"> <!-- greek small letter phi, U+03C6 ISOgrk3 -->\n<!ENTITY chi      \"&#967;\"> <!-- greek small letter chi, U+03C7 ISOgrk3 -->\n<!ENTITY psi      \"&#968;\"> <!-- greek small letter psi, U+03C8 ISOgrk3 -->\n<!ENTITY omega    \"&#969;\"> <!-- greek small letter omega,\n                                    U+03C9 ISOgrk3 -->\n<!ENTITY thetasym \"&#977;\"> <!-- greek theta symbol,\n                                    U+03D1 NEW -->\n<!ENTITY upsih    \"&#978;\"> <!-- greek upsilon with hook symbol,\n                                    U+03D2 NEW -->\n<!ENTITY piv      \"&#982;\"> <!-- greek pi symbol, U+03D6 ISOgrk3 -->\n\n<!-- General Punctuation -->\n<!ENTITY bull     \"&#8226;\"> <!-- bullet = black small circle,\n                                     U+2022 ISOpub  -->\n<!-- bullet is NOT the same as bullet operator, U+2219 -->\n<!ENTITY hellip   \"&#8230;\"> <!-- horizontal ellipsis = three dot leader,\n                                     U+2026 ISOpub  -->\n<!ENTITY prime    \"&#8242;\"> <!-- prime = minutes = feet, U+2032 ISOtech -->\n<!ENTITY Prime    \"&#8243;\"> <!-- double prime = seconds = inches,\n                                     U+2033 ISOtech -->\n<!ENTITY oline    \"&#8254;\"> <!-- overline = spacing overscore,\n                                     U+203E NEW -->\n<!ENTITY frasl    \"&#8260;\"> <!-- fraction slash, U+2044 NEW -->\n\n<!-- Letterlike Symbols -->\n<!ENTITY weierp   \"&#8472;\"> <!-- script capital P = power set\n                                     = Weierstrass p, U+2118 ISOamso -->\n<!ENTITY image    \"&#8465;\"> <!-- black-letter capital I = imaginary part,\n                                     U+2111 ISOamso -->\n<!ENTITY real     \"&#8476;\"> <!-- black-letter capital R = real part symbol,\n                                     U+211C ISOamso -->\n<!ENTITY trade    \"&#8482;\"> <!-- trade mark sign, U+2122 ISOnum -->\n<!ENTITY alefsym  \"&#8501;\"> <!-- alef symbol = first transfinite cardinal,\n                                     U+2135 NEW -->\n<!-- alef symbol is NOT the same as hebrew letter alef,\n     U+05D0 although the same glyph could be used to depict both characters -->\n\n<!-- Arrows -->\n<!ENTITY larr     \"&#8592;\"> <!-- leftwards arrow, U+2190 ISOnum -->\n<!ENTITY uarr     \"&#8593;\"> <!-- upwards arrow, U+2191 ISOnum-->\n<!ENTITY rarr     \"&#8594;\"> <!-- rightwards arrow, U+2192 ISOnum -->\n<!ENTITY darr     \"&#8595;\"> <!-- downwards arrow, U+2193 ISOnum -->\n<!ENTITY harr     \"&#8596;\"> <!-- left right arrow, U+2194 ISOamsa -->\n<!ENTITY crarr    \"&#8629;\"> <!-- downwards arrow with corner leftwards\n                                     = carriage return, U+21B5 NEW -->\n<!ENTITY lArr     \"&#8656;\"> <!-- leftwards double arrow, U+21D0 ISOtech -->\n<!-- Unicode does not say that lArr is the same as the 'is implied by' arrow\n    but also does not have any other character for that function. So lArr can\n    be used for 'is implied by' as ISOtech suggests -->\n<!ENTITY uArr     \"&#8657;\"> <!-- upwards double arrow, U+21D1 ISOamsa -->\n<!ENTITY rArr     \"&#8658;\"> <!-- rightwards double arrow,\n                                     U+21D2 ISOtech -->\n<!-- Unicode does not say this is the 'implies' character but does not have \n     another character with this function so rArr can be used for 'implies'\n     as ISOtech suggests -->\n<!ENTITY dArr     \"&#8659;\"> <!-- downwards double arrow, U+21D3 ISOamsa -->\n<!ENTITY hArr     \"&#8660;\"> <!-- left right double arrow,\n                                     U+21D4 ISOamsa -->\n\n<!-- Mathematical Operators -->\n<!ENTITY forall   \"&#8704;\"> <!-- for all, U+2200 ISOtech -->\n<!ENTITY part     \"&#8706;\"> <!-- partial differential, U+2202 ISOtech  -->\n<!ENTITY exist    \"&#8707;\"> <!-- there exists, U+2203 ISOtech -->\n<!ENTITY empty    \"&#8709;\"> <!-- empty set = null set, U+2205 ISOamso -->\n<!ENTITY nabla    \"&#8711;\"> <!-- nabla = backward difference,\n                                     U+2207 ISOtech -->\n<!ENTITY isin     \"&#8712;\"> <!-- element of, U+2208 ISOtech -->\n<!ENTITY notin    \"&#8713;\"> <!-- not an element of, U+2209 ISOtech -->\n<!ENTITY ni       \"&#8715;\"> <!-- contains as member, U+220B ISOtech -->\n<!ENTITY prod     \"&#8719;\"> <!-- n-ary product = product sign,\n                                     U+220F ISOamsb -->\n<!-- prod is NOT the same character as U+03A0 'greek capital letter pi' though\n     the same glyph might be used for both -->\n<!ENTITY sum      \"&#8721;\"> <!-- n-ary summation, U+2211 ISOamsb -->\n<!-- sum is NOT the same character as U+03A3 'greek capital letter sigma'\n     though the same glyph might be used for both -->\n<!ENTITY minus    \"&#8722;\"> <!-- minus sign, U+2212 ISOtech -->\n<!ENTITY lowast   \"&#8727;\"> <!-- asterisk operator, U+2217 ISOtech -->\n<!ENTITY radic    \"&#8730;\"> <!-- square root = radical sign,\n                                     U+221A ISOtech -->\n<!ENTITY prop     \"&#8733;\"> <!-- proportional to, U+221D ISOtech -->\n<!ENTITY infin    \"&#8734;\"> <!-- infinity, U+221E ISOtech -->\n<!ENTITY ang      \"&#8736;\"> <!-- angle, U+2220 ISOamso -->\n<!ENTITY and      \"&#8743;\"> <!-- logical and = wedge, U+2227 ISOtech -->\n<!ENTITY or       \"&#8744;\"> <!-- logical or = vee, U+2228 ISOtech -->\n<!ENTITY cap      \"&#8745;\"> <!-- intersection = cap, U+2229 ISOtech -->\n<!ENTITY cup      \"&#8746;\"> <!-- union = cup, U+222A ISOtech -->\n<!ENTITY int      \"&#8747;\"> <!-- integral, U+222B ISOtech -->\n<!ENTITY there4   \"&#8756;\"> <!-- therefore, U+2234 ISOtech -->\n<!ENTITY sim      \"&#8764;\"> <!-- tilde operator = varies with = similar to,\n                                     U+223C ISOtech -->\n<!-- tilde operator is NOT the same character as the tilde, U+007E,\n     although the same glyph might be used to represent both  -->\n<!ENTITY cong     \"&#8773;\"> <!-- approximately equal to, U+2245 ISOtech -->\n<!ENTITY asymp    \"&#8776;\"> <!-- almost equal to = asymptotic to,\n                                     U+2248 ISOamsr -->\n<!ENTITY ne       \"&#8800;\"> <!-- not equal to, U+2260 ISOtech -->\n<!ENTITY equiv    \"&#8801;\"> <!-- identical to, U+2261 ISOtech -->\n<!ENTITY le       \"&#8804;\"> <!-- less-than or equal to, U+2264 ISOtech -->\n<!ENTITY ge       \"&#8805;\"> <!-- greater-than or equal to,\n                                     U+2265 ISOtech -->\n<!ENTITY sub      \"&#8834;\"> <!-- subset of, U+2282 ISOtech -->\n<!ENTITY sup      \"&#8835;\"> <!-- superset of, U+2283 ISOtech -->\n<!ENTITY nsub     \"&#8836;\"> <!-- not a subset of, U+2284 ISOamsn -->\n<!ENTITY sube     \"&#8838;\"> <!-- subset of or equal to, U+2286 ISOtech -->\n<!ENTITY supe     \"&#8839;\"> <!-- superset of or equal to,\n                                     U+2287 ISOtech -->\n<!ENTITY oplus    \"&#8853;\"> <!-- circled plus = direct sum,\n                                     U+2295 ISOamsb -->\n<!ENTITY otimes   \"&#8855;\"> <!-- circled times = vector product,\n                                     U+2297 ISOamsb -->\n<!ENTITY perp     \"&#8869;\"> <!-- up tack = orthogonal to = perpendicular,\n                                     U+22A5 ISOtech -->\n<!ENTITY sdot     \"&#8901;\"> <!-- dot operator, U+22C5 ISOamsb -->\n<!-- dot operator is NOT the same character as U+00B7 middle dot -->\n\n<!-- Miscellaneous Technical -->\n<!ENTITY lceil    \"&#8968;\"> <!-- left ceiling = APL upstile,\n                                     U+2308 ISOamsc  -->\n<!ENTITY rceil    \"&#8969;\"> <!-- right ceiling, U+2309 ISOamsc  -->\n<!ENTITY lfloor   \"&#8970;\"> <!-- left floor = APL downstile,\n                                     U+230A ISOamsc  -->\n<!ENTITY rfloor   \"&#8971;\"> <!-- right floor, U+230B ISOamsc  -->\n<!ENTITY lang     \"&#9001;\"> <!-- left-pointing angle bracket = bra,\n                                     U+2329 ISOtech -->\n<!-- lang is NOT the same character as U+003C 'less than sign' \n     or U+2039 'single left-pointing angle quotation mark' -->\n<!ENTITY rang     \"&#9002;\"> <!-- right-pointing angle bracket = ket,\n                                     U+232A ISOtech -->\n<!-- rang is NOT the same character as U+003E 'greater than sign' \n     or U+203A 'single right-pointing angle quotation mark' -->\n\n<!-- Geometric Shapes -->\n<!ENTITY loz      \"&#9674;\"> <!-- lozenge, U+25CA ISOpub -->\n\n<!-- Miscellaneous Symbols -->\n<!ENTITY spades   \"&#9824;\"> <!-- black spade suit, U+2660 ISOpub -->\n<!-- black here seems to mean filled as opposed to hollow -->\n<!ENTITY clubs    \"&#9827;\"> <!-- black club suit = shamrock,\n                                     U+2663 ISOpub -->\n<!ENTITY hearts   \"&#9829;\"> <!-- black heart suit = valentine,\n                                     U+2665 ISOpub -->\n<!ENTITY diams    \"&#9830;\"> <!-- black diamond suit, U+2666 ISOpub -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent.1",
    "content": "<!-- Mathematical, Greek and Symbolic characters for XHTML -->\n\n<!-- Character entity set. Typical invocation:\n     <!ENTITY % HTMLsymbol PUBLIC\n        \"-//W3C//ENTITIES Symbols for XHTML//EN\"\n        \"http://www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent\">\n     %HTMLsymbol;\n-->\n\n<!-- Portions (C) International Organization for Standardization 1986:\n     Permission to copy in any form is granted for use with\n     conforming SGML systems and applications as defined in\n     ISO 8879, provided this notice is included in all copies.\n-->\n\n<!-- Relevant ISO entity set is given unless names are newly introduced.\n     New names (i.e., not in ISO 8879 list) do not clash with any\n     existing ISO 8879 entity names. ISO 10646 character numbers\n     are given for each character, in hex. values are decimal\n     conversions of the ISO 10646 values and refer to the document\n     character set. Names are Unicode names. \n-->\n\n<!-- Latin Extended-B -->\n<!ENTITY fnof     \"&#402;\"> <!-- latin small letter f with hook = function\n                                    = florin, U+0192 ISOtech -->\n\n<!-- Greek -->\n<!ENTITY Alpha    \"&#913;\"> <!-- greek capital letter alpha, U+0391 -->\n<!ENTITY Beta     \"&#914;\"> <!-- greek capital letter beta, U+0392 -->\n<!ENTITY Gamma    \"&#915;\"> <!-- greek capital letter gamma,\n                                    U+0393 ISOgrk3 -->\n<!ENTITY Delta    \"&#916;\"> <!-- greek capital letter delta,\n                                    U+0394 ISOgrk3 -->\n<!ENTITY Epsilon  \"&#917;\"> <!-- greek capital letter epsilon, U+0395 -->\n<!ENTITY Zeta     \"&#918;\"> <!-- greek capital letter zeta, U+0396 -->\n<!ENTITY Eta      \"&#919;\"> <!-- greek capital letter eta, U+0397 -->\n<!ENTITY Theta    \"&#920;\"> <!-- greek capital letter theta,\n                                    U+0398 ISOgrk3 -->\n<!ENTITY Iota     \"&#921;\"> <!-- greek capital letter iota, U+0399 -->\n<!ENTITY Kappa    \"&#922;\"> <!-- greek capital letter kappa, U+039A -->\n<!ENTITY Lambda   \"&#923;\"> <!-- greek capital letter lamda,\n                                    U+039B ISOgrk3 -->\n<!ENTITY Mu       \"&#924;\"> <!-- greek capital letter mu, U+039C -->\n<!ENTITY Nu       \"&#925;\"> <!-- greek capital letter nu, U+039D -->\n<!ENTITY Xi       \"&#926;\"> <!-- greek capital letter xi, U+039E ISOgrk3 -->\n<!ENTITY Omicron  \"&#927;\"> <!-- greek capital letter omicron, U+039F -->\n<!ENTITY Pi       \"&#928;\"> <!-- greek capital letter pi, U+03A0 ISOgrk3 -->\n<!ENTITY Rho      \"&#929;\"> <!-- greek capital letter rho, U+03A1 -->\n<!-- there is no Sigmaf, and no U+03A2 character either -->\n<!ENTITY Sigma    \"&#931;\"> <!-- greek capital letter sigma,\n                                    U+03A3 ISOgrk3 -->\n<!ENTITY Tau      \"&#932;\"> <!-- greek capital letter tau, U+03A4 -->\n<!ENTITY Upsilon  \"&#933;\"> <!-- greek capital letter upsilon,\n                                    U+03A5 ISOgrk3 -->\n<!ENTITY Phi      \"&#934;\"> <!-- greek capital letter phi,\n                                    U+03A6 ISOgrk3 -->\n<!ENTITY Chi      \"&#935;\"> <!-- greek capital letter chi, U+03A7 -->\n<!ENTITY Psi      \"&#936;\"> <!-- greek capital letter psi,\n                                    U+03A8 ISOgrk3 -->\n<!ENTITY Omega    \"&#937;\"> <!-- greek capital letter omega,\n                                    U+03A9 ISOgrk3 -->\n\n<!ENTITY alpha    \"&#945;\"> <!-- greek small letter alpha,\n                                    U+03B1 ISOgrk3 -->\n<!ENTITY beta     \"&#946;\"> <!-- greek small letter beta, U+03B2 ISOgrk3 -->\n<!ENTITY gamma    \"&#947;\"> <!-- greek small letter gamma,\n                                    U+03B3 ISOgrk3 -->\n<!ENTITY delta    \"&#948;\"> <!-- greek small letter delta,\n                                    U+03B4 ISOgrk3 -->\n<!ENTITY epsilon  \"&#949;\"> <!-- greek small letter epsilon,\n                                    U+03B5 ISOgrk3 -->\n<!ENTITY zeta     \"&#950;\"> <!-- greek small letter zeta, U+03B6 ISOgrk3 -->\n<!ENTITY eta      \"&#951;\"> <!-- greek small letter eta, U+03B7 ISOgrk3 -->\n<!ENTITY theta    \"&#952;\"> <!-- greek small letter theta,\n                                    U+03B8 ISOgrk3 -->\n<!ENTITY iota     \"&#953;\"> <!-- greek small letter iota, U+03B9 ISOgrk3 -->\n<!ENTITY kappa    \"&#954;\"> <!-- greek small letter kappa,\n                                    U+03BA ISOgrk3 -->\n<!ENTITY lambda   \"&#955;\"> <!-- greek small letter lamda,\n                                    U+03BB ISOgrk3 -->\n<!ENTITY mu       \"&#956;\"> <!-- greek small letter mu, U+03BC ISOgrk3 -->\n<!ENTITY nu       \"&#957;\"> <!-- greek small letter nu, U+03BD ISOgrk3 -->\n<!ENTITY xi       \"&#958;\"> <!-- greek small letter xi, U+03BE ISOgrk3 -->\n<!ENTITY omicron  \"&#959;\"> <!-- greek small letter omicron, U+03BF NEW -->\n<!ENTITY pi       \"&#960;\"> <!-- greek small letter pi, U+03C0 ISOgrk3 -->\n<!ENTITY rho      \"&#961;\"> <!-- greek small letter rho, U+03C1 ISOgrk3 -->\n<!ENTITY sigmaf   \"&#962;\"> <!-- greek small letter final sigma,\n                                    U+03C2 ISOgrk3 -->\n<!ENTITY sigma    \"&#963;\"> <!-- greek small letter sigma,\n                                    U+03C3 ISOgrk3 -->\n<!ENTITY tau      \"&#964;\"> <!-- greek small letter tau, U+03C4 ISOgrk3 -->\n<!ENTITY upsilon  \"&#965;\"> <!-- greek small letter upsilon,\n                                    U+03C5 ISOgrk3 -->\n<!ENTITY phi      \"&#966;\"> <!-- greek small letter phi, U+03C6 ISOgrk3 -->\n<!ENTITY chi      \"&#967;\"> <!-- greek small letter chi, U+03C7 ISOgrk3 -->\n<!ENTITY psi      \"&#968;\"> <!-- greek small letter psi, U+03C8 ISOgrk3 -->\n<!ENTITY omega    \"&#969;\"> <!-- greek small letter omega,\n                                    U+03C9 ISOgrk3 -->\n<!ENTITY thetasym \"&#977;\"> <!-- greek theta symbol,\n                                    U+03D1 NEW -->\n<!ENTITY upsih    \"&#978;\"> <!-- greek upsilon with hook symbol,\n                                    U+03D2 NEW -->\n<!ENTITY piv      \"&#982;\"> <!-- greek pi symbol, U+03D6 ISOgrk3 -->\n\n<!-- General Punctuation -->\n<!ENTITY bull     \"&#8226;\"> <!-- bullet = black small circle,\n                                     U+2022 ISOpub  -->\n<!-- bullet is NOT the same as bullet operator, U+2219 -->\n<!ENTITY hellip   \"&#8230;\"> <!-- horizontal ellipsis = three dot leader,\n                                     U+2026 ISOpub  -->\n<!ENTITY prime    \"&#8242;\"> <!-- prime = minutes = feet, U+2032 ISOtech -->\n<!ENTITY Prime    \"&#8243;\"> <!-- double prime = seconds = inches,\n                                     U+2033 ISOtech -->\n<!ENTITY oline    \"&#8254;\"> <!-- overline = spacing overscore,\n                                     U+203E NEW -->\n<!ENTITY frasl    \"&#8260;\"> <!-- fraction slash, U+2044 NEW -->\n\n<!-- Letterlike Symbols -->\n<!ENTITY weierp   \"&#8472;\"> <!-- script capital P = power set\n                                     = Weierstrass p, U+2118 ISOamso -->\n<!ENTITY image    \"&#8465;\"> <!-- black-letter capital I = imaginary part,\n                                     U+2111 ISOamso -->\n<!ENTITY real     \"&#8476;\"> <!-- black-letter capital R = real part symbol,\n                                     U+211C ISOamso -->\n<!ENTITY trade    \"&#8482;\"> <!-- trade mark sign, U+2122 ISOnum -->\n<!ENTITY alefsym  \"&#8501;\"> <!-- alef symbol = first transfinite cardinal,\n                                     U+2135 NEW -->\n<!-- alef symbol is NOT the same as hebrew letter alef,\n     U+05D0 although the same glyph could be used to depict both characters -->\n\n<!-- Arrows -->\n<!ENTITY larr     \"&#8592;\"> <!-- leftwards arrow, U+2190 ISOnum -->\n<!ENTITY uarr     \"&#8593;\"> <!-- upwards arrow, U+2191 ISOnum-->\n<!ENTITY rarr     \"&#8594;\"> <!-- rightwards arrow, U+2192 ISOnum -->\n<!ENTITY darr     \"&#8595;\"> <!-- downwards arrow, U+2193 ISOnum -->\n<!ENTITY harr     \"&#8596;\"> <!-- left right arrow, U+2194 ISOamsa -->\n<!ENTITY crarr    \"&#8629;\"> <!-- downwards arrow with corner leftwards\n                                     = carriage return, U+21B5 NEW -->\n<!ENTITY lArr     \"&#8656;\"> <!-- leftwards double arrow, U+21D0 ISOtech -->\n<!-- Unicode does not say that lArr is the same as the 'is implied by' arrow\n    but also does not have any other character for that function. So lArr can\n    be used for 'is implied by' as ISOtech suggests -->\n<!ENTITY uArr     \"&#8657;\"> <!-- upwards double arrow, U+21D1 ISOamsa -->\n<!ENTITY rArr     \"&#8658;\"> <!-- rightwards double arrow,\n                                     U+21D2 ISOtech -->\n<!-- Unicode does not say this is the 'implies' character but does not have \n     another character with this function so rArr can be used for 'implies'\n     as ISOtech suggests -->\n<!ENTITY dArr     \"&#8659;\"> <!-- downwards double arrow, U+21D3 ISOamsa -->\n<!ENTITY hArr     \"&#8660;\"> <!-- left right double arrow,\n                                     U+21D4 ISOamsa -->\n\n<!-- Mathematical Operators -->\n<!ENTITY forall   \"&#8704;\"> <!-- for all, U+2200 ISOtech -->\n<!ENTITY part     \"&#8706;\"> <!-- partial differential, U+2202 ISOtech  -->\n<!ENTITY exist    \"&#8707;\"> <!-- there exists, U+2203 ISOtech -->\n<!ENTITY empty    \"&#8709;\"> <!-- empty set = null set, U+2205 ISOamso -->\n<!ENTITY nabla    \"&#8711;\"> <!-- nabla = backward difference,\n                                     U+2207 ISOtech -->\n<!ENTITY isin     \"&#8712;\"> <!-- element of, U+2208 ISOtech -->\n<!ENTITY notin    \"&#8713;\"> <!-- not an element of, U+2209 ISOtech -->\n<!ENTITY ni       \"&#8715;\"> <!-- contains as member, U+220B ISOtech -->\n<!ENTITY prod     \"&#8719;\"> <!-- n-ary product = product sign,\n                                     U+220F ISOamsb -->\n<!-- prod is NOT the same character as U+03A0 'greek capital letter pi' though\n     the same glyph might be used for both -->\n<!ENTITY sum      \"&#8721;\"> <!-- n-ary summation, U+2211 ISOamsb -->\n<!-- sum is NOT the same character as U+03A3 'greek capital letter sigma'\n     though the same glyph might be used for both -->\n<!ENTITY minus    \"&#8722;\"> <!-- minus sign, U+2212 ISOtech -->\n<!ENTITY lowast   \"&#8727;\"> <!-- asterisk operator, U+2217 ISOtech -->\n<!ENTITY radic    \"&#8730;\"> <!-- square root = radical sign,\n                                     U+221A ISOtech -->\n<!ENTITY prop     \"&#8733;\"> <!-- proportional to, U+221D ISOtech -->\n<!ENTITY infin    \"&#8734;\"> <!-- infinity, U+221E ISOtech -->\n<!ENTITY ang      \"&#8736;\"> <!-- angle, U+2220 ISOamso -->\n<!ENTITY and      \"&#8743;\"> <!-- logical and = wedge, U+2227 ISOtech -->\n<!ENTITY or       \"&#8744;\"> <!-- logical or = vee, U+2228 ISOtech -->\n<!ENTITY cap      \"&#8745;\"> <!-- intersection = cap, U+2229 ISOtech -->\n<!ENTITY cup      \"&#8746;\"> <!-- union = cup, U+222A ISOtech -->\n<!ENTITY int      \"&#8747;\"> <!-- integral, U+222B ISOtech -->\n<!ENTITY there4   \"&#8756;\"> <!-- therefore, U+2234 ISOtech -->\n<!ENTITY sim      \"&#8764;\"> <!-- tilde operator = varies with = similar to,\n                                     U+223C ISOtech -->\n<!-- tilde operator is NOT the same character as the tilde, U+007E,\n     although the same glyph might be used to represent both  -->\n<!ENTITY cong     \"&#8773;\"> <!-- approximately equal to, U+2245 ISOtech -->\n<!ENTITY asymp    \"&#8776;\"> <!-- almost equal to = asymptotic to,\n                                     U+2248 ISOamsr -->\n<!ENTITY ne       \"&#8800;\"> <!-- not equal to, U+2260 ISOtech -->\n<!ENTITY equiv    \"&#8801;\"> <!-- identical to, U+2261 ISOtech -->\n<!ENTITY le       \"&#8804;\"> <!-- less-than or equal to, U+2264 ISOtech -->\n<!ENTITY ge       \"&#8805;\"> <!-- greater-than or equal to,\n                                     U+2265 ISOtech -->\n<!ENTITY sub      \"&#8834;\"> <!-- subset of, U+2282 ISOtech -->\n<!ENTITY sup      \"&#8835;\"> <!-- superset of, U+2283 ISOtech -->\n<!ENTITY nsub     \"&#8836;\"> <!-- not a subset of, U+2284 ISOamsn -->\n<!ENTITY sube     \"&#8838;\"> <!-- subset of or equal to, U+2286 ISOtech -->\n<!ENTITY supe     \"&#8839;\"> <!-- superset of or equal to,\n                                     U+2287 ISOtech -->\n<!ENTITY oplus    \"&#8853;\"> <!-- circled plus = direct sum,\n                                     U+2295 ISOamsb -->\n<!ENTITY otimes   \"&#8855;\"> <!-- circled times = vector product,\n                                     U+2297 ISOamsb -->\n<!ENTITY perp     \"&#8869;\"> <!-- up tack = orthogonal to = perpendicular,\n                                     U+22A5 ISOtech -->\n<!ENTITY sdot     \"&#8901;\"> <!-- dot operator, U+22C5 ISOamsb -->\n<!-- dot operator is NOT the same character as U+00B7 middle dot -->\n\n<!-- Miscellaneous Technical -->\n<!ENTITY lceil    \"&#8968;\"> <!-- left ceiling = APL upstile,\n                                     U+2308 ISOamsc  -->\n<!ENTITY rceil    \"&#8969;\"> <!-- right ceiling, U+2309 ISOamsc  -->\n<!ENTITY lfloor   \"&#8970;\"> <!-- left floor = APL downstile,\n                                     U+230A ISOamsc  -->\n<!ENTITY rfloor   \"&#8971;\"> <!-- right floor, U+230B ISOamsc  -->\n<!ENTITY lang     \"&#9001;\"> <!-- left-pointing angle bracket = bra,\n                                     U+2329 ISOtech -->\n<!-- lang is NOT the same character as U+003C 'less than sign' \n     or U+2039 'single left-pointing angle quotation mark' -->\n<!ENTITY rang     \"&#9002;\"> <!-- right-pointing angle bracket = ket,\n                                     U+232A ISOtech -->\n<!-- rang is NOT the same character as U+003E 'greater than sign' \n     or U+203A 'single right-pointing angle quotation mark' -->\n\n<!-- Geometric Shapes -->\n<!ENTITY loz      \"&#9674;\"> <!-- lozenge, U+25CA ISOpub -->\n\n<!-- Miscellaneous Symbols -->\n<!ENTITY spades   \"&#9824;\"> <!-- black spade suit, U+2660 ISOpub -->\n<!-- black here seems to mean filled as opposed to hollow -->\n<!ENTITY clubs    \"&#9827;\"> <!-- black club suit = shamrock,\n                                     U+2663 ISOpub -->\n<!ENTITY hearts   \"&#9829;\"> <!-- black heart suit = valentine,\n                                     U+2665 ISOpub -->\n<!ENTITY diams    \"&#9830;\"> <!-- black diamond suit, U+2666 ISOpub -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-table-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Table Module  .................................................. -->\n<!-- file: xhtml-table-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-table-1.mod,v 1.1 2010/07/29 13:42:48 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Tables 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-table-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Tables\n\n        table, caption, thead, tfoot, tbody, colgroup, col, tr, th, td\n\n     This module declares element types and attributes used to provide\n     table markup similar to HTML 4, including features that enable\n     better accessibility for non-visual user agents.\n-->\n\n<!-- declare qualified element type names:\n-->\n<!ENTITY % table.qname  \"table\" >\n<!ENTITY % caption.qname  \"caption\" >\n<!ENTITY % thead.qname  \"thead\" >\n<!ENTITY % tfoot.qname  \"tfoot\" >\n<!ENTITY % tbody.qname  \"tbody\" >\n<!ENTITY % colgroup.qname  \"colgroup\" >\n<!ENTITY % col.qname  \"col\" >\n<!ENTITY % tr.qname  \"tr\" >\n<!ENTITY % th.qname  \"th\" >\n<!ENTITY % td.qname  \"td\" >\n\n<!-- The frame attribute specifies which parts of the frame around\n     the table should be rendered. The values are not the same as\n     CALS to avoid a name clash with the valign attribute.\n-->\n<!ENTITY % frame.attrib\n     \"frame        ( void\n                   | above\n                   | below\n                   | hsides\n                   | lhs\n                   | rhs\n                   | vsides\n                   | box\n                   | border )               #IMPLIED\"\n>\n\n<!-- The rules attribute defines which rules to draw between cells:\n\n     If rules is absent then assume:\n\n       \"none\" if border is absent or border=\"0\" otherwise \"all\"\n-->\n<!ENTITY % rules.attrib\n     \"rules        ( none\n                   | groups\n                   | rows\n                   | cols\n                   | all )                  #IMPLIED\"\n>\n\n<!-- horizontal alignment attributes for cell contents\n-->\n<!ENTITY % CellHAlign.attrib\n     \"align        ( left\n                   | center\n                   | right\n                   | justify\n                   | char )                 #IMPLIED\n      char         %Character.datatype;     #IMPLIED\n      charoff      %Length.datatype;        #IMPLIED\"\n>\n\n<!-- vertical alignment attribute for cell contents\n-->\n<!ENTITY % CellVAlign.attrib\n     \"valign       ( top\n                   | middle\n                   | bottom\n                   | baseline )             #IMPLIED\"\n>\n\n<!-- scope is simpler than axes attribute for common tables\n-->\n<!ENTITY % scope.attrib\n     \"scope        ( row\n                   | col\n                   | rowgroup\n                   | colgroup )             #IMPLIED\"\n>\n\n<!-- table: Table Element .............................. -->\n\n<!ENTITY % table.element  \"INCLUDE\" >\n<![%table.element;[\n<!ENTITY % table.content\n     \"( %caption.qname;?, ( %col.qname;* | %colgroup.qname;* ),\n      (( %thead.qname;?, %tfoot.qname;?, %tbody.qname;+ ) | ( %tr.qname;+ )))\"\n>\n<!ELEMENT %table.qname;  %table.content; >\n<!-- end of table.element -->]]>\n\n<!ENTITY % table.attlist  \"INCLUDE\" >\n<![%table.attlist;[\n<!ATTLIST %table.qname;\n      %Common.attrib;\n      summary      %Text.datatype;          #IMPLIED\n      width        %Length.datatype;        #IMPLIED\n      border       %Pixels.datatype;        #IMPLIED\n      %frame.attrib;\n      %rules.attrib;\n      cellspacing  %Length.datatype;        #IMPLIED\n      cellpadding  %Length.datatype;        #IMPLIED\n>\n<!-- end of table.attlist -->]]>\n\n<!-- caption: Table Caption ............................ -->\n\n<!ENTITY % caption.element  \"INCLUDE\" >\n<![%caption.element;[\n<!ENTITY % caption.content\n     \"( #PCDATA | %Inline.mix; )*\"\n>\n<!ELEMENT %caption.qname;  %caption.content; >\n<!-- end of caption.element -->]]>\n\n<!ENTITY % caption.attlist  \"INCLUDE\" >\n<![%caption.attlist;[\n<!ATTLIST %caption.qname;\n      %Common.attrib;\n>\n<!-- end of caption.attlist -->]]>\n\n<!-- thead: Table Header ............................... -->\n\n<!-- Use thead to duplicate headers when breaking table\n     across page boundaries, or for static headers when\n     tbody sections are rendered in scrolling panel.\n-->\n\n<!ENTITY % thead.element  \"INCLUDE\" >\n<![%thead.element;[\n<!ENTITY % thead.content  \"( %tr.qname; )+\" >\n<!ELEMENT %thead.qname;  %thead.content; >\n<!-- end of thead.element -->]]>\n\n<!ENTITY % thead.attlist  \"INCLUDE\" >\n<![%thead.attlist;[\n<!ATTLIST %thead.qname;\n      %Common.attrib;\n      %CellHAlign.attrib;\n      %CellVAlign.attrib;\n>\n<!-- end of thead.attlist -->]]>\n\n<!-- tfoot: Table Footer ............................... -->\n\n<!-- Use tfoot to duplicate footers when breaking table\n     across page boundaries, or for static footers when\n     tbody sections are rendered in scrolling panel.\n-->\n\n<!ENTITY % tfoot.element  \"INCLUDE\" >\n<![%tfoot.element;[\n<!ENTITY % tfoot.content  \"( %tr.qname; )+\" >\n<!ELEMENT %tfoot.qname;  %tfoot.content; >\n<!-- end of tfoot.element -->]]>\n\n<!ENTITY % tfoot.attlist  \"INCLUDE\" >\n<![%tfoot.attlist;[\n<!ATTLIST %tfoot.qname;\n      %Common.attrib;\n      %CellHAlign.attrib;\n      %CellVAlign.attrib;\n>\n<!-- end of tfoot.attlist -->]]>\n\n<!-- tbody: Table Body ................................. -->\n\n<!-- Use multiple tbody sections when rules are needed\n     between groups of table rows.\n-->\n\n<!ENTITY % tbody.element  \"INCLUDE\" >\n<![%tbody.element;[\n<!ENTITY % tbody.content  \"( %tr.qname; )+\" >\n<!ELEMENT %tbody.qname;  %tbody.content; >\n<!-- end of tbody.element -->]]>\n\n<!ENTITY % tbody.attlist  \"INCLUDE\" >\n<![%tbody.attlist;[\n<!ATTLIST %tbody.qname;\n      %Common.attrib;\n      %CellHAlign.attrib;\n      %CellVAlign.attrib;\n>\n<!-- end of tbody.attlist -->]]>\n\n<!-- colgroup: Table Column Group ...................... -->\n\n<!-- colgroup groups a set of col elements. It allows you\n     to group several semantically-related columns together.\n-->\n\n<!ENTITY % colgroup.element  \"INCLUDE\" >\n<![%colgroup.element;[\n<!ENTITY % colgroup.content  \"( %col.qname; )*\" >\n<!ELEMENT %colgroup.qname;  %colgroup.content; >\n<!-- end of colgroup.element -->]]>\n\n<!ENTITY % colgroup.attlist  \"INCLUDE\" >\n<![%colgroup.attlist;[\n<!ATTLIST %colgroup.qname;\n      %Common.attrib;\n      span         %Number.datatype;        '1'\n      width        %MultiLength.datatype;   #IMPLIED\n      %CellHAlign.attrib;\n      %CellVAlign.attrib;\n>\n<!-- end of colgroup.attlist -->]]>\n\n<!-- col: Table Column ................................. -->\n\n<!-- col elements define the alignment properties for\n     cells in one or more columns.\n\n     The width attribute specifies the width of the\n     columns, e.g.\n\n       width=\"64\"        width in screen pixels\n       width=\"0.5*\"      relative width of 0.5\n\n     The span attribute causes the attributes of one\n     col element to apply to more than one column.\n-->\n\n<!ENTITY % col.element  \"INCLUDE\" >\n<![%col.element;[\n<!ENTITY % col.content  \"EMPTY\" >\n<!ELEMENT %col.qname;  %col.content; >\n<!-- end of col.element -->]]>\n\n<!ENTITY % col.attlist  \"INCLUDE\" >\n<![%col.attlist;[\n<!ATTLIST %col.qname;\n      %Common.attrib;\n      span         %Number.datatype;        '1'\n      width        %MultiLength.datatype;   #IMPLIED\n      %CellHAlign.attrib;\n      %CellVAlign.attrib;\n>\n<!-- end of col.attlist -->]]>\n\n<!-- tr: Table Row ..................................... -->\n\n<!ENTITY % tr.element  \"INCLUDE\" >\n<![%tr.element;[\n<!ENTITY % tr.content  \"( %th.qname; | %td.qname; )+\" >\n<!ELEMENT %tr.qname;  %tr.content; >\n<!-- end of tr.element -->]]>\n\n<!ENTITY % tr.attlist  \"INCLUDE\" >\n<![%tr.attlist;[\n<!ATTLIST %tr.qname;\n      %Common.attrib;\n      %CellHAlign.attrib;\n      %CellVAlign.attrib;\n>\n<!-- end of tr.attlist -->]]>\n\n<!-- th: Table Header Cell ............................. -->\n\n<!-- th is for header cells, td for data,\n     but for cells acting as both use td\n-->\n\n<!ENTITY % th.element  \"INCLUDE\" >\n<![%th.element;[\n<!ENTITY % th.content\n     \"( #PCDATA | %Flow.mix; )*\"\n>\n<!ELEMENT %th.qname;  %th.content; >\n<!-- end of th.element -->]]>\n\n<!ENTITY % th.attlist  \"INCLUDE\" >\n<![%th.attlist;[\n<!ATTLIST %th.qname;\n      %Common.attrib;\n      abbr         %Text.datatype;          #IMPLIED\n      axis         CDATA                    #IMPLIED\n      headers      IDREFS                   #IMPLIED\n      %scope.attrib;\n      rowspan      %Number.datatype;        '1'\n      colspan      %Number.datatype;        '1'\n      %CellHAlign.attrib;\n      %CellVAlign.attrib;\n>\n<!-- end of th.attlist -->]]>\n\n<!-- td: Table Data Cell ............................... -->\n\n<!ENTITY % td.element  \"INCLUDE\" >\n<![%td.element;[\n<!ENTITY % td.content\n     \"( #PCDATA | %Flow.mix; )*\"\n>\n<!ELEMENT %td.qname;  %td.content; >\n<!-- end of td.element -->]]>\n\n<!ENTITY % td.attlist  \"INCLUDE\" >\n<![%td.attlist;[\n<!ATTLIST %td.qname;\n      %Common.attrib;\n      abbr         %Text.datatype;          #IMPLIED\n      axis         CDATA                    #IMPLIED\n      headers      IDREFS                   #IMPLIED\n      %scope.attrib;\n      rowspan      %Number.datatype;        '1'\n      colspan      %Number.datatype;        '1'\n      %CellHAlign.attrib;\n      %CellVAlign.attrib;\n>\n<!-- end of td.attlist -->]]>\n\n<!-- end of xhtml-table-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-text-1.mod",
    "content": "<!-- ...................................................................... -->\n<!-- XHTML Text Module  ................................................... -->\n<!-- file: xhtml-text-1.mod\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.\n     Revision: $Id: xhtml-text-1.mod,v 1.1 2010/07/29 13:42:48 bertails Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ELEMENTS XHTML Text 1.0//EN\"\n       SYSTEM \"http://www.w3.org/MarkUp/DTD/xhtml-text-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- Textual Content\n\n     The Text module includes declarations for all core\n     text container elements and their attributes.\n-->\n\n<!ENTITY % xhtml-inlstruct.module \"INCLUDE\" >\n<![%xhtml-inlstruct.module;[\n<!ENTITY % xhtml-inlstruct.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Inline Structural 1.0//EN\"\n            \"xhtml-inlstruct-1.mod\" >\n%xhtml-inlstruct.mod;]]>\n\n<!ENTITY % xhtml-inlphras.module \"INCLUDE\" >\n<![%xhtml-inlphras.module;[\n<!ENTITY % xhtml-inlphras.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Inline Phrasal 1.0//EN\"\n            \"xhtml-inlphras-1.mod\" >\n%xhtml-inlphras.mod;]]>\n\n<!ENTITY % xhtml-blkstruct.module \"INCLUDE\" >\n<![%xhtml-blkstruct.module;[\n<!ENTITY % xhtml-blkstruct.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Block Structural 1.0//EN\"\n            \"xhtml-blkstruct-1.mod\" >\n%xhtml-blkstruct.mod;]]>\n\n<!ENTITY % xhtml-blkphras.module \"INCLUDE\" >\n<![%xhtml-blkphras.module;[\n<!ENTITY % xhtml-blkphras.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Block Phrasal 1.0//EN\"\n            \"xhtml-blkphras-1.mod\" >\n%xhtml-blkphras.mod;]]>\n\n<!-- end of xhtml-text-1.mod -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml11-model-1.mod",
    "content": "<!-- ....................................................................... -->\n<!-- XHTML 1.1 Document Model Module  ...................................... -->\n<!-- file: xhtml11-model-1.mod\n\n     This is XHTML 1.1, a reformulation of HTML as a modular XML application.\n     Copyright 1998-2001 W3C (MIT, INRIA, Keio), All Rights Reserved.\n     Revision: $Id: xhtml11-model-1.mod,v 1.13 2001/05/29 16:37:01 ahby Exp $ SMI\n\n     This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n       PUBLIC \"-//W3C//ENTITIES XHTML 1.1 Document Model 1.0//EN\"\n       SYSTEM \"http://www.w3.org/TR/xhtml11/DTD/xhtml11-model-1.mod\"\n\n     Revisions:\n     (none)\n     ....................................................................... -->\n\n<!-- XHTML 1.1 Document Model\n\n     This module describes the groupings of elements that make up\n     common content models for XHTML elements.\n\n     XHTML has three basic content models:\n\n         %Inline.mix;  character-level elements\n         %Block.mix;   block-like elements, eg., paragraphs and lists\n         %Flow.mix;    any block or inline elements\n\n     Any parameter entities declared in this module may be used\n     to create element content models, but the above three are\n     considered 'global' (insofar as that term applies here).\n\n     The reserved word '#PCDATA' (indicating a text string) is now\n     included explicitly with each element declaration that is\n     declared as mixed content, as XML requires that this token\n     occur first in a content model specification.\n-->\n<!-- Extending the Model\n\n     While in some cases this module may need to be rewritten to\n     accommodate changes to the document model, minor extensions\n     may be accomplished by redeclaring any of the three *.extra;\n     parameter entities to contain extension element types as follows:\n\n         %Misc.extra;    whose parent may be any block or\n                         inline element.\n\n         %Inline.extra;  whose parent may be any inline element.\n\n         %Block.extra;   whose parent may be any block element.\n\n     If used, these parameter entities must be an OR-separated\n     list beginning with an OR separator (\"|\"), eg., \"| a | b | c\"\n\n     All block and inline *.class parameter entities not part\n     of the *struct.class classes begin with \"| \" to allow for\n     exclusion from mixes.\n-->\n\n<!-- ..............  Optional Elements in head  .................. -->\n\n<!ENTITY % HeadOpts.mix\n     \"( %script.qname; | %style.qname; | %meta.qname;\n      | %link.qname; | %object.qname; )*\"\n>\n\n<!-- .................  Miscellaneous Elements  .................. -->\n\n<!-- ins and del are used to denote editing changes\n-->\n<!ENTITY % Edit.class \"| %ins.qname; | %del.qname;\" >\n\n<!-- script and noscript are used to contain scripts\n     and alternative content\n-->\n<!ENTITY % Script.class \"| %script.qname; | %noscript.qname;\" >\n\n<!ENTITY % Misc.extra \"\" >\n\n<!-- These elements are neither block nor inline, and can\n     essentially be used anywhere in the document body.\n-->\n<!ENTITY % Misc.class\n     \"%Edit.class;\n      %Script.class;\n      %Misc.extra;\"\n>\n\n<!-- ....................  Inline Elements  ...................... -->\n\n<!ENTITY % InlStruct.class \"%br.qname; | %span.qname;\" >\n\n<!ENTITY % InlPhras.class\n     \"| %em.qname; | %strong.qname; | %dfn.qname; | %code.qname;\n      | %samp.qname; | %kbd.qname; | %var.qname; | %cite.qname;\n      | %abbr.qname; | %acronym.qname; | %q.qname;\" >\n\n<!ENTITY % InlPres.class\n     \"| %tt.qname; | %i.qname; | %b.qname; | %big.qname;\n      | %small.qname; | %sub.qname; | %sup.qname;\" >\n\n<!ENTITY % I18n.class \"| %bdo.qname;\" >\n\n<!ENTITY % Anchor.class \"| %a.qname;\" >\n\n<!ENTITY % InlSpecial.class\n     \"| %img.qname; | %map.qname;\n      | %object.qname;\" >\n\n<!ENTITY % InlForm.class\n     \"| %input.qname; | %select.qname; | %textarea.qname;\n      | %label.qname; | %button.qname;\" >\n\n<!ENTITY % Inline.extra \"\" >\n\n<!ENTITY % Ruby.class \"| %ruby.qname;\" >\n\n<!-- %Inline.class; includes all inline elements,\n     used as a component in mixes\n-->\n<!ENTITY % Inline.class\n     \"%InlStruct.class;\n      %InlPhras.class;\n      %InlPres.class;\n      %I18n.class;\n      %Anchor.class;\n      %InlSpecial.class;\n      %InlForm.class;\n      %Ruby.class;\n      %Inline.extra;\"\n>\n\n<!-- %InlNoRuby.class; includes all inline elements\n     except ruby, used as a component in mixes\n-->\n<!ENTITY % InlNoRuby.class\n     \"%InlStruct.class;\n      %InlPhras.class;\n      %InlPres.class;\n      %I18n.class;\n      %Anchor.class;\n      %InlSpecial.class;\n      %InlForm.class;\n      %Inline.extra;\"\n>\n\n<!-- %NoRuby.content; includes all inlines except ruby\n-->\n<!ENTITY % NoRuby.content\n     \"( #PCDATA\n      | %InlNoRuby.class;\n      %Misc.class; )*\"\n>\n\n<!-- %InlNoAnchor.class; includes all non-anchor inlines,\n     used as a component in mixes\n-->\n<!ENTITY % InlNoAnchor.class\n     \"%InlStruct.class;\n      %InlPhras.class;\n      %InlPres.class;\n      %I18n.class;\n      %InlSpecial.class;\n      %InlForm.class;\n      %Ruby.class;\n      %Inline.extra;\"\n>\n\n<!-- %InlNoAnchor.mix; includes all non-anchor inlines\n-->\n<!ENTITY % InlNoAnchor.mix\n     \"%InlNoAnchor.class;\n      %Misc.class;\"\n>\n\n<!-- %Inline.mix; includes all inline elements, including %Misc.class;\n-->\n<!ENTITY % Inline.mix\n     \"%Inline.class;\n      %Misc.class;\"\n>\n\n<!-- .....................  Block Elements  ...................... -->\n\n<!-- In the HTML 4.0 DTD, heading and list elements were included\n     in the %block; parameter entity. The %Heading.class; and\n     %List.class; parameter entities must now be included explicitly\n     on element declarations where desired.\n-->\n\n<!ENTITY % Heading.class\n     \"%h1.qname; | %h2.qname; | %h3.qname;\n      | %h4.qname; | %h5.qname; | %h6.qname;\" >\n\n<!ENTITY % List.class \"%ul.qname; | %ol.qname; | %dl.qname;\" >\n\n<!ENTITY % Table.class \"| %table.qname;\" >\n\n<!ENTITY % Form.class  \"| %form.qname;\" >\n\n<!ENTITY % Fieldset.class  \"| %fieldset.qname;\" >\n\n<!ENTITY % BlkStruct.class \"%p.qname; | %div.qname;\" >\n\n<!ENTITY % BlkPhras.class\n     \"| %pre.qname; | %blockquote.qname; | %address.qname;\" >\n\n<!ENTITY % BlkPres.class \"| %hr.qname;\" >\n\n<!ENTITY % BlkSpecial.class\n     \"%Table.class;\n      %Form.class;\n      %Fieldset.class;\"\n>\n\n<!ENTITY % Block.extra \"\" >\n\n<!-- %Block.class; includes all block elements,\n     used as an component in mixes\n-->\n<!ENTITY % Block.class\n     \"%BlkStruct.class;\n      %BlkPhras.class;\n      %BlkPres.class;\n      %BlkSpecial.class;\n      %Block.extra;\"\n>\n\n<!-- %Block.mix; includes all block elements plus %Misc.class;\n-->\n<!ENTITY % Block.mix\n     \"%Heading.class;\n      | %List.class;\n      | %Block.class;\n      %Misc.class;\"\n>\n\n<!-- ................  All Content Elements  .................. -->\n\n<!-- %Flow.mix; includes all text content, block and inline\n-->\n<!ENTITY % Flow.mix\n     \"%Heading.class;\n      | %List.class;\n      | %Block.class;\n      | %Inline.class;\n      %Misc.class;\"\n>\n\n<!-- end of xhtml11-model-1.mod -->\n\n\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent",
    "content": "<!-- Portions (C) International Organization for Standardization 1986\n     Permission to copy in any form is granted for use with\n     conforming SGML systems and applications as defined in\n     ISO 8879, provided this notice is included in all copies.\n-->\n<!-- Character entity set. Typical invocation:\n    <!ENTITY % HTMLlat1 PUBLIC\n       \"-//W3C//ENTITIES Latin 1 for XHTML//EN\"\n       \"http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent\">\n    %HTMLlat1;\n-->\n\n<!ENTITY nbsp   \"&#160;\"> <!-- no-break space = non-breaking space,\n                                  U+00A0 ISOnum -->\n<!ENTITY iexcl  \"&#161;\"> <!-- inverted exclamation mark, U+00A1 ISOnum -->\n<!ENTITY cent   \"&#162;\"> <!-- cent sign, U+00A2 ISOnum -->\n<!ENTITY pound  \"&#163;\"> <!-- pound sign, U+00A3 ISOnum -->\n<!ENTITY curren \"&#164;\"> <!-- currency sign, U+00A4 ISOnum -->\n<!ENTITY yen    \"&#165;\"> <!-- yen sign = yuan sign, U+00A5 ISOnum -->\n<!ENTITY brvbar \"&#166;\"> <!-- broken bar = broken vertical bar,\n                                  U+00A6 ISOnum -->\n<!ENTITY sect   \"&#167;\"> <!-- section sign, U+00A7 ISOnum -->\n<!ENTITY uml    \"&#168;\"> <!-- diaeresis = spacing diaeresis,\n                                  U+00A8 ISOdia -->\n<!ENTITY copy   \"&#169;\"> <!-- copyright sign, U+00A9 ISOnum -->\n<!ENTITY ordf   \"&#170;\"> <!-- feminine ordinal indicator, U+00AA ISOnum -->\n<!ENTITY laquo  \"&#171;\"> <!-- left-pointing double angle quotation mark\n                                  = left pointing guillemet, U+00AB ISOnum -->\n<!ENTITY not    \"&#172;\"> <!-- not sign = angled dash,\n                                  U+00AC ISOnum -->\n<!ENTITY shy    \"&#173;\"> <!-- soft hyphen = discretionary hyphen,\n                                  U+00AD ISOnum -->\n<!ENTITY reg    \"&#174;\"> <!-- registered sign = registered trade mark sign,\n                                  U+00AE ISOnum -->\n<!ENTITY macr   \"&#175;\"> <!-- macron = spacing macron = overline\n                                  = APL overbar, U+00AF ISOdia -->\n<!ENTITY deg    \"&#176;\"> <!-- degree sign, U+00B0 ISOnum -->\n<!ENTITY plusmn \"&#177;\"> <!-- plus-minus sign = plus-or-minus sign,\n                                  U+00B1 ISOnum -->\n<!ENTITY sup2   \"&#178;\"> <!-- superscript two = superscript digit two\n                                  = squared, U+00B2 ISOnum -->\n<!ENTITY sup3   \"&#179;\"> <!-- superscript three = superscript digit three\n                                  = cubed, U+00B3 ISOnum -->\n<!ENTITY acute  \"&#180;\"> <!-- acute accent = spacing acute,\n                                  U+00B4 ISOdia -->\n<!ENTITY micro  \"&#181;\"> <!-- micro sign, U+00B5 ISOnum -->\n<!ENTITY para   \"&#182;\"> <!-- pilcrow sign = paragraph sign,\n                                  U+00B6 ISOnum -->\n<!ENTITY middot \"&#183;\"> <!-- middle dot = Georgian comma\n                                  = Greek middle dot, U+00B7 ISOnum -->\n<!ENTITY cedil  \"&#184;\"> <!-- cedilla = spacing cedilla, U+00B8 ISOdia -->\n<!ENTITY sup1   \"&#185;\"> <!-- superscript one = superscript digit one,\n                                  U+00B9 ISOnum -->\n<!ENTITY ordm   \"&#186;\"> <!-- masculine ordinal indicator,\n                                  U+00BA ISOnum -->\n<!ENTITY raquo  \"&#187;\"> <!-- right-pointing double angle quotation mark\n                                  = right pointing guillemet, U+00BB ISOnum -->\n<!ENTITY frac14 \"&#188;\"> <!-- vulgar fraction one quarter\n                                  = fraction one quarter, U+00BC ISOnum -->\n<!ENTITY frac12 \"&#189;\"> <!-- vulgar fraction one half\n                                  = fraction one half, U+00BD ISOnum -->\n<!ENTITY frac34 \"&#190;\"> <!-- vulgar fraction three quarters\n                                  = fraction three quarters, U+00BE ISOnum -->\n<!ENTITY iquest \"&#191;\"> <!-- inverted question mark\n                                  = turned question mark, U+00BF ISOnum -->\n<!ENTITY Agrave \"&#192;\"> <!-- latin capital letter A with grave\n                                  = latin capital letter A grave,\n                                  U+00C0 ISOlat1 -->\n<!ENTITY Aacute \"&#193;\"> <!-- latin capital letter A with acute,\n                                  U+00C1 ISOlat1 -->\n<!ENTITY Acirc  \"&#194;\"> <!-- latin capital letter A with circumflex,\n                                  U+00C2 ISOlat1 -->\n<!ENTITY Atilde \"&#195;\"> <!-- latin capital letter A with tilde,\n                                  U+00C3 ISOlat1 -->\n<!ENTITY Auml   \"&#196;\"> <!-- latin capital letter A with diaeresis,\n                                  U+00C4 ISOlat1 -->\n<!ENTITY Aring  \"&#197;\"> <!-- latin capital letter A with ring above\n                                  = latin capital letter A ring,\n                                  U+00C5 ISOlat1 -->\n<!ENTITY AElig  \"&#198;\"> <!-- latin capital letter AE\n                                  = latin capital ligature AE,\n                                  U+00C6 ISOlat1 -->\n<!ENTITY Ccedil \"&#199;\"> <!-- latin capital letter C with cedilla,\n                                  U+00C7 ISOlat1 -->\n<!ENTITY Egrave \"&#200;\"> <!-- latin capital letter E with grave,\n                                  U+00C8 ISOlat1 -->\n<!ENTITY Eacute \"&#201;\"> <!-- latin capital letter E with acute,\n                                  U+00C9 ISOlat1 -->\n<!ENTITY Ecirc  \"&#202;\"> <!-- latin capital letter E with circumflex,\n                                  U+00CA ISOlat1 -->\n<!ENTITY Euml   \"&#203;\"> <!-- latin capital letter E with diaeresis,\n                                  U+00CB ISOlat1 -->\n<!ENTITY Igrave \"&#204;\"> <!-- latin capital letter I with grave,\n                                  U+00CC ISOlat1 -->\n<!ENTITY Iacute \"&#205;\"> <!-- latin capital letter I with acute,\n                                  U+00CD ISOlat1 -->\n<!ENTITY Icirc  \"&#206;\"> <!-- latin capital letter I with circumflex,\n                                  U+00CE ISOlat1 -->\n<!ENTITY Iuml   \"&#207;\"> <!-- latin capital letter I with diaeresis,\n                                  U+00CF ISOlat1 -->\n<!ENTITY ETH    \"&#208;\"> <!-- latin capital letter ETH, U+00D0 ISOlat1 -->\n<!ENTITY Ntilde \"&#209;\"> <!-- latin capital letter N with tilde,\n                                  U+00D1 ISOlat1 -->\n<!ENTITY Ograve \"&#210;\"> <!-- latin capital letter O with grave,\n                                  U+00D2 ISOlat1 -->\n<!ENTITY Oacute \"&#211;\"> <!-- latin capital letter O with acute,\n                                  U+00D3 ISOlat1 -->\n<!ENTITY Ocirc  \"&#212;\"> <!-- latin capital letter O with circumflex,\n                                  U+00D4 ISOlat1 -->\n<!ENTITY Otilde \"&#213;\"> <!-- latin capital letter O with tilde,\n                                  U+00D5 ISOlat1 -->\n<!ENTITY Ouml   \"&#214;\"> <!-- latin capital letter O with diaeresis,\n                                  U+00D6 ISOlat1 -->\n<!ENTITY times  \"&#215;\"> <!-- multiplication sign, U+00D7 ISOnum -->\n<!ENTITY Oslash \"&#216;\"> <!-- latin capital letter O with stroke\n                                  = latin capital letter O slash,\n                                  U+00D8 ISOlat1 -->\n<!ENTITY Ugrave \"&#217;\"> <!-- latin capital letter U with grave,\n                                  U+00D9 ISOlat1 -->\n<!ENTITY Uacute \"&#218;\"> <!-- latin capital letter U with acute,\n                                  U+00DA ISOlat1 -->\n<!ENTITY Ucirc  \"&#219;\"> <!-- latin capital letter U with circumflex,\n                                  U+00DB ISOlat1 -->\n<!ENTITY Uuml   \"&#220;\"> <!-- latin capital letter U with diaeresis,\n                                  U+00DC ISOlat1 -->\n<!ENTITY Yacute \"&#221;\"> <!-- latin capital letter Y with acute,\n                                  U+00DD ISOlat1 -->\n<!ENTITY THORN  \"&#222;\"> <!-- latin capital letter THORN,\n                                  U+00DE ISOlat1 -->\n<!ENTITY szlig  \"&#223;\"> <!-- latin small letter sharp s = ess-zed,\n                                  U+00DF ISOlat1 -->\n<!ENTITY agrave \"&#224;\"> <!-- latin small letter a with grave\n                                  = latin small letter a grave,\n                                  U+00E0 ISOlat1 -->\n<!ENTITY aacute \"&#225;\"> <!-- latin small letter a with acute,\n                                  U+00E1 ISOlat1 -->\n<!ENTITY acirc  \"&#226;\"> <!-- latin small letter a with circumflex,\n                                  U+00E2 ISOlat1 -->\n<!ENTITY atilde \"&#227;\"> <!-- latin small letter a with tilde,\n                                  U+00E3 ISOlat1 -->\n<!ENTITY auml   \"&#228;\"> <!-- latin small letter a with diaeresis,\n                                  U+00E4 ISOlat1 -->\n<!ENTITY aring  \"&#229;\"> <!-- latin small letter a with ring above\n                                  = latin small letter a ring,\n                                  U+00E5 ISOlat1 -->\n<!ENTITY aelig  \"&#230;\"> <!-- latin small letter ae\n                                  = latin small ligature ae, U+00E6 ISOlat1 -->\n<!ENTITY ccedil \"&#231;\"> <!-- latin small letter c with cedilla,\n                                  U+00E7 ISOlat1 -->\n<!ENTITY egrave \"&#232;\"> <!-- latin small letter e with grave,\n                                  U+00E8 ISOlat1 -->\n<!ENTITY eacute \"&#233;\"> <!-- latin small letter e with acute,\n                                  U+00E9 ISOlat1 -->\n<!ENTITY ecirc  \"&#234;\"> <!-- latin small letter e with circumflex,\n                                  U+00EA ISOlat1 -->\n<!ENTITY euml   \"&#235;\"> <!-- latin small letter e with diaeresis,\n                                  U+00EB ISOlat1 -->\n<!ENTITY igrave \"&#236;\"> <!-- latin small letter i with grave,\n                                  U+00EC ISOlat1 -->\n<!ENTITY iacute \"&#237;\"> <!-- latin small letter i with acute,\n                                  U+00ED ISOlat1 -->\n<!ENTITY icirc  \"&#238;\"> <!-- latin small letter i with circumflex,\n                                  U+00EE ISOlat1 -->\n<!ENTITY iuml   \"&#239;\"> <!-- latin small letter i with diaeresis,\n                                  U+00EF ISOlat1 -->\n<!ENTITY eth    \"&#240;\"> <!-- latin small letter eth, U+00F0 ISOlat1 -->\n<!ENTITY ntilde \"&#241;\"> <!-- latin small letter n with tilde,\n                                  U+00F1 ISOlat1 -->\n<!ENTITY ograve \"&#242;\"> <!-- latin small letter o with grave,\n                                  U+00F2 ISOlat1 -->\n<!ENTITY oacute \"&#243;\"> <!-- latin small letter o with acute,\n                                  U+00F3 ISOlat1 -->\n<!ENTITY ocirc  \"&#244;\"> <!-- latin small letter o with circumflex,\n                                  U+00F4 ISOlat1 -->\n<!ENTITY otilde \"&#245;\"> <!-- latin small letter o with tilde,\n                                  U+00F5 ISOlat1 -->\n<!ENTITY ouml   \"&#246;\"> <!-- latin small letter o with diaeresis,\n                                  U+00F6 ISOlat1 -->\n<!ENTITY divide \"&#247;\"> <!-- division sign, U+00F7 ISOnum -->\n<!ENTITY oslash \"&#248;\"> <!-- latin small letter o with stroke,\n                                  = latin small letter o slash,\n                                  U+00F8 ISOlat1 -->\n<!ENTITY ugrave \"&#249;\"> <!-- latin small letter u with grave,\n                                  U+00F9 ISOlat1 -->\n<!ENTITY uacute \"&#250;\"> <!-- latin small letter u with acute,\n                                  U+00FA ISOlat1 -->\n<!ENTITY ucirc  \"&#251;\"> <!-- latin small letter u with circumflex,\n                                  U+00FB ISOlat1 -->\n<!ENTITY uuml   \"&#252;\"> <!-- latin small letter u with diaeresis,\n                                  U+00FC ISOlat1 -->\n<!ENTITY yacute \"&#253;\"> <!-- latin small letter y with acute,\n                                  U+00FD ISOlat1 -->\n<!ENTITY thorn  \"&#254;\"> <!-- latin small letter thorn,\n                                  U+00FE ISOlat1 -->\n<!ENTITY yuml   \"&#255;\"> <!-- latin small letter y with diaeresis,\n                                  U+00FF ISOlat1 -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-special.ent",
    "content": "<!-- Special characters for XHTML -->\n\n<!-- Character entity set. Typical invocation:\n     <!ENTITY % HTMLspecial PUBLIC\n        \"-//W3C//ENTITIES Special for XHTML//EN\"\n        \"http://www.w3.org/TR/xhtml1/DTD/xhtml-special.ent\">\n     %HTMLspecial;\n-->\n\n<!-- Portions (C) International Organization for Standardization 1986:\n     Permission to copy in any form is granted for use with\n     conforming SGML systems and applications as defined in\n     ISO 8879, provided this notice is included in all copies.\n-->\n\n<!-- Relevant ISO entity set is given unless names are newly introduced.\n     New names (i.e., not in ISO 8879 list) do not clash with any\n     existing ISO 8879 entity names. ISO 10646 character numbers\n     are given for each character, in hex. values are decimal\n     conversions of the ISO 10646 values and refer to the document\n     character set. Names are Unicode names. \n-->\n\n<!-- C0 Controls and Basic Latin -->\n<!ENTITY quot    \"&#34;\"> <!--  quotation mark, U+0022 ISOnum -->\n<!ENTITY amp     \"&#38;#38;\"> <!--  ampersand, U+0026 ISOnum -->\n<!ENTITY lt      \"&#38;#60;\"> <!--  less-than sign, U+003C ISOnum -->\n<!ENTITY gt      \"&#62;\"> <!--  greater-than sign, U+003E ISOnum -->\n<!ENTITY apos\t \"&#39;\"> <!--  apostrophe = APL quote, U+0027 ISOnum -->\n\n<!-- Latin Extended-A -->\n<!ENTITY OElig   \"&#338;\"> <!--  latin capital ligature OE,\n                                    U+0152 ISOlat2 -->\n<!ENTITY oelig   \"&#339;\"> <!--  latin small ligature oe, U+0153 ISOlat2 -->\n<!-- ligature is a misnomer, this is a separate character in some languages -->\n<!ENTITY Scaron  \"&#352;\"> <!--  latin capital letter S with caron,\n                                    U+0160 ISOlat2 -->\n<!ENTITY scaron  \"&#353;\"> <!--  latin small letter s with caron,\n                                    U+0161 ISOlat2 -->\n<!ENTITY Yuml    \"&#376;\"> <!--  latin capital letter Y with diaeresis,\n                                    U+0178 ISOlat2 -->\n\n<!-- Spacing Modifier Letters -->\n<!ENTITY circ    \"&#710;\"> <!--  modifier letter circumflex accent,\n                                    U+02C6 ISOpub -->\n<!ENTITY tilde   \"&#732;\"> <!--  small tilde, U+02DC ISOdia -->\n\n<!-- General Punctuation -->\n<!ENTITY ensp    \"&#8194;\"> <!-- en space, U+2002 ISOpub -->\n<!ENTITY emsp    \"&#8195;\"> <!-- em space, U+2003 ISOpub -->\n<!ENTITY thinsp  \"&#8201;\"> <!-- thin space, U+2009 ISOpub -->\n<!ENTITY zwnj    \"&#8204;\"> <!-- zero width non-joiner,\n                                    U+200C NEW RFC 2070 -->\n<!ENTITY zwj     \"&#8205;\"> <!-- zero width joiner, U+200D NEW RFC 2070 -->\n<!ENTITY lrm     \"&#8206;\"> <!-- left-to-right mark, U+200E NEW RFC 2070 -->\n<!ENTITY rlm     \"&#8207;\"> <!-- right-to-left mark, U+200F NEW RFC 2070 -->\n<!ENTITY ndash   \"&#8211;\"> <!-- en dash, U+2013 ISOpub -->\n<!ENTITY mdash   \"&#8212;\"> <!-- em dash, U+2014 ISOpub -->\n<!ENTITY lsquo   \"&#8216;\"> <!-- left single quotation mark,\n                                    U+2018 ISOnum -->\n<!ENTITY rsquo   \"&#8217;\"> <!-- right single quotation mark,\n                                    U+2019 ISOnum -->\n<!ENTITY sbquo   \"&#8218;\"> <!-- single low-9 quotation mark, U+201A NEW -->\n<!ENTITY ldquo   \"&#8220;\"> <!-- left double quotation mark,\n                                    U+201C ISOnum -->\n<!ENTITY rdquo   \"&#8221;\"> <!-- right double quotation mark,\n                                    U+201D ISOnum -->\n<!ENTITY bdquo   \"&#8222;\"> <!-- double low-9 quotation mark, U+201E NEW -->\n<!ENTITY dagger  \"&#8224;\"> <!-- dagger, U+2020 ISOpub -->\n<!ENTITY Dagger  \"&#8225;\"> <!-- double dagger, U+2021 ISOpub -->\n<!ENTITY permil  \"&#8240;\"> <!-- per mille sign, U+2030 ISOtech -->\n<!ENTITY lsaquo  \"&#8249;\"> <!-- single left-pointing angle quotation mark,\n                                    U+2039 ISO proposed -->\n<!-- lsaquo is proposed but not yet ISO standardized -->\n<!ENTITY rsaquo  \"&#8250;\"> <!-- single right-pointing angle quotation mark,\n                                    U+203A ISO proposed -->\n<!-- rsaquo is proposed but not yet ISO standardized -->\n\n<!-- Currency Symbols -->\n<!ENTITY euro   \"&#8364;\"> <!--  euro sign, U+20AC NEW -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent",
    "content": "<!-- Mathematical, Greek and Symbolic characters for XHTML -->\n\n<!-- Character entity set. Typical invocation:\n     <!ENTITY % HTMLsymbol PUBLIC\n        \"-//W3C//ENTITIES Symbols for XHTML//EN\"\n        \"http://www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent\">\n     %HTMLsymbol;\n-->\n\n<!-- Portions (C) International Organization for Standardization 1986:\n     Permission to copy in any form is granted for use with\n     conforming SGML systems and applications as defined in\n     ISO 8879, provided this notice is included in all copies.\n-->\n\n<!-- Relevant ISO entity set is given unless names are newly introduced.\n     New names (i.e., not in ISO 8879 list) do not clash with any\n     existing ISO 8879 entity names. ISO 10646 character numbers\n     are given for each character, in hex. values are decimal\n     conversions of the ISO 10646 values and refer to the document\n     character set. Names are Unicode names. \n-->\n\n<!-- Latin Extended-B -->\n<!ENTITY fnof     \"&#402;\"> <!-- latin small letter f with hook = function\n                                    = florin, U+0192 ISOtech -->\n\n<!-- Greek -->\n<!ENTITY Alpha    \"&#913;\"> <!-- greek capital letter alpha, U+0391 -->\n<!ENTITY Beta     \"&#914;\"> <!-- greek capital letter beta, U+0392 -->\n<!ENTITY Gamma    \"&#915;\"> <!-- greek capital letter gamma,\n                                    U+0393 ISOgrk3 -->\n<!ENTITY Delta    \"&#916;\"> <!-- greek capital letter delta,\n                                    U+0394 ISOgrk3 -->\n<!ENTITY Epsilon  \"&#917;\"> <!-- greek capital letter epsilon, U+0395 -->\n<!ENTITY Zeta     \"&#918;\"> <!-- greek capital letter zeta, U+0396 -->\n<!ENTITY Eta      \"&#919;\"> <!-- greek capital letter eta, U+0397 -->\n<!ENTITY Theta    \"&#920;\"> <!-- greek capital letter theta,\n                                    U+0398 ISOgrk3 -->\n<!ENTITY Iota     \"&#921;\"> <!-- greek capital letter iota, U+0399 -->\n<!ENTITY Kappa    \"&#922;\"> <!-- greek capital letter kappa, U+039A -->\n<!ENTITY Lambda   \"&#923;\"> <!-- greek capital letter lamda,\n                                    U+039B ISOgrk3 -->\n<!ENTITY Mu       \"&#924;\"> <!-- greek capital letter mu, U+039C -->\n<!ENTITY Nu       \"&#925;\"> <!-- greek capital letter nu, U+039D -->\n<!ENTITY Xi       \"&#926;\"> <!-- greek capital letter xi, U+039E ISOgrk3 -->\n<!ENTITY Omicron  \"&#927;\"> <!-- greek capital letter omicron, U+039F -->\n<!ENTITY Pi       \"&#928;\"> <!-- greek capital letter pi, U+03A0 ISOgrk3 -->\n<!ENTITY Rho      \"&#929;\"> <!-- greek capital letter rho, U+03A1 -->\n<!-- there is no Sigmaf, and no U+03A2 character either -->\n<!ENTITY Sigma    \"&#931;\"> <!-- greek capital letter sigma,\n                                    U+03A3 ISOgrk3 -->\n<!ENTITY Tau      \"&#932;\"> <!-- greek capital letter tau, U+03A4 -->\n<!ENTITY Upsilon  \"&#933;\"> <!-- greek capital letter upsilon,\n                                    U+03A5 ISOgrk3 -->\n<!ENTITY Phi      \"&#934;\"> <!-- greek capital letter phi,\n                                    U+03A6 ISOgrk3 -->\n<!ENTITY Chi      \"&#935;\"> <!-- greek capital letter chi, U+03A7 -->\n<!ENTITY Psi      \"&#936;\"> <!-- greek capital letter psi,\n                                    U+03A8 ISOgrk3 -->\n<!ENTITY Omega    \"&#937;\"> <!-- greek capital letter omega,\n                                    U+03A9 ISOgrk3 -->\n\n<!ENTITY alpha    \"&#945;\"> <!-- greek small letter alpha,\n                                    U+03B1 ISOgrk3 -->\n<!ENTITY beta     \"&#946;\"> <!-- greek small letter beta, U+03B2 ISOgrk3 -->\n<!ENTITY gamma    \"&#947;\"> <!-- greek small letter gamma,\n                                    U+03B3 ISOgrk3 -->\n<!ENTITY delta    \"&#948;\"> <!-- greek small letter delta,\n                                    U+03B4 ISOgrk3 -->\n<!ENTITY epsilon  \"&#949;\"> <!-- greek small letter epsilon,\n                                    U+03B5 ISOgrk3 -->\n<!ENTITY zeta     \"&#950;\"> <!-- greek small letter zeta, U+03B6 ISOgrk3 -->\n<!ENTITY eta      \"&#951;\"> <!-- greek small letter eta, U+03B7 ISOgrk3 -->\n<!ENTITY theta    \"&#952;\"> <!-- greek small letter theta,\n                                    U+03B8 ISOgrk3 -->\n<!ENTITY iota     \"&#953;\"> <!-- greek small letter iota, U+03B9 ISOgrk3 -->\n<!ENTITY kappa    \"&#954;\"> <!-- greek small letter kappa,\n                                    U+03BA ISOgrk3 -->\n<!ENTITY lambda   \"&#955;\"> <!-- greek small letter lamda,\n                                    U+03BB ISOgrk3 -->\n<!ENTITY mu       \"&#956;\"> <!-- greek small letter mu, U+03BC ISOgrk3 -->\n<!ENTITY nu       \"&#957;\"> <!-- greek small letter nu, U+03BD ISOgrk3 -->\n<!ENTITY xi       \"&#958;\"> <!-- greek small letter xi, U+03BE ISOgrk3 -->\n<!ENTITY omicron  \"&#959;\"> <!-- greek small letter omicron, U+03BF NEW -->\n<!ENTITY pi       \"&#960;\"> <!-- greek small letter pi, U+03C0 ISOgrk3 -->\n<!ENTITY rho      \"&#961;\"> <!-- greek small letter rho, U+03C1 ISOgrk3 -->\n<!ENTITY sigmaf   \"&#962;\"> <!-- greek small letter final sigma,\n                                    U+03C2 ISOgrk3 -->\n<!ENTITY sigma    \"&#963;\"> <!-- greek small letter sigma,\n                                    U+03C3 ISOgrk3 -->\n<!ENTITY tau      \"&#964;\"> <!-- greek small letter tau, U+03C4 ISOgrk3 -->\n<!ENTITY upsilon  \"&#965;\"> <!-- greek small letter upsilon,\n                                    U+03C5 ISOgrk3 -->\n<!ENTITY phi      \"&#966;\"> <!-- greek small letter phi, U+03C6 ISOgrk3 -->\n<!ENTITY chi      \"&#967;\"> <!-- greek small letter chi, U+03C7 ISOgrk3 -->\n<!ENTITY psi      \"&#968;\"> <!-- greek small letter psi, U+03C8 ISOgrk3 -->\n<!ENTITY omega    \"&#969;\"> <!-- greek small letter omega,\n                                    U+03C9 ISOgrk3 -->\n<!ENTITY thetasym \"&#977;\"> <!-- greek theta symbol,\n                                    U+03D1 NEW -->\n<!ENTITY upsih    \"&#978;\"> <!-- greek upsilon with hook symbol,\n                                    U+03D2 NEW -->\n<!ENTITY piv      \"&#982;\"> <!-- greek pi symbol, U+03D6 ISOgrk3 -->\n\n<!-- General Punctuation -->\n<!ENTITY bull     \"&#8226;\"> <!-- bullet = black small circle,\n                                     U+2022 ISOpub  -->\n<!-- bullet is NOT the same as bullet operator, U+2219 -->\n<!ENTITY hellip   \"&#8230;\"> <!-- horizontal ellipsis = three dot leader,\n                                     U+2026 ISOpub  -->\n<!ENTITY prime    \"&#8242;\"> <!-- prime = minutes = feet, U+2032 ISOtech -->\n<!ENTITY Prime    \"&#8243;\"> <!-- double prime = seconds = inches,\n                                     U+2033 ISOtech -->\n<!ENTITY oline    \"&#8254;\"> <!-- overline = spacing overscore,\n                                     U+203E NEW -->\n<!ENTITY frasl    \"&#8260;\"> <!-- fraction slash, U+2044 NEW -->\n\n<!-- Letterlike Symbols -->\n<!ENTITY weierp   \"&#8472;\"> <!-- script capital P = power set\n                                     = Weierstrass p, U+2118 ISOamso -->\n<!ENTITY image    \"&#8465;\"> <!-- black-letter capital I = imaginary part,\n                                     U+2111 ISOamso -->\n<!ENTITY real     \"&#8476;\"> <!-- black-letter capital R = real part symbol,\n                                     U+211C ISOamso -->\n<!ENTITY trade    \"&#8482;\"> <!-- trade mark sign, U+2122 ISOnum -->\n<!ENTITY alefsym  \"&#8501;\"> <!-- alef symbol = first transfinite cardinal,\n                                     U+2135 NEW -->\n<!-- alef symbol is NOT the same as hebrew letter alef,\n     U+05D0 although the same glyph could be used to depict both characters -->\n\n<!-- Arrows -->\n<!ENTITY larr     \"&#8592;\"> <!-- leftwards arrow, U+2190 ISOnum -->\n<!ENTITY uarr     \"&#8593;\"> <!-- upwards arrow, U+2191 ISOnum-->\n<!ENTITY rarr     \"&#8594;\"> <!-- rightwards arrow, U+2192 ISOnum -->\n<!ENTITY darr     \"&#8595;\"> <!-- downwards arrow, U+2193 ISOnum -->\n<!ENTITY harr     \"&#8596;\"> <!-- left right arrow, U+2194 ISOamsa -->\n<!ENTITY crarr    \"&#8629;\"> <!-- downwards arrow with corner leftwards\n                                     = carriage return, U+21B5 NEW -->\n<!ENTITY lArr     \"&#8656;\"> <!-- leftwards double arrow, U+21D0 ISOtech -->\n<!-- Unicode does not say that lArr is the same as the 'is implied by' arrow\n    but also does not have any other character for that function. So lArr can\n    be used for 'is implied by' as ISOtech suggests -->\n<!ENTITY uArr     \"&#8657;\"> <!-- upwards double arrow, U+21D1 ISOamsa -->\n<!ENTITY rArr     \"&#8658;\"> <!-- rightwards double arrow,\n                                     U+21D2 ISOtech -->\n<!-- Unicode does not say this is the 'implies' character but does not have \n     another character with this function so rArr can be used for 'implies'\n     as ISOtech suggests -->\n<!ENTITY dArr     \"&#8659;\"> <!-- downwards double arrow, U+21D3 ISOamsa -->\n<!ENTITY hArr     \"&#8660;\"> <!-- left right double arrow,\n                                     U+21D4 ISOamsa -->\n\n<!-- Mathematical Operators -->\n<!ENTITY forall   \"&#8704;\"> <!-- for all, U+2200 ISOtech -->\n<!ENTITY part     \"&#8706;\"> <!-- partial differential, U+2202 ISOtech  -->\n<!ENTITY exist    \"&#8707;\"> <!-- there exists, U+2203 ISOtech -->\n<!ENTITY empty    \"&#8709;\"> <!-- empty set = null set, U+2205 ISOamso -->\n<!ENTITY nabla    \"&#8711;\"> <!-- nabla = backward difference,\n                                     U+2207 ISOtech -->\n<!ENTITY isin     \"&#8712;\"> <!-- element of, U+2208 ISOtech -->\n<!ENTITY notin    \"&#8713;\"> <!-- not an element of, U+2209 ISOtech -->\n<!ENTITY ni       \"&#8715;\"> <!-- contains as member, U+220B ISOtech -->\n<!ENTITY prod     \"&#8719;\"> <!-- n-ary product = product sign,\n                                     U+220F ISOamsb -->\n<!-- prod is NOT the same character as U+03A0 'greek capital letter pi' though\n     the same glyph might be used for both -->\n<!ENTITY sum      \"&#8721;\"> <!-- n-ary summation, U+2211 ISOamsb -->\n<!-- sum is NOT the same character as U+03A3 'greek capital letter sigma'\n     though the same glyph might be used for both -->\n<!ENTITY minus    \"&#8722;\"> <!-- minus sign, U+2212 ISOtech -->\n<!ENTITY lowast   \"&#8727;\"> <!-- asterisk operator, U+2217 ISOtech -->\n<!ENTITY radic    \"&#8730;\"> <!-- square root = radical sign,\n                                     U+221A ISOtech -->\n<!ENTITY prop     \"&#8733;\"> <!-- proportional to, U+221D ISOtech -->\n<!ENTITY infin    \"&#8734;\"> <!-- infinity, U+221E ISOtech -->\n<!ENTITY ang      \"&#8736;\"> <!-- angle, U+2220 ISOamso -->\n<!ENTITY and      \"&#8743;\"> <!-- logical and = wedge, U+2227 ISOtech -->\n<!ENTITY or       \"&#8744;\"> <!-- logical or = vee, U+2228 ISOtech -->\n<!ENTITY cap      \"&#8745;\"> <!-- intersection = cap, U+2229 ISOtech -->\n<!ENTITY cup      \"&#8746;\"> <!-- union = cup, U+222A ISOtech -->\n<!ENTITY int      \"&#8747;\"> <!-- integral, U+222B ISOtech -->\n<!ENTITY there4   \"&#8756;\"> <!-- therefore, U+2234 ISOtech -->\n<!ENTITY sim      \"&#8764;\"> <!-- tilde operator = varies with = similar to,\n                                     U+223C ISOtech -->\n<!-- tilde operator is NOT the same character as the tilde, U+007E,\n     although the same glyph might be used to represent both  -->\n<!ENTITY cong     \"&#8773;\"> <!-- approximately equal to, U+2245 ISOtech -->\n<!ENTITY asymp    \"&#8776;\"> <!-- almost equal to = asymptotic to,\n                                     U+2248 ISOamsr -->\n<!ENTITY ne       \"&#8800;\"> <!-- not equal to, U+2260 ISOtech -->\n<!ENTITY equiv    \"&#8801;\"> <!-- identical to, U+2261 ISOtech -->\n<!ENTITY le       \"&#8804;\"> <!-- less-than or equal to, U+2264 ISOtech -->\n<!ENTITY ge       \"&#8805;\"> <!-- greater-than or equal to,\n                                     U+2265 ISOtech -->\n<!ENTITY sub      \"&#8834;\"> <!-- subset of, U+2282 ISOtech -->\n<!ENTITY sup      \"&#8835;\"> <!-- superset of, U+2283 ISOtech -->\n<!ENTITY nsub     \"&#8836;\"> <!-- not a subset of, U+2284 ISOamsn -->\n<!ENTITY sube     \"&#8838;\"> <!-- subset of or equal to, U+2286 ISOtech -->\n<!ENTITY supe     \"&#8839;\"> <!-- superset of or equal to,\n                                     U+2287 ISOtech -->\n<!ENTITY oplus    \"&#8853;\"> <!-- circled plus = direct sum,\n                                     U+2295 ISOamsb -->\n<!ENTITY otimes   \"&#8855;\"> <!-- circled times = vector product,\n                                     U+2297 ISOamsb -->\n<!ENTITY perp     \"&#8869;\"> <!-- up tack = orthogonal to = perpendicular,\n                                     U+22A5 ISOtech -->\n<!ENTITY sdot     \"&#8901;\"> <!-- dot operator, U+22C5 ISOamsb -->\n<!-- dot operator is NOT the same character as U+00B7 middle dot -->\n\n<!-- Miscellaneous Technical -->\n<!ENTITY lceil    \"&#8968;\"> <!-- left ceiling = APL upstile,\n                                     U+2308 ISOamsc  -->\n<!ENTITY rceil    \"&#8969;\"> <!-- right ceiling, U+2309 ISOamsc  -->\n<!ENTITY lfloor   \"&#8970;\"> <!-- left floor = APL downstile,\n                                     U+230A ISOamsc  -->\n<!ENTITY rfloor   \"&#8971;\"> <!-- right floor, U+230B ISOamsc  -->\n<!ENTITY lang     \"&#9001;\"> <!-- left-pointing angle bracket = bra,\n                                     U+2329 ISOtech -->\n<!-- lang is NOT the same character as U+003C 'less than sign' \n     or U+2039 'single left-pointing angle quotation mark' -->\n<!ENTITY rang     \"&#9002;\"> <!-- right-pointing angle bracket = ket,\n                                     U+232A ISOtech -->\n<!-- rang is NOT the same character as U+003E 'greater than sign' \n     or U+203A 'single right-pointing angle quotation mark' -->\n\n<!-- Geometric Shapes -->\n<!ENTITY loz      \"&#9674;\"> <!-- lozenge, U+25CA ISOpub -->\n\n<!-- Miscellaneous Symbols -->\n<!ENTITY spades   \"&#9824;\"> <!-- black spade suit, U+2660 ISOpub -->\n<!-- black here seems to mean filled as opposed to hollow -->\n<!ENTITY clubs    \"&#9827;\"> <!-- black club suit = shamrock,\n                                     U+2663 ISOpub -->\n<!ENTITY hearts   \"&#9829;\"> <!-- black heart suit = valentine,\n                                     U+2665 ISOpub -->\n<!ENTITY diams    \"&#9830;\"> <!-- black diamond suit, U+2666 ISOpub -->\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd",
    "content": "<!--\n   Extensible HTML version 1.0 Strict DTD\n\n   This is the same as HTML 4 Strict except for\n   changes due to the differences between XML and SGML.\n\n   Namespace = http://www.w3.org/1999/xhtml\n\n   For further information, see: http://www.w3.org/TR/xhtml1\n\n   Copyright (c) 1998-2002 W3C (MIT, INRIA, Keio),\n   All Rights Reserved. \n\n   This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n   PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n   SYSTEM \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\"\n\n   $Revision: 1.1 $\n   $Date: 2002/08/01 13:56:03 $\n\n-->\n\n<!--================ Character mnemonic entities =========================-->\n\n<!ENTITY % HTMLlat1 PUBLIC\n   \"-//W3C//ENTITIES Latin 1 for XHTML//EN\"\n   \"xhtml-lat1.ent\">\n%HTMLlat1;\n\n<!ENTITY % HTMLsymbol PUBLIC\n   \"-//W3C//ENTITIES Symbols for XHTML//EN\"\n   \"xhtml-symbol.ent\">\n%HTMLsymbol;\n\n<!ENTITY % HTMLspecial PUBLIC\n   \"-//W3C//ENTITIES Special for XHTML//EN\"\n   \"xhtml-special.ent\">\n%HTMLspecial;\n\n<!--================== Imported Names ====================================-->\n\n<!ENTITY % ContentType \"CDATA\">\n    <!-- media type, as per [RFC2045] -->\n\n<!ENTITY % ContentTypes \"CDATA\">\n    <!-- comma-separated list of media types, as per [RFC2045] -->\n\n<!ENTITY % Charset \"CDATA\">\n    <!-- a character encoding, as per [RFC2045] -->\n\n<!ENTITY % Charsets \"CDATA\">\n    <!-- a space separated list of character encodings, as per [RFC2045] -->\n\n<!ENTITY % LanguageCode \"NMTOKEN\">\n    <!-- a language code, as per [RFC3066] -->\n\n<!ENTITY % Character \"CDATA\">\n    <!-- a single character, as per section 2.2 of [XML] -->\n\n<!ENTITY % Number \"CDATA\">\n    <!-- one or more digits -->\n\n<!ENTITY % LinkTypes \"CDATA\">\n    <!-- space-separated list of link types -->\n\n<!ENTITY % MediaDesc \"CDATA\">\n    <!-- single or comma-separated list of media descriptors -->\n\n<!ENTITY % URI \"CDATA\">\n    <!-- a Uniform Resource Identifier, see [RFC2396] -->\n\n<!ENTITY % UriList \"CDATA\">\n    <!-- a space separated list of Uniform Resource Identifiers -->\n\n<!ENTITY % Datetime \"CDATA\">\n    <!-- date and time information. ISO date format -->\n\n<!ENTITY % Script \"CDATA\">\n    <!-- script expression -->\n\n<!ENTITY % StyleSheet \"CDATA\">\n    <!-- style sheet data -->\n\n<!ENTITY % Text \"CDATA\">\n    <!-- used for titles etc. -->\n\n<!ENTITY % Length \"CDATA\">\n    <!-- nn for pixels or nn% for percentage length -->\n\n<!ENTITY % MultiLength \"CDATA\">\n    <!-- pixel, percentage, or relative -->\n\n<!ENTITY % Pixels \"CDATA\">\n    <!-- integer representing length in pixels -->\n\n<!-- these are used for image maps -->\n\n<!ENTITY % Shape \"(rect|circle|poly|default)\">\n\n<!ENTITY % Coords \"CDATA\">\n    <!-- comma separated list of lengths -->\n\n<!--=================== Generic Attributes ===============================-->\n\n<!-- core attributes common to most elements\n  id       document-wide unique id\n  class    space separated list of classes\n  style    associated style info\n  title    advisory title/amplification\n-->\n<!ENTITY % coreattrs\n \"id          ID             #IMPLIED\n  class       CDATA          #IMPLIED\n  style       %StyleSheet;   #IMPLIED\n  title       %Text;         #IMPLIED\"\n  >\n\n<!-- internationalization attributes\n  lang        language code (backwards compatible)\n  xml:lang    language code (as per XML 1.0 spec)\n  dir         direction for weak/neutral text\n-->\n<!ENTITY % i18n\n \"lang        %LanguageCode; #IMPLIED\n  xml:lang    %LanguageCode; #IMPLIED\n  dir         (ltr|rtl)      #IMPLIED\"\n  >\n\n<!-- attributes for common UI events\n  onclick     a pointer button was clicked\n  ondblclick  a pointer button was double clicked\n  onmousedown a pointer button was pressed down\n  onmouseup   a pointer button was released\n  onmousemove a pointer was moved onto the element\n  onmouseout  a pointer was moved away from the element\n  onkeypress  a key was pressed and released\n  onkeydown   a key was pressed down\n  onkeyup     a key was released\n-->\n<!ENTITY % events\n \"onclick     %Script;       #IMPLIED\n  ondblclick  %Script;       #IMPLIED\n  onmousedown %Script;       #IMPLIED\n  onmouseup   %Script;       #IMPLIED\n  onmouseover %Script;       #IMPLIED\n  onmousemove %Script;       #IMPLIED\n  onmouseout  %Script;       #IMPLIED\n  onkeypress  %Script;       #IMPLIED\n  onkeydown   %Script;       #IMPLIED\n  onkeyup     %Script;       #IMPLIED\"\n  >\n\n<!-- attributes for elements that can get the focus\n  accesskey   accessibility key character\n  tabindex    position in tabbing order\n  onfocus     the element got the focus\n  onblur      the element lost the focus\n-->\n<!ENTITY % focus\n \"accesskey   %Character;    #IMPLIED\n  tabindex    %Number;       #IMPLIED\n  onfocus     %Script;       #IMPLIED\n  onblur      %Script;       #IMPLIED\"\n  >\n\n<!ENTITY % attrs \"%coreattrs; %i18n; %events;\">\n\n<!--=================== Text Elements ====================================-->\n\n<!ENTITY % special.pre\n   \"br | span | bdo | map\">\n\n\n<!ENTITY % special\n   \"%special.pre; | object | img \">\n\n<!ENTITY % fontstyle \"tt | i | b | big | small \">\n\n<!ENTITY % phrase \"em | strong | dfn | code | q |\n                   samp | kbd | var | cite | abbr | acronym | sub | sup \">\n\n<!ENTITY % inline.forms \"input | select | textarea | label | button\">\n\n<!-- these can occur at block or inline level -->\n<!ENTITY % misc.inline \"ins | del | script\">\n\n<!-- these can only occur at block level -->\n<!ENTITY % misc \"noscript | %misc.inline;\">\n\n<!ENTITY % inline \"a | %special; | %fontstyle; | %phrase; | %inline.forms;\">\n\n<!-- %Inline; covers inline or \"text-level\" elements -->\n<!ENTITY % Inline \"(#PCDATA | %inline; | %misc.inline;)*\">\n\n<!--================== Block level elements ==============================-->\n\n<!ENTITY % heading \"h1|h2|h3|h4|h5|h6\">\n<!ENTITY % lists \"ul | ol | dl\">\n<!ENTITY % blocktext \"pre | hr | blockquote | address\">\n\n<!ENTITY % block\n     \"p | %heading; | div | %lists; | %blocktext; | fieldset | table\">\n\n<!ENTITY % Block \"(%block; | form | %misc;)*\">\n\n<!-- %Flow; mixes block and inline and is used for list items etc. -->\n<!ENTITY % Flow \"(#PCDATA | %block; | form | %inline; | %misc;)*\">\n\n<!--================== Content models for exclusions =====================-->\n\n<!-- a elements use %Inline; excluding a -->\n\n<!ENTITY % a.content\n   \"(#PCDATA | %special; | %fontstyle; | %phrase; | %inline.forms; | %misc.inline;)*\">\n\n<!-- pre uses %Inline excluding big, small, sup or sup -->\n\n<!ENTITY % pre.content\n   \"(#PCDATA | a | %fontstyle; | %phrase; | %special.pre; | %misc.inline;\n      | %inline.forms;)*\">\n\n<!-- form uses %Block; excluding form -->\n\n<!ENTITY % form.content \"(%block; | %misc;)*\">\n\n<!-- button uses %Flow; but excludes a, form and form controls -->\n\n<!ENTITY % button.content\n   \"(#PCDATA | p | %heading; | div | %lists; | %blocktext; |\n    table | %special; | %fontstyle; | %phrase; | %misc;)*\">\n\n<!--================ Document Structure ==================================-->\n\n<!-- the namespace URI designates the document profile -->\n\n<!ELEMENT html (head, body)>\n<!ATTLIST html\n  %i18n;\n  id          ID             #IMPLIED\n  xmlns       %URI;          #FIXED 'http://www.w3.org/1999/xhtml'\n  >\n\n<!--================ Document Head =======================================-->\n\n<!ENTITY % head.misc \"(script|style|meta|link|object)*\">\n\n<!-- content model is %head.misc; combined with a single\n     title and an optional base element in any order -->\n\n<!ELEMENT head (%head.misc;,\n     ((title, %head.misc;, (base, %head.misc;)?) |\n      (base, %head.misc;, (title, %head.misc;))))>\n\n<!ATTLIST head\n  %i18n;\n  id          ID             #IMPLIED\n  profile     %URI;          #IMPLIED\n  >\n\n<!-- The title element is not considered part of the flow of text.\n       It should be displayed, for example as the page header or\n       window title. Exactly one title is required per document.\n    -->\n<!ELEMENT title (#PCDATA)>\n<!ATTLIST title \n  %i18n;\n  id          ID             #IMPLIED\n  >\n\n<!-- document base URI -->\n\n<!ELEMENT base EMPTY>\n<!ATTLIST base\n  href        %URI;          #REQUIRED\n  id          ID             #IMPLIED\n  >\n\n<!-- generic metainformation -->\n<!ELEMENT meta EMPTY>\n<!ATTLIST meta\n  %i18n;\n  id          ID             #IMPLIED\n  http-equiv  CDATA          #IMPLIED\n  name        CDATA          #IMPLIED\n  content     CDATA          #REQUIRED\n  scheme      CDATA          #IMPLIED\n  >\n\n<!--\n  Relationship values can be used in principle:\n\n   a) for document specific toolbars/menus when used\n      with the link element in document head e.g.\n        start, contents, previous, next, index, end, help\n   b) to link to a separate style sheet (rel=\"stylesheet\")\n   c) to make a link to a script (rel=\"script\")\n   d) by stylesheets to control how collections of\n      html nodes are rendered into printed documents\n   e) to make a link to a printable version of this document\n      e.g. a PostScript or PDF version (rel=\"alternate\" media=\"print\")\n-->\n\n<!ELEMENT link EMPTY>\n<!ATTLIST link\n  %attrs;\n  charset     %Charset;      #IMPLIED\n  href        %URI;          #IMPLIED\n  hreflang    %LanguageCode; #IMPLIED\n  type        %ContentType;  #IMPLIED\n  rel         %LinkTypes;    #IMPLIED\n  rev         %LinkTypes;    #IMPLIED\n  media       %MediaDesc;    #IMPLIED\n  >\n\n<!-- style info, which may include CDATA sections -->\n<!ELEMENT style (#PCDATA)>\n<!ATTLIST style\n  %i18n;\n  id          ID             #IMPLIED\n  type        %ContentType;  #REQUIRED\n  media       %MediaDesc;    #IMPLIED\n  title       %Text;         #IMPLIED\n  xml:space   (preserve)     #FIXED 'preserve'\n  >\n\n<!-- script statements, which may include CDATA sections -->\n<!ELEMENT script (#PCDATA)>\n<!ATTLIST script\n  id          ID             #IMPLIED\n  charset     %Charset;      #IMPLIED\n  type        %ContentType;  #REQUIRED\n  src         %URI;          #IMPLIED\n  defer       (defer)        #IMPLIED\n  xml:space   (preserve)     #FIXED 'preserve'\n  >\n\n<!-- alternate content container for non script-based rendering -->\n\n<!ELEMENT noscript %Block;>\n<!ATTLIST noscript\n  %attrs;\n  >\n\n<!--=================== Document Body ====================================-->\n\n<!ELEMENT body %Block;>\n<!ATTLIST body\n  %attrs;\n  onload          %Script;   #IMPLIED\n  onunload        %Script;   #IMPLIED\n  >\n\n<!ELEMENT div %Flow;>  <!-- generic language/style container -->\n<!ATTLIST div\n  %attrs;\n  >\n\n<!--=================== Paragraphs =======================================-->\n\n<!ELEMENT p %Inline;>\n<!ATTLIST p\n  %attrs;\n  >\n\n<!--=================== Headings =========================================-->\n\n<!--\n  There are six levels of headings from h1 (the most important)\n  to h6 (the least important).\n-->\n\n<!ELEMENT h1  %Inline;>\n<!ATTLIST h1\n   %attrs;\n   >\n\n<!ELEMENT h2 %Inline;>\n<!ATTLIST h2\n   %attrs;\n   >\n\n<!ELEMENT h3 %Inline;>\n<!ATTLIST h3\n   %attrs;\n   >\n\n<!ELEMENT h4 %Inline;>\n<!ATTLIST h4\n   %attrs;\n   >\n\n<!ELEMENT h5 %Inline;>\n<!ATTLIST h5\n   %attrs;\n   >\n\n<!ELEMENT h6 %Inline;>\n<!ATTLIST h6\n   %attrs;\n   >\n\n<!--=================== Lists ============================================-->\n\n<!-- Unordered list -->\n\n<!ELEMENT ul (li)+>\n<!ATTLIST ul\n  %attrs;\n  >\n\n<!-- Ordered (numbered) list -->\n\n<!ELEMENT ol (li)+>\n<!ATTLIST ol\n  %attrs;\n  >\n\n<!-- list item -->\n\n<!ELEMENT li %Flow;>\n<!ATTLIST li\n  %attrs;\n  >\n\n<!-- definition lists - dt for term, dd for its definition -->\n\n<!ELEMENT dl (dt|dd)+>\n<!ATTLIST dl\n  %attrs;\n  >\n\n<!ELEMENT dt %Inline;>\n<!ATTLIST dt\n  %attrs;\n  >\n\n<!ELEMENT dd %Flow;>\n<!ATTLIST dd\n  %attrs;\n  >\n\n<!--=================== Address ==========================================-->\n\n<!-- information on author -->\n\n<!ELEMENT address %Inline;>\n<!ATTLIST address\n  %attrs;\n  >\n\n<!--=================== Horizontal Rule ==================================-->\n\n<!ELEMENT hr EMPTY>\n<!ATTLIST hr\n  %attrs;\n  >\n\n<!--=================== Preformatted Text ================================-->\n\n<!-- content is %Inline; excluding \"img|object|big|small|sub|sup\" -->\n\n<!ELEMENT pre %pre.content;>\n<!ATTLIST pre\n  %attrs;\n  xml:space (preserve) #FIXED 'preserve'\n  >\n\n<!--=================== Block-like Quotes ================================-->\n\n<!ELEMENT blockquote %Block;>\n<!ATTLIST blockquote\n  %attrs;\n  cite        %URI;          #IMPLIED\n  >\n\n<!--=================== Inserted/Deleted Text ============================-->\n\n<!--\n  ins/del are allowed in block and inline content, but its\n  inappropriate to include block content within an ins element\n  occurring in inline content.\n-->\n<!ELEMENT ins %Flow;>\n<!ATTLIST ins\n  %attrs;\n  cite        %URI;          #IMPLIED\n  datetime    %Datetime;     #IMPLIED\n  >\n\n<!ELEMENT del %Flow;>\n<!ATTLIST del\n  %attrs;\n  cite        %URI;          #IMPLIED\n  datetime    %Datetime;     #IMPLIED\n  >\n\n<!--================== The Anchor Element ================================-->\n\n<!-- content is %Inline; except that anchors shouldn't be nested -->\n\n<!ELEMENT a %a.content;>\n<!ATTLIST a\n  %attrs;\n  %focus;\n  charset     %Charset;      #IMPLIED\n  type        %ContentType;  #IMPLIED\n  name        NMTOKEN        #IMPLIED\n  href        %URI;          #IMPLIED\n  hreflang    %LanguageCode; #IMPLIED\n  rel         %LinkTypes;    #IMPLIED\n  rev         %LinkTypes;    #IMPLIED\n  shape       %Shape;        \"rect\"\n  coords      %Coords;       #IMPLIED\n  >\n\n<!--===================== Inline Elements ================================-->\n\n<!ELEMENT span %Inline;> <!-- generic language/style container -->\n<!ATTLIST span\n  %attrs;\n  >\n\n<!ELEMENT bdo %Inline;>  <!-- I18N BiDi over-ride -->\n<!ATTLIST bdo\n  %coreattrs;\n  %events;\n  lang        %LanguageCode; #IMPLIED\n  xml:lang    %LanguageCode; #IMPLIED\n  dir         (ltr|rtl)      #REQUIRED\n  >\n\n<!ELEMENT br EMPTY>   <!-- forced line break -->\n<!ATTLIST br\n  %coreattrs;\n  >\n\n<!ELEMENT em %Inline;>   <!-- emphasis -->\n<!ATTLIST em %attrs;>\n\n<!ELEMENT strong %Inline;>   <!-- strong emphasis -->\n<!ATTLIST strong %attrs;>\n\n<!ELEMENT dfn %Inline;>   <!-- definitional -->\n<!ATTLIST dfn %attrs;>\n\n<!ELEMENT code %Inline;>   <!-- program code -->\n<!ATTLIST code %attrs;>\n\n<!ELEMENT samp %Inline;>   <!-- sample -->\n<!ATTLIST samp %attrs;>\n\n<!ELEMENT kbd %Inline;>  <!-- something user would type -->\n<!ATTLIST kbd %attrs;>\n\n<!ELEMENT var %Inline;>   <!-- variable -->\n<!ATTLIST var %attrs;>\n\n<!ELEMENT cite %Inline;>   <!-- citation -->\n<!ATTLIST cite %attrs;>\n\n<!ELEMENT abbr %Inline;>   <!-- abbreviation -->\n<!ATTLIST abbr %attrs;>\n\n<!ELEMENT acronym %Inline;>   <!-- acronym -->\n<!ATTLIST acronym %attrs;>\n\n<!ELEMENT q %Inline;>   <!-- inlined quote -->\n<!ATTLIST q\n  %attrs;\n  cite        %URI;          #IMPLIED\n  >\n\n<!ELEMENT sub %Inline;> <!-- subscript -->\n<!ATTLIST sub %attrs;>\n\n<!ELEMENT sup %Inline;> <!-- superscript -->\n<!ATTLIST sup %attrs;>\n\n<!ELEMENT tt %Inline;>   <!-- fixed pitch font -->\n<!ATTLIST tt %attrs;>\n\n<!ELEMENT i %Inline;>   <!-- italic font -->\n<!ATTLIST i %attrs;>\n\n<!ELEMENT b %Inline;>   <!-- bold font -->\n<!ATTLIST b %attrs;>\n\n<!ELEMENT big %Inline;>   <!-- bigger font -->\n<!ATTLIST big %attrs;>\n\n<!ELEMENT small %Inline;>   <!-- smaller font -->\n<!ATTLIST small %attrs;>\n\n<!--==================== Object ======================================-->\n<!--\n  object is used to embed objects as part of HTML pages.\n  param elements should precede other content. Parameters\n  can also be expressed as attribute/value pairs on the\n  object element itself when brevity is desired.\n-->\n\n<!ELEMENT object (#PCDATA | param | %block; | form | %inline; | %misc;)*>\n<!ATTLIST object\n  %attrs;\n  declare     (declare)      #IMPLIED\n  classid     %URI;          #IMPLIED\n  codebase    %URI;          #IMPLIED\n  data        %URI;          #IMPLIED\n  type        %ContentType;  #IMPLIED\n  codetype    %ContentType;  #IMPLIED\n  archive     %UriList;      #IMPLIED\n  standby     %Text;         #IMPLIED\n  height      %Length;       #IMPLIED\n  width       %Length;       #IMPLIED\n  usemap      %URI;          #IMPLIED\n  name        NMTOKEN        #IMPLIED\n  tabindex    %Number;       #IMPLIED\n  >\n\n<!--\n  param is used to supply a named property value.\n  In XML it would seem natural to follow RDF and support an\n  abbreviated syntax where the param elements are replaced\n  by attribute value pairs on the object start tag.\n-->\n<!ELEMENT param EMPTY>\n<!ATTLIST param\n  id          ID             #IMPLIED\n  name        CDATA          #IMPLIED\n  value       CDATA          #IMPLIED\n  valuetype   (data|ref|object) \"data\"\n  type        %ContentType;  #IMPLIED\n  >\n\n<!--=================== Images ===========================================-->\n\n<!--\n   To avoid accessibility problems for people who aren't\n   able to see the image, you should provide a text\n   description using the alt and longdesc attributes.\n   In addition, avoid the use of server-side image maps.\n   Note that in this DTD there is no name attribute. That\n   is only available in the transitional and frameset DTD.\n-->\n\n<!ELEMENT img EMPTY>\n<!ATTLIST img\n  %attrs;\n  src         %URI;          #REQUIRED\n  alt         %Text;         #REQUIRED\n  longdesc    %URI;          #IMPLIED\n  height      %Length;       #IMPLIED\n  width       %Length;       #IMPLIED\n  usemap      %URI;          #IMPLIED\n  ismap       (ismap)        #IMPLIED\n  >\n\n<!-- usemap points to a map element which may be in this document\n  or an external document, although the latter is not widely supported -->\n\n<!--================== Client-side image maps ============================-->\n\n<!-- These can be placed in the same document or grouped in a\n     separate document although this isn't yet widely supported -->\n\n<!ELEMENT map ((%block; | form | %misc;)+ | area+)>\n<!ATTLIST map\n  %i18n;\n  %events;\n  id          ID             #REQUIRED\n  class       CDATA          #IMPLIED\n  style       %StyleSheet;   #IMPLIED\n  title       %Text;         #IMPLIED\n  name        NMTOKEN        #IMPLIED\n  >\n\n<!ELEMENT area EMPTY>\n<!ATTLIST area\n  %attrs;\n  %focus;\n  shape       %Shape;        \"rect\"\n  coords      %Coords;       #IMPLIED\n  href        %URI;          #IMPLIED\n  nohref      (nohref)       #IMPLIED\n  alt         %Text;         #REQUIRED\n  >\n\n<!--================ Forms ===============================================-->\n<!ELEMENT form %form.content;>   <!-- forms shouldn't be nested -->\n\n<!ATTLIST form\n  %attrs;\n  action      %URI;          #REQUIRED\n  method      (get|post)     \"get\"\n  enctype     %ContentType;  \"application/x-www-form-urlencoded\"\n  onsubmit    %Script;       #IMPLIED\n  onreset     %Script;       #IMPLIED\n  accept      %ContentTypes; #IMPLIED\n  accept-charset %Charsets;  #IMPLIED\n  >\n\n<!--\n  Each label must not contain more than ONE field\n  Label elements shouldn't be nested.\n-->\n<!ELEMENT label %Inline;>\n<!ATTLIST label\n  %attrs;\n  for         IDREF          #IMPLIED\n  accesskey   %Character;    #IMPLIED\n  onfocus     %Script;       #IMPLIED\n  onblur      %Script;       #IMPLIED\n  >\n\n<!ENTITY % InputType\n  \"(text | password | checkbox |\n    radio | submit | reset |\n    file | hidden | image | button)\"\n   >\n\n<!-- the name attribute is required for all but submit & reset -->\n\n<!ELEMENT input EMPTY>     <!-- form control -->\n<!ATTLIST input\n  %attrs;\n  %focus;\n  type        %InputType;    \"text\"\n  name        CDATA          #IMPLIED\n  value       CDATA          #IMPLIED\n  checked     (checked)      #IMPLIED\n  disabled    (disabled)     #IMPLIED\n  readonly    (readonly)     #IMPLIED\n  size        CDATA          #IMPLIED\n  maxlength   %Number;       #IMPLIED\n  src         %URI;          #IMPLIED\n  alt         CDATA          #IMPLIED\n  usemap      %URI;          #IMPLIED\n  onselect    %Script;       #IMPLIED\n  onchange    %Script;       #IMPLIED\n  accept      %ContentTypes; #IMPLIED\n  >\n\n<!ELEMENT select (optgroup|option)+>  <!-- option selector -->\n<!ATTLIST select\n  %attrs;\n  name        CDATA          #IMPLIED\n  size        %Number;       #IMPLIED\n  multiple    (multiple)     #IMPLIED\n  disabled    (disabled)     #IMPLIED\n  tabindex    %Number;       #IMPLIED\n  onfocus     %Script;       #IMPLIED\n  onblur      %Script;       #IMPLIED\n  onchange    %Script;       #IMPLIED\n  >\n\n<!ELEMENT optgroup (option)+>   <!-- option group -->\n<!ATTLIST optgroup\n  %attrs;\n  disabled    (disabled)     #IMPLIED\n  label       %Text;         #REQUIRED\n  >\n\n<!ELEMENT option (#PCDATA)>     <!-- selectable choice -->\n<!ATTLIST option\n  %attrs;\n  selected    (selected)     #IMPLIED\n  disabled    (disabled)     #IMPLIED\n  label       %Text;         #IMPLIED\n  value       CDATA          #IMPLIED\n  >\n\n<!ELEMENT textarea (#PCDATA)>     <!-- multi-line text field -->\n<!ATTLIST textarea\n  %attrs;\n  %focus;\n  name        CDATA          #IMPLIED\n  rows        %Number;       #REQUIRED\n  cols        %Number;       #REQUIRED\n  disabled    (disabled)     #IMPLIED\n  readonly    (readonly)     #IMPLIED\n  onselect    %Script;       #IMPLIED\n  onchange    %Script;       #IMPLIED\n  >\n\n<!--\n  The fieldset element is used to group form fields.\n  Only one legend element should occur in the content\n  and if present should only be preceded by whitespace.\n-->\n<!ELEMENT fieldset (#PCDATA | legend | %block; | form | %inline; | %misc;)*>\n<!ATTLIST fieldset\n  %attrs;\n  >\n\n<!ELEMENT legend %Inline;>     <!-- fieldset label -->\n<!ATTLIST legend\n  %attrs;\n  accesskey   %Character;    #IMPLIED\n  >\n\n<!--\n Content is %Flow; excluding a, form and form controls\n--> \n<!ELEMENT button %button.content;>  <!-- push button -->\n<!ATTLIST button\n  %attrs;\n  %focus;\n  name        CDATA          #IMPLIED\n  value       CDATA          #IMPLIED\n  type        (button|submit|reset) \"submit\"\n  disabled    (disabled)     #IMPLIED\n  >\n\n<!--======================= Tables =======================================-->\n\n<!-- Derived from IETF HTML table standard, see [RFC1942] -->\n\n<!--\n The border attribute sets the thickness of the frame around the\n table. The default units are screen pixels.\n\n The frame attribute specifies which parts of the frame around\n the table should be rendered. The values are not the same as\n CALS to avoid a name clash with the valign attribute.\n-->\n<!ENTITY % TFrame \"(void|above|below|hsides|lhs|rhs|vsides|box|border)\">\n\n<!--\n The rules attribute defines which rules to draw between cells:\n\n If rules is absent then assume:\n     \"none\" if border is absent or border=\"0\" otherwise \"all\"\n-->\n\n<!ENTITY % TRules \"(none | groups | rows | cols | all)\">\n  \n<!-- horizontal alignment attributes for cell contents\n\n  char        alignment char, e.g. char=':'\n  charoff     offset for alignment char\n-->\n<!ENTITY % cellhalign\n  \"align      (left|center|right|justify|char) #IMPLIED\n   char       %Character;    #IMPLIED\n   charoff    %Length;       #IMPLIED\"\n  >\n\n<!-- vertical alignment attributes for cell contents -->\n<!ENTITY % cellvalign\n  \"valign     (top|middle|bottom|baseline) #IMPLIED\"\n  >\n\n<!ELEMENT table\n     (caption?, (col*|colgroup*), thead?, tfoot?, (tbody+|tr+))>\n<!ELEMENT caption  %Inline;>\n<!ELEMENT thead    (tr)+>\n<!ELEMENT tfoot    (tr)+>\n<!ELEMENT tbody    (tr)+>\n<!ELEMENT colgroup (col)*>\n<!ELEMENT col      EMPTY>\n<!ELEMENT tr       (th|td)+>\n<!ELEMENT th       %Flow;>\n<!ELEMENT td       %Flow;>\n\n<!ATTLIST table\n  %attrs;\n  summary     %Text;         #IMPLIED\n  width       %Length;       #IMPLIED\n  border      %Pixels;       #IMPLIED\n  frame       %TFrame;       #IMPLIED\n  rules       %TRules;       #IMPLIED\n  cellspacing %Length;       #IMPLIED\n  cellpadding %Length;       #IMPLIED\n  >\n\n<!ATTLIST caption\n  %attrs;\n  >\n\n<!--\ncolgroup groups a set of col elements. It allows you to group\nseveral semantically related columns together.\n-->\n<!ATTLIST colgroup\n  %attrs;\n  span        %Number;       \"1\"\n  width       %MultiLength;  #IMPLIED\n  %cellhalign;\n  %cellvalign;\n  >\n\n<!--\n col elements define the alignment properties for cells in\n one or more columns.\n\n The width attribute specifies the width of the columns, e.g.\n\n     width=64        width in screen pixels\n     width=0.5*      relative width of 0.5\n\n The span attribute causes the attributes of one\n col element to apply to more than one column.\n-->\n<!ATTLIST col\n  %attrs;\n  span        %Number;       \"1\"\n  width       %MultiLength;  #IMPLIED\n  %cellhalign;\n  %cellvalign;\n  >\n\n<!--\n    Use thead to duplicate headers when breaking table\n    across page boundaries, or for static headers when\n    tbody sections are rendered in scrolling panel.\n\n    Use tfoot to duplicate footers when breaking table\n    across page boundaries, or for static footers when\n    tbody sections are rendered in scrolling panel.\n\n    Use multiple tbody sections when rules are needed\n    between groups of table rows.\n-->\n<!ATTLIST thead\n  %attrs;\n  %cellhalign;\n  %cellvalign;\n  >\n\n<!ATTLIST tfoot\n  %attrs;\n  %cellhalign;\n  %cellvalign;\n  >\n\n<!ATTLIST tbody\n  %attrs;\n  %cellhalign;\n  %cellvalign;\n  >\n\n<!ATTLIST tr\n  %attrs;\n  %cellhalign;\n  %cellvalign;\n  >\n\n\n<!-- Scope is simpler than headers attribute for common tables -->\n<!ENTITY % Scope \"(row|col|rowgroup|colgroup)\">\n\n<!-- th is for headers, td for data and for cells acting as both -->\n\n<!ATTLIST th\n  %attrs;\n  abbr        %Text;         #IMPLIED\n  axis        CDATA          #IMPLIED\n  headers     IDREFS         #IMPLIED\n  scope       %Scope;        #IMPLIED\n  rowspan     %Number;       \"1\"\n  colspan     %Number;       \"1\"\n  %cellhalign;\n  %cellvalign;\n  >\n\n<!ATTLIST td\n  %attrs;\n  abbr        %Text;         #IMPLIED\n  axis        CDATA          #IMPLIED\n  headers     IDREFS         #IMPLIED\n  scope       %Scope;        #IMPLIED\n  rowspan     %Number;       \"1\"\n  colspan     %Number;       \"1\"\n  %cellhalign;\n  %cellvalign;\n  >\n\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd",
    "content": "<!--\n   Extensible HTML version 1.0 Transitional DTD\n\n   This is the same as HTML 4 Transitional except for\n   changes due to the differences between XML and SGML.\n\n   Namespace = http://www.w3.org/1999/xhtml\n\n   For further information, see: http://www.w3.org/TR/xhtml1\n\n   Copyright (c) 1998-2002 W3C (MIT, INRIA, Keio),\n   All Rights Reserved. \n\n   This DTD module is identified by the PUBLIC and SYSTEM identifiers:\n\n   PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n   SYSTEM \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"\n\n   $Revision: 1.2 $\n   $Date: 2002/08/01 18:37:55 $\n\n-->\n\n<!--================ Character mnemonic entities =========================-->\n\n<!ENTITY % HTMLlat1 PUBLIC\n   \"-//W3C//ENTITIES Latin 1 for XHTML//EN\"\n   \"xhtml-lat1.ent\">\n%HTMLlat1;\n\n<!ENTITY % HTMLsymbol PUBLIC\n   \"-//W3C//ENTITIES Symbols for XHTML//EN\"\n   \"xhtml-symbol.ent\">\n%HTMLsymbol;\n\n<!ENTITY % HTMLspecial PUBLIC\n   \"-//W3C//ENTITIES Special for XHTML//EN\"\n   \"xhtml-special.ent\">\n%HTMLspecial;\n\n<!--================== Imported Names ====================================-->\n\n<!ENTITY % ContentType \"CDATA\">\n    <!-- media type, as per [RFC2045] -->\n\n<!ENTITY % ContentTypes \"CDATA\">\n    <!-- comma-separated list of media types, as per [RFC2045] -->\n\n<!ENTITY % Charset \"CDATA\">\n    <!-- a character encoding, as per [RFC2045] -->\n\n<!ENTITY % Charsets \"CDATA\">\n    <!-- a space separated list of character encodings, as per [RFC2045] -->\n\n<!ENTITY % LanguageCode \"NMTOKEN\">\n    <!-- a language code, as per [RFC3066] -->\n\n<!ENTITY % Character \"CDATA\">\n    <!-- a single character, as per section 2.2 of [XML] -->\n\n<!ENTITY % Number \"CDATA\">\n    <!-- one or more digits -->\n\n<!ENTITY % LinkTypes \"CDATA\">\n    <!-- space-separated list of link types -->\n\n<!ENTITY % MediaDesc \"CDATA\">\n    <!-- single or comma-separated list of media descriptors -->\n\n<!ENTITY % URI \"CDATA\">\n    <!-- a Uniform Resource Identifier, see [RFC2396] -->\n\n<!ENTITY % UriList \"CDATA\">\n    <!-- a space separated list of Uniform Resource Identifiers -->\n\n<!ENTITY % Datetime \"CDATA\">\n    <!-- date and time information. ISO date format -->\n\n<!ENTITY % Script \"CDATA\">\n    <!-- script expression -->\n\n<!ENTITY % StyleSheet \"CDATA\">\n    <!-- style sheet data -->\n\n<!ENTITY % Text \"CDATA\">\n    <!-- used for titles etc. -->\n\n<!ENTITY % FrameTarget \"NMTOKEN\">\n    <!-- render in this frame -->\n\n<!ENTITY % Length \"CDATA\">\n    <!-- nn for pixels or nn% for percentage length -->\n\n<!ENTITY % MultiLength \"CDATA\">\n    <!-- pixel, percentage, or relative -->\n\n<!ENTITY % Pixels \"CDATA\">\n    <!-- integer representing length in pixels -->\n\n<!-- these are used for image maps -->\n\n<!ENTITY % Shape \"(rect|circle|poly|default)\">\n\n<!ENTITY % Coords \"CDATA\">\n    <!-- comma separated list of lengths -->\n\n<!-- used for object, applet, img, input and iframe -->\n<!ENTITY % ImgAlign \"(top|middle|bottom|left|right)\">\n\n<!-- a color using sRGB: #RRGGBB as Hex values -->\n<!ENTITY % Color \"CDATA\">\n\n<!-- There are also 16 widely known color names with their sRGB values:\n\n    Black  = #000000    Green  = #008000\n    Silver = #C0C0C0    Lime   = #00FF00\n    Gray   = #808080    Olive  = #808000\n    White  = #FFFFFF    Yellow = #FFFF00\n    Maroon = #800000    Navy   = #000080\n    Red    = #FF0000    Blue   = #0000FF\n    Purple = #800080    Teal   = #008080\n    Fuchsia= #FF00FF    Aqua   = #00FFFF\n-->\n\n<!--=================== Generic Attributes ===============================-->\n\n<!-- core attributes common to most elements\n  id       document-wide unique id\n  class    space separated list of classes\n  style    associated style info\n  title    advisory title/amplification\n-->\n<!ENTITY % coreattrs\n \"id          ID             #IMPLIED\n  class       CDATA          #IMPLIED\n  style       %StyleSheet;   #IMPLIED\n  title       %Text;         #IMPLIED\"\n  >\n\n<!-- internationalization attributes\n  lang        language code (backwards compatible)\n  xml:lang    language code (as per XML 1.0 spec)\n  dir         direction for weak/neutral text\n-->\n<!ENTITY % i18n\n \"lang        %LanguageCode; #IMPLIED\n  xml:lang    %LanguageCode; #IMPLIED\n  dir         (ltr|rtl)      #IMPLIED\"\n  >\n\n<!-- attributes for common UI events\n  onclick     a pointer button was clicked\n  ondblclick  a pointer button was double clicked\n  onmousedown a pointer button was pressed down\n  onmouseup   a pointer button was released\n  onmousemove a pointer was moved onto the element\n  onmouseout  a pointer was moved away from the element\n  onkeypress  a key was pressed and released\n  onkeydown   a key was pressed down\n  onkeyup     a key was released\n-->\n<!ENTITY % events\n \"onclick     %Script;       #IMPLIED\n  ondblclick  %Script;       #IMPLIED\n  onmousedown %Script;       #IMPLIED\n  onmouseup   %Script;       #IMPLIED\n  onmouseover %Script;       #IMPLIED\n  onmousemove %Script;       #IMPLIED\n  onmouseout  %Script;       #IMPLIED\n  onkeypress  %Script;       #IMPLIED\n  onkeydown   %Script;       #IMPLIED\n  onkeyup     %Script;       #IMPLIED\"\n  >\n\n<!-- attributes for elements that can get the focus\n  accesskey   accessibility key character\n  tabindex    position in tabbing order\n  onfocus     the element got the focus\n  onblur      the element lost the focus\n-->\n<!ENTITY % focus\n \"accesskey   %Character;    #IMPLIED\n  tabindex    %Number;       #IMPLIED\n  onfocus     %Script;       #IMPLIED\n  onblur      %Script;       #IMPLIED\"\n  >\n\n<!ENTITY % attrs \"%coreattrs; %i18n; %events;\">\n\n<!-- text alignment for p, div, h1-h6. The default is\n     align=\"left\" for ltr headings, \"right\" for rtl -->\n\n<!ENTITY % TextAlign \"align (left|center|right|justify) #IMPLIED\">\n\n<!--=================== Text Elements ====================================-->\n\n<!ENTITY % special.extra\n   \"object | applet | img | map | iframe\">\n\t\n<!ENTITY % special.basic\n\t\"br | span | bdo\">\n\n<!ENTITY % special\n   \"%special.basic; | %special.extra;\">\n\n<!ENTITY % fontstyle.extra \"big | small | font | basefont\">\n\n<!ENTITY % fontstyle.basic \"tt | i | b | u\n                      | s | strike \">\n\n<!ENTITY % fontstyle \"%fontstyle.basic; | %fontstyle.extra;\">\n\n<!ENTITY % phrase.extra \"sub | sup\">\n<!ENTITY % phrase.basic \"em | strong | dfn | code | q |\n                   samp | kbd | var | cite | abbr | acronym\">\n\n<!ENTITY % phrase \"%phrase.basic; | %phrase.extra;\">\n\n<!ENTITY % inline.forms \"input | select | textarea | label | button\">\n\n<!-- these can occur at block or inline level -->\n<!ENTITY % misc.inline \"ins | del | script\">\n\n<!-- these can only occur at block level -->\n<!ENTITY % misc \"noscript | %misc.inline;\">\n\n<!ENTITY % inline \"a | %special; | %fontstyle; | %phrase; | %inline.forms;\">\n\n<!-- %Inline; covers inline or \"text-level\" elements -->\n<!ENTITY % Inline \"(#PCDATA | %inline; | %misc.inline;)*\">\n\n<!--================== Block level elements ==============================-->\n\n<!ENTITY % heading \"h1|h2|h3|h4|h5|h6\">\n<!ENTITY % lists \"ul | ol | dl | menu | dir\">\n<!ENTITY % blocktext \"pre | hr | blockquote | address | center | noframes\">\n\n<!ENTITY % block\n    \"p | %heading; | div | %lists; | %blocktext; | isindex |fieldset | table\">\n\n<!-- %Flow; mixes block and inline and is used for list items etc. -->\n<!ENTITY % Flow \"(#PCDATA | %block; | form | %inline; | %misc;)*\">\n\n<!--================== Content models for exclusions =====================-->\n\n<!-- a elements use %Inline; excluding a -->\n\n<!ENTITY % a.content\n   \"(#PCDATA | %special; | %fontstyle; | %phrase; | %inline.forms; | %misc.inline;)*\">\n\n<!-- pre uses %Inline excluding img, object, applet, big, small,\n     font, or basefont -->\n\n<!ENTITY % pre.content\n   \"(#PCDATA | a | %special.basic; | %fontstyle.basic; | %phrase.basic; |\n\t   %inline.forms; | %misc.inline;)*\">\n\n<!-- form uses %Flow; excluding form -->\n\n<!ENTITY % form.content \"(#PCDATA | %block; | %inline; | %misc;)*\">\n\n<!-- button uses %Flow; but excludes a, form, form controls, iframe -->\n\n<!ENTITY % button.content\n   \"(#PCDATA | p | %heading; | div | %lists; | %blocktext; |\n      table | br | span | bdo | object | applet | img | map |\n      %fontstyle; | %phrase; | %misc;)*\">\n\n<!--================ Document Structure ==================================-->\n\n<!-- the namespace URI designates the document profile -->\n\n<!ELEMENT html (head, body)>\n<!ATTLIST html\n  %i18n;\n  id          ID             #IMPLIED\n  xmlns       %URI;          #FIXED 'http://www.w3.org/1999/xhtml'\n  >\n\n<!--================ Document Head =======================================-->\n\n<!ENTITY % head.misc \"(script|style|meta|link|object|isindex)*\">\n\n<!-- content model is %head.misc; combined with a single\n     title and an optional base element in any order -->\n\n<!ELEMENT head (%head.misc;,\n     ((title, %head.misc;, (base, %head.misc;)?) |\n      (base, %head.misc;, (title, %head.misc;))))>\n\n<!ATTLIST head\n  %i18n;\n  id          ID             #IMPLIED\n  profile     %URI;          #IMPLIED\n  >\n\n<!-- The title element is not considered part of the flow of text.\n       It should be displayed, for example as the page header or\n       window title. Exactly one title is required per document.\n    -->\n<!ELEMENT title (#PCDATA)>\n<!ATTLIST title \n  %i18n;\n  id          ID             #IMPLIED\n  >\n\n<!-- document base URI -->\n\n<!ELEMENT base EMPTY>\n<!ATTLIST base\n  id          ID             #IMPLIED\n  href        %URI;          #IMPLIED\n  target      %FrameTarget;  #IMPLIED\n  >\n\n<!-- generic metainformation -->\n<!ELEMENT meta EMPTY>\n<!ATTLIST meta\n  %i18n;\n  id          ID             #IMPLIED\n  http-equiv  CDATA          #IMPLIED\n  name        CDATA          #IMPLIED\n  content     CDATA          #REQUIRED\n  scheme      CDATA          #IMPLIED\n  >\n\n<!--\n  Relationship values can be used in principle:\n\n   a) for document specific toolbars/menus when used\n      with the link element in document head e.g.\n        start, contents, previous, next, index, end, help\n   b) to link to a separate style sheet (rel=\"stylesheet\")\n   c) to make a link to a script (rel=\"script\")\n   d) by stylesheets to control how collections of\n      html nodes are rendered into printed documents\n   e) to make a link to a printable version of this document\n      e.g. a PostScript or PDF version (rel=\"alternate\" media=\"print\")\n-->\n\n<!ELEMENT link EMPTY>\n<!ATTLIST link\n  %attrs;\n  charset     %Charset;      #IMPLIED\n  href        %URI;          #IMPLIED\n  hreflang    %LanguageCode; #IMPLIED\n  type        %ContentType;  #IMPLIED\n  rel         %LinkTypes;    #IMPLIED\n  rev         %LinkTypes;    #IMPLIED\n  media       %MediaDesc;    #IMPLIED\n  target      %FrameTarget;  #IMPLIED\n  >\n\n<!-- style info, which may include CDATA sections -->\n<!ELEMENT style (#PCDATA)>\n<!ATTLIST style\n  %i18n;\n  id          ID             #IMPLIED\n  type        %ContentType;  #REQUIRED\n  media       %MediaDesc;    #IMPLIED\n  title       %Text;         #IMPLIED\n  xml:space   (preserve)     #FIXED 'preserve'\n  >\n\n<!-- script statements, which may include CDATA sections -->\n<!ELEMENT script (#PCDATA)>\n<!ATTLIST script\n  id          ID             #IMPLIED\n  charset     %Charset;      #IMPLIED\n  type        %ContentType;  #REQUIRED\n  language    CDATA          #IMPLIED\n  src         %URI;          #IMPLIED\n  defer       (defer)        #IMPLIED\n  xml:space   (preserve)     #FIXED 'preserve'\n  >\n\n<!-- alternate content container for non script-based rendering -->\n\n<!ELEMENT noscript %Flow;>\n<!ATTLIST noscript\n  %attrs;\n  >\n\n<!--======================= Frames =======================================-->\n\n<!-- inline subwindow -->\n\n<!ELEMENT iframe %Flow;>\n<!ATTLIST iframe\n  %coreattrs;\n  longdesc    %URI;          #IMPLIED\n  name        NMTOKEN        #IMPLIED\n  src         %URI;          #IMPLIED\n  frameborder (1|0)          \"1\"\n  marginwidth %Pixels;       #IMPLIED\n  marginheight %Pixels;      #IMPLIED\n  scrolling   (yes|no|auto)  \"auto\"\n  align       %ImgAlign;     #IMPLIED\n  height      %Length;       #IMPLIED\n  width       %Length;       #IMPLIED\n  >\n\n<!-- alternate content container for non frame-based rendering -->\n\n<!ELEMENT noframes %Flow;>\n<!ATTLIST noframes\n  %attrs;\n  >\n\n<!--=================== Document Body ====================================-->\n\n<!ELEMENT body %Flow;>\n<!ATTLIST body\n  %attrs;\n  onload      %Script;       #IMPLIED\n  onunload    %Script;       #IMPLIED\n  background  %URI;          #IMPLIED\n  bgcolor     %Color;        #IMPLIED\n  text        %Color;        #IMPLIED\n  link        %Color;        #IMPLIED\n  vlink       %Color;        #IMPLIED\n  alink       %Color;        #IMPLIED\n  >\n\n<!ELEMENT div %Flow;>  <!-- generic language/style container -->\n<!ATTLIST div\n  %attrs;\n  %TextAlign;\n  >\n\n<!--=================== Paragraphs =======================================-->\n\n<!ELEMENT p %Inline;>\n<!ATTLIST p\n  %attrs;\n  %TextAlign;\n  >\n\n<!--=================== Headings =========================================-->\n\n<!--\n  There are six levels of headings from h1 (the most important)\n  to h6 (the least important).\n-->\n\n<!ELEMENT h1  %Inline;>\n<!ATTLIST h1\n  %attrs;\n  %TextAlign;\n  >\n\n<!ELEMENT h2 %Inline;>\n<!ATTLIST h2\n  %attrs;\n  %TextAlign;\n  >\n\n<!ELEMENT h3 %Inline;>\n<!ATTLIST h3\n  %attrs;\n  %TextAlign;\n  >\n\n<!ELEMENT h4 %Inline;>\n<!ATTLIST h4\n  %attrs;\n  %TextAlign;\n  >\n\n<!ELEMENT h5 %Inline;>\n<!ATTLIST h5\n  %attrs;\n  %TextAlign;\n  >\n\n<!ELEMENT h6 %Inline;>\n<!ATTLIST h6\n  %attrs;\n  %TextAlign;\n  >\n\n<!--=================== Lists ============================================-->\n\n<!-- Unordered list bullet styles -->\n\n<!ENTITY % ULStyle \"(disc|square|circle)\">\n\n<!-- Unordered list -->\n\n<!ELEMENT ul (li)+>\n<!ATTLIST ul\n  %attrs;\n  type        %ULStyle;     #IMPLIED\n  compact     (compact)     #IMPLIED\n  >\n\n<!-- Ordered list numbering style\n\n    1   arabic numbers      1, 2, 3, ...\n    a   lower alpha         a, b, c, ...\n    A   upper alpha         A, B, C, ...\n    i   lower roman         i, ii, iii, ...\n    I   upper roman         I, II, III, ...\n\n    The style is applied to the sequence number which by default\n    is reset to 1 for the first list item in an ordered list.\n-->\n<!ENTITY % OLStyle \"CDATA\">\n\n<!-- Ordered (numbered) list -->\n\n<!ELEMENT ol (li)+>\n<!ATTLIST ol\n  %attrs;\n  type        %OLStyle;      #IMPLIED\n  compact     (compact)      #IMPLIED\n  start       %Number;       #IMPLIED\n  >\n\n<!-- single column list (DEPRECATED) --> \n<!ELEMENT menu (li)+>\n<!ATTLIST menu\n  %attrs;\n  compact     (compact)     #IMPLIED\n  >\n\n<!-- multiple column list (DEPRECATED) --> \n<!ELEMENT dir (li)+>\n<!ATTLIST dir\n  %attrs;\n  compact     (compact)     #IMPLIED\n  >\n\n<!-- LIStyle is constrained to: \"(%ULStyle;|%OLStyle;)\" -->\n<!ENTITY % LIStyle \"CDATA\">\n\n<!-- list item -->\n\n<!ELEMENT li %Flow;>\n<!ATTLIST li\n  %attrs;\n  type        %LIStyle;      #IMPLIED\n  value       %Number;       #IMPLIED\n  >\n\n<!-- definition lists - dt for term, dd for its definition -->\n\n<!ELEMENT dl (dt|dd)+>\n<!ATTLIST dl\n  %attrs;\n  compact     (compact)      #IMPLIED\n  >\n\n<!ELEMENT dt %Inline;>\n<!ATTLIST dt\n  %attrs;\n  >\n\n<!ELEMENT dd %Flow;>\n<!ATTLIST dd\n  %attrs;\n  >\n\n<!--=================== Address ==========================================-->\n\n<!-- information on author -->\n\n<!ELEMENT address (#PCDATA | %inline; | %misc.inline; | p)*>\n<!ATTLIST address\n  %attrs;\n  >\n\n<!--=================== Horizontal Rule ==================================-->\n\n<!ELEMENT hr EMPTY>\n<!ATTLIST hr\n  %attrs;\n  align       (left|center|right) #IMPLIED\n  noshade     (noshade)      #IMPLIED\n  size        %Pixels;       #IMPLIED\n  width       %Length;       #IMPLIED\n  >\n\n<!--=================== Preformatted Text ================================-->\n\n<!-- content is %Inline; excluding \n        \"img|object|applet|big|small|sub|sup|font|basefont\" -->\n\n<!ELEMENT pre %pre.content;>\n<!ATTLIST pre\n  %attrs;\n  width       %Number;      #IMPLIED\n  xml:space   (preserve)    #FIXED 'preserve'\n  >\n\n<!--=================== Block-like Quotes ================================-->\n\n<!ELEMENT blockquote %Flow;>\n<!ATTLIST blockquote\n  %attrs;\n  cite        %URI;          #IMPLIED\n  >\n\n<!--=================== Text alignment ===================================-->\n\n<!-- center content -->\n<!ELEMENT center %Flow;>\n<!ATTLIST center\n  %attrs;\n  >\n\n<!--=================== Inserted/Deleted Text ============================-->\n\n<!--\n  ins/del are allowed in block and inline content, but its\n  inappropriate to include block content within an ins element\n  occurring in inline content.\n-->\n<!ELEMENT ins %Flow;>\n<!ATTLIST ins\n  %attrs;\n  cite        %URI;          #IMPLIED\n  datetime    %Datetime;     #IMPLIED\n  >\n\n<!ELEMENT del %Flow;>\n<!ATTLIST del\n  %attrs;\n  cite        %URI;          #IMPLIED\n  datetime    %Datetime;     #IMPLIED\n  >\n\n<!--================== The Anchor Element ================================-->\n\n<!-- content is %Inline; except that anchors shouldn't be nested -->\n\n<!ELEMENT a %a.content;>\n<!ATTLIST a\n  %attrs;\n  %focus;\n  charset     %Charset;      #IMPLIED\n  type        %ContentType;  #IMPLIED\n  name        NMTOKEN        #IMPLIED\n  href        %URI;          #IMPLIED\n  hreflang    %LanguageCode; #IMPLIED\n  rel         %LinkTypes;    #IMPLIED\n  rev         %LinkTypes;    #IMPLIED\n  shape       %Shape;        \"rect\"\n  coords      %Coords;       #IMPLIED\n  target      %FrameTarget;  #IMPLIED\n  >\n\n<!--===================== Inline Elements ================================-->\n\n<!ELEMENT span %Inline;> <!-- generic language/style container -->\n<!ATTLIST span\n  %attrs;\n  >\n\n<!ELEMENT bdo %Inline;>  <!-- I18N BiDi over-ride -->\n<!ATTLIST bdo\n  %coreattrs;\n  %events;\n  lang        %LanguageCode; #IMPLIED\n  xml:lang    %LanguageCode; #IMPLIED\n  dir         (ltr|rtl)      #REQUIRED\n  >\n\n<!ELEMENT br EMPTY>   <!-- forced line break -->\n<!ATTLIST br\n  %coreattrs;\n  clear       (left|all|right|none) \"none\"\n  >\n\n<!ELEMENT em %Inline;>   <!-- emphasis -->\n<!ATTLIST em %attrs;>\n\n<!ELEMENT strong %Inline;>   <!-- strong emphasis -->\n<!ATTLIST strong %attrs;>\n\n<!ELEMENT dfn %Inline;>   <!-- definitional -->\n<!ATTLIST dfn %attrs;>\n\n<!ELEMENT code %Inline;>   <!-- program code -->\n<!ATTLIST code %attrs;>\n\n<!ELEMENT samp %Inline;>   <!-- sample -->\n<!ATTLIST samp %attrs;>\n\n<!ELEMENT kbd %Inline;>  <!-- something user would type -->\n<!ATTLIST kbd %attrs;>\n\n<!ELEMENT var %Inline;>   <!-- variable -->\n<!ATTLIST var %attrs;>\n\n<!ELEMENT cite %Inline;>   <!-- citation -->\n<!ATTLIST cite %attrs;>\n\n<!ELEMENT abbr %Inline;>   <!-- abbreviation -->\n<!ATTLIST abbr %attrs;>\n\n<!ELEMENT acronym %Inline;>   <!-- acronym -->\n<!ATTLIST acronym %attrs;>\n\n<!ELEMENT q %Inline;>   <!-- inlined quote -->\n<!ATTLIST q\n  %attrs;\n  cite        %URI;          #IMPLIED\n  >\n\n<!ELEMENT sub %Inline;> <!-- subscript -->\n<!ATTLIST sub %attrs;>\n\n<!ELEMENT sup %Inline;> <!-- superscript -->\n<!ATTLIST sup %attrs;>\n\n<!ELEMENT tt %Inline;>   <!-- fixed pitch font -->\n<!ATTLIST tt %attrs;>\n\n<!ELEMENT i %Inline;>   <!-- italic font -->\n<!ATTLIST i %attrs;>\n\n<!ELEMENT b %Inline;>   <!-- bold font -->\n<!ATTLIST b %attrs;>\n\n<!ELEMENT big %Inline;>   <!-- bigger font -->\n<!ATTLIST big %attrs;>\n\n<!ELEMENT small %Inline;>   <!-- smaller font -->\n<!ATTLIST small %attrs;>\n\n<!ELEMENT u %Inline;>   <!-- underline -->\n<!ATTLIST u %attrs;>\n\n<!ELEMENT s %Inline;>   <!-- strike-through -->\n<!ATTLIST s %attrs;>\n\n<!ELEMENT strike %Inline;>   <!-- strike-through -->\n<!ATTLIST strike %attrs;>\n\n<!ELEMENT basefont EMPTY>  <!-- base font size -->\n<!ATTLIST basefont\n  id          ID             #IMPLIED\n  size        CDATA          #REQUIRED\n  color       %Color;        #IMPLIED\n  face        CDATA          #IMPLIED\n  >\n\n<!ELEMENT font %Inline;> <!-- local change to font -->\n<!ATTLIST font\n  %coreattrs;\n  %i18n;\n  size        CDATA          #IMPLIED\n  color       %Color;        #IMPLIED\n  face        CDATA          #IMPLIED\n  >\n\n<!--==================== Object ======================================-->\n<!--\n  object is used to embed objects as part of HTML pages.\n  param elements should precede other content. Parameters\n  can also be expressed as attribute/value pairs on the\n  object element itself when brevity is desired.\n-->\n\n<!ELEMENT object (#PCDATA | param | %block; | form | %inline; | %misc;)*>\n<!ATTLIST object\n  %attrs;\n  declare     (declare)      #IMPLIED\n  classid     %URI;          #IMPLIED\n  codebase    %URI;          #IMPLIED\n  data        %URI;          #IMPLIED\n  type        %ContentType;  #IMPLIED\n  codetype    %ContentType;  #IMPLIED\n  archive     %UriList;      #IMPLIED\n  standby     %Text;         #IMPLIED\n  height      %Length;       #IMPLIED\n  width       %Length;       #IMPLIED\n  usemap      %URI;          #IMPLIED\n  name        NMTOKEN        #IMPLIED\n  tabindex    %Number;       #IMPLIED\n  align       %ImgAlign;     #IMPLIED\n  border      %Pixels;       #IMPLIED\n  hspace      %Pixels;       #IMPLIED\n  vspace      %Pixels;       #IMPLIED\n  >\n\n<!--\n  param is used to supply a named property value.\n  In XML it would seem natural to follow RDF and support an\n  abbreviated syntax where the param elements are replaced\n  by attribute value pairs on the object start tag.\n-->\n<!ELEMENT param EMPTY>\n<!ATTLIST param\n  id          ID             #IMPLIED\n  name        CDATA          #REQUIRED\n  value       CDATA          #IMPLIED\n  valuetype   (data|ref|object) \"data\"\n  type        %ContentType;  #IMPLIED\n  >\n\n<!--=================== Java applet ==================================-->\n<!--\n  One of code or object attributes must be present.\n  Place param elements before other content.\n-->\n<!ELEMENT applet (#PCDATA | param | %block; | form | %inline; | %misc;)*>\n<!ATTLIST applet\n  %coreattrs;\n  codebase    %URI;          #IMPLIED\n  archive     CDATA          #IMPLIED\n  code        CDATA          #IMPLIED\n  object      CDATA          #IMPLIED\n  alt         %Text;         #IMPLIED\n  name        NMTOKEN        #IMPLIED\n  width       %Length;       #REQUIRED\n  height      %Length;       #REQUIRED\n  align       %ImgAlign;     #IMPLIED\n  hspace      %Pixels;       #IMPLIED\n  vspace      %Pixels;       #IMPLIED\n  >\n\n<!--=================== Images ===========================================-->\n\n<!--\n   To avoid accessibility problems for people who aren't\n   able to see the image, you should provide a text\n   description using the alt and longdesc attributes.\n   In addition, avoid the use of server-side image maps.\n-->\n\n<!ELEMENT img EMPTY>\n<!ATTLIST img\n  %attrs;\n  src         %URI;          #REQUIRED\n  alt         %Text;         #REQUIRED\n  name        NMTOKEN        #IMPLIED\n  longdesc    %URI;          #IMPLIED\n  height      %Length;       #IMPLIED\n  width       %Length;       #IMPLIED\n  usemap      %URI;          #IMPLIED\n  ismap       (ismap)        #IMPLIED\n  align       %ImgAlign;     #IMPLIED\n  border      %Length;       #IMPLIED\n  hspace      %Pixels;       #IMPLIED\n  vspace      %Pixels;       #IMPLIED\n  >\n\n<!-- usemap points to a map element which may be in this document\n  or an external document, although the latter is not widely supported -->\n\n<!--================== Client-side image maps ============================-->\n\n<!-- These can be placed in the same document or grouped in a\n     separate document although this isn't yet widely supported -->\n\n<!ELEMENT map ((%block; | form | %misc;)+ | area+)>\n<!ATTLIST map\n  %i18n;\n  %events;\n  id          ID             #REQUIRED\n  class       CDATA          #IMPLIED\n  style       %StyleSheet;   #IMPLIED\n  title       %Text;         #IMPLIED\n  name        CDATA          #IMPLIED\n  >\n\n<!ELEMENT area EMPTY>\n<!ATTLIST area\n  %attrs;\n  %focus;\n  shape       %Shape;        \"rect\"\n  coords      %Coords;       #IMPLIED\n  href        %URI;          #IMPLIED\n  nohref      (nohref)       #IMPLIED\n  alt         %Text;         #REQUIRED\n  target      %FrameTarget;  #IMPLIED\n  >\n\n<!--================ Forms ===============================================-->\n\n<!ELEMENT form %form.content;>   <!-- forms shouldn't be nested -->\n\n<!ATTLIST form\n  %attrs;\n  action      %URI;          #REQUIRED\n  method      (get|post)     \"get\"\n  name        NMTOKEN        #IMPLIED\n  enctype     %ContentType;  \"application/x-www-form-urlencoded\"\n  onsubmit    %Script;       #IMPLIED\n  onreset     %Script;       #IMPLIED\n  accept      %ContentTypes; #IMPLIED\n  accept-charset %Charsets;  #IMPLIED\n  target      %FrameTarget;  #IMPLIED\n  >\n\n<!--\n  Each label must not contain more than ONE field\n  Label elements shouldn't be nested.\n-->\n<!ELEMENT label %Inline;>\n<!ATTLIST label\n  %attrs;\n  for         IDREF          #IMPLIED\n  accesskey   %Character;    #IMPLIED\n  onfocus     %Script;       #IMPLIED\n  onblur      %Script;       #IMPLIED\n  >\n\n<!ENTITY % InputType\n  \"(text | password | checkbox |\n    radio | submit | reset |\n    file | hidden | image | button)\"\n   >\n\n<!-- the name attribute is required for all but submit & reset -->\n\n<!ELEMENT input EMPTY>     <!-- form control -->\n<!ATTLIST input\n  %attrs;\n  %focus;\n  type        %InputType;    \"text\"\n  name        CDATA          #IMPLIED\n  value       CDATA          #IMPLIED\n  checked     (checked)      #IMPLIED\n  disabled    (disabled)     #IMPLIED\n  readonly    (readonly)     #IMPLIED\n  size        CDATA          #IMPLIED\n  maxlength   %Number;       #IMPLIED\n  src         %URI;          #IMPLIED\n  alt         CDATA          #IMPLIED\n  usemap      %URI;          #IMPLIED\n  onselect    %Script;       #IMPLIED\n  onchange    %Script;       #IMPLIED\n  accept      %ContentTypes; #IMPLIED\n  align       %ImgAlign;     #IMPLIED\n  >\n\n<!ELEMENT select (optgroup|option)+>  <!-- option selector -->\n<!ATTLIST select\n  %attrs;\n  name        CDATA          #IMPLIED\n  size        %Number;       #IMPLIED\n  multiple    (multiple)     #IMPLIED\n  disabled    (disabled)     #IMPLIED\n  tabindex    %Number;       #IMPLIED\n  onfocus     %Script;       #IMPLIED\n  onblur      %Script;       #IMPLIED\n  onchange    %Script;       #IMPLIED\n  >\n\n<!ELEMENT optgroup (option)+>   <!-- option group -->\n<!ATTLIST optgroup\n  %attrs;\n  disabled    (disabled)     #IMPLIED\n  label       %Text;         #REQUIRED\n  >\n\n<!ELEMENT option (#PCDATA)>     <!-- selectable choice -->\n<!ATTLIST option\n  %attrs;\n  selected    (selected)     #IMPLIED\n  disabled    (disabled)     #IMPLIED\n  label       %Text;         #IMPLIED\n  value       CDATA          #IMPLIED\n  >\n\n<!ELEMENT textarea (#PCDATA)>     <!-- multi-line text field -->\n<!ATTLIST textarea\n  %attrs;\n  %focus;\n  name        CDATA          #IMPLIED\n  rows        %Number;       #REQUIRED\n  cols        %Number;       #REQUIRED\n  disabled    (disabled)     #IMPLIED\n  readonly    (readonly)     #IMPLIED\n  onselect    %Script;       #IMPLIED\n  onchange    %Script;       #IMPLIED\n  >\n\n<!--\n  The fieldset element is used to group form fields.\n  Only one legend element should occur in the content\n  and if present should only be preceded by whitespace.\n-->\n<!ELEMENT fieldset (#PCDATA | legend | %block; | form | %inline; | %misc;)*>\n<!ATTLIST fieldset\n  %attrs;\n  >\n\n<!ENTITY % LAlign \"(top|bottom|left|right)\">\n\n<!ELEMENT legend %Inline;>     <!-- fieldset label -->\n<!ATTLIST legend\n  %attrs;\n  accesskey   %Character;    #IMPLIED\n  align       %LAlign;       #IMPLIED\n  >\n\n<!--\n Content is %Flow; excluding a, form, form controls, iframe\n--> \n<!ELEMENT button %button.content;>  <!-- push button -->\n<!ATTLIST button\n  %attrs;\n  %focus;\n  name        CDATA          #IMPLIED\n  value       CDATA          #IMPLIED\n  type        (button|submit|reset) \"submit\"\n  disabled    (disabled)     #IMPLIED\n  >\n\n<!-- single-line text input control (DEPRECATED) -->\n<!ELEMENT isindex EMPTY>\n<!ATTLIST isindex\n  %coreattrs;\n  %i18n;\n  prompt      %Text;         #IMPLIED\n  >\n\n<!--======================= Tables =======================================-->\n\n<!-- Derived from IETF HTML table standard, see [RFC1942] -->\n\n<!--\n The border attribute sets the thickness of the frame around the\n table. The default units are screen pixels.\n\n The frame attribute specifies which parts of the frame around\n the table should be rendered. The values are not the same as\n CALS to avoid a name clash with the valign attribute.\n-->\n<!ENTITY % TFrame \"(void|above|below|hsides|lhs|rhs|vsides|box|border)\">\n\n<!--\n The rules attribute defines which rules to draw between cells:\n\n If rules is absent then assume:\n     \"none\" if border is absent or border=\"0\" otherwise \"all\"\n-->\n\n<!ENTITY % TRules \"(none | groups | rows | cols | all)\">\n  \n<!-- horizontal placement of table relative to document -->\n<!ENTITY % TAlign \"(left|center|right)\">\n\n<!-- horizontal alignment attributes for cell contents\n\n  char        alignment char, e.g. char=':'\n  charoff     offset for alignment char\n-->\n<!ENTITY % cellhalign\n  \"align      (left|center|right|justify|char) #IMPLIED\n   char       %Character;    #IMPLIED\n   charoff    %Length;       #IMPLIED\"\n  >\n\n<!-- vertical alignment attributes for cell contents -->\n<!ENTITY % cellvalign\n  \"valign     (top|middle|bottom|baseline) #IMPLIED\"\n  >\n\n<!ELEMENT table\n     (caption?, (col*|colgroup*), thead?, tfoot?, (tbody+|tr+))>\n<!ELEMENT caption  %Inline;>\n<!ELEMENT thead    (tr)+>\n<!ELEMENT tfoot    (tr)+>\n<!ELEMENT tbody    (tr)+>\n<!ELEMENT colgroup (col)*>\n<!ELEMENT col      EMPTY>\n<!ELEMENT tr       (th|td)+>\n<!ELEMENT th       %Flow;>\n<!ELEMENT td       %Flow;>\n\n<!ATTLIST table\n  %attrs;\n  summary     %Text;         #IMPLIED\n  width       %Length;       #IMPLIED\n  border      %Pixels;       #IMPLIED\n  frame       %TFrame;       #IMPLIED\n  rules       %TRules;       #IMPLIED\n  cellspacing %Length;       #IMPLIED\n  cellpadding %Length;       #IMPLIED\n  align       %TAlign;       #IMPLIED\n  bgcolor     %Color;        #IMPLIED\n  >\n\n<!ENTITY % CAlign \"(top|bottom|left|right)\">\n\n<!ATTLIST caption\n  %attrs;\n  align       %CAlign;       #IMPLIED\n  >\n\n<!--\ncolgroup groups a set of col elements. It allows you to group\nseveral semantically related columns together.\n-->\n<!ATTLIST colgroup\n  %attrs;\n  span        %Number;       \"1\"\n  width       %MultiLength;  #IMPLIED\n  %cellhalign;\n  %cellvalign;\n  >\n\n<!--\n col elements define the alignment properties for cells in\n one or more columns.\n\n The width attribute specifies the width of the columns, e.g.\n\n     width=64        width in screen pixels\n     width=0.5*      relative width of 0.5\n\n The span attribute causes the attributes of one\n col element to apply to more than one column.\n-->\n<!ATTLIST col\n  %attrs;\n  span        %Number;       \"1\"\n  width       %MultiLength;  #IMPLIED\n  %cellhalign;\n  %cellvalign;\n  >\n\n<!--\n    Use thead to duplicate headers when breaking table\n    across page boundaries, or for static headers when\n    tbody sections are rendered in scrolling panel.\n\n    Use tfoot to duplicate footers when breaking table\n    across page boundaries, or for static footers when\n    tbody sections are rendered in scrolling panel.\n\n    Use multiple tbody sections when rules are needed\n    between groups of table rows.\n-->\n<!ATTLIST thead\n  %attrs;\n  %cellhalign;\n  %cellvalign;\n  >\n\n<!ATTLIST tfoot\n  %attrs;\n  %cellhalign;\n  %cellvalign;\n  >\n\n<!ATTLIST tbody\n  %attrs;\n  %cellhalign;\n  %cellvalign;\n  >\n\n<!ATTLIST tr\n  %attrs;\n  %cellhalign;\n  %cellvalign;\n  bgcolor     %Color;        #IMPLIED\n  >\n\n<!-- Scope is simpler than headers attribute for common tables -->\n<!ENTITY % Scope \"(row|col|rowgroup|colgroup)\">\n\n<!-- th is for headers, td for data and for cells acting as both -->\n\n<!ATTLIST th\n  %attrs;\n  abbr        %Text;         #IMPLIED\n  axis        CDATA          #IMPLIED\n  headers     IDREFS         #IMPLIED\n  scope       %Scope;        #IMPLIED\n  rowspan     %Number;       \"1\"\n  colspan     %Number;       \"1\"\n  %cellhalign;\n  %cellvalign;\n  nowrap      (nowrap)       #IMPLIED\n  bgcolor     %Color;        #IMPLIED\n  width       %Length;       #IMPLIED\n  height      %Length;       #IMPLIED\n  >\n\n<!ATTLIST td\n  %attrs;\n  abbr        %Text;         #IMPLIED\n  axis        CDATA          #IMPLIED\n  headers     IDREFS         #IMPLIED\n  scope       %Scope;        #IMPLIED\n  rowspan     %Number;       \"1\"\n  colspan     %Number;       \"1\"\n  %cellhalign;\n  %cellvalign;\n  nowrap      (nowrap)       #IMPLIED\n  bgcolor     %Color;        #IMPLIED\n  width       %Length;       #IMPLIED\n  height      %Length;       #IMPLIED\n  >\n\n"
  },
  {
    "path": "src/main/resources/dtd/www.w3.org/TR/xhtml11/DTD/xhtml11.dtd",
    "content": "<!-- ....................................................................... -->\n<!-- XHTML 1.1 DTD  ........................................................ -->\n<!-- file: xhtml11.dtd\n-->\n\n<!-- XHTML 1.1 DTD\n\n     This is XHTML, a reformulation of HTML as a modular XML application.\n\n     The Extensible HyperText Markup Language (XHTML)\n     Copyright 1998-2001 World Wide Web Consortium\n        (Massachusetts Institute of Technology, Institut National de\n         Recherche en Informatique et en Automatique, Keio University).\n         All Rights Reserved.\n\n     Permission to use, copy, modify and distribute the XHTML DTD and its \n     accompanying documentation for any purpose and without fee is hereby \n     granted in perpetuity, provided that the above copyright notice and \n     this paragraph appear in all copies.  The copyright holders make no \n     representation about the suitability of the DTD for any purpose.\n\n     It is provided \"as is\" without expressed or implied warranty.\n\n        Author:     Murray M. Altheim <altheim@eng.sun.com>\n        Revision:   $Id: xhtml11.dtd,v 1.21 2001/05/29 16:37:01 ahby Exp $\n\n-->\n<!-- This is the driver file for version 1.1 of the XHTML DTD.\n\n     Please use this formal public identifier to identify it:\n\n         \"-//W3C//DTD XHTML 1.1//EN\"\n-->\n<!ENTITY % XHTML.version  \"-//W3C//DTD XHTML 1.1//EN\" >\n\n<!-- Use this URI to identify the default namespace:\n\n         \"http://www.w3.org/1999/xhtml\"\n\n     See the Qualified Names module for information\n     on the use of namespace prefixes in the DTD.\n-->\n<!ENTITY % NS.prefixed \"IGNORE\" >\n<!ENTITY % XHTML.prefix \"\" >\n\n<!-- Reserved for use with the XLink namespace:\n-->\n<!ENTITY % XLINK.xmlns \"\" >\n<!ENTITY % XLINK.xmlns.attrib \"\" >\n\n<!-- For example, if you are using XHTML 1.1 directly, use the FPI\n     in the DOCTYPE declaration, with the xmlns attribute on the\n     document element to identify the default namespace:\n\n       <?xml version=\"1.0\"?>\n       <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"xhtml11.dtd\">\n       <html xmlns=\"http://www.w3.org/1999/xhtml\"\n             xml:lang=\"en\">\n       ...\n       </html>\n\n     Revisions:\n     (none)\n-->\n\n<!-- reserved for future use with document profiles -->\n<!ENTITY % XHTML.profile  \"\" >\n\n<!-- Bidirectional Text features\n     This feature-test entity is used to declare elements\n     and attributes used for bidirectional text support.\n-->\n<!ENTITY % XHTML.bidi  \"INCLUDE\" >\n\n<?doc type=\"doctype\" role=\"title\" { XHTML 1.1 } ?>\n\n<!-- ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -->\n\n<!-- Pre-Framework Redeclaration placeholder  .................... -->\n<!-- this serves as a location to insert markup declarations\n     into the DTD prior to the framework declarations.\n-->\n<!ENTITY % xhtml-prefw-redecl.module \"IGNORE\" >\n<![%xhtml-prefw-redecl.module;[\n%xhtml-prefw-redecl.mod;\n<!-- end of xhtml-prefw-redecl.module -->]]>\n\n<!ENTITY % xhtml-events.module \"INCLUDE\" >\n\n<!-- Inline Style Module  ........................................ -->\n<!ENTITY % xhtml-inlstyle.module \"INCLUDE\" >\n<![%xhtml-inlstyle.module;[\n<!ENTITY % xhtml-inlstyle.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Inline Style 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstyle-1.mod\" >\n%xhtml-inlstyle.mod;]]>\n\n<!-- declare Document Model module instantiated in framework\n-->\n<!ENTITY % xhtml-model.mod\n     PUBLIC \"-//W3C//ENTITIES XHTML 1.1 Document Model 1.0//EN\"\n            \"xhtml11-model-1.mod\" >\n\n<!-- Modular Framework Module (required) ......................... -->\n<!ENTITY % xhtml-framework.module \"INCLUDE\" >\n<![%xhtml-framework.module;[\n<!ENTITY % xhtml-framework.mod\n     PUBLIC \"-//W3C//ENTITIES XHTML Modular Framework 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-framework-1.mod\" >\n%xhtml-framework.mod;]]>\n\n<!-- Post-Framework Redeclaration placeholder  ................... -->\n<!-- this serves as a location to insert markup declarations\n     into the DTD following the framework declarations.\n-->\n<!ENTITY % xhtml-postfw-redecl.module \"IGNORE\" >\n<![%xhtml-postfw-redecl.module;[\n%xhtml-postfw-redecl.mod;\n<!-- end of xhtml-postfw-redecl.module -->]]>\n\n<!-- Text Module (Required)  ..................................... -->\n<!ENTITY % xhtml-text.module \"INCLUDE\" >\n<![%xhtml-text.module;[\n<!ENTITY % xhtml-text.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Text 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-text-1.mod\" >\n%xhtml-text.mod;]]>\n\n<!-- Hypertext Module (required) ................................. -->\n<!ENTITY % xhtml-hypertext.module \"INCLUDE\" >\n<![%xhtml-hypertext.module;[\n<!ENTITY % xhtml-hypertext.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Hypertext 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-hypertext-1.mod\" >\n%xhtml-hypertext.mod;]]>\n\n<!-- Lists Module (required)  .................................... -->\n<!ENTITY % xhtml-list.module \"INCLUDE\" >\n<![%xhtml-list.module;[\n<!ENTITY % xhtml-list.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Lists 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-list-1.mod\" >\n%xhtml-list.mod;]]>\n\n<!-- ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: -->\n\n<!-- Edit Module  ................................................ -->\n<!ENTITY % xhtml-edit.module \"INCLUDE\" >\n<![%xhtml-edit.module;[\n<!ENTITY % xhtml-edit.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Editing Elements 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-edit-1.mod\" >\n%xhtml-edit.mod;]]>\n\n<!-- BIDI Override Module  ....................................... -->\n<!ENTITY % xhtml-bdo.module \"%XHTML.bidi;\" >\n<![%xhtml-bdo.module;[\n<!ENTITY % xhtml-bdo.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML BIDI Override Element 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-bdo-1.mod\" >\n%xhtml-bdo.mod;]]>\n\n<!-- Ruby Module  ................................................ -->\n<!ENTITY % Ruby.common.attlists \"INCLUDE\" >\n<!ENTITY % Ruby.common.attrib \"%Common.attrib;\" >\n<!ENTITY % xhtml-ruby.module \"INCLUDE\" >\n<![%xhtml-ruby.module;[\n<!ENTITY % xhtml-ruby.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Ruby 1.0//EN\"\n            \"http://www.w3.org/TR/ruby/xhtml-ruby-1.mod\" >\n%xhtml-ruby.mod;]]>\n\n<!-- Presentation Module  ........................................ -->\n<!ENTITY % xhtml-pres.module \"INCLUDE\" >\n<![%xhtml-pres.module;[\n<!ENTITY % xhtml-pres.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Presentation 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-pres-1.mod\" >\n%xhtml-pres.mod;]]>\n\n<!-- Link Element Module  ........................................ -->\n<!ENTITY % xhtml-link.module \"INCLUDE\" >\n<![%xhtml-link.module;[\n<!ENTITY % xhtml-link.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Link Element 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-link-1.mod\" >\n%xhtml-link.mod;]]>\n\n<!-- Document Metainformation Module  ............................ -->\n<!ENTITY % xhtml-meta.module \"INCLUDE\" >\n<![%xhtml-meta.module;[\n<!ENTITY % xhtml-meta.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Metainformation 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-meta-1.mod\" >\n%xhtml-meta.mod;]]>\n\n<!-- Base Element Module  ........................................ -->\n<!ENTITY % xhtml-base.module \"INCLUDE\" >\n<![%xhtml-base.module;[\n<!ENTITY % xhtml-base.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Base Element 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-base-1.mod\" >\n%xhtml-base.mod;]]>\n\n<!-- Scripting Module  ........................................... -->\n<!ENTITY % xhtml-script.module \"INCLUDE\" >\n<![%xhtml-script.module;[\n<!ENTITY % xhtml-script.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Scripting 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-script-1.mod\" >\n%xhtml-script.mod;]]>\n\n<!-- Style Sheets Module  ......................................... -->\n<!ENTITY % xhtml-style.module \"INCLUDE\" >\n<![%xhtml-style.module;[\n<!ENTITY % xhtml-style.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Style Sheets 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-style-1.mod\" >\n%xhtml-style.mod;]]>\n\n<!-- Image Module  ............................................... -->\n<!ENTITY % xhtml-image.module \"INCLUDE\" >\n<![%xhtml-image.module;[\n<!ENTITY % xhtml-image.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Images 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-image-1.mod\" >\n%xhtml-image.mod;]]>\n\n<!-- Client-side Image Map Module  ............................... -->\n<!ENTITY % xhtml-csismap.module \"INCLUDE\" >\n<![%xhtml-csismap.module;[\n<!ENTITY % xhtml-csismap.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Client-side Image Maps 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-csismap-1.mod\" >\n%xhtml-csismap.mod;]]>\n\n<!-- Server-side Image Map Module  ............................... -->\n<!ENTITY % xhtml-ssismap.module \"INCLUDE\" >\n<![%xhtml-ssismap.module;[\n<!ENTITY % xhtml-ssismap.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Server-side Image Maps 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-ssismap-1.mod\" >\n%xhtml-ssismap.mod;]]>\n\n<!-- Param Element Module  ....................................... -->\n<!ENTITY % xhtml-param.module \"INCLUDE\" >\n<![%xhtml-param.module;[\n<!ENTITY % xhtml-param.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Param Element 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-param-1.mod\" >\n%xhtml-param.mod;]]>\n\n<!-- Embedded Object Module  ..................................... -->\n<!ENTITY % xhtml-object.module \"INCLUDE\" >\n<![%xhtml-object.module;[\n<!ENTITY % xhtml-object.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Embedded Object 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-object-1.mod\" >\n%xhtml-object.mod;]]>\n\n<!-- Tables Module ............................................... -->\n<!ENTITY % xhtml-table.module \"INCLUDE\" >\n<![%xhtml-table.module;[\n<!ENTITY % xhtml-table.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Tables 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-table-1.mod\" >\n%xhtml-table.mod;]]>\n\n<!-- Forms Module  ............................................... -->\n<!ENTITY % xhtml-form.module \"INCLUDE\" >\n<![%xhtml-form.module;[\n<!ENTITY % xhtml-form.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Forms 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-form-1.mod\" >\n%xhtml-form.mod;]]>\n\n<!-- Legacy Markup ............................................... -->\n<!ENTITY % xhtml-legacy.module \"IGNORE\" >\n<![%xhtml-legacy.module;[\n<!ENTITY % xhtml-legacy.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Legacy Markup 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-legacy-1.mod\" >\n%xhtml-legacy.mod;]]>\n\n<!-- Document Structure Module (required)  ....................... -->\n<!ENTITY % xhtml-struct.module \"INCLUDE\" >\n<![%xhtml-struct.module;[\n<!ENTITY % xhtml-struct.mod\n     PUBLIC \"-//W3C//ELEMENTS XHTML Document Structure 1.0//EN\"\n            \"http://www.w3.org/TR/xhtml-modularization/DTD/xhtml-struct-1.mod\" >\n%xhtml-struct.mod;]]>\n\n<!-- end of XHTML 1.1 DTD  ................................................. -->\n<!-- ....................................................................... -->\n"
  },
  {
    "path": "src/main/resources/epub/chapter.html",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <title>Chapter</title>\n    <link href=\"../Styles/fonts.css\" type=\"text/css\" rel=\"stylesheet\"/>\n    <link href=\"../Styles/main.css\" type=\"text/css\" rel=\"stylesheet\"/>\n</head>\n<body>\n<h2 class=\"head\">{title}</h2>\n{content}\n</body>\n</html>\n"
  },
  {
    "path": "src/main/resources/epub/cover.html",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <title>Cover</title>\n    <style type=\"text/css\">\n\t\t.pic {\n\t\t\tmargin: 50% 30% 0 30%;\n\t\t\tpadding: 2px 2px;\n\t\t\tborder: 1px solid #f5f5dc;\n\t\t\tbackground-color: rgba(250,250,250, 0);\n\t\t\tborder-radius: 1px;\n\t\t}\n    </style>\n</head>\n<body style=\"text-align: center;\">\n<div class=\"pic\"><img src=\"../Images/cover.jpg\" style=\"width: 100%; height: auto;\"/></div>\n<h1 style=\"margin-top: 5%; font-size: 110%;\">{name}</h1>\n<div class=\"author\" style=\"margin-top: 0;\"><b>{author}</b> <span style=\"font-size: smaller;\">/ 著</span></div>\n</body>\n</html>"
  },
  {
    "path": "src/main/resources/epub/fonts.css",
    "content": "@charset \"utf-8\";\n/*---常用---*/\n\n@font-face {\n    font-family: \"zw\";\n    src:\n\tlocal(\"宋体\"),local(\"明体\"),local(\"明朝\"),\n\tlocal(\"Songti\"),local(\"Songti SC\"),local(\"Songti TC\"),\t\t\t/*iOS6+iBooks3*/\n\tlocal(\"Song S\"),local(\"Song T\"),local(\"STBShusong\"),local(\"TBMincho\"),local(\"HYMyeongJo\"),\t\t\t/*Kindle Paperwihite*/\n\tlocal(\"DK-SONGTI\"),\n\turl(../Fonts/zw.ttf),\n\turl(res:///opt/sony/ebook/FONT/zw.ttf),\n\turl(res:///Data/FONT/zw.ttf),\n\turl(res:///opt/sony/ebook/FONT/tt0011m_.ttf),\n\turl(res:///ebook/fonts/../../mnt/sdcard/fonts/zw.ttf),\n\turl(res:///ebook/fonts/../../mnt/extsd/fonts/zw.ttf),\n\turl(res:///ebook/fonts/zw.ttf),\n\turl(res:///ebook/fonts/DroidSansFallback.ttf),\n\turl(res:///fonts/ttf/zw.ttf),\n\turl(res:///../../media/mmcblk0p1/fonts/zw.ttf),\n\turl(file:///mnt/us/DK_System/system/fonts/zw.ttf),\t\t\t\t/*Duokan Old Path*/\n\turl(file:///mnt/us/DK_System/xKindle/res/userfonts/zw.ttf),\t\t/*Duokan 2012 Path*/\n\turl(res:///abook/fonts/zw.ttf),\n\turl(res:///system/fonts/zw.ttf),\n\turl(res:///system/media/sdcard/fonts/zw.ttf),\n\turl(res:///media/fonts/zw.ttf),\n\turl(res:///sdcard/fonts/zw.ttf),\n\turl(res:///system/fonts/DroidSansFallback.ttf),\n\turl(res:///mnt/MOVIFAT/font/zw.ttf),\n\turl(res:///media/flash/fonts/zw.ttf),\n\turl(res:///media/sd/fonts/zw.ttf),\n\turl(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),\n\turl(res:///../../fonts/zw.ttf),\n\turl(res:///../fonts/zw.ttf),\n\turl(../../../../../zw.ttf),\t\t\t\t\t\t\t\t\t\t/*EpubReaderI*/\n\turl(res:///mnt/sdcard/fonts/zw.ttf),\t\t\t\t\t\t\t/*Nook for Android: fonts in TF Card*/\n\turl(res:///fonts/zw.ttf),\t\t\t\t\t\t\t\t\t\t/*ADE1,8, 2.0 Program Path*/\n\turl(res:///../../../../Windows/fonts/zw.ttf);\n    /*ADE1,8, 2.0 Windows Path*/;\n}\n\n@font-face {\n    font-family: \"fs\";\n    src:\n\tlocal(\"amasis30\"),local(\"仿宋\"),local(\"仿宋_GB2312\"),\n\tlocal(\"Yuanti\"),local(\"Yuanti SC\"),local(\"Yuanti TC\"),\t\t\t/*iOS6+iBooks3*/\n\tlocal(\"DK-FANGSONG\"),\n\turl(../Fonts/fs.ttf),\n\turl(res:///opt/sony/ebook/FONT/fs.ttf),\n\turl(res:///Data/FONT/fs.ttf),\n\turl(res:///opt/sony/ebook/FONT/tt0011m_.ttf),\n\turl(res:///ebook/fonts/../../mnt/sdcard/fonts/fs.ttf),\n\turl(res:///ebook/fonts/../../mnt/extsd/fonts/fs.ttf),\n\turl(res:///ebook/fonts/fs.ttf),\n\turl(res:///ebook/fonts/DroidSansFallback.ttf),\n\turl(res:///fonts/ttf/fs.ttf),\n\turl(res:///../../media/mmcblk0p1/fonts/fs.ttf),\n\turl(file:///mnt/us/DK_System/system/fonts/fs.ttf),\t\t\t\t/*Duokan Old Path*/\n\turl(file:///mnt/us/DK_System/xKindle/res/userfonts/fs.ttf),\t\t/*Duokan 2012 Path*/\n\turl(res:///abook/fonts/fs.ttf),\n\turl(res:///system/fonts/fs.ttf),\n\turl(res:///system/media/sdcard/fonts/fs.ttf),\n\turl(res:///media/fonts/fs.ttf),\n\turl(res:///sdcard/fonts/fs.ttf),\n\turl(res:///system/fonts/DroidSansFallback.ttf),\n\turl(res:///mnt/MOVIFAT/font/fs.ttf),\n\turl(res:///media/flash/fonts/fs.ttf),\n\turl(res:///media/sd/fonts/fs.ttf),\n\turl(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),\n\turl(res:///../../fonts/fs.ttf),\n\turl(res:///../fonts/fs.ttf),\n\turl(../../../../../fs.ttf),\t\t\t\t\t\t\t\t\t\t/*EpubReaderI*/\n\turl(res:///mnt/sdcard/fonts/fs.ttf),\t\t\t\t\t\t\t/*Nook for Android: fonts in TF Card*/\n\turl(res:///fonts/fs.ttf),\t\t\t\t\t\t\t\t\t\t/*ADE1,8, 2.0 Program Path*/\n\turl(res:///../../../../Windows/fonts/fs.ttf);\n    /*ADE1,8, 2.0 Windows Path*/;\n}\n\n@font-face {\n    font-family: \"kt\";\n    src:\n\tlocal(\"Caecilia\"),local(\"楷体\"),local(\"楷体_GB2312\"),\n\tlocal(\"Kaiti\"),local(\"Kaiti SC\"),local(\"Kaiti TC\"),\t\t\t\t/*iOS6+iBooks3*/\n\tlocal(\"MKai PRC\"),local(\"MKaiGB18030C-Medium\"),local(\"MKaiGB18030C-Bold\"),\t\t\t/*Kindle Paperwihite*/\n\tlocal(\"DK-KAITI\"),\n\turl(../Fonts/kt.ttf),\n\turl(res:///opt/sony/ebook/FONT/kt.ttf),\n\turl(res:///Data/FONT/kt.ttf),\n\turl(res:///opt/sony/ebook/FONT/tt0011m_.ttf),\n\turl(res:///ebook/fonts/../../mnt/sdcard/fonts/kt.ttf),\n\turl(res:///ebook/fonts/../../mnt/extsd/fonts/kt.ttf),\n\turl(res:///ebook/fonts/kt.ttf),\n\turl(res:///ebook/fonts/DroidSansFallback.ttf),\n\turl(res:///fonts/ttf/kt.ttf),\n\turl(res:///../../media/mmcblk0p1/fonts/kt.ttf),\n\turl(file:///mnt/us/DK_System/system/fonts/kt.ttf),\t\t\t\t/*Duokan Old Path*/\n\turl(file:///mnt/us/DK_System/xKindle/res/userfonts/kt.ttf),\t\t/*Duokan 2012 Path*/\n\turl(res:///abook/fonts/kt.ttf),\n\turl(res:///system/fonts/kt.ttf),\n\turl(res:///system/media/sdcard/fonts/kt.ttf),\n\turl(res:///media/fonts/kt.ttf),\n\turl(res:///sdcard/fonts/kt.ttf),\n\turl(res:///system/fonts/DroidSansFallback.ttf),\n\turl(res:///mnt/MOVIFAT/font/kt.ttf),\n\turl(res:///media/flash/fonts/kt.ttf),\n\turl(res:///media/sd/fonts/kt.ttf),\n\turl(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),\n\turl(res:///../../fonts/kt.ttf),\n\turl(res:///../fonts/kt.ttf),\n\turl(../../../../../kt.ttf),\t\t\t\t\t\t\t\t\t\t/*EpubReaderI*/\n\turl(res:///mnt/sdcard/fonts/kt.ttf),\t\t\t\t\t\t\t/*Nook for Android: fonts in TF Card*/\n\turl(res:///fonts/kt.ttf),\t\t\t\t\t\t\t\t\t\t/*ADE1,8, 2.0 Program Path*/\n\turl(res:///../../../../Windows/fonts/kt.ttf);\n    /*ADE1,8, 2.0 Windows Path*/;\n}\n\n@font-face {\n    font-family: \"ht\";\n    src:\n\tlocal(\"黑体\"),local(\"微软雅黑\"),\n\tlocal(\"Heiti\"),local(\"Heiti SC\"),local(\"Heiti TC\"),\t\t\t\t/*iOS6+iBooks3*/\n\tlocal(\"MYing Hei S\"),local(\"MYing Hei T\"),local(\"TBGothic\"),\t\t\t\t\t\t/*Kindle Paperwihite*/\n\tlocal(\"DK-HEITI\"),\n\turl(../Fonts/ht.ttf),\n\turl(res:///opt/sony/ebook/FONT/ht.ttf),\n\turl(res:///Data/FONT/ht.ttf),\n\turl(res:///opt/sony/ebook/FONT/tt0011m_.ttf),\n\turl(res:///ebook/fonts/../../mnt/sdcard/fonts/ht.ttf),\n\turl(res:///ebook/fonts/../../mnt/extsd/fonts/ht.ttf),\n\turl(res:///ebook/fonts/ht.ttf),\n\turl(res:///ebook/fonts/DroidSansFallback.ttf),\n\turl(res:///fonts/ttf/ht.ttf),\n\turl(res:///../../media/mmcblk0p1/fonts/ht.ttf),\n\turl(file:///mnt/us/DK_System/system/fonts/ht.ttf),\t\t\t\t/*Duokan Old Path*/\n\turl(file:///mnt/us/DK_System/xKindle/res/userfonts/ht.ttf),\t\t/*Duokan 2012 Path*/\n\turl(res:///abook/fonts/ht.ttf),\n\turl(res:///system/fonts/ht.ttf),\n\turl(res:///system/media/sdcard/fonts/ht.ttf),\n\turl(res:///media/fonts/ht.ttf),\n\turl(res:///sdcard/fonts/ht.ttf),\n\turl(res:///system/fonts/DroidSansFallback.ttf),\n\turl(res:///mnt/MOVIFAT/font/ht.ttf),\n\turl(res:///media/flash/fonts/ht.ttf),\n\turl(res:///media/sd/fonts/ht.ttf),\n\turl(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),\n\turl(res:///../../fonts/ht.ttf),\n\turl(res:///../fonts/ht.ttf),\n\turl(../../../../../ht.ttf),\t\t\t\t\t\t\t\t\t\t/*EpubReaderI*/\n\turl(res:///mnt/sdcard/fonts/ht.ttf),\t\t\t\t\t\t\t/*Nook for Android: fonts in TF Card*/\n\turl(res:///fonts/ht.ttf),\t\t\t\t\t\t\t\t\t\t/*ADE1,8, 2.0 Program Path*/\n\turl(res:///../../../../Windows/fonts/ht.ttf);\n    /*ADE1,8, 2.0 Windows Path*/;\n}\n@font-face {\n\tfont-family:\"h1\";\n\tsrc:\n\tlocal(\"方正兰亭特黑长_GBK\"),local(\"方正兰亭特黑长简体\"),local(\"方正兰亭特黑长繁体\"),\n\tlocal(\"LantingTeheichang\"),\n\tlocal(\"Yuanti\"),local(\"Yuanti SC\"),local(\"Yuanti TC\"),\n\tlocal(\"DK-HEITI\"),\n\turl(../Fonts/h1.ttf),\n\turl(res:///opt/sony/ebook/FONT/h1.ttf),\n\turl(res:///Data/FONT/h1.ttf),\n\turl(res:///opt/sony/ebook/FONT/tt0011m_.ttf),\n\turl(res:///ebook/fonts/../../mnt/sdcard/fonts/h1.ttf),\n\turl(res:///ebook/fonts/../../mnt/extsd/fonts/h1.ttf),\n\turl(res:///ebook/fonts/h1.ttf),\n\turl(res:///ebook/fonts/DroidSansFallback.ttf),\n\turl(res:///fonts/ttf/h1.ttf),\n\turl(res:///../../media/mmcblk0p1/fonts/h1.ttf),\n\turl(file:///mnt/us/DK_System/system/fonts/h1.ttf),\t\t\t\t/*Duokan Old Path*/\n\turl(file:///mnt/us/DK_System/xKindle/res/userfonts/h1.ttf),\t\t/*Duokan 2012 Path*/\n\turl(res:///abook/fonts/h1.ttf),\n\turl(res:///system/fonts/h1.ttf),\n\turl(res:///system/media/sdcard/fonts/h1.ttf),\n\turl(res:///media/fonts/h1.ttf),\n\turl(res:///sdcard/fonts/h1.ttf),\n\turl(res:///system/fonts/DroidSansFallback.ttf),\n\turl(res:///mnt/MOVIFAT/font/h1.ttf),\n\turl(res:///media/flash/fonts/h1.ttf),\n\turl(res:///media/sd/fonts/h1.ttf),\n\turl(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),\n\turl(res:///../../fonts/h1.ttf),\n\turl(res:///../fonts/h1.ttf),\n\turl(../../../../../h1.ttf),\t\t\t\t\t\t\t\t\t\t/*EpubReaderI*/\n\turl(res:///mnt/sdcard/fonts/h1.ttf),\t\t\t\t\t\t\t/*Nook for Android: fonts in TF Card*/\n\turl(res:///fonts/h1.ttf),\t\t\t\t\t\t\t\t\t\t/*ADE1,8, 2.0 Program Path*/\n\turl(res:///../../../../Windows/fonts/h1.ttf);\t\t\t\t\t/*ADE1,8, 2.0 Windows Path*/\n}\n@font-face {\n\tfont-family:\"h2\";\n\tsrc:\n\tlocal(\"方正大标宋_GBK\"),local(\"方正大标宋简体\"),local(\"方正大标宋繁体\"),\n\tlocal(\"Dabiaosong\"),\n\tlocal(\"Heiti\"),local(\"Heiti SC\"),local(\"Heiti TC\"),\n\tlocal(\"DK-XIAOBIAOSONG\"),\n\turl(../Fonts/h2.ttf),\n\turl(res:///opt/sony/ebook/FONT/h2.ttf),\n\turl(res:///Data/FONT/h2.ttf),\n\turl(res:///opt/sony/ebook/FONT/tt0011m_.ttf),\n\turl(res:///ebook/fonts/../../mnt/sdcard/fonts/h2.ttf),\n\turl(res:///ebook/fonts/../../mnt/extsd/fonts/h2.ttf),\n\turl(res:///ebook/fonts/h2.ttf),\n\turl(res:///ebook/fonts/DroidSansFallback.ttf),\n\turl(res:///fonts/ttf/h2.ttf),\n\turl(res:///../../media/mmcblk0p1/fonts/h2.ttf),\n\turl(file:///mnt/us/DK_System/system/fonts/h2.ttf),\t\t\t\t/*Duokan Old Path*/\n\turl(file:///mnt/us/DK_System/xKindle/res/userfonts/h2.ttf),\t\t/*Duokan 2012 Path*/\n\turl(res:///abook/fonts/h2.ttf),\n\turl(res:///system/fonts/h2.ttf),\n\turl(res:///system/media/sdcard/fonts/h2.ttf),\n\turl(res:///media/fonts/h2.ttf),\n\turl(res:///sdcard/fonts/h2.ttf),\n\turl(res:///system/fonts/DroidSansFallback.ttf),\n\turl(res:///mnt/MOVIFAT/font/h2.ttf),\n\turl(res:///media/flash/fonts/h2.ttf),\n\turl(res:///media/sd/fonts/h2.ttf),\n\turl(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),\n\turl(res:///../../fonts/h2.ttf),\n\turl(res:///../fonts/h2.ttf),\n\turl(../../../../../h2.ttf),\t\t\t\t\t\t\t\t\t\t/*EpubReaderI*/\n\turl(res:///mnt/sdcard/fonts/h2.ttf),\t\t\t\t\t\t\t/*Nook for Android: fonts in TF Card*/\n\turl(res:///fonts/h2.ttf),\t\t\t\t\t\t\t\t\t\t/*ADE1,8, 2.0 Program Path*/\n\turl(res:///../../../../Windows/fonts/h2.ttf);\t\t\t\t\t/*ADE1,8, 2.0 Windows Path*/\n}\n\n@font-face {\n\tfont-family:\"h3\";\n\tsrc:\n\tlocal(\"方正华隶_GBK\"),local(\"方正行黑简体\"),local(\"方正行黑繁体\"),\n\tlocal(\"Yuanti\"),local(\"Yuanti SC\"),local(\"Yuanti TC\"),\n\tlocal(\"DK-FANGSONG\"),\n\turl(../Fonts/h3.ttf),\n\turl(res:///opt/sony/ebook/FONT/h3.ttf),\n\turl(res:///Data/FONT/h3.ttf),\n\turl(res:///opt/sony/ebook/FONT/tt0011m_.ttf),\n\turl(res:///ebook/fonts/../../mnt/sdcard/fonts/h3.ttf),\n\turl(res:///ebook/fonts/../../mnt/extsd/fonts/h3.ttf),\n\turl(res:///ebook/fonts/h3.ttf),\n\turl(res:///ebook/fonts/DroidSansFallback.ttf),\n\turl(res:///fonts/ttf/h3.ttf),\n\turl(res:///../../media/mmcblk0p1/fonts/h3.ttf),\n\turl(file:///mnt/us/DK_System/system/fonts/h3.ttf),\t\t\t\t/*Duokan Old Path*/\n\turl(file:///mnt/us/DK_System/xKindle/res/userfonts/h3.ttf),\t\t/*Duokan 2012 Path*/\n\turl(res:///abook/fonts/h3.ttf),\n\turl(res:///system/fonts/h3.ttf),\n\turl(res:///system/media/sdcard/fonts/h3.ttf),\n\turl(res:///media/fonts/h3.ttf),\n\turl(res:///sdcard/fonts/h3.ttf),\n\turl(res:///system/fonts/DroidSansFallback.ttf),\n\turl(res:///mnt/MOVIFAT/font/h3.ttf),\n\turl(res:///media/flash/fonts/h3.ttf),\n\turl(res:///media/sd/fonts/h3.ttf),\n\turl(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf),\n\turl(res:///../../fonts/h3.ttf),\n\turl(res:///../fonts/h3.ttf),\n\turl(../../../../../h3.ttf),\t\t\t\t\t\t\t\t\t\t/*EpubReaderI*/\n\turl(res:///mnt/sdcard/fonts/h3.ttf),\t\t\t\t\t\t\t/*Nook for Android: fonts in TF Card*/\n\turl(res:///fonts/h3.ttf),\t\t\t\t\t\t\t\t\t\t/*ADE1,8, 2.0 Program Path*/\n\turl(res:///../../../../Windows/fonts/h3.ttf);\t\t\t\t\t/*ADE1,8, 2.0 Windows Path*/\n}\n\n@font-face {\n\tfont-family:\"luohua\";\n\tsrc:local(\"汉仪落花体\"),\n\t     url(\"../Fonts/hylh.ttf\");\n}"
  },
  {
    "path": "src/main/resources/epub/intro.html",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"zh-CN\">\n<head>\n    <title>Intro</title>\n    <link href=\"../Styles/fonts.css\" type=\"text/css\" rel=\"stylesheet\" />\n    <link href=\"../Styles/main.css\" type=\"text/css\" rel=\"stylesheet\" />\n</head>\n<body>\n<h1 class=\"head\" style=\"margin-bottom:2em;\">内容简介</h1>{intro}</body>\n</html>\n"
  },
  {
    "path": "src/main/resources/epub/main.css",
    "content": "@charset \"utf-8\";\n@import url(\"../Styles/fonts.css\");\nbody {\n    padding: 0%;\n    margin-top: 0%;\n    margin-bottom: 0%;\n    margin-left: 0.5%;\n    margin-right: 0.5%;\n    line-height: 130%;\n    text-align: justify;\n    font-family: \"DK-SONGTI\",\"st\",\"宋体\",\"zw\",sans-serif;\n}\n\np {\n    text-align: justify;\n    text-indent: 2em;\n    line-height: 130%;\n    margin-right: 0.5%;\n    margin-left: 0.5%;\n    font-family: \"DK-SONGTI\",\"st\",\"宋体\",\"zw\",sans-serif;\n}\np.kaiti {\n    font-family: \"DK-KAITI\",\"kt\",\"楷体\",\"zw\",serif;\n}\n\np.fangsong {\n    font-family: \"DK-FANGSONG\",\"fs\",\"仿宋\",\"zw\",serif;\n}\n\nspan.xinli {\n    font-family: \"DK-KAITI\",\"kt\",\"楷体\",\"zw\",serif;\n    color: #4e753f;\n}\n/** 英文斜体字 **/\nspan.english{\n\tfont-style: italic;\n}\ndiv {\n    margin: 0px;\n    padding: 0px;\n    line-height: 120%;\n    text-align: justify;\n    font-family: \"zw\";\n}\ndiv.foot {\n    text-indent: 2em;\n    margin: 30% 5% 0 5%;\n    padding: 8px 0;\n}\np.foot {\n    font-family: \"DK-KAITI\",\"kt\",\"楷体\",\"zw\",serif;\n}\n\n/*扉页*/\n.booksubtitle {\n    padding: 10px 0 0px 0;\n    text-indent: 0em;\n    font-size: 75%;\n    font-family: \"ht\";    \n}\n\n.booktitle {\n   padding: 9% 0 0 0;\n   font-size: 1.3em;\n   font-family: \"方正小标宋_GBK\",\"DK-XIAOBIAOSONG\";\n   font-weight: normal;\n   text-indent: 0em;\n    color: #000;\n   text-align: center;\n   line-height: 1.6;\n}\n\n.booktitle0 {\n   font-size: 1.2em;\n   font-family: \"fs\";\n   text-indent: 0em;\n   text-align: center;\n   line-height: 1.8;\n}\n\n.booktitle1 {\n   padding: 0 0 0 0;\n   font-size: 0.85em;\n   font-family: \"fs\";\n   text-indent: 0em;\n   text-align: center;\n   line-height: 1.6;\n}\n\n.bookauthor {\n    font-family: \"DK-FANGSONG\",仿宋,\"fs\",\"fangsong\",sans-serif;\n    padding: 5% 5px 0px 5px;\n    text-indent: 0em;\n    text-align: center;  \n    color: #000;\n    font-size: 90%;\n    line-height: 1.3;\n}\n\n.booktranslator {\n    padding: 1% 5px 0px 5px;\n    text-indent: 0em;\n    text-align: center;   \n    font-size: 85%;\n    line-height: 1.3;\n}\n\n.bookpub {\n    font-family: \"DK-KAITI\",\"kt\",\"楷体\",\"楷体_gb2312\";\n    padding: 30% 5px 5px 5px;\n    text-indent: 0em;\n    color: #000;\n    text-align: center;  \n    font-size: 80%;\n}\n\n/*标题页*/\nbody.head {\n \tbackground-repeat:no-repeat no-repeat;\n\tbackground-size:160px 229px;\n\tbackground-position:bottom right;\n\tbackground-attachment:fixed;\n}\n\nbody.xhead {\n    background-color: #FDF5E6;\n}\n\nh1.head {\n    font-family: \"DK-HEITI\",黑体,sans-serif;\n    font-size: 1.2em;\n    font-weight: bold;\n    color: #311a02;\n    text-indent: 0em;\n    font-weight: normal;\n    duokan-text-indent: 0em;\n    padding: auto;\n    text-align: center;\n    margin-top: -8em;\n}\n\ndiv.head {\n    border: solid 2px #ffffff;\n    padding: 2px;\n    margin: 2em auto 0.7em auto;\n    text-align: center;\n    width: 1em;\n}\n\nh1.head b {\n    font-family: \"方正小标宋_GBK\",\"DK-XIAOBIAOSONG\";\n    font-weight: bold;\n    font-size: 1.2em;\n    text-align: center;\n    text-indent: 0em;\n    duokan-text-indent: 0em;\n    color: #311a02;\n    margin: 0.5em auto;\n    line-height: 140%;\n}\n\ndiv.back {\n    text-align: center;\n    text-indent: 0em;\n    duokan-text-indent: 0em;\n    margin: 4em auto;\n}\n\nimg.back {\n    width: 70%;\n}\nimg.back2 {\n    width: 40%;\n    margin: 2em 0 0 0;\n}\n/*正文*/\n/**楷体引文**/\n.titou {\n    font-family: \"DK-FANGSONG\",仿宋,\"fs\",\"fangsong\",sans-serif;\n}\n.yinwen {\n    font-family: \"DK-KAITI\",\"kt\",\"楷体\",\"zw\",serif;\n\tmargin-left: 2em;\n\ttext-indent: 0em;\n}\n.nicename {\n    font-family: \"DK-HEITI\",黑体,sans-serif;\n    font-weight: bold;\n    font-size: 0.9em;\n}\nbody.head3 {\n    background-color: #a7bdcc;\n    color: #354f66;\n}\n\nbody.head4 {\n    background-color: #bfd19b;\n    color: #4e753f;\n}\n\nh2.head {\n    font-family: \"小标宋\";\n    text-align: left;\n    font-weight: bold;\n    font-size: 1.1em;\n    margin: -3em 2em 2em 0;\n    color: #3f83e8;\n    line-height: 140%;\n}\n\nh2.head span {\n    font-family: \"仿宋\";\n    font-size: 0.7em;\n    background-color: #3f83e8;\n    border-radius: 9px;\n    padding: 4px;\n    color: #fff;\n}\n\n\ndiv.logo {\n    margin: -2em 0% 0 0;\n    text-align: right;\n}\n\nimg.logo {\n    width: 40%;\n}\n.imgl {\n    /*图片居右*/\n\tmargin: -8.8em 1em 4em 0em;\n    width: 80%;\n\ttext-align: right;\n}\n\nh1.head {\n\tline-height:130%;\n\tfont-size:1.4em;\n\ttext-align: center;\n\tcolor: #BA2213;\n\tfont-weight: bold;\n\tmargin-top: 2em;\n\tmargin-bottom: 1em;\n    font-family: \"方正小标宋_GBK\",\"DK-XIAOBIAOSONG\";\n\t\n}\nh3 {\n    font-family: \"DK-HEITI\",黑体,sans-serif;\n    font-size: 1.1em;\n    margin: 1em 0;\n    border-left: 1.2em solid #00a1e9;\n    line-height: 120%;\n    padding-left: 3px;\n\tcolor: #00a1e9;\n}\nh4 {\n    font-family: \"DK-HEITI\",黑体,sans-serif;\n    font-size: 1.1em;\n\ttext-align: center;\n    margin: 1em 0;\n    line-height: 120%;\n\tcolor: #000;\n}\nh1.post {\n    font-family: \"方正小标宋_GBK\",\"DK-XIAOBIAOSONG\";\n    text-align: center;\n    font-size: 1.3em;\n\tcolor: #026fca;\n    margin: 3em auto 2em auto;\n}\n.banquan {\n    font-family: \"DK-FANGSONG\",仿宋,\"fs\",\"fangsong\",sans-serif;\n    text-align: left;\n    color: #000;\n\tfont-size:1.1em;\n    margin-bottom:1em;\n    text-indent: 1em;\n    duokan-text-indent: 1em;\n}\np.post {\n    font-family: \"DK-FANGSONG\",仿宋,\"fs\",\"fangsong\",sans-serif;\n}\np.zy {\n    font-family: \"DK-FANGSONG\",仿宋,\"fs\",\"fangsong\",sans-serif;\n    margin: 1em 0 0 1em;\n    padding: 5px 0px 5px 10px;\n    text-indent: 0em;\n    border-left: 5px solid #a9b5c1;\n}\n.sign {\n    font-family: \"DK-KAITI\",\"kt\",\"楷体\",\"zw\",serif;\n    margin: 1em 2px 0 auto;\n    text-align: right;\n    font-size: 0.8em;\n    text-indent: 0em;\n    duokan-text-indent: 0em;\n}\n\n.mark {\n    font-family: \"DK-HEITI\",黑体,sans-serif;\n    font-size: 0.9em;\n    color: #fff;\n    text-indent: 0em;\n    duokan-text-indent: 0em;\n    background-color: maroon;\n    text-align: center;\n    padding: 0px;\n    margin: 2em 30%;\n}\n\n/*出版社*/\n.chubanshe img{\n\twidth:106px;\n\theight:28px;\n}\n.chubanshe {\n\tmargin-top:20px;\n}\n.cr {\n\tfont-size:0.9em;\n}\n\n/*多看画廊*/\ndiv.duokan-image-single {\n    text-align: center;\n    margin: 0.5em auto; /*插图盒子上下外边距为0.5em，左右设置auto是为了水平居中这个盒子*/\n}\nimg.picture-80 {\n    margin: 0; /*清除img元素的外边距*/\n    width: 80%; /*预览窗口的宽度*/\n    box-shadow: 3px 3px 10px #bfbfbf; /*给图片添加阴影效果*/\n}\np.duokan-image-maintitle {\n    margin: 1em 0 0; /*图片说明的段间距*/\n    font-family: \"楷体\"; /*图片说明使用的字体*/\n    font-size: 0.9em; /*字体大小*/\n    text-indent: 0; /*首行缩进为零，当你使用单标签p来指定首行缩进为2em时，记得在需要居中的文本中清除缩进，因为样式是叠加的*/\n    text-align: center; /*图片说明水平居中*/\n    color: #a52a2a; /*字体颜色*/\n    line-height: 1.25em; /*行高，防止有很长的图片说明*/\n}\n\n\n/*制作说明页*/\nbody.description {\n    background-image: url(../Images/001.png);\n    background-position: bottom center;\n    background-repeat: no-repeat;\n    background-size: cover;\n    padding: 25% 10% 0;\n    font-size: 0.9em;\n}\n\ndiv.description-body {\n    width: 55%;\n    padding: 2em 1.3em;\n    border-radius: 0.5em;\n    font-size: 0.9em;\n    border-style: solid;\n    border-color: #393939;\n    border-width: 0.3em;\n    border-radius: 5em;\n    background-color: #5a5a5a;\n    box-shadow: 2px 2px 3px #828281;\n}\n\nh1.description-title {\n    text-align: center;\n    font-family: \"黑体\";\n    font-size: 1.2em;\n    margin: 0 0 1em 0;\n    color: #FF9;\n    text-shadow: 1px 1px 0 black;\n}\n\np.description-text {\n    color: #f9ddd2;\n    font-family: \"准圆\";\n    margin: 0;\n    text-align: justify;\n    text-indent: 0;\n    duokan-text-indent: 0;\n}\n\nhr.description-hr {\n    margin: 0.5em -1em;\n    border-style: dotted;\n    border-color: #9C9;\n    border-width: 0.05em 0 0 0;\n}\n\np.tips {\n    text-align: justify;\n    text-indent: 0;\n    duokan-text-indent: 0;\n    font-family: \"楷体\";\n    font-size: 0.7em;\n    color: #FFC;\n    margin: 0;\n}\n\n/*版本说明页*/\n.ver {\n    font-family: \"DK-CODE\",\"DK-XIHEITI\",细黑体,\"xihei\",sans-serif;\n\tfont-weight: bold;\n    font-size: 100%;\n    color: #000;\n    margin: 1em 0 1em 0;\n    text-align: center;\n}\n\n.vertitle {\n    font-family: \"DK-FANGSONG\",仿宋,\"fs\",\"fangsong\",sans-serif;\n    font-size: 100%;\n    text-indent: 0em;\n    text-align: left;\n    duokan-text-indent: 0em;\n}\n\n.vertxt {\n    font-family: \"DK-FANGSONG\",仿宋,\"fs\",\"fangsong\",sans-serif;\n\tline-height: 100%;\n    font-size: 85%;\n    text-indent: 0em;\n    text-align: left;\n    duokan-text-indent: 0em;\n}\n.verchar {\n    font-family: \"DK-KAITI\",\"kt\",\"楷体\",\"楷体_gb2312\";\n    text-align: left;\n    text-indent: 1em;\n    duokan-text-indent: 1em;\n\tmargin-bottom: 1em;\n\tmargin-top: 1em;\n}\n.vernote {\n    font-family: \"DK-FANGSONG\",仿宋,\"fs\",\"fangsong\",sans-serif;\n    font-size: 75%;\n    color: #686d70;\n    text-indent: 0em;\n    text-align: left;\n    duokan-text-indent: 0em;\n    padding-bottom: 15px;\n}\n\n.line {\n    border: dotted #A2906A;\n    border-width: 1px 0 0 0;\n}\n\n.entry {\n    margin-left: 18px;\n    font-size: 83%;\n    color: #8fe0a3;\n    text-indent: 0em;\n    duokan-text-indent: 0em;\n}\n/*版权信息*/\n.vol {\n    text-indent: 0em;\n    text-align: center;\n    padding: 0.8em;\n    margin: 0 auto 3px auto;\n    color: #000;\n    font-family: \"方正小标宋_GBK\",\"DK-XIAOBIAOSONG\";\n    font-size: 130%;\n    text-shadow: none;\n}\n\n.cp {\n    font-family: \"DK-CODE\",\"DK-XIHEITI\",细黑体,\"xihei\",sans-serif;\n    color: #412938;\n    font-size: 70%;\n    text-align: left;\n    text-indent: 0em;\n    duokan-text-indent: 0em;\n}\n\n.xchar {\n    font-family: \"DK-KAITI\",\"kt\",\"楷体\",\"楷体_gb2312\";\n    text-indent: 0em;\n    duokan-text-indent: 0em;\n}\n/*多看弹注*/\nsup img {\n\tline-height: 100%;\n\twidth: auto;\n    height: 1.0em;\n    margin: 0em;\n    padding: 0em;\n\tvertical-align: text-top;\n}\n\nol {\n\tmargin-bottom:0;\n\tpadding:0 auto;\n\tlist-style-type: decimal;\n}\n.hr {\n\twidth:50%;\n\tmargin:2em 0 0 0.5em;\n\tpadding:0;\n\theight:2px;\n\tbackground-color: #F3221D;\n}\n\n.duokan-footnote-content{\n\tpadding:0 auto;\n\ttext-align: left;\n}\n\n.duokan-footnote-item {\n\tfont-family:\"DK-XIHEITI\",细黑体,\"xihei\",sans-serif;\n\ttext-align: left;\n\tfont-size: 80%;\n\tline-height: 100%;\n\tclear: both;\n\tcolor:#000;\n\tlist-style-type:decimal;\n}\n\nli.duokan-footnote-item a {\n\tfont-family:\"DK-HEITI\";\n\ttext-align: left;\n}\na{\n\ttext-decoration: none;\n\tcolor: #222;\n}\n\na:hover {background: #81caf9}\t\na:active {background: yellow}\n.duokan-image-maintitle {\n\tfont-family:\"DK-HEITI\",黑体,\"hei\",sans-serif;\n\ttext-align: center;\n\ttext-indent: 0em;\n\tduokan-text-indent: 0em;\n\tfont-size:90%;\n\tcolor: #1F4150;\n\tmargin-top: 1em;\n}\n\n.duokan-image-subtitle {\n\tfont-family:\"DK-XIHEITI\",细黑体,\"xihei\",sans-serif;\n\ttext-align: center;\n\ttext-indent: 0em;\n\tduokan-text-indent: 0em;\n\tfont-size:70%;\n\tcolor: #3A3348;\n\tmargin-top: 1em;\n}"
  },
  {
    "path": "src/main/resources/logback-spring.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n\n    <springProperty scope=\"context\" name=\"logPath\" source=\"logging.path\" defaultValue=\"./logs\" />\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder class=\"ch.qos.logback.classic.encoder.PatternLayoutEncoder\">\n            <Pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</Pattern>\n        </encoder>\n    </appender>\n\n\n    <appender name=\"file\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">\n        <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">\n            <fileNamePattern>${logPath}/reader-%d{yyyy-MM-dd}.%i.log</fileNamePattern>\n            <timeBasedFileNamingAndTriggeringPolicy class=\"ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP\">\n                <maxFileSize>100MB</maxFileSize>\n            </timeBasedFileNamingAndTriggeringPolicy>\n        </rollingPolicy>\n        <encoder>\n            <pattern>[%thread] %d{HH:mm:ss.SSS} %-5level %logger{0} - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <appender name=\"ASYNC_ROLLING_FILE\" class=\"ch.qos.logback.classic.AsyncAppender\">\n        <appender-ref ref=\"file\" />\n    </appender>\n\n    <logger name=\"io.netty\" level=\"warn\"/>\n    <!--    <logger name=\"io.vertx\" level=\"TRACE\"/>-->\n    <logger name=\"com.htmake.reader\" level=\"debug\"/>\n\n    <springProfile name=\"default\">\n        <root level=\"INFO\">\n            <appender-ref ref=\"STDOUT\"/>\n        </root>\n    </springProfile>\n\n\n    <springProfile name=\"prod\">\n        <root level=\"INFO\">\n            <appender-ref ref=\"ASYNC_ROLLING_FILE\"/>\n        </root>\n    </springProfile>\n\n</configuration>"
  },
  {
    "path": "src/test/java/com/htmake/reader/ReaderApplicationTests.java",
    "content": "package com.htmake.reader;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.junit4.SpringRunner;\n\n@RunWith(SpringRunner.class)\n@SpringBootTest\npublic class ReaderApplicationTests {\n\n    @Test\n    public void contextLoads() {\n    }\n\n}\n"
  },
  {
    "path": "vetur.config.js",
    "content": "// vetur.config.js\n/** @type {import('vls').VeturConfig} */\nmodule.exports = {\n    // **optional** default: `{}`\n    // override vscode settings\n    // Notice: It only affects the settings used by Vetur.\n    settings: {\n      \"vetur.useWorkspaceDependencies\": true,\n      \"vetur.experimental.templateInterpolationService\": true\n    },\n    // **optional** default: `[{ root: './' }]`\n    projects: [\n      './web'\n    ]\n  }"
  },
  {
    "path": "web/.browserslistrc",
    "content": "> 0.5%\nlast 2 chrome version\nlast 2 firefox version\nlast 2 edge version\nlast 2 opera version\nlast 22 ios versions\nlast 65 android versions\nlast 4 ie versions"
  },
  {
    "path": "web/.eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  env: {\n    node: true\n  },\n  extends: [\"plugin:vue/essential\", \"@vue/prettier\"],\n  globals: {\n    workbox: \"writable\"\n  },\n  rules: {\n    \"no-console\": process.env.NODE_ENV === \"production\" ? \"error\" : \"off\",\n    \"no-debugger\": process.env.NODE_ENV === \"production\" ? \"error\" : \"off\"\n  },\n  parserOptions: {\n    parser: \"babel-eslint\"\n  }\n};\n"
  },
  {
    "path": "web/.gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n#Carlo build output\n/dist_carlo\n/.profile\ndist*.zip\n\nyarn.lock\npnpm-lock.yaml"
  },
  {
    "path": "web/LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "web/README.md",
    "content": "# 「阅读3.0」 web 端（可设置IP）\n\n本程序为「阅读3.0」的配套 web 端，需要保证手机和电脑在同一局域网内，然后手机端打开 web 服务。\n\n在线地址 http://alanskycn.gitee.io/vip/reader/\n\n## 具体实现\n\n使用 Vue2 开发\n\n## 功能特性\n\n- 本地存储阅读记录与设置\n- 阅读主题切换\n- 夜间模式\n- 字号调节\n- 字体调节\n- 阅读宽度调节\n\n## 使用方法\n\n```shell\nyarn install\n#安装项目\nyarn serve\n#开发模式\nyarn build\n#打包\nyarn lint\n#格式化代码\n```\n - ~~点击`Star`自动编译，可在Actions查看~~\n - ~~编译失败，可先点击`Unstar`，再点击`Star`重新开始~~\n"
  },
  {
    "path": "web/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      \"@vue/app\",\n      {\n        polyfills: [\"es.promise\", \"es.symbol\"]\n      }\n    ]\n  ],\n  plugins: [\n    [\n      \"component\",\n      {\n        libraryName: \"element-ui\",\n        styleLibraryName: \"theme-chalk\"\n      }\n    ]\n  ]\n};\n"
  },
  {
    "path": "web/jsconfig.json",
    "content": "{\n    \"include\": [\n        \"./src/**/*\"\n    ]\n}"
  },
  {
    "path": "web/package.json",
    "content": "{\n  \"name\": \"reader\",\n  \"version\": \"2.5.4\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-cli-service serve\",\n    \"build\": \"vue-cli-service build\",\n    \"lint\": \"vue-cli-service lint\",\n    \"sync\": \"yarn build && rm -rf ../src/main/resources/web && mv dist ../src/main/resources/web\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^0.21.1\",\n    \"codejar\": \"^3.5.0\",\n    \"core-js\": \"^3.3.2\",\n    \"element-ui\": \"^2.15.9\",\n    \"localforage\": \"^1.10.0\",\n    \"prismjs\": \"^1.25.0\",\n    \"register-service-worker\": \"^1.7.1\",\n    \"sortablejs\": \"^1.15.0\",\n    \"stylus\": \"^0.54.7\",\n    \"stylus-loader\": \"^3.0.2\",\n    \"vue\": \"^2.6.10\",\n    \"vue-lazyload\": \"^1.3.3\",\n    \"vue-router\": \"^3.1.3\",\n    \"vuex\": \"^3.1.1\"\n  },\n  \"devDependencies\": {\n    \"@vue/cli-plugin-babel\": \"^4.0.0\",\n    \"@vue/cli-plugin-eslint\": \"^4.0.0\",\n    \"@vue/cli-plugin-pwa\": \"^4.0.0\",\n    \"@vue/cli-plugin-router\": \"^4.0.0\",\n    \"@vue/cli-service\": \"^4.0.0\",\n    \"@vue/eslint-config-prettier\": \"^5.0.0\",\n    \"babel-eslint\": \"^10.0.3\",\n    \"babel-plugin-component\": \"^1.1.1\",\n    \"eslint\": \"^5.16.0\",\n    \"eslint-plugin-prettier\": \"^3.1.1\",\n    \"eslint-plugin-vue\": \"^5.0.0\",\n    \"prettier\": \"^1.18.2\",\n    \"vue-cli-plugin-element\": \"^1.0.1\",\n    \"vue-template-compiler\": \"^2.6.10\"\n  }\n}\n"
  },
  {
    "path": "web/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    autoprefixer: {}\n  }\n};\n"
  },
  {
    "path": "web/public/bookSourceDebug/index.css",
    "content": "body {\n  margin: 0;\n}\n.editor {\n  display: flex;\n  align-items: stretch;\n}\n.setbox,\n.menu,\n.outbox {\n  flex: 1;\n  display: flex;\n  flex-flow: column;\n  max-height: 100vh;\n  overflow-y: auto;\n}\n.menu {\n  justify-content: center;\n  max-width: 90px;\n  margin: 0 5px;\n}\n.menu .button {\n  width: 90px;\n  height: 30px;\n  min-height: 30px;\n  margin: 5px 0px;\n  cursor: pointer;\n}\n@keyframes stroker {\n  0% {\n    stroke-dashoffset: 0;\n  }\n  100% {\n    stroke-dashoffset: -240;\n  }\n}\n.button rect {\n  width: 100%;\n  height: 100%;\n  fill: transparent;\n  stroke: #666;\n  stroke-width: 2px;\n}\n.button rect.busy {\n  stroke: #fd1850;\n  stroke-dasharray: 30 90;\n  animation: stroker 1s linear infinite;\n}\n.button text {\n  text-anchor: middle;\n  dominant-baseline: middle;\n}\n.setbox {\n  min-width: 40em;\n}\n.rules {\n  overflow: auto;\n}\n.tabbox {\n  flex: 1;\n  display: flex;\n  flex-flow: column;\n}\n.rules > * {\n  display: flex;\n  margin: 2px 0;\n}\n.rules textarea {\n  flex: 1;\n  margin-left: 5px;\n}\n.rules > *,\n.rules > * > div,\n.rules textarea {\n  min-height: 1.2em;\n}\ntextarea {\n  word-break: break-all;\n}\n.tabtitle {\n  display: flex;\n  z-index: 1;\n  justify-content: flex-end;\n}\n.tabtitle > div {\n  cursor: pointer;\n  padding: 1px 10px 0 10px;\n  border-bottom: 3px solid transparent;\n  font-weight: bold;\n}\n.tabtitle > .this {\n  color: #4f9da6;\n  border-bottom-color: #4ebbe4;\n}\n.tabbody {\n  flex: 1;\n  display: flex;\n  margin-top: -1px;\n  border: 1px solid #a9a9a9;\n  height: 0;\n}\n.tabbody > * {\n  flex: 1;\n  flex-flow: column;\n  display: none;\n}\n.tabbody > .this {\n  display: flex;\n}\n.tabbody > * > .titlebar {\n  display: flex;\n}\n.tabbody > * > .titlebar > * {\n  flex: 1;\n  margin: 1px 1px 1px 1px;\n}\n.tabbody > * > .context {\n  flex: 1;\n  flex-flow: column;\n  border: 0;\n  padding: 5px;\n  overflow-y: auto;\n}\n.tabbody > * > .inputbox {\n  border: 0;\n  border-bottom: #a9a9a9 solid 1px;\n  height: 15px;\n  text-align: center;\n}\n.link > * {\n  display: flex;\n  margin: 5px;\n  border-bottom: 1px solid;\n  text-decoration: none;\n}\n#RuleList > label > * {\n  background: #eee;\n  padding-left: 3px;\n  margin: 2px 0;\n  cursor: pointer;\n}\n#RuleList input[type=\"radio\"] {\n  display: none;\n}\n#RuleList input[type=\"radio\"]:checked + * {\n  background: #15cda8;\n}\n.isError {\n  color: #ff0000;\n}\n"
  },
  {
    "path": "web/public/bookSourceDebug/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta charset=\"UTF-8\">\n    <title>Reader书源编辑器</title>\n    <link rel=\"icon\" href=\"../favicon.ico\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"index.css\"/>\n</head>\n\n<body>\n<div class=\"editor\">\n    <div class=\"setbox\">\n        <div>\n            <a href=\"../index.html\">←主页</a>\n            <b>书源</b>\n        </div>\n        <div class=\"rules\">\n            <div><b>基本</b></div>\n            <div>\n                <div>源域名　:</div>\n                <textarea rows=\"1\" id=\"bookSourceUrl\" class=\"base\" title=\"bookSourceUrl\"\n                          placeholder=\"<必填>通常填写网站主页,例: https://www.qidian.com\"></textarea>\n            </div>\n            <div>\n                <div>源类型　:</div>\n                <textarea rows=\"1\" id=\"bookSourceType\" class=\"base\" title=\"bookSourceType\"\n                          placeholder=\"&lt;必填&gt;0:文本 1:音频 2:图片 3:文件(只提供下载的网站)\"></textarea>\n            </div>\n            <div>\n                <div>源名称　:</div>\n                <textarea rows=\"1\" id=\"bookSourceName\" class=\"base\" title=\"bookSourceName\"\n                          placeholder=\"&lt;必填&gt;会显示在源列表\"></textarea>\n            </div>\n            <div>\n                <div>源分组　:</div>\n                <textarea rows=\"1\" id=\"bookSourceGroup\" class=\"base\" title=\"bookSourceGroup\"\n                          placeholder=\"&lt;选填&gt;描述源的特征信息\"></textarea>\n            </div>\n            <div>\n                <div>源注释　:</div>\n                <textarea rows=\"1\" id=\"bookSourceComment\" class=\"base\" title=\"bookSourceComment\"\n                          placeholder=\"&lt;选填&gt;描述源作者和状态\"></textarea>\n            </div>\n            <div style=\"display: none;\">\n                <div>登录地址:</div>\n                <textarea rows=\"1\" id=\"loginUrl\" class=\"base\" title=\"loginUrl\"\n                          placeholder=\"&lt;选填&gt;填写网站登录网址,仅在需要登录的源有用\"></textarea>\n            </div>\n            <div style=\"display: none;\">\n                <div>登录界面:</div>\n                <textarea rows=\"3\" id=\"loginUi\" class=\"base\" title=\"loginUi\"\n                          placeholder=\"&lt;选填&gt;自定义登录界面\"></textarea>\n            </div>\n            <div style=\"display: none;\">\n                <div>登录检测:</div>\n                <textarea rows=\"3\" id=\"loginCheckJs\" class=\"base\" title=\"loginCheckJs\"\n                          placeholder=\"&lt;选填&gt;登录检测js\"></textarea>\n            </div>\n            <div style=\"display: none;\">\n                <div>并发率　:</div>\n                <textarea rows=\"1\" id=\"concurrentRate\" class=\"base\" title=\"concurrentRate\"\n                          placeholder=\"&lt;选填&gt;并发率\"></textarea>\n            </div>\n            <div>\n                <div>请求头　:</div>\n                <textarea rows=\"3\" id=\"header\" class=\"base\" title=\"header\"\n                          placeholder=\"&lt;选填&gt;客户端标识\"></textarea>\n            </div>\n            <div>\n                <div>链接验证:</div>\n                <textarea rows=\"1\" id=\"bookUrlPattern\" class=\"base\" title=\"bookUrlPattern\"\n                          placeholder=\"&lt;选填&gt;当详情页URL与源URL的域名不一致时有效，用于添加网址\"></textarea>\n            </div>\n            <p></p>\n            <div><b>搜索</b></div>\n            <div>\n                <div>搜索地址:</div>\n                <textarea rows=\"1\" id=\"searchUrl\" class=\"base\" title=\"searchUrl\"\n                          placeholder=\"[域名可省略]/search.php@kw={{key}}\"></textarea>\n            </div>\n            <div style=\"display: none\">\n                <div>校验文字:</div>\n                <textarea rows=\"1\" id=\"ruleSearch_checkKeyWord\" class=\"ruleSearch\"\n                          title=\"checkKeyWord\"\n                          placeholder=\"校验关键字\"></textarea>\n            </div>\n            <div>\n                <div>列表规则:</div>\n                <textarea rows=\"1\" id=\"ruleSearch_bookList\" class=\"ruleSearch\" title=\"bookList\"\n                          placeholder=\"选择书籍节点 (规则结果为List&lt;Element&gt;)\"></textarea>\n            </div>\n            <div>\n                <div>书名规则:</div>\n                <textarea rows=\"1\" id=\"ruleSearch_name\" class=\"ruleSearch\" title=\"name\"\n                          placeholder=\"选择节点书名 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>作者规则:</div>\n                <textarea rows=\"1\" id=\"ruleSearch_author\" class=\"ruleSearch\" title=\"author\"\n                          placeholder=\"选择节点作者 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>分类规则:</div>\n                <textarea rows=\"1\" id=\"ruleSearch_kind\" class=\"ruleSearch\" title=\"kind\"\n                          placeholder=\"选择节点分类信息 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>字数规则:</div>\n                <textarea rows=\"1\" id=\"ruleSearch_wordCount\" class=\"ruleSearch\" title=\"wordCount\"\n                          placeholder=\"选择节点字数信息 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>最新章节:</div>\n                <textarea rows=\"1\" id=\"ruleSearch_lastChapter\" class=\"ruleSearch\"\n                          title=\"lastChapter\"\n                          placeholder=\"选择节点最新章节 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>简介规则:</div>\n                <textarea rows=\"1\" id=\"ruleSearch_intro\" class=\"ruleSearch\" title=\"intro\"\n                          placeholder=\"选择节点书籍简介 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>封面规则:</div>\n                <textarea rows=\"1\" id=\"ruleSearch_coverUrl\" class=\"ruleSearch\" title=\"coverUrl\"\n                          placeholder=\"选择节点书籍封面 (规则结果为String类型的url)\"></textarea>\n            </div>\n            <div>\n                <div>详情地址:</div>\n                <textarea rows=\"1\" id=\"ruleSearch_bookUrl\" class=\"ruleSearch\" title=\"bookUrl\"\n                          placeholder=\"选择书籍详情页网址 (规则结果为String类型的url)\"></textarea>\n            </div>\n            <p></p>\n            <div><b>发现</b></div>\n            <div>\n                <div>发现地址:</div>\n                <textarea rows=\"6\" id=\"exploreUrl\" class=\"base\" title=\"exploreUrl\"\n                          placeholder=\"内容能显示在发现菜单&#10;每行一条发现分类(网址域名可省略)，例：&#10;名称1::网址(Url)1&#10;名称2::网址(Url)2&#10;...\"></textarea>\n            </div>\n            <div>\n                <div>列表规则:</div>\n                <textarea rows=\"1\" id=\"ruleExplore_bookList\" class=\"ruleExplore\" title=\"bookList\"\n                          placeholder=\"选择书籍节点 (规则结果为List&lt;Element&gt;)\"></textarea>\n            </div>\n            <div>\n                <div>书名规则:</div>\n                <textarea rows=\"1\" id=\"ruleExplore_name\" class=\"ruleExplore\" title=\"name\"\n                          placeholder=\"选择节点书名 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>作者规则:</div>\n                <textarea rows=\"1\" id=\"ruleExplore_author\" class=\"ruleExplore\" title=\"author\"\n                          placeholder=\"选择节点作者 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>分类规则:</div>\n                <textarea rows=\"1\" id=\"ruleExplore_kind\" class=\"ruleExplore\" title=\"kind\"\n                          placeholder=\"选择节点分类信息 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>字数规则:</div>\n                <textarea rows=\"1\" id=\"ruleExplore_wordCount\" class=\"ruleExplore\" title=\"wordCount\"\n                          placeholder=\"选择节点字数信息 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>最新章节:</div>\n                <textarea rows=\"1\" id=\"ruleExplore_lastChapter\" class=\"ruleExplore\"\n                          title=\"lastChapter\"\n                          placeholder=\"选择节点最新章节 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>简介规则:</div>\n                <textarea rows=\"1\" id=\"ruleExplore_intro\" class=\"ruleExplore\" title=\"intro\"\n                          placeholder=\"选择节点书籍简介 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>封面规则:</div>\n                <textarea rows=\"1\" id=\"ruleExplore_coverUrl\" class=\"ruleExplore\" title=\"coverUrl\"\n                          placeholder=\"选择节点书籍封面 (规则结果为String类型的url)\"></textarea>\n            </div>\n            <div>\n                <div>详情地址:</div>\n                <textarea rows=\"1\" id=\"ruleExplore_bookUrl\" class=\"ruleExplore\" title=\"bookUrl\"\n                          placeholder=\"选择书籍详情页网址 (规则结果为String类型的url)\"></textarea>\n            </div>\n            <p></p>\n            <div><b>详情</b></div>\n            <div>\n                <div>预处理　:</div>\n                <textarea rows=\"3\" id=\"ruleBookInfo_init\" class=\"ruleBookInfo\" title=\"init\"\n                          placeholder=\"用于加速详情信息检索，只支持AllInOne规则\"></textarea>\n            </div>\n            <div>\n                <div>书名规则:</div>\n                <textarea rows=\"1\" id=\"ruleBookInfo_name\" class=\"ruleBookInfo\" title=\"name\"\n                          placeholder=\"选择节点书名 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>作者规则:</div>\n                <textarea rows=\"1\" id=\"ruleBookInfo_author\" class=\"ruleBookInfo\" title=\"author\"\n                          placeholder=\"选择节点作者 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>分类规则:</div>\n                <textarea rows=\"1\" id=\"ruleBookInfo_kind\" class=\"ruleBookInfo\" title=\"kind\"\n                          placeholder=\"选择节点分类信息 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>字数规则:</div>\n                <textarea rows=\"1\" id=\"ruleBookInfo_wordCount\" class=\"ruleBookInfo\"\n                          title=\"wordCount\"\n                          placeholder=\"选择节点字数信息 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>最新章节:</div>\n                <textarea rows=\"1\" id=\"ruleBookInfo_lastChapter\" class=\"ruleBookInfo\"\n                          title=\"lastChapter\"\n                          placeholder=\"选择节点最新章节 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>简介规则:</div>\n                <textarea rows=\"1\" id=\"ruleBookInfo_intro\" class=\"ruleBookInfo\" title=\"intro\"\n                          placeholder=\"选择节点书籍简介 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>封面规则:</div>\n                <textarea rows=\"1\" id=\"ruleBookInfo_coverUrl\" class=\"ruleBookInfo\" title=\"coverUrl\"\n                          placeholder=\"选择节点书籍封面 (规则结果为String类型的url)\"></textarea>\n            </div>\n            <div>\n                <div>目录地址:</div>\n                <textarea rows=\"1\" id=\"ruleBookInfo_tocUrl\" class=\"ruleBookInfo\" title=\"tocUrl\"\n                          placeholder=\"选择书籍详情页网址 (规则结果为String类型的url, 与详情页相同时可省略)\"></textarea>\n            </div>\n            <p></p>\n            <div><b>目录</b></div>\n            <div>\n                <div>列表规则:</div>\n                <textarea rows=\"3\" id=\"ruleToc_chapterList\" class=\"ruleToc\" title=\"chapterList\"\n                          placeholder=\"选择目录列表的章节节点 (规则结果为List&lt;Element&gt;)\"></textarea>\n            </div>\n            <div>\n                <div>章节名称:</div>\n                <textarea rows=\"1\" id=\"ruleToc_chapterName\" class=\"ruleToc\" title=\"chapterName\"\n                          placeholder=\"选择章节名称 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>章节地址:</div>\n                <textarea rows=\"1\" id=\"ruleToc_chapterUrl\" class=\"ruleToc\" title=\"chapterUrl\"\n                          placeholder=\"选择章节链接 (规则结果为String类型的Url)\"></textarea>\n            </div>\n            <div>\n                <div>卷名标识:</div>\n                <textarea rows=\"1\" id=\"ruleToc_isVolume\" class=\"ruleToc\" title=\"isVolume\"\n                          placeholder=\"章节名称是否是卷名 (规则结果为Bool)\"></textarea>\n            </div>\n            <div>\n                <div>收费标识:</div>\n                <textarea rows=\"1\" id=\"ruleToc_isVip\" class=\"ruleToc\" title=\"isVip\"\n                          placeholder=\"章节是否为VIP章节 (规则结果为Bool)\"></textarea>\n            </div>\n            <div style=\"display: none;\">\n                <div>购买标识:</div>\n                <textarea rows=\"1\" id=\"ruleToc_isPay\" class=\"ruleToc\" title=\"isPay\"\n                          placeholder=\"章节是否为已购买 (规则结果为Bool)\"></textarea>\n            </div>\n            <div>\n                <div>章节信息:</div>\n                <textarea rows=\"1\" id=\"ruleToc_updateTime\" class=\"ruleToc\" title=\"updateTime\"\n                          placeholder=\"选择章节信息 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>翻页规则:</div>\n                <textarea rows=\"1\" id=\"ruleToc_nextTocUrl\" class=\"ruleToc\" title=\"nextTocUrl\"\n                          placeholder=\"选择目录下一页链接 (规则结果为List&lt;String&gt;或String)\"></textarea>\n            </div>\n            <p></p>\n            <div><b>正文</b></div>\n            <div>\n                <div>脚本注入:</div>\n                <textarea rows=\"3\" id=\"ruleContent_webJs\" class=\"ruleContent\" title=\"webJs\"\n                          placeholder=\"注入javascript，用于模拟鼠标点击等，必须有返回值，一般为String类型\"></textarea>\n            </div>\n            <div>\n                <div>正文规则:</div>\n                <textarea rows=\"1\" id=\"ruleContent_content\" class=\"ruleContent\" title=\"content\"\n                          placeholder=\"选择正文内容 (规则结果为String)\"></textarea>\n            </div>\n            <div>\n                <div>翻页规则:</div>\n                <textarea rows=\"1\" id=\"ruleContent_nextContentUrl\" class=\"ruleContent\"\n                          title=\"nextContentUrl\"\n                          placeholder=\"选择下一分页(不是下一章)链接 (规则结果为String类型的Url)\"></textarea>\n            </div>\n            <div>\n                <div>资源正则:</div>\n                <textarea rows=\"1\" id=\"ruleContent_sourceRegex\" class=\"ruleContent\"\n                          title=\"sourceRegex\"\n                          placeholder=\"匹配资源的url特征，用于嗅探\"></textarea>\n            </div>\n            <div>\n                <div>替换规则:</div>\n                <textarea rows=\"1\" id=\"ruleContent_replaceRegex\" class=\"ruleContent\"\n                          title=\"replaceRegex\"\n                          placeholder=\"多页内容合并后替换，用于正文净化\"></textarea>\n            </div>\n            <div>\n                <div>图片样式:</div>\n                <textarea rows=\"1\" id=\"ruleContent_imageStyle\" class=\"ruleContent\"\n                          title=\"imageStyle\"\n                          placeholder=\"FULL:铺满 不填:默认样式\"></textarea>\n            </div>\n            <div style=\"display: none;\">\n                <div>购买操作:</div>\n                <textarea rows=\"1\" id=\"ruleContent_payAction\" class=\"ruleContent\"\n                          title=\"payAction\"\n                          placeholder=\"购买章节 返回链接或js\"></textarea>\n            </div>\n            <p></p>\n            <div><b>其它规则</b></div>\n            <div>\n                <div>启用搜索:</div>\n                <textarea rows=\"1\" id=\"enabled\" class=\"base\" title=\"enabled\"\n                          placeholder=\"启用: true  关闭: false (可选,默认true)\"></textarea>\n            </div>\n            <div>\n                <div>启用发现:</div>\n                <textarea rows=\"1\" id=\"enabledExplore\" class=\"base\" title=\"enabledExplore\"\n                          placeholder=\"启用: true  关闭: false (可选,默认true)\"></textarea>\n            </div>\n            <div style=\"display: none;\">\n                <div>搜索权重:</div>\n                <textarea rows=\"1\" id=\"weight\" class=\"base\" title=\"weight\"\n                          placeholder=\"整数: 0~N (可选,默认0) | 数字越大越靠前\"></textarea>\n            </div>\n            <div style=\"display: none;\">\n                <div>排序编号:</div>\n                <textarea rows=\"1\" id=\"customOrder\" class=\"base\" title=\"customOrder\"\n                          placeholder=\"整数: 0~N (可选,默认0) | 数字越小越靠前\"></textarea>\n            </div>\n            <div style=\"display:none;\">\n                <div>更新时间:</div>\n                <textarea rows=\"1\" id=\"lastUpdateTime\" class=\"base\" title=\"lastUpdateTime\"\n                          placeholder=\"毫秒级时间戳 (自动生成) | 请勿手动填写\"></textarea>\n            </div>\n        </div>\n    </div>\n    <div class=\"menu\">\n        <svg class=\"button\">\n            <text x=\"50%\" y=\"55%\">⇈推送源</text>\n            <rect id=\"push\"></rect>\n        </svg>\n        <svg class=\"button\">\n            <text x=\"50%\" y=\"55%\">⇊拉取源</text>\n            <rect id=\"pull\"></rect>\n        </svg>\n        <svg class=\"button\">\n            <text x=\"50%\" y=\"55%\">⋘编辑源</text>\n            <rect id=\"editor\"></rect>\n        </svg>\n        <svg class=\"button\">\n            <text x=\"50%\" y=\"55%\">⋙生成源</text>\n            <rect id=\"conver\"></rect>\n        </svg>\n        <svg class=\"button\">\n            <text x=\"50%\" y=\"55%\">✗清空表单</text>\n            <rect id=\"initial\"></rect>\n        </svg>\n        <svg class=\"button\">\n            <text x=\"50%\" y=\"55%\">↶撤销操作</text>\n            <rect id=\"undo\"></rect>\n        </svg>\n        <svg class=\"button\">\n            <text x=\"50%\" y=\"55%\">↷重做操作</text>\n            <rect id=\"redo\"></rect>\n        </svg>\n        <svg class=\"button\">\n            <text x=\"50%\" y=\"55%\">⇏调试源</text>\n            <rect id=\"debug\"></rect>\n        </svg>\n        <svg class=\"button\">\n            <text x=\"50%\" y=\"55%\">✓保存源</text>\n            <rect id=\"accept\"></rect>\n        </svg>\n    </div>\n    <div class=\"outbox\">\n        <div class=\"tabbox\">\n            <div class=\"tabtitle\">\n                <div name=\"编辑源\" class=\"tab1 this\">编辑源</div>\n                <div name=\"调试源\" class=\"tab2\">调试源</div>\n                <div name=\"源列表\" class=\"tab3\">源列表</div>\n                <div name=\"帮助信息\" class=\"tab4\">帮助信息</div>\n            </div>\n            <div class=\"tabbody\">\n                <div class=\"tab1 this\">\n                        <textarea class=\"context\" id=\"RuleJsonString\"\n                                  placeholder=\"这里输出序列化的JSON数据,可直接导入'阅读'APP\"></textarea>\n                </div>\n                <div class=\"tab2\">\n                    <input type=\"text\" class=\"inputbox\" id=\"DebugKey\" placeholder=\"输入搜索关键字，默认搜「我的」\">\n                    <textarea class=\"context\" id=\"DebugConsole\" placeholder=\"这里用于输出调试信息\"></textarea>\n                </div>\n                <div class=\"tab3\">\n                    <input type=\"text\" class=\"inputbox\" id=\"Filter\"\n                           placeholder=\"输入筛选关键词（源名称、源URL或源分组）后按回车筛选源\">\n                    <div class=\"titlebar\">\n                        <button id=\"Import\">导入源文件</button>\n                        <button id=\"Export\">导出源文件</button>\n                        <button id=\"Delete\">删除选中源</button>\n                        <button id=\"ClrAll\">清空列表</button>\n                    </div>\n                    <div class=\"context\" id=\"RuleList\"></div>\n                </div>\n                <div class=\"tab4\">\n                    <div class=\"context link\">\n                        <a target=\"_blank\" href=\"https://alanskycn.gitee.io/teachme\">源制作教程</a>\n                        <a target=\"_blank\"\n                           href=\"https://zhuanlan.zhihu.com/p/29436838\">Xpath基础教程</a>\n                        <a target=\"_blank\"\n                           href=\"https://zhuanlan.zhihu.com/p/32187820\">Xpath高级教程</a>\n                        <a target=\"_blank\" href=\"https://www.w3cschool.cn/regex_rmjc\">正则表达式教程</a>\n                        <a target=\"_blank\" href=\"https://regexr.com\">正则表达式在线验证工具</a>\n                        <div>^$()[]{}.?+*| 这些是Java正则特殊符号,匹配需转义\n                            <br>(?s) 前缀表示跨行解析\n                            <br>(?m) 前缀表示逐行匹配\n                            <br>(?i) 前缀表示忽略大小写\n                        </div>\n                        <a target=\"_blank\" href=\"https://www.browxy.com/\">代码在线运行工具</a>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n<script type=\"text/javascript\" src=\"index.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "web/public/bookSourceDebug/index.js",
    "content": "/* eslint-disable no-inner-declarations */\n/* eslint-disable no-case-declarations */\n// 简化js原生选择器\nfunction $(selector) {\n  return document.querySelector(selector);\n}\nfunction $$(selector) {\n  return document.querySelectorAll(selector);\n}\n// 读写Hash值(val未赋值时为读取)\nfunction hashParam(key, val) {\n  let hashstr = decodeURIComponent(window.location.hash);\n  let regKey = new RegExp(`${key}=([^&]*)`);\n  let getVal = regKey.test(hashstr) ? hashstr.match(regKey)[1] : null;\n  if (val == undefined) return getVal;\n  if (hashstr == \"\" || hashstr == \"#\") {\n    window.location.hash = `#${key}=${val}`;\n  } else {\n    if (getVal) window.location.hash = hashstr.replace(getVal, val);\n    else {\n      window.location.hash =\n        hashstr.indexOf(key) > -1\n          ? hashstr.replace(regKey, `${key}=${val}`)\n          : `${hashstr}&${key}=${val}`;\n    }\n  }\n}\n// 创建源规则容器对象\nfunction Container() {\n  let ruleJson = {};\n  let searchJson = {};\n  let exploreJson = {};\n  let bookInfoJson = {};\n  let tocJson = {};\n  let contentJson = {};\n\n  // 基本以及其他\n  $$(\".rules .base\").forEach((item) => (ruleJson[item.title] = \"\"));\n  ruleJson.lastUpdateTime = 0;\n  ruleJson.customOrder = 0;\n  ruleJson.weight = 0;\n  ruleJson.enabled = true;\n  ruleJson.enabledExplore = true;\n\n  // 搜索规则\n  $$(\".rules .ruleSearch\").forEach((item) => (searchJson[item.title] = \"\"));\n  ruleJson.ruleSearch = searchJson;\n\n  // 发现规则\n  $$(\".rules .ruleExplore\").forEach((item) => (exploreJson[item.title] = \"\"));\n  ruleJson.ruleExplore = exploreJson;\n\n  // 详情页规则\n  $$(\".rules .ruleBookInfo\").forEach((item) => (bookInfoJson[item.title] = \"\"));\n  ruleJson.ruleBookInfo = bookInfoJson;\n\n  // 目录规则\n  $$(\".rules .ruleToc\").forEach((item) => (tocJson[item.title] = \"\"));\n  ruleJson.ruleToc = tocJson;\n\n  // 正文规则\n  $$(\".rules .ruleContent\").forEach((item) => (contentJson[item.title] = \"\"));\n  ruleJson.ruleContent = contentJson;\n\n  return ruleJson;\n}\n// 选项卡Tab切换事件处理\nfunction showTab(tabName) {\n  $$(\".tabtitle>*\").forEach((node) => {\n    node.className = node.className.replace(\" this\", \"\");\n  });\n  $$(\".tabbody>*\").forEach((node) => {\n    node.className = node.className.replace(\" this\", \"\");\n  });\n  $(`.tabbody>.${$(`.tabtitle>*[name=${tabName}]`).className}`).className +=\n    \" this\";\n  $(`.tabtitle>*[name=${tabName}]`).className += \" this\";\n  hashParam(\"tab\", tabName);\n}\n// 源列表列表标签构造函数\nfunction newRule(rule) {\n  return `<label for=\"${rule.bookSourceUrl}\"><input type=\"radio\" name=\"rule\" id=\"${rule.bookSourceUrl}\"><div>${rule.bookSourceName}<br>${rule.bookSourceUrl}</div></label>`;\n}\n// 缓存规则列表\nvar RuleSources = [];\nif (localStorage.getItem(\"debug@BookSources\")) {\n  RuleSources = JSON.parse(localStorage.getItem(\"debug@BookSources\"));\n  RuleSources.forEach((item) => ($(\"#RuleList\").innerHTML += newRule(item)));\n}\n// 页面加载完成事件\nwindow.onload = () => {\n  $$(\".tabtitle>*\").forEach((item) => {\n    item.addEventListener(\"click\", () => {\n      showTab(item.innerHTML);\n    });\n  });\n  if (hashParam(\"tab\")) showTab(hashParam(\"tab\"));\n};\n// 获取数据\nfunction HttpGet(url) {\n  return fetch(hashParam(\"domain\") ? hashParam(\"domain\") + url : url, {\n    mode: \"cors\",\n    credentials: \"include\"\n  })\n    .then((res) => res.json())\n    .catch((err) => console.error(\"Error:\", err));\n}\n// 提交数据\nfunction HttpPost(url, data) {\n  return fetch(hashParam(\"domain\") ? hashParam(\"domain\") + url : url, {\n    body: JSON.stringify(data),\n    method: \"POST\",\n    mode: \"cors\",\n    credentials: \"include\",\n    headers: new Headers({\n      \"Content-Type\": \"application/json;charset=utf-8\"\n    })\n  })\n    .then((res) => res.json())\n    .catch((err) => console.error(\"Error:\", err));\n}\n// 将源表单转化为源对象\nfunction rule2json() {\n  let RuleJSON = Container();\n  // 转换base\n  Object.keys(RuleJSON).forEach((key) => {\n    if (!key.startsWith(\"rule\")) {\n      RuleJSON[key] = $(\"#\" + key).value;\n    }\n  });\n\n  // 转换搜索规则\n  let searchJson = {};\n  Object.keys(RuleJSON.ruleSearch).forEach((key) => {\n    if ($(\"#\" + \"ruleSearch_\" + key).value)\n      searchJson[key] = $(\"#\" + \"ruleSearch_\" + key).value;\n  });\n  RuleJSON.ruleSearch = searchJson;\n\n  // 转换发现规则\n  let exploreJson = {};\n  Object.keys(RuleJSON.ruleExplore).forEach((key) => {\n    if ($(\"#\" + \"ruleExplore_\" + key).value)\n      exploreJson[key] = $(\"#\" + \"ruleExplore_\" + key).value;\n  });\n  RuleJSON.ruleExplore = exploreJson;\n\n  // 转换详情页规则\n  let bookInfoJson = {};\n  Object.keys(RuleJSON.ruleBookInfo).forEach((key) => {\n    if ($(\"#\" + \"ruleBookInfo_\" + key).value)\n      bookInfoJson[key] = $(\"#\" + \"ruleBookInfo_\" + key).value;\n  });\n  RuleJSON.ruleBookInfo = bookInfoJson;\n\n  // 转换目录规则\n  let tocJson = {};\n  Object.keys(RuleJSON.ruleToc).forEach((key) => {\n    if ($(\"#\" + \"ruleToc_\" + key).value)\n      tocJson[key] = $(\"#\" + \"ruleToc_\" + key).value;\n  });\n  RuleJSON.ruleToc = tocJson;\n\n  // 转换正文规则\n  let contentJson = {};\n  Object.keys(RuleJSON.ruleContent).forEach((key) => {\n    if ($(\"#\" + \"ruleContent_\" + key).value)\n      contentJson[key] = $(\"#\" + \"ruleContent_\" + key).value;\n  });\n  RuleJSON.ruleContent = contentJson;\n\n  RuleJSON.lastUpdateTime = new Date().getTime();\n  RuleJSON.customOrder =\n    RuleJSON.customOrder == \"\" ? 0 : parseInt(RuleJSON.customOrder);\n  RuleJSON.weight = RuleJSON.weight == \"\" ? 0 : parseInt(RuleJSON.weight);\n  RuleJSON.bookSourceType =\n    RuleJSON.bookSourceType == \"\" ? 0 : parseInt(RuleJSON.bookSourceType);\n  RuleJSON.enabled =\n    RuleJSON.enabled == \"\" ||\n    String(RuleJSON.enabled)\n      .toLocaleLowerCase()\n      .replace(/^\\s*|\\s*$/g, \"\") == \"true\";\n  RuleJSON.enabledExplore =\n    RuleJSON.enabledExplore == \"\" ||\n    String(RuleJSON.enabledExplore)\n      .toLocaleLowerCase()\n      .replace(/^\\s*|\\s*$/g, \"\") == \"true\";\n  return RuleJSON;\n}\n// 将源对象填充到源表单\nfunction json2rule(RuleEditor) {\n  let RuleJSON = Container();\n  // 转换base\n  Object.keys(RuleJSON).forEach((key) => {\n    if (!key.startsWith(\"rule\")) {\n      let val = RuleEditor[key];\n      if (typeof val == \"number\") {\n        $(\"#\" + key).value = val ? String(val) : \"0\";\n      } else if (typeof val == \"boolean\") {\n        $(\"#\" + key).value = val ? String(val) : \"false\";\n      } else {\n        $(\"#\" + key).value = val ? String(val) : \"\";\n      }\n    }\n  });\n\n  // 转换搜索规则\n  if (RuleEditor.ruleSearch) {\n    let searchJson = RuleEditor.ruleSearch;\n    Object.keys(RuleJSON.ruleSearch).forEach((key) => {\n      $(\"#\" + \"ruleSearch_\" + key).value = searchJson[key]\n        ? searchJson[key]\n        : \"\";\n    });\n  }\n\n  // 转换发现规则\n  if (RuleEditor.ruleExplore) {\n    let exploreJson = RuleEditor.ruleExplore;\n    Object.keys(RuleJSON.ruleExplore).forEach((key) => {\n      $(\"#\" + \"ruleExplore_\" + key).value = exploreJson[key]\n        ? exploreJson[key]\n        : \"\";\n    });\n  }\n\n  // 转换详情页规则\n  if (RuleEditor.ruleBookInfo) {\n    let bookInfoJson = RuleEditor.ruleBookInfo;\n    Object.keys(RuleJSON.ruleBookInfo).forEach((key) => {\n      $(\"#\" + \"ruleBookInfo_\" + key).value = bookInfoJson[key]\n        ? bookInfoJson[key]\n        : \"\";\n    });\n  }\n\n  // 转换目录规则\n  if (RuleEditor.ruleToc) {\n    let tocJson = RuleEditor.ruleToc;\n    Object.keys(RuleJSON.ruleToc).forEach((key) => {\n      $(\"#\" + \"ruleToc_\" + key).value = tocJson[key] ? tocJson[key] : \"\";\n    });\n  }\n\n  // 转换正文规则\n  if (RuleEditor.ruleContent) {\n    let contentJson = RuleEditor.ruleContent;\n    Object.keys(RuleJSON.ruleContent).forEach((key) => {\n      $(\"#\" + \"ruleContent_\" + key).value = contentJson[key]\n        ? contentJson[key]\n        : \"\";\n    });\n  }\n}\n// 记录操作过程\nvar course = { old: [], now: {}, new: [] };\nif (localStorage.getItem(\"debug@bookSourceRecord\")) {\n  course = JSON.parse(localStorage.getItem(\"debug@bookSourceRecord\"));\n  json2rule(course.now);\n} else {\n  course.now = rule2json();\n  window.localStorage.setItem(\"debug@bookSourceRecord\", JSON.stringify(course));\n}\nfunction todo() {\n  course.old.push(Object.assign({}, course.now));\n  course.now = rule2json();\n  course.new = [];\n  if (course.old.length > 50) course.old.shift(); // 限制历史记录堆栈大小\n  localStorage.setItem(\"debug@bookSourceRecord\", JSON.stringify(course));\n}\nfunction undo() {\n  course = JSON.parse(localStorage.getItem(\"debug@bookSourceRecord\"));\n  if (course.old.length > 0) {\n    course.new.push(course.now);\n    course.now = course.old.pop();\n    localStorage.setItem(\"debug@bookSourceRecord\", JSON.stringify(course));\n    json2rule(course.now);\n  }\n}\nfunction redo() {\n  course = JSON.parse(localStorage.getItem(\"debug@bookSourceRecord\"));\n  if (course.new.length > 0) {\n    course.old.push(course.now);\n    course.now = course.new.pop();\n    localStorage.setItem(\"debug@bookSourceRecord\", JSON.stringify(course));\n    json2rule(course.now);\n  }\n}\nfunction setRule(editRule) {\n  let checkRule = RuleSources.find(\n    (x) => x.bookSourceUrl == editRule.bookSourceUrl\n  );\n  if ($(`input[id=\"${editRule.bookSourceUrl}\"]`)) {\n    Object.keys(checkRule).forEach((key) => {\n      checkRule[key] = editRule[key];\n    });\n    $(\n      `input[id=\"${editRule.bookSourceUrl}\"]+*`\n    ).innerHTML = `${editRule.bookSourceName}<br>${editRule.bookSourceUrl}`;\n  } else {\n    RuleSources.push(editRule);\n    $(\"#RuleList\").innerHTML += newRule(editRule);\n  }\n}\n$$(\"input\").forEach((item) => {\n  item.addEventListener(\"change\", () => {\n    todo();\n  });\n});\n$$(\"textarea\").forEach((item) => {\n  item.addEventListener(\"change\", () => {\n    todo();\n  });\n});\n// 处理按钮点击事件\n$(\".menu\").addEventListener(\"click\", (e) => {\n  let thisNode = e.target;\n  thisNode =\n    thisNode.parentNode.nodeName == \"svg\"\n      ? thisNode.parentNode.querySelector(\"rect\")\n      : thisNode.nodeName == \"svg\"\n      ? thisNode.querySelector(\"rect\")\n      : null;\n  if (!thisNode) return;\n  if (thisNode.getAttribute(\"class\") == \"busy\") return;\n  thisNode.setAttribute(\"class\", \"busy\");\n  switch (thisNode.id) {\n    case \"push\":\n      $$(\"#RuleList>label>div\").forEach((item) => {\n        item.className = \"\";\n      });\n      (async () => {\n        await HttpPost(`/saveBookSources`, RuleSources)\n          .then((json) => {\n            if (json.isSuccess) {\n              let okData = json.data;\n              if (Array.isArray(okData)) {\n                let failMsg = ``;\n                if (RuleSources.length > okData.length) {\n                  RuleSources.forEach((item) => {\n                    if (\n                      okData.find((x) => x.bookSourceUrl == item.bookSourceUrl)\n                    ) {\n                      //\n                    } else {\n                      $(`#RuleList #${item.bookSourceUrl}+*`).className +=\n                        \"isError\";\n                    }\n                  });\n                  failMsg = \"\\n推送失败的源将用红色字体标注!\";\n                }\n                alert(\n                  `批量推送源到「Reader」\\n共计: ${\n                    RuleSources.length\n                  } 条\\n成功: ${okData.length} 条\\n失败: ${RuleSources.length -\n                    okData.length} 条${failMsg}`\n                );\n              } else {\n                alert(\n                  `批量推送源到「Reader」成功!\\n共计: ${RuleSources.length} 条`\n                );\n              }\n            } else {\n              alert(`批量推送源失败!\\nErrorMsg: ${json.errorMsg}`);\n            }\n          })\n          .catch((err) => {\n            alert(`批量推送源失败,无法连接到「Reader」!\\n${err}`);\n          });\n        thisNode.setAttribute(\"class\", \"\");\n      })();\n      return;\n    case \"pull\":\n      showTab(\"源列表\");\n      (async () => {\n        await HttpGet(`/getBookSources`)\n          .then((json) => {\n            if (json.isSuccess) {\n              $(\"#RuleList\").innerHTML = \"\";\n              localStorage.setItem(\n                \"debug@BookSources\",\n                JSON.stringify((RuleSources = json.data))\n              );\n              RuleSources.forEach((item) => {\n                $(\"#RuleList\").innerHTML += newRule(item);\n              });\n              alert(`成功拉取 ${RuleSources.length} 条源`);\n            } else {\n              alert(`批量拉取源失败!\\nErrorMsg: ${json.errorMsg}`);\n            }\n          })\n          .catch((err) => {\n            alert(`批量拉取源失败,无法连接到「Reader」!\\n${err}`);\n          });\n        thisNode.setAttribute(\"class\", \"\");\n      })();\n      return;\n    case \"editor\":\n      if ($(\"#RuleJsonString\").value == \"\") break;\n      try {\n        json2rule(JSON.parse($(\"#RuleJsonString\").value));\n        todo();\n      } catch (error) {\n        console.log(error);\n        alert(error);\n      }\n      break;\n    case \"conver\":\n      showTab(\"编辑源\");\n      $(\"#RuleJsonString\").value = JSON.stringify(rule2json(), null, 4);\n      break;\n    case \"initial\":\n      $$(\".rules textarea\").forEach((item) => {\n        item.value = \"\";\n      });\n      todo();\n      break;\n    case \"undo\":\n      undo();\n      break;\n    case \"redo\":\n      redo();\n      break;\n    case \"debug\":\n      showTab(\"调试源\");\n      // let wsOrigin = (hashParam(\"domain\") || location.origin)\n      //   .replace(/^.*?:/, \"ws:\")\n      //   .replace(/\\d+$/, port => parseInt(port) + 1);\n      let DebugInfos = $(\"#DebugConsole\");\n      function DebugPrint(msg) {\n        DebugInfos.value += `\\n${msg}`;\n        DebugInfos.scrollTop = DebugInfos.scrollHeight;\n      }\n      let saveRule = [rule2json()];\n      HttpPost(`/saveBookSources`, saveRule)\n        .then((sResult) => {\n          if (sResult.isSuccess) {\n            let sKey = $(\"#DebugKey\").value ? $(\"#DebugKey\").value : \"我的\";\n            $(\n              \"#DebugConsole\"\n            ).value = `源《${saveRule[0].bookSourceName}》保存成功！使用搜索关键字“${sKey}”开始调试...`;\n\n            let url =\n              (hashParam(\"domain\") || location.origin) +\n              \"/bookSourceDebugSSE?bookSourceUrl=\" +\n              encodeURIComponent(saveRule[0].bookSourceUrl) +\n              \"&keyword=\" +\n              encodeURIComponent(sKey);\n\n            const tryClose = () => {\n              try {\n                thisNode.setAttribute(\"class\", \"\");\n                if (\n                  window.searchEventSource &&\n                  window.searchEventSource.readyState !=\n                    window.searchEventSource.CLOSED\n                ) {\n                  window.searchEventSource.close();\n                }\n                window.searchEventSource = null;\n              } catch (error) {\n                //\n              }\n            };\n            window.searchEventSource = new EventSource(url, {\n              withCredentials: true\n            });\n            window.searchEventSource.addEventListener(\"error\", e => {\n              tryClose();\n              try {\n                if (e.data) {\n                  const result = JSON.parse(e.data);\n                  if (result && result.errorMsg) {\n                    DebugPrint(result.errorMsg);\n                  }\n                }\n              } catch (error) {\n                //\n              }\n            });\n            window.searchEventSource.addEventListener(\"end\", () => {\n              tryClose();\n            });\n            window.searchEventSource.addEventListener(\"message\", e => {\n              try {\n                if (e.data) {\n                  const result = JSON.parse(e.data);\n                  if (result.msg) {\n                    DebugPrint(result.msg);\n                  }\n                }\n              } catch (error) {\n                //\n              }\n            });\n            // let ws = new WebSocket(`${wsOrigin}/bookSourceDebug`);\n            // ws.onopen = () => {\n            //   ws.send(\n            //     `{\"tag\":\"${saveRule[0].bookSourceUrl}\", \"key\":\"${sKey}\"}`\n            //   );\n            // };\n            // ws.onmessage = msg => {\n            //   console.log(\"[调试]\", msg);\n            //   DebugPrint(msg.data);\n            // };\n            // ws.onerror = err => {\n            //   throw `${err.data}`;\n            // };\n            // ws.onclose = () => {\n            //   thisNode.setAttribute(\"class\", \"\");\n            //   DebugPrint(`\\n调试服务已关闭!`);\n            // };\n          } else throw `${sResult.errorMsg}`;\n        })\n        .catch((err) => {\n          DebugPrint(`调试过程意外中止，以下是详细错误信息:\\n${err}`);\n          thisNode.setAttribute(\"class\", \"\");\n        });\n      return;\n    case \"accept\":\n      (async () => {\n        let saveRule = [rule2json()];\n        await HttpPost(`/saveBookSource`, saveRule[0])\n          .then((json) => {\n            alert(\n              json.isSuccess\n                ? `源《${saveRule[0].bookSourceName}》已成功保存到「Reader」`\n                : `源《${saveRule[0].bookSourceName}》保存失败!\\nErrorMsg: ${json.errorMsg}`\n            );\n            setRule(saveRule[0]);\n          })\n          .catch((err) => {\n            alert(`保存源失败,无法连接到「Reader」!\\n${err}`);\n          });\n        thisNode.setAttribute(\"class\", \"\");\n      })();\n      return;\n    default:\n  }\n  setTimeout(() => {\n    thisNode.setAttribute(\"class\", \"\");\n  }, 500);\n});\n$(\"#DebugKey\").addEventListener(\"keydown\", (e) => {\n  if (e.keyCode == 13) {\n    let clickEvent = document.createEvent(\"MouseEvents\");\n    clickEvent.initEvent(\"click\", true, false);\n    $(\"#debug\").dispatchEvent(clickEvent);\n  }\n});\n$(\"#Filter\").addEventListener(\"keydown\", (e) => {\n  if (e.keyCode == 13) {\n    let cashList = [];\n    $(\"#RuleList\").innerHTML = \"\";\n    let sKey = $(\"#Filter\").value ? $(\"#Filter\").value : \"\";\n    if (sKey == \"\") {\n      cashList = RuleSources;\n    } else {\n      let patt = new RegExp(sKey);\n      RuleSources.forEach((source) => {\n        if (\n          patt.test(source.bookSourceUrl) ||\n          patt.test(source.bookSourceName) ||\n          patt.test(source.bookSourceGroup)\n        ) {\n          cashList.push(source);\n        }\n      });\n    }\n    cashList.forEach((source) => {\n      $(\"#RuleList\").innerHTML += newRule(source);\n    });\n  }\n});\n\n// 列表规则更改事件\n$(\"#RuleList\").addEventListener(\"click\", (e) => {\n  let editRule = null;\n  if (e.target && e.target.getAttribute(\"name\") == \"rule\") {\n    editRule = rule2json();\n    json2rule(RuleSources.find((x) => x.bookSourceUrl == e.target.id));\n  } else return;\n  if (editRule.bookSourceUrl == \"\") return;\n  if (editRule.bookSourceName == \"\")\n    editRule.bookSourceName = editRule.bookSourceUrl.replace(\n      /.*?\\/\\/|\\/.*/g,\n      \"\"\n    );\n  setRule(editRule);\n  localStorage.setItem(\"debug@BookSources\", JSON.stringify(RuleSources));\n});\n// 处理列表按钮事件\n$(\".tab3>.titlebar\").addEventListener(\"click\", (e) => {\n  let thisNode = e.target;\n  if (thisNode.nodeName != \"BUTTON\") return;\n  switch (thisNode.id) {\n    case \"Import\":\n      let fileImport = document.createElement(\"input\");\n      fileImport.type = \"file\";\n      fileImport.accept = \".json\";\n      fileImport.addEventListener(\n        \"change\",\n        () => {\n          let file = fileImport.files[0];\n          let reader = new FileReader();\n          reader.onloadend = function(evt) {\n            if (evt.target.readyState == FileReader.DONE) {\n              let fileText = evt.target.result;\n              try {\n                let fileJson = JSON.parse(fileText);\n                let newSources = [];\n                newSources.push(...fileJson);\n                if (\n                  window.confirm(\n                    `如何处理导入的源?\\n\"确定\": 覆盖当前列表(不会删除APP源)\\n\"取消\": 插入列表尾部(自动忽略重复源)`\n                  )\n                ) {\n                  localStorage.setItem(\n                    \"debug@BookSources\",\n                    JSON.stringify((RuleSources = newSources))\n                  );\n                  $(\"#RuleList\").innerHTML = \"\";\n                  RuleSources.forEach((item) => {\n                    $(\"#RuleList\").innerHTML += newRule(item);\n                  });\n                } else {\n                  newSources = newSources.filter(\n                    (item) =>\n                      !JSON.stringify(RuleSources).includes(item.bookSourceUrl)\n                  );\n                  RuleSources.push(...newSources);\n                  localStorage.setItem(\n                    \"debug@BookSources\",\n                    JSON.stringify(RuleSources)\n                  );\n                  newSources.forEach((item) => {\n                    $(\"#RuleList\").innerHTML += newRule(item);\n                  });\n                }\n                alert(`成功导入 ${newSources.length} 条源`);\n              } catch (err) {\n                alert(`导入源文件失败!\\n${err}`);\n              }\n            }\n          };\n          reader.readAsText(file);\n        },\n        false\n      );\n      fileImport.click();\n      break;\n    case \"Export\":\n      let fileExport = document.createElement(\"a\");\n      fileExport.download = `Rules${Date()\n        .replace(/.*?\\s(\\d+)\\s(\\d+)\\s(\\d+:\\d+:\\d+).*/, \"$2$1$3\")\n        .replace(/:/g, \"\")}.json`;\n      let myBlob = new Blob([JSON.stringify(RuleSources, null, 4)], {\n        type: \"application/json\",\n      });\n      fileExport.href = window.URL.createObjectURL(myBlob);\n      fileExport.click();\n      break;\n    case \"Delete\":\n      let selectRule = $(\"#RuleList input:checked\");\n      if (!selectRule) {\n        alert(`没有源被选中!`);\n        return;\n      }\n      if (confirm(`确定要删除选定源吗?\\n(同时删除APP内源)`)) {\n        let selectRuleUrl = selectRule.id;\n        let deleteSources = RuleSources.filter(\n          (item) => item.bookSourceUrl == selectRuleUrl\n        ); // 提取待删除的源\n        let laveSources = RuleSources.filter(\n          (item) => !(item.bookSourceUrl == selectRuleUrl)\n        ); // 提取待留下的源\n        HttpPost(`/deleteBookSources`, deleteSources)\n          .then((json) => {\n            if (json.isSuccess) {\n              let selectNode = document.getElementById(selectRuleUrl)\n                .parentNode;\n              selectNode.parentNode.removeChild(selectNode);\n              localStorage.setItem(\n                \"debug@BookSources\",\n                JSON.stringify((RuleSources = laveSources))\n              );\n              if ($(\"#bookSourceUrl\").value == selectRuleUrl) {\n                $$(\".rules textarea\").forEach((item) => {\n                  item.value = \"\";\n                });\n                todo();\n              }\n              console.log(deleteSources);\n              console.log(`以上源已删除!`);\n            }\n          })\n          .catch((err) => {\n            alert(`删除源失败,无法连接到「Reader」!\\n${err}`);\n          });\n      }\n      break;\n    case \"ClrAll\":\n      if (confirm(`确定要清空当前源列表吗?\\n(不会删除APP内源)`)) {\n        localStorage.setItem(\n          \"debug@BookSources\",\n          JSON.stringify((RuleSources = []))\n        );\n        $(\"#RuleList\").innerHTML = \"\";\n      }\n      break;\n    default:\n  }\n});\n"
  },
  {
    "path": "web/public/browsertest.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>ES5测试</title>\n    <style>\n        .name {\n            display: inline-block;\n            width: 300px;\n        }\n        .result {\n            display: inline-block;\n            margin-left: 100px;\n        }\n        span.success {\n            color: green;\n        }\n        span.error {\n            color: red;\n        }\n    </style>\n</head>\n<body>\n    <div>\n        <p><span class=\"name\">ES5 特性测试结果：</span><span id=\"result\" class=\"result\"></span></p>\n    </div>\n    <div id=\"log\">\n\n    </div>\n    <script>\n        var global = (typeof global != \"undefined\") ? global : runIt(\"return this\");\n\n        function runIt(code) {\n\t        return (new Function(code))();\n        }\n\n        function tryPassFail(code) {\n            try {\n                runIt(code);\n                return true;\n            }\n            catch (err) {\n                return false;\n            }\n        }\n\n        function tryReturn(code) {\n            try {\n                return runIt(code);\n            }\n            catch (err) {\n                return false;\n            }\n        }\n\n        function runTests(tests) {\n            var res = {}, all_passed = true;\n\n            // run the tests\n            for (var key in tests) {\n                if (tests[key].passes) {\n                    res[key] = tryPassFail(tests[key].passes);\n                }\n                else if (tests[key].fails) {\n                    res[key] = !tryPassFail(tests[key].fails);\n                }\n                else if (tests[key].is) {\n                    res[key] = tryReturn(tests[key].is);\n                }\n                else if (tests[key].not) {\n                    res[key] = !tryReturn(tests[key].not);\n                }\n            }\n\n            // re-run to check for dependency failures\n            for (var key in tests) {\n                if (tests[key].dependencies) {\n                    // did any of the listed dependencies already fail?\n                    var isTrue = true;\n                    for (var dep in tests[key].dependencies) {\n                        if (!tests[dep]) {\n                            isTrue = false;\n                            break;\n                        }\n                    }\n\n                    if (!isTrue) {\n                        res[key] = false;\n                    }\n                }\n\n                if (!res[key]) all_passed = false;\n            }\n\n            res.everything = all_passed;\n\n            return res;\n        }\n        var logDom = document.getElementById(\"log\");\n        var resultDom = document.getElementById(\"result\");\n\n        try {\n            window.onerror = function(event, source, lineno, colno, error) {\n                window.alert(\n                    \"event: \" + event +\n                    \"   source: \" + source +\n                    \"   lineno: \" + lineno +\n                    \"   colno: \" + colno +\n                    \"   error: \" + error);\n            };\n\n            var html = \"\";\n            var es5 = {\n                \"Object.defineProperties\": {\n                    is: 'return (\"defineProperties\" in Object)'\n                },\n                \"Object.create\": {\n                    is: 'return (\"create\" in Object)'\n                },\n                \"Object.getOwnPropertyNames\": {\n                    is: 'return (\"getOwnPropertyNames\" in Object)'\n                },\n                \"Object.getPrototypeOf\": {\n                    is: 'return (\"getPrototypeOf\" in Object)'\n                },\n                \"Object.getOwnPropertyDescriptor\": {\n                    is: 'return (\"getOwnPropertyDescriptor\" in Object)'\n                },\n                \"Object.preventExtensions\": {\n                    is: 'return (\"preventExtensions\" in Object)'\n                },\n                \"Object.isExtensible\": {\n                    is: 'return (\"isExtensible\" in Object)'\n                },\n                \"Object.seal\": {\n                    is: 'return (\"seal\" in Object)'\n                },\n                \"Object.isSealed\": {\n                    is: 'return (\"isSealed\" in Object)'\n                },\n                \"Object.freeze\": {\n                    is: 'return (\"freeze\" in Object)'\n                },\n                \"Object.isFrozen\": {\n                    is: 'return (\"isFrozen\" in Object)'\n                },\n                \"Object.keys\": {\n                    is: 'return (\"keys\" in Object)'\n                },\n                \"function.bind\": {\n                    is: 'return (\"bind\" in function(){})'\n                },\n                \"Array.prototype.indexOf\": {\n                    is: 'return (\"indexOf\" in Array.prototype)'\n                },\n                \"Array.prototype.lastIndexOf\": {\n                    is: 'return (\"lastIndexOf\" in Array.prototype)'\n                },\n                \"Array.prototype.every\": {\n                    is: 'return (\"every\" in Array.prototype)'\n                },\n                \"Array.prototype.some\": {\n                    is: 'return (\"some\" in Array.prototype)'\n                },\n                \"Array.prototype.forEach\": {\n                    is: 'return (\"forEach\" in Array.prototype)'\n                },\n                \"Array.prototype.map\": {\n                    is: 'return (\"map\" in Array.prototype)'\n                },\n                \"Array.prototype.filter\": {\n                    is: 'return (\"filter\" in Array.prototype)'\n                },\n                \"Array.prototype.reduce\": {\n                    is: 'return (\"reduce\" in Array.prototype)'\n                },\n                \"Array.prototype.reduceRight\": {\n                    is: 'return (\"reduceRight\" in Array.prototype)'\n                },\n                \"JSON\": {\n                    is: 'return (\"JSON\" in global) && (\"stringify\" in JSON) && (\"parse\" in JSON)'\n                },\n                \"Strict Mode\": {\n                    fails: '\"use strict\"; var module = { x: 42, getX: function() { return this.x;}}; var unboundGetX = module.getX;console.log(unboundGetX());'\n                },\n                \"Getter Setter\": {\n                    passes: 'var o = (function(){ var age = 0; return { get age (){return age;}, set age (v){ age = v; }}})();console.log(o.age);o.age =12;console.log(o.age);'\n                }\n            }\n\n            var result = runTests(es5);\n\n            var totalTest = 0;\n            var passedTest = 0;\n            for (var test in result) {\n                if (test != 'everything') {\n                    totalTest++;\n                    if (result[test]) {\n                        passedTest++;\n                    }\n                    html += \"<p><span class='name'>\" + test + \"</span><span class='result \" + (result[test] ? \"success\" : \"error\") + \"'>\" + (result[test] ? \"YES\" : \"NO\") + \"</span></p>\";\n                }\n            }\n\n            logDom.innerHTML = html;\n\n            resultDom.innerHTML = \"<span class='\" + (result.everything ? 'success' : 'error') + \"'>\" + passedTest + \"/\" + totalTest + \"</span>\";\n        } catch (error) {\n            // window.alert(error);\n            resultDom.innerHTML = \"<span class='error'>检测出错: \" + error + \"</span>\";\n        }\n    </script>\n</body>\n</html>"
  },
  {
    "path": "web/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=0,minimal-ui,viewport-fit=cover\" />\n    <meta name=\"keyword\" content=\"阅读3,在线版\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"mobile-web-app-capable\" content=\"yes\" />\n    <meta name=\"apple-touch-fullscreen\" content=\"yes\">\n    <meta name=\"format-detection\" content=\"telephone=no,email=no,address=no\">\n    <meta name=\"renderer\" content=\"webkit\">\n    <meta name=\"HandheldFriendly\" content=\"true\">\n    <meta name=\"MobileOptimized\" content=\"320\">\n    <meta name=\"screen-orientation\" content=\"portrait\">\n    <meta name=\"x5-orientation\" content=\"portrait\">\n    <meta name=\"full-screen\" content=\"yes\">\n    <meta name=\"x5-fullscreen\" content=\"true\">\n    <meta name=\"browsermode\" content=\"application\">\n    <meta name=\"x5-page-mode\" content=\"app\">\n    <meta name=\"msapplication-tap-highlight\" content=\"no\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\" />\n    <title>阅读</title>\n    <script>\n      // 通用加载脚本方法\n      function loadScript(src, callback, async) {\n        var script = document.createElement('script');\n        if (async) script.async = 'async';\n        script.src = src;\n\n        if (callback) {\n          script.onload = callback;\n        }\n\n        document.head.appendChild(script);\n      }\n\n      // 通用加载样式方法\n      function loadLink(href, callback, rel, type) {\n        var link = document.createElement('link');\n        link.rel = rel || \"stylesheet\";\n        link.type = type || \"text/css\";\n        link.href = href;\n\n        if (callback) {\n          link.onload = callback;\n        }\n\n        document.getElementsByTagName(\"head\")[0].appendChild(link);\n      }\n\n      // 获取单个查询参数\n      function getQueryString(sVar, first) {\n        return decodeURI(window.location.search.replace(new RegExp('^(?:.*' + (first ? '?' : '') + '[&\\\\?]' + encodeURI(sVar).replace(/[\\.\\+\\*]/g, '\\\\$&') + '(?:\\\\=([^&]*))?)?.*$', 'i'), '$1'));\n      }\n\n      window.onerror = function() {\n        if (window.getQueryString('debug')) {\n          alert(JSON.stringify(arguments));\n        }\n      }\n\n      if (window.getQueryString('debug')) {\n        window.loadScript('https://cdn.bootcdn.net/ajax/libs/vConsole/3.9.1/vconsole.min.js', function() {\n          new VConsole();\n        }, true);\n      }\n    </script>\n  </head>\n  <style>\n    :root {\n      --sat: constant(safe-area-inset-top);\n      --sat: env(safe-area-inset-top);\n      --sar: constant(safe-area-inset-right);\n      --sar: env(safe-area-inset-right);\n      --sab: constant(safe-area-inset-bottom);\n      --sab: env(safe-area-inset-bottom);\n      --sal: constant(safe-area-inset-left);\n      --sal: env(safe-area-inset-left);\n    }\n    body::-webkit-scrollbar {\n      display: none;\n    }\n    body.night-theme::-webkit-scrollbar {\n      background-color: #333 !important;\n    }\n    html,body {\n      width: 100%;\n      height: 100%;\n      margin: 0;\n      padding: 0;\n    }\n  </style>\n  <body>\n    <noscript>\n      <strong>请启用浏览器的Javascript支持</strong>\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "web/public/robots.txt",
    "content": "User-agent: *\nDisallow:\n"
  },
  {
    "path": "web/public/sw.js",
    "content": "self.addEventListener(\"message\", event => {\n  if (event.data && event.data.type === \"CLEAR_HOME_CACHE\") {\n    self.caches.delete(\"home\");\n  }\n});\n\nworkbox.routing.setDefaultHandler(async ({ event }) => {\n  let { request, target } = event;\n\n  // 处理首页请求 networkFirst\n  try {\n    const url = new URL(request.url);\n    if (\n      url.origin === target.origin &&\n      (!url.pathname || url.pathname === \"/\")\n    ) {\n      const homeCache = await self.caches.open(\"home\");\n      return fetch(request)\n        .then(fetchRes => {\n          if (fetchRes.type !== \"opaque\") {\n            let resClone = fetchRes.clone();\n            homeCache.put(request, fetchRes);\n            return resClone;\n          }\n        })\n        .catch(() => {\n          return homeCache.match(request);\n        });\n    }\n  } catch (error) {\n    //\n  }\n\n  // 判断是否有 precache\n  const precache = await self.caches.open(workbox.core.cacheNames.precache);\n  const precacheRes = await precache.match(\n    workbox.precaching.getCacheKeyForURL(request.url)\n  );\n  if (precacheRes) {\n    return precacheRes;\n  }\n\n  // 处理api请求 networkOnly\n  if (request.url.indexOf(\"/reader3/\") !== -1) {\n    return fetch(request);\n  }\n\n  const siteCache = await self.caches.open(\"SITE_CAHCE\");\n  const opaqueCache = await self.caches.open(\"OPAQUE_CAHCE\");\n\n  /**\n   *\n   * @param {Response} res\n   */\n  const isImage = function(res) {\n    const contentType = res.headers.get(\"Content-Type\");\n    if (contentType && contentType.indexOf(\"image/\") !== -1) {\n      return true;\n    }\n    return false;\n  };\n\n  /**\n   *\n   * @param {Response} res\n   */\n  const isFont = function(res) {\n    const contentType = res.headers.get(\"Content-Type\");\n    if (contentType && contentType.indexOf(\"application/x-font\") !== -1) {\n      return true;\n    }\n    return false;\n  };\n\n  // 通用请求，缓存 opaque 资源 和 图片资源\n  const doRequest = function(request) {\n    var originRequest = request;\n    // 站外资源去掉 referrer\n    if (\n      request.mode !== \"navigate\" &&\n      request.url.indexOf(request.referrer) === -1\n    ) {\n      // 站外资源强制缓存\n      request = new Request(request, { referrer: \"\" });\n    }\n\n    // 对于不在 caches 中的资源进行请求\n    return fetch(request).then(fetchRes => {\n      if (fetchRes.type === \"opaque\") {\n        let resClone = fetchRes.clone();\n        opaqueCache.put(originRequest, fetchRes);\n        return resClone;\n      }\n      // 这里只缓存成功 && 请求是 GET 方式的结果，对于 POST 等请求，可把 indexDB 给用上\n      if (!fetchRes || fetchRes.status !== 200 || request.method !== \"GET\") {\n        return fetchRes;\n      }\n\n      // 只能缓存同源的图片、字体，跨域的资源都访问不了\n      let resClone = fetchRes.clone();\n      if (isImage(fetchRes) || isFont(fetchRes)) {\n        siteCache.put(originRequest, fetchRes);\n      }\n\n      return resClone;\n    });\n  };\n\n  // 先从 caches 中寻找是否有匹配\n  return opaqueCache.match(request).then(res => {\n    if (res) {\n      return res;\n    }\n    return siteCache.match(request).then(res => {\n      if (res) {\n        return res;\n      }\n      return doRequest(request);\n    });\n  });\n});\n"
  },
  {
    "path": "web/src/App.vue",
    "content": "<template>\n  <div id=\"app\">\n    <keep-alive>\n      <router-view></router-view>\n    </keep-alive>\n    <el-dialog :visible.sync=\"showLogin\" :width=\"dialogWidth\" :top=\"dialogTop\">\n      <div class=\"custom-dialog-title\" slot=\"title\">\n        <span class=\"el-dialog__title\"\n          >{{ isLogin ? \"登录\" : \"注册\" }}\n          <span class=\"float-right span-btn\" @click=\"isLogin = !isLogin\">{{\n            isLogin ? \"注册\" : \"登录\"\n          }}</span>\n        </span>\n      </div>\n      <el-form :model=\"loginForm\">\n        <el-form-item label=\"用户名\">\n          <el-input v-model=\"loginForm.username\" autocomplete=\"on\"></el-input>\n        </el-form-item>\n        <el-form-item label=\"密码\">\n          <el-input\n            type=\"password\"\n            v-model=\"loginForm.password\"\n            autocomplete=\"on\"\n            show-password\n            @keyup.enter.native=\"login\"\n          ></el-input>\n        </el-form-item>\n        <el-form-item label=\"邀请码(没有则不填)\" v-if=\"!isLogin\">\n          <el-input\n            v-model=\"loginForm.code\"\n            autocomplete=\"off\"\n            @keyup.enter.native=\"login\"\n          ></el-input>\n        </el-form-item>\n        <el-checkbox v-model=\"remember\">记住登录信息</el-checkbox>\n      </el-form>\n      <div slot=\"footer\" class=\"dialog-footer\">\n        <el-button size=\"medium\" @click=\"cancel\">取 消</el-button>\n        <el-button size=\"medium\" type=\"primary\" @click=\"login\">确 定</el-button>\n      </div>\n    </el-dialog>\n\n    <el-dialog\n      :title=\"editorTitle\"\n      :visible.sync=\"showEditor\"\n      :width=\"dialogWidth\"\n      :top=\"$store.state.miniInterface ? '0' : '10vh'\"\n      :fullscreen=\"$store.state.miniInterface\"\n    >\n      <div class=\"code-editor language-json\" ref=\"editorRef\"></div>\n      <div slot=\"footer\" class=\"dialog-footer\">\n        <el-button size=\"medium\" @click=\"closeEditor\">取 消</el-button>\n        <el-button size=\"medium\" type=\"primary\" @click=\"saveEditor\"\n          >保 存</el-button\n        >\n      </div>\n    </el-dialog>\n\n    <ImageViewer\n      :z-index=\"3200\"\n      :initial-index=\"$store.state.previewImageIndex\"\n      v-if=\"$store.state.showImageViewer\"\n      :on-close=\"closeViewer\"\n      :url-list=\"$store.state.previewImgList\"\n    />\n\n    <ReplaceRuleForm\n      v-model=\"showReplaceRuleForm\"\n      :rule=\"replaceRule\"\n      :isAdd=\"isAddReplaceRule\"\n    />\n\n    <ReplaceRule v-model=\"showReplaceRuleDialog\" />\n\n    <MPCode v-model=\"showMPCodeDialog\" />\n\n    <BookManage v-model=\"showBookManageDialog\" />\n\n    <BookInfo v-model=\"showBookInfoDialog\" />\n\n    <UserManage v-model=\"showUserManageDialog\" />\n\n    <AddUser v-model=\"showAddUserDialog\" />\n\n    <BookGroup v-model=\"showBookGroupDialog\" :isSet=\"isSetBookGroup\" />\n\n    <RssSourceList v-model=\"showRssSourceListDialog\" />\n    <RssArticleList v-model=\"showRssArticleListDialog\" :rssSource=\"rssSource\" />\n    <RssArticle\n      v-model=\"showRssArticleDialog\"\n      :rssArticleInfo=\"rssArticleInfo\"\n    />\n\n    <SearchBookContent\n      v-model=\"showSearchBookContentDialog\"\n      :book=\"searchBook\"\n    />\n\n    <BookmarkForm\n      v-model=\"showBookmarkForm\"\n      :bookmark=\"bookmark\"\n      :isAdd=\"isAddBookmark\"\n    />\n\n    <Bookmark v-model=\"showBookmarkDialog\" :book=\"bookmarkInBook\" />\n  </div>\n</template>\n\n<script>\nimport Axios from \"./plugins/axios\";\nimport eventBus from \"./plugins/eventBus\";\nimport ImageViewer from \"element-ui/packages/image/src/image-viewer.vue\";\nimport ReplaceRule from \"./components/ReplaceRule.vue\";\nimport ReplaceRuleForm from \"./components/ReplaceRuleForm.vue\";\nimport MPCode from \"./components/MPCode.vue\";\nimport BookManage from \"./components/BookManage.vue\";\nimport BookInfo from \"./components/BookInfo.vue\";\nimport UserManage from \"./components/UserManage.vue\";\nimport AddUser from \"./components/AddUser.vue\";\nimport BookGroup from \"./components/BookGroup.vue\";\nimport RssSourceList from \"./components/RssSourceList.vue\";\nimport RssArticleList from \"./components/RssArticleList.vue\";\nimport RssArticle from \"./components/RssArticle.vue\";\nimport SearchBookContent from \"./components/SearchBookContent.vue\";\nimport Bookmark from \"./components/Bookmark.vue\";\nimport BookmarkForm from \"./components/BookmarkForm.vue\";\nimport { CodeJar } from \"codejar\";\nimport Prism from \"prismjs\";\nimport \"prismjs/components/prism-json\";\nimport \"prismjs/themes/prism.css\";\nimport \"./assets/fonts/iconfont.css\";\nimport {\n  cacheFirstRequest,\n  isMiniInterface,\n  networkFirstRequest\n} from \"./plugins/helper\";\n\nDate.prototype.format = function(fmt) {\n  var o = {\n    \"M+\": this.getMonth() + 1, //月份\n    \"d+\": this.getDate(), //日\n    \"h+\": this.getHours(), //小时\n    \"m+\": this.getMinutes(), //分\n    \"s+\": this.getSeconds(), //秒\n    \"q+\": Math.floor((this.getMonth() + 3) / 3), //季度\n    S: this.getMilliseconds() //毫秒\n  };\n  if (/(y+)/.test(fmt)) {\n    fmt = fmt.replace(\n      RegExp.$1,\n      (this.getFullYear() + \"\").substr(4 - RegExp.$1.length)\n    );\n  }\n  for (var k in o) {\n    if (new RegExp(\"(\" + k + \")\").test(fmt)) {\n      fmt = fmt.replace(\n        RegExp.$1,\n        RegExp.$1.length == 1 ? o[k] : (\"00\" + o[k]).substr((\"\" + o[k]).length)\n      );\n    }\n  }\n  return fmt;\n};\n\n//字符编码数值对应的存储长度：\n//UCS-2编码(16进制) UTF-8 字节流(二进制)\n//0000 - 007F       0xxxxxxx （1字节）\n//0080 - 07FF       110xxxxx 10xxxxxx （2字节）\n//0800 - FFFF       1110xxxx 10xxxxxx 10xxxxxx （3字节）\nString.prototype.getBytesLength = function() {\n  var totalLength = 0;\n  var charCode;\n  for (var i = 0; i < this.length; i++) {\n    charCode = this.charCodeAt(i);\n    if (charCode < 0x007f) {\n      totalLength++;\n    } else if (0x0080 <= charCode && charCode <= 0x07ff) {\n      totalLength += 2;\n    } else if (0x0800 <= charCode && charCode <= 0xffff) {\n      totalLength += 3;\n    } else {\n      totalLength += 4;\n    }\n  }\n  return totalLength;\n};\n\nexport default {\n  name: \"app\",\n  components: {\n    ImageViewer,\n    ReplaceRule,\n    ReplaceRuleForm,\n    MPCode,\n    BookManage,\n    BookInfo,\n    UserManage,\n    AddUser,\n    BookGroup,\n    RssSourceList,\n    RssArticleList,\n    RssArticle,\n    SearchBookContent,\n    Bookmark,\n    BookmarkForm\n  },\n  data() {\n    return {\n      remember: true,\n      loginForm: {\n        username: \"\",\n        password: \"\",\n        code: \"\"\n      },\n      showEditor: false,\n      editorTitle: \"编辑器\",\n      editorContent: \"\",\n\n      showReplaceRuleDialog: false,\n\n      showReplaceRuleForm: false,\n      replaceRule: {},\n      isAddReplaceRule: true,\n\n      showMPCodeDialog: false,\n\n      showBookManageDialog: false,\n\n      showBookInfoDialog: false,\n\n      showUserManageDialog: false,\n      showAddUserDialog: false,\n\n      showBookGroupDialog: false,\n      isSetBookGroup: false,\n\n      showRssSourceListDialog: false,\n      showRssArticleListDialog: false,\n      rssSource: {},\n      showRssArticleDialog: false,\n      rssArticleInfo: {},\n\n      showSearchBookContentDialog: false,\n      searchBook: {},\n\n      isLogin: true,\n\n      showBookmarkDialog: false,\n\n      showBookmarkForm: false,\n      bookmark: {},\n      isAddBookmark: true,\n      bookmarkInBook: {}\n    };\n  },\n  beforeCreate() {\n    this.$store.dispatch(\"syncFromLocalStorage\");\n\n    this.$store.commit(\"setMiniInterface\", isMiniInterface());\n\n    document.documentElement.style.setProperty(\n      \"--vh\",\n      `${window.innerHeight * 0.01}px`\n    );\n\n    window.onresize = () => {\n      document.documentElement.style.setProperty(\n        \"--vh\",\n        `${window.innerHeight * 0.01}px`\n      );\n      this.$store.commit(\"setMiniInterface\", isMiniInterface());\n      this.$store.commit(\"setWindowSize\", {\n        width: window.innerWidth,\n        height: window.innerHeight\n      });\n      this.$store.commit(\"setTouchable\", \"ontouchstart\" in document);\n    };\n\n    const api = window.getQueryString(\"api\");\n    if (api) {\n      this.$store.commit(\"setApi\", api);\n    }\n\n    if (\n      window.navigator.userAgent.indexOf(\"iPhone\") >= 0 ||\n      window.navigator.userAgent.indexOf(\"iPad\") >= 0\n    ) {\n      document.documentElement.style.setProperty(\"height\", \"100vh\");\n      document.body.style.setProperty(\"height\", \"100vh\");\n    }\n\n    // window.webAppDistance =\n    //   window.navigator.userAgent.indexOf(\"iPhone\") >= 0 &&\n    //   window.navigator.standalone\n    //     ? (window.devicePixelRatio - 1 || 1) * 20\n    //     : 0;\n    // document.documentElement.style.setProperty(\n    //   \"--status-bar-height\",\n    //   `${window.webAppDistance}px`\n    // );\n\n    try {\n      const docStyle = getComputedStyle(document.documentElement);\n      this.$store.commit(\"setSafeArea\", {\n        top: docStyle.getPropertyValue(\"--sat\") | 0,\n        bottom: docStyle.getPropertyValue(\"--sab\") | 0,\n        left: docStyle.getPropertyValue(\"--sal\") | 0,\n        right: docStyle.getPropertyValue(\"--sar\") | 0\n      });\n    } catch (error) {\n      //\n    }\n  },\n  created() {\n    window\n      .matchMedia(\"(prefers-color-scheme: dark)\")\n      .addEventListener(\"change\", () => {\n        this.autoSetTheme(this.autoTheme);\n      });\n    this.autoSetTheme(this.autoTheme);\n\n    this.getUserInfo().then(() => {\n      this.$store.dispatch(\"syncFromLocalStorage\");\n      this.init();\n    });\n    this.loadTxtTocRules();\n  },\n  beforeMount() {\n    this.setTheme(this.isNight);\n    this.setMiniInterfaceClass();\n    this.setPageTypeClass();\n    this.eventBus = eventBus;\n    eventBus.$on(\"showEditor\", this.showEditorListener);\n    eventBus.$on(\"showReplaceRuleForm\", this.showReplaceRuleFormListener);\n    eventBus.$on(\"showReplaceRuleDialog\", () => {\n      this.showReplaceRuleDialog = true;\n    });\n    eventBus.$on(\"showMPCodeDialog\", () => {\n      this.showMPCodeDialog = true;\n    });\n    eventBus.$on(\"showBookManageDialog\", () => {\n      this.showBookManageDialog = true;\n    });\n    eventBus.$on(\"showBookInfoDialog\", book => {\n      this.showBookInfo = book;\n      this.showBookInfoDialog = true;\n    });\n    eventBus.$on(\"showUserManageDialog\", () => {\n      this.showUserManageDialog = true;\n    });\n    eventBus.$on(\"showAddUserDialog\", () => {\n      this.showAddUserDialog = true;\n    });\n    eventBus.$on(\"showBookGroupDialog\", isSet => {\n      this.showBookGroupDialog = true;\n      this.isSetBookGroup = !!isSet;\n    });\n    eventBus.$on(\"showRssArticleListDialog\", rssSource => {\n      this.showRssArticleListDialog = true;\n      this.rssSource = rssSource;\n    });\n    eventBus.$on(\"showRssSourceListDialog\", () => {\n      this.showRssSourceListDialog = true;\n    });\n    eventBus.$on(\"showRssArticleDialog\", rssArticleInfo => {\n      this.showRssArticleDialog = true;\n      this.rssArticleInfo = rssArticleInfo;\n    });\n    eventBus.$on(\"showSearchBookContentDialog\", searchBook => {\n      this.showSearchBookContentDialog = true;\n      this.searchBook = searchBook;\n    });\n    eventBus.$on(\"showBookmarkForm\", (bookmark, isAddBookmark, callback) => {\n      this.bookmark = bookmark;\n      this.isAddBookmark = isAddBookmark;\n      this.bookmarkCallback = callback;\n      this.showBookmarkForm = true;\n    });\n    eventBus.$on(\"showBookmarkDialog\", book => {\n      this.showBookmarkDialog = true;\n      this.bookmarkInBook = book;\n    });\n  },\n  mounted() {\n    document.documentElement.style.setProperty(\n      \"--vh\",\n      `${window.innerHeight * 0.01}px`\n    );\n    window.reader = this;\n  },\n  computed: {\n    isNight() {\n      return this.$store.getters.isNight;\n    },\n    autoTheme() {\n      return this.$store.getters.config.autoTheme;\n    },\n    dialogWidth() {\n      return this.$store.getters.dialogSmallWidth;\n    },\n    dialogTop() {\n      return this.$store.getters.dialogTop;\n    },\n    showLogin: {\n      get() {\n        return this.$store.state.showLogin;\n      },\n      set(value) {\n        this.$store.commit(\"setShowLogin\", value);\n      }\n    },\n    connected() {\n      return this.$store.state.connected;\n    },\n    showBookInfo: {\n      get() {\n        return this.$store.state.showBookInfo;\n      },\n      set(val) {\n        this.$store.commit(\"setShowBookInfo\", val);\n      }\n    }\n  },\n  watch: {\n    isNight(val) {\n      this.setTheme(val);\n    },\n    autoTheme(val) {\n      this.autoSetTheme(val);\n    },\n    miniInterface() {\n      this.setMiniInterfaceClass();\n    },\n    connected(val) {\n      if (val) {\n        // 连接后端成功，加载自定义样式\n        window.customCSSLoad ||\n          window.loadLink(this.$store.getters.customCSSUrl, () => {\n            window.customCSSLoad = true;\n          });\n      }\n    },\n    \"$store.state.config.pageType\": function() {\n      this.setPageTypeClass();\n    },\n    showReplaceRuleForm(val) {\n      if (!val) {\n        if (this.replaceRuleCallback) {\n          this.replaceRuleCallback();\n          this.replaceRuleCallback = null;\n        }\n      }\n    },\n    showBookmarkForm(val) {\n      if (!val) {\n        if (this.bookmarkCallback) {\n          this.bookmarkCallback();\n          this.bookmarkCallback = null;\n        }\n      }\n    },\n    showLogin(val) {\n      if (!val) {\n        this.isLogin = true;\n      }\n    }\n  },\n  methods: {\n    autoSetTheme(autoTheme) {\n      if (autoTheme) {\n        if (window.matchMedia(\"(prefers-color-scheme: dark)\").matches) {\n          // 是暗色模式\n          this.$store.commit(\"setNightTheme\", true);\n        } else {\n          // 非暗色模式\n          this.$store.commit(\"setNightTheme\", false);\n        }\n      }\n    },\n    setTheme(isNight) {\n      if (isNight) {\n        document.body.className =\n          (document.body.className || \"\").replace(\"night-theme\", \"\") +\n          \" night-theme\";\n      } else {\n        document.body.className = (document.body.className || \"\").replace(\n          \"night-theme\",\n          \"\"\n        );\n      }\n    },\n    setMiniInterfaceClass() {\n      if (this.$store.state.miniInterface) {\n        document.body.className =\n          (document.body.className || \"\").replace(\"mini-interface\", \"\") +\n          \" mini-interface\";\n      } else {\n        document.body.className = (document.body.className || \"\").replace(\n          \"mini-interface\",\n          \"\"\n        );\n      }\n    },\n    setPageTypeClass() {\n      if (this.$store.getters.isKindlePage) {\n        document.body.className =\n          (document.body.className || \"\").replace(\"kindle-page\", \"\") +\n          \" kindle-page\";\n      } else {\n        document.body.className = (document.body.className || \"\").replace(\n          \"kindle-page\",\n          \"\"\n        );\n      }\n    },\n    cancel() {\n      this.showLogin = false;\n      this.loginForm = {\n        username: \"\",\n        password: \"\",\n        code: \"\"\n      };\n    },\n    async login() {\n      const res = await Axios.post(\"/login\", {\n        ...this.loginForm,\n        isLogin: this.isLogin\n      });\n      if (res.data.isSuccess) {\n        this.$store.commit(\"setShowLogin\", false);\n        this.$nextTick(() => {\n          this.$store.commit(\"setLoginAuth\", true);\n        });\n        if (this.remember && res.data.data && res.data.data.accessToken) {\n          this.$store.commit(\"setToken\", res.data.data.accessToken);\n        }\n        this.getUserInfo().then(() => {\n          this.$store.dispatch(\"syncFromLocalStorage\");\n          this.init(true);\n        });\n      }\n    },\n    async init(refresh) {\n      if (this.initing) {\n        refresh &&\n          setTimeout(() => {\n            this.init(refresh);\n          }, 100);\n        return;\n      }\n      this.initing = true;\n      if (refresh || !this.$store.state.shelfBooks.length) {\n        await this.loadBookShelf().catch(() => {\n          this.initing = false;\n        });\n      }\n      await Promise.all([\n        // 加载书源列表\n        this.loadBookSource(refresh),\n        // 加载分组列表\n        this.loadBookGroup(refresh),\n        // 加载RSS订阅列表\n        this.loadRssSources(refresh),\n        // 加载替换规则\n        this.loadReplaceRules(refresh),\n        // 加载书签\n        this.loadBookmarks(refresh)\n      ]);\n      // 加载书架\n      this.initing = false;\n    },\n    getUserInfo() {\n      return networkFirstRequest(\n        () => Axios.get(this.api + \"/getUserInfo\"),\n        \"userInfo\"\n      ).then(\n        res => {\n          this.$store.commit(\"setConnected\", true);\n          if (res.data.isSuccess) {\n            this.$store.commit(\"setIsSecureMode\", res.data.data.secure);\n            if (res.data.data.secure && res.data.data.secureKey) {\n              this.$store.commit(\"setShowManagerMode\", true);\n            }\n            if (res.data.data.userInfo) {\n              this.$store.commit(\"setUserInfo\", res.data.data.userInfo);\n            }\n          }\n        },\n        error => {\n          this.$message.error(\n            \"加载用户信息失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    loadTxtTocRules() {\n      return cacheFirstRequest(\n        () => Axios.get(\"/getTxtTocRules\"),\n        \"txtTocRules\"\n      ).then(\n        res => {\n          const data = res.data.data || [];\n          this.$store.commit(\"setTxtTocRules\", data);\n        },\n        error => {\n          this.$message.error(\n            \"加载txt章节规则失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    showEditorListener(title, content, callback) {\n      this.editorTitle = title;\n      this.editorContent = content;\n      this.showEditor = true;\n      this.callback = callback;\n      this.$nextTick(() => {\n        this.initEditor();\n      });\n    },\n    showReplaceRuleFormListener(replaceRule, isAddReplaceRule, callback) {\n      this.replaceRule = replaceRule;\n      this.isAddReplaceRule = isAddReplaceRule;\n      this.replaceRuleCallback = callback;\n      this.showReplaceRuleForm = true;\n    },\n    loadBookShelf(refresh, api) {\n      api = api || this.api;\n      return networkFirstRequest(\n        () => Axios.get(api + \"/getBookshelf?refresh=\" + (refresh ? 1 : 0)),\n        \"getBookshelf@\" + this.currentUserName\n      )\n        .then(response => {\n          this.$store.commit(\"setConnected\", true);\n          if (response.data.isSuccess) {\n            this.$store.commit(\"setShelfBooks\", response.data.data);\n          }\n        })\n        .catch(error => {\n          this.$store.commit(\"setConnected\", false);\n          this.$message.error(\"后端连接失败 \" + (error && error.toString()));\n          throw error;\n        });\n    },\n    loadBookGroup(refresh) {\n      return cacheFirstRequest(\n        () => Axios.get(this.api + \"/getBookGroups\"),\n        \"bookGroup@\" + this.currentUserName,\n        refresh\n      ).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$store.commit(\"setBookGroupList\", res.data.data || []);\n          }\n        },\n        error => {\n          this.$message.error(\n            \"加载分组列表失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    loadRssSources(refresh) {\n      return cacheFirstRequest(\n        () =>\n          Axios.get(this.api + \"/getRssSources\", {\n            params: {\n              simple: 1\n            }\n          }),\n        \"rssSources@\" + this.currentUserName,\n        refresh\n      ).then(\n        res => {\n          const data = res.data.data || [];\n          this.$store.commit(\"setRssSourceList\", data);\n        },\n        error => {\n          this.$message.error(\n            \"加载RSS订阅列表失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    loadBookSource(refresh) {\n      return cacheFirstRequest(\n        () =>\n          Axios.get(this.api + \"/getBookSources\", {\n            params: {\n              simple: 1\n            }\n          }),\n        \"bookSourceList@\" + this.currentUserName,\n        refresh\n      ).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$store.commit(\"setBookSourceList\", res.data.data || []);\n          }\n        },\n        error => {\n          this.$message.error(\n            \"加载书源列表失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    loadReplaceRules(refresh) {\n      return cacheFirstRequest(\n        () => Axios.get(this.api + \"/getReplaceRules\"),\n        \"replaceRule@\" + this.currentUserName,\n        refresh\n      ).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$store.commit(\"setFilterRules\", res.data.data || []);\n          }\n        },\n        error => {\n          this.$message.error(\n            \"加载替换规则失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    loadBookmarks(refresh) {\n      return cacheFirstRequest(\n        () => Axios.get(this.api + \"/getBookmarks\"),\n        \"bookmark@\" + this.currentUserName,\n        refresh\n      ).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$store.commit(\"setBookmarks\", res.data.data || []);\n          }\n        },\n        error => {\n          this.$message.error(\"加载书签失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async isInShelf(book, addTip) {\n      if (!book || !book.bookUrl || !book.origin) {\n        this.$message.error(\"书籍信息错误\");\n        return false;\n      }\n      // 判断是否加入了书架\n      const isInShelf = this.$store.getters.shelfBooks.find(\n        v => v.bookUrl === book.bookUrl\n      );\n      if (!isInShelf) {\n        if (addTip) {\n          const res = await this.$confirm(addTip, \"提示\", {\n            confirmButtonText: \"确定\",\n            cancelButtonText: \"取消\",\n            type: \"warning\"\n          }).catch(() => {\n            return false;\n          });\n          if (!res) {\n            return false;\n          }\n          // 加入书架\n          return Axios.post(this.api + \"/saveBook\", book).then(\n            res => {\n              if (res.data.isSuccess) {\n                return true;\n              }\n            },\n            () => {\n              this.$message.error(\"导入书籍失败\");\n              return false;\n            }\n          );\n        }\n      }\n      return !!isInShelf;\n    },\n    getBookContent(chapterIndex, options, refresh, cache, book) {\n      book = book || {\n        name: this.$store.getters.readingBook.name,\n        author: this.$store.getters.readingBook.author,\n        bookUrl: this.$store.getters.readingBook.bookUrl\n      };\n      const params = {\n        url: book.bookUrl,\n        index: chapterIndex\n      };\n      if (refresh) {\n        params.refresh = 1;\n      }\n      if (cache) {\n        params.cache = 1;\n      }\n      return cacheFirstRequest(\n        () =>\n          Axios.post(this.api + \"/getBookContent\", params, {\n            timeout: 30000,\n            ...options\n          }),\n        book.name +\n          \"_\" +\n          book.author +\n          \"@\" +\n          book.bookUrl +\n          \"@chapterContent-\" +\n          chapterIndex,\n        refresh\n      );\n    },\n    initEditor() {\n      const editor = this.$refs.editorRef;\n      if (!editor) {\n        setTimeout(() => {\n          this.initEditor();\n        }, 10);\n      }\n      try {\n        this.jar = CodeJar(editor, Prism.highlightElement, { tab: \"\\t\" });\n\n        // Update code\n        this.jar.updateCode(this.editorContent);\n\n        // Listen to updates\n        this.jar.onUpdate(code => {\n          // console.log(code);\n          this.editorContent = code;\n        });\n      } catch (e) {\n        //\n      }\n    },\n    closeEditor() {\n      this.jar && this.jar.destroy && this.jar.destroy();\n      this.showEditor = false;\n      this.editorTitle = \"\";\n      this.editorContent = \"\";\n      this.callback = null;\n    },\n    saveEditor() {\n      if (this.callback) {\n        this.callback(this.editorContent, () => {\n          this.closeEditor();\n        });\n      }\n    },\n    closeViewer() {\n      this.$store.commit(\"setPreviewImgList\", false);\n    }\n  }\n};\n</script>\n\n<style>\n#app {\n  font-family: \"Avenir\", Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  color: #2c3e50;\n  margin: 0;\n  height: 100%;\n  /* height: calc(100% + var(--status-bar-height, 0px)); */\n  position: relative;\n}\n\n@font-face {\n  font-family: \"reader-st\";\n  src: local(\"Songti SC\"), local(\"Noto Serif CJK SC\"),\n    local(\"Source Han Serif SC\"), local(\"Source Han Serif CN\"), local(\"STSong\"),\n    local(\"宋体\"), local(\"明体\"), local(\"明朝\"), local(\"Songti\"),\n    local(\"Songti TC\"), /*iOS6+iBooks3*/ local(\"Song S\"), local(\"Song T\"),\n    local(\"STBShusong\"), local(\"TBMincho\"), local(\"HYMyeongJo\"),\n    /*Kindle Paperwihite*/ local(\"DK-SONGTI\");\n}\n\n@font-face {\n  font-family: \"reader-fs\";\n  src: local(\"STFangsong\"), local(\"FangSong\"), local(\"FangSong_GB2312\"),\n    local(\"amasis30\"), local(\"仿宋\"), local(\"仿宋_GB2312\"), local(\"Yuanti\"),\n    local(\"Yuanti SC\"), local(\"Yuanti TC\"),\n    /*iOS6+iBooks3*/ local(\"DK-FANGSONG\");\n}\n\n@font-face {\n  font-family: \"reader-kt\";\n  src: local(\"Kaiti SC\"), local(\"STKaiti\"), local(\"Caecilia\"), local(\"楷体\"),\n    local(\"楷体_GB2312\"), local(\"Kaiti\"), local(\"Kaiti SC\"), local(\"Kaiti TC\"),\n    /*iOS6+iBooks3*/ local(\"MKai PRC\"), local(\"MKaiGB18030C-Medium\"),\n    local(\"MKaiGB18030C-Bold\"), /*Kindle Paperwihite*/ local(\"DK-KAITI\");\n}\n\n@font-face {\n  font-family: \"reader-ht\";\n  src: local(\"Noto Sans CJK SC\"), local(\"Source Han Sans SC\"),\n    local(\"Source Han Sans CN\"), local(\"Microsoft YaHei\"), local(\"PingFang SC\"),\n    local(\"Hiragino Sans GB\"), local(\"黑体\"), local(\"微软雅黑\"), local(\"Heiti\"),\n    local(\"Heiti SC\"), local(\"Heiti TC\"), /*iOS6+iBooks3*/ local(\"MYing Hei S\"),\n    local(\"MYing Hei T\"), local(\"TBGothic\"),\n    /*Kindle Paperwihite*/ local(\"DK-HEITI\");\n}\n*::-webkit-scrollbar {\n  display: none;\n  width: 0 !important;\n  height: 0 !important;\n}\n*:focus {\n  outline: none !important;\n}\n.el-dialog .el-dialog__header {\n  padding: 20px 40px 10px 20px;\n}\n.el-dialog__header .el-dialog__headerbtn {\n  margin: 0;\n  font-size: 22px;\n  line-height: 24px;\n}\n</style>\n<style lang=\"stylus\">\n.popper-component {\n  top: 0 !important;\n}\n.code-editor {\n  max-height: calc(var(--vh, 1vh) * 80 - 54px - 60px - 66px);\n  overflow-y: auto;\n}\n.mini-interface {\n  .popper-component {\n    top: 0 !important;\n    left: 0 !important;\n    width: 100vw !important;\n    box-sizing: border-box;\n    margin: 0 !important;\n    overflow-x: hidden;\n  }\n  .code-editor {\n    max-height: calc(var(--vh, 1vh) * 100 - 54px - 40px - 66px);\n  }\n}\n.night-theme {\n  background-color: #222;\n\n  .el-message-box {\n    background: #212121;\n    border: 1px solid #212121;\n    .el-message-box__title {\n      color: #888;\n    }\n    .el-message-box__content {\n      color: #777;\n    }\n  }\n  .el-button--default {\n    background: #888;\n    color: #ddd;\n    border: 1px solid #888;\n  }\n  .el-button:focus, .el-button:hover {\n      color: #eee;\n      border-color: #bbb;\n      background-color: #bbb;\n  }\n  .el-button--text:focus, .el-button--text:hover {\n      color: #66b1ff;\n      border-color: transparent;\n      background-color: transparent;\n  }\n  .el-button.is-disabled, .el-button.is-disabled:focus, .el-button.is-disabled:hover {\n      color: #666;\n  }\n  .el-button--primary {\n    background: #185798;\n    border: 1px solid #185798;\n  }\n  .el-button--primary:focus, .el-button--primary:hover {\n      background: #2b67bb;\n      border-color: #2b67bb;\n      color: #FFF;\n  }\n  .el-input-number__increase, .el-input-number__decrease {\n      background-color: #909399;\n      border-color: #909399;\n      color: #fff;\n  }\n  .el-checkbox__inner {\n    background: #bbb;\n  }\n  .el-input__inner {\n    background-color: #444;\n    border: 1px solid #444 !important;\n    color: #ddd;\n  }\n  .el-textarea__inner {\n    background-color: #444;\n    border: 1px solid #444 !important;\n    color: #ddd;\n  }\n  .el-tabs__item {\n    color: #ddd;\n  }\n  .el-tabs__nav-next, .el-tabs__nav-prev {\n    color: #aaa;\n  }\n  .el-tabs__nav-wrap::after {\n    background-color: #444;\n  }\n  .el-select-dropdown {\n    background-color: #333;\n    border: 1px solid #333 !important;\n  }\n  .el-select-dropdown__item {\n    color: #ddd;\n  }\n  .el-select-dropdown__item.hover, .el-select-dropdown__item:hover {\n    background-color: #444;\n  }\n  .el-select .el-tag.el-tag--info {\n    background-color: #777;\n    border-color: #777;\n    color: #ddd;\n  }\n  .el-select-dropdown.is-multiple .el-select-dropdown__item.selected.hover,\n  .el-select-dropdown.is-multiple .el-select-dropdown__item.hover {\n    background-color: #555;\n  }\n  .el-select-dropdown.is-multiple .el-select-dropdown__item.selected {\n    background-color: #444;\n  }\n  .el-popper[x-placement^=\"bottom\"] .popper__arrow, .el-popper[x-placement^=\"bottom\"] .popper__arrow::after {\n    border-bottom-color: #333 !important;\n  }\n  .el-popper[x-placement^=\"top\"] .popper__arrow, .el-popper[x-placement^=\"top\"] .popper__arrow::after {\n    border-top-color: #333 !important;\n  }\n  .el-dialog {\n    background-color: #222;\n  }\n  .el-dialog__title {\n    color: #bbb;\n  }\n  .el-pagination .btn-next, .el-pagination .btn-prev {\n    background: center center no-repeat #444;\n    color: #ddd;\n  }\n  .el-pager li {\n    background: #444;\n    color: #ddd;\n  }\n  .el-pager li.btn-quicknext, .el-pager li.btn-quickprev {\n    color: #ddd;\n  }\n  .el-pager li.active {\n    color: #409EFF;\n  }\n  .code-editor {\n    .token.operator,\n    .token.entity,\n    .token.url,\n    .language-css .token.string,\n    .style .token.string {\n      /* This background color was intended by the author of this theme. */\n      background: inherit;\n    }\n  }\n\n  .el-table {\n    background-color: transparent;\n  }\n  .el-table__expanded-cell {\n    background-color: transparent;\n  }\n  .el-table th, .el-table tr{\n    background-color: #222 !important;\n  }\n  .el-table td {\n    border-bottom: 1px solid #555;\n  }\n  .el-table th.is-leaf {\n    border-bottom: 1px solid #555;\n  }\n  .el-table td.el-table__cell, .el-table th.el-table__cell.is-leaf {\n    border-bottom: 1px solid #555;\n  }\n  .el-dropdown-menu {\n    background-color: #444 !important;\n    border-color: #444;\n  }\n  .el-dropdown-menu__item:focus, .el-dropdown-menu__item:not(.is-disabled):hover {\n    background-color: #666 !important;\n    border-color: #666;\n  }\n  .el-dropdown-menu__item {\n    color: #bbb;\n  }\n  .el-table--border::after {\n    background-color: transparent;\n  }\n  .el-table--group::after {\n    background-color: transparent;\n  }\n  .el-table::before {\n    background-color: transparent;\n  }\n  .el-table {\n    color: #888;\n    background-color: transparent;\n  }\n  .el-table--enable-row-hover .el-table__body tr:hover>td {\n    background-color: #333;\n  }\n  .el-table__fixed-right::before, .el-table__fixed::before {\n    background-color: #333;\n  }\n  .el-table__body tr.hover-row.current-row>td,\n  .el-table__body tr.hover-row.el-table__row--striped.current-row>td,\n  .el-table__body tr.hover-row.el-table__row--striped>td,\n  .el-table__body tr.hover-row>td {\n    background-color: #444;\n  }\n  .el-table__body tr.hover-row.current-row>td.el-table__cell,\n  .el-table__body tr.hover-row.el-table__row--striped.current-row>td.el-table__cell,\n  .el-table__body tr.hover-row.el-table__row--striped>td.el-table__cell,\n  .el-table__body tr.hover-row>td.el-table__cell {\n    background-color: #444;\n    color: #ccc;\n  }\n  .el-table--enable-row-hover .el-table__body tr:hover>td.el-table__cell {\n    background-color: #444;\n    color: #ccc;\n  }\n  .el-table__body-wrapper::-webkit-scrollbar {\n    background-color: #333 !important;\n  }\n\n  .el-dialog__wrapper::-webkit-scrollbar {\n    background-color: #333 !important;\n  }\n\n  .check-tip {\n    color: #bbb;\n  }\n}\n.el-popover:focus, .el-popover:focus:active, .el-popover__reference:focus:hover, .el-popover__reference:focus:not(.focusing) {\n  outline: none;\n}\n.el-message-box {\n  max-width: 85vw;\n}\n.el-dialog__header {\n  position: relative;\n}\n.el-dialog.is-fullscreen {\n  padding-top: 0;\n  padding-top: constant(safe-area-inset-top) !important;\n  padding-top: env(safe-area-inset-top) !important;\n}\n.popper-component.el-popover {\n  border: none;\n  box-shadow: none;\n}\n.kindle-page {\n  -webkit-tap-highlight-color: rbga(255, 255, 255, 0);\n  -webkit-user-select: none;\n}\n.check-tip {\n  display: inline-block;\n  float: left;\n  line-height: 40px;\n  margin-left: 10px;\n  font-size: 14px;\n}\n.float-left {\n  float: left;\n}\n.float-right {\n  float: right;\n}\n.custom-dialog-title {\n  .span-btn {\n    display: inline-block;\n    cursor: pointer;\n    font-size: 15px;\n    margin-right: 10px;\n  }\n}\n</style>\n"
  },
  {
    "path": "web/src/assets/fonts/iconfont.css",
    "content": "@charset \"UTF-8\";\n@font-face {\n  font-family: \"iconfont\";\n  src: url(\"./iconfont.woff\") format(\"woff\");\n}\n\n@font-face {\n  font-family: \"reader-iconfont\"; /* Project id 2841133 */\n  src: url('./reader-iconfont.woff2?t=1657702223137') format('woff2'),\n       url('./reader-iconfont.woff?t=1657702223137') format('woff'),\n       url('./reader-iconfont.ttf?t=1657702223137') format('truetype');\n}\n\n.reader-iconfont {\n  font-family: \"reader-iconfont\" !important;\n  font-size: 16px;\n  font-style: normal;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.reader-icon-shezhi:before {\n  content: \"\\e654\";\n}\n\n.reader-icon-volume-off:before {\n  content: \"\\e631\";\n}\n\n.reader-icon-volume:before {\n  content: \"\\e632\";\n}\n\n.reader-icon-15s:before {\n  content: \"\\e61f\";\n}\n\n.reader-icon-jian15s:before {\n  content: \"\\e620\";\n}\n\n.reader-icon-player-pause:before {\n  content: \"\\ea2b\";\n}\n\n.reader-icon-player-forward-step:before {\n  content: \"\\ea2c\";\n}\n\n.reader-icon-player-backward-step:before {\n  content: \"\\ea2d\";\n}\n\n.reader-icon-player-play:before {\n  content: \"\\ea2e\";\n}\n\n"
  },
  {
    "path": "web/src/components/AddUser.vue",
    "content": "<template>\n  <el-dialog\n    title=\"新增用户\"\n    :visible.sync=\"show\"\n    :width=\"dialogSmallWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    v-if=\"$store.getters.isNormalPage\"\n    :before-close=\"cancel\"\n  >\n    <el-form :model=\"addUserForm\">\n      <el-form-item label=\"用户名\">\n        <el-input v-model=\"addUserForm.username\" autocomplete=\"on\"></el-input>\n      </el-form-item>\n      <el-form-item label=\"密码\">\n        <el-input\n          type=\"password\"\n          v-model=\"addUserForm.password\"\n          autocomplete=\"on\"\n          show-password\n          @keyup.enter.native=\"login\"\n        ></el-input>\n      </el-form-item>\n    </el-form>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button size=\"medium\" @click=\"cancel\">取 消</el-button>\n      <el-button size=\"medium\" type=\"primary\" @click=\"save\">确 定</el-button>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\n\nconst defaultForm = {\n  username: \"\",\n  password: \"\"\n};\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"AddUser\",\n  data() {\n    return {\n      addUserForm: { ...defaultForm }\n    };\n  },\n  props: [\"show\", \"rule\", \"isAdd\"],\n  computed: {\n    ...mapGetters([\"dialogSmallWidth\", \"dialogTop\"])\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        this.addUserForm = { ...defaultForm };\n      }\n    }\n  },\n  methods: {\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    save() {\n      if (!this.addUserForm.username) {\n        this.$message.success(\"用户名不能为空\");\n        return;\n      }\n      if (!this.addUserForm.password) {\n        this.$message.success(\"密码不能为空\");\n        return;\n      }\n      Axios.post(this.api + \"/addUser\", this.addUserForm).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"新增成功\");\n            this.cancel();\n            const userList = res.data.data.map(v => ({\n              ...v,\n              userNS: v.username\n            }));\n            this.$store.commit(\"setUserList\", userList);\n          }\n        },\n        error => {\n          this.$message.error(\"新增失败 \" + (error && error.toString()));\n        }\n      );\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped></style>\n"
  },
  {
    "path": "web/src/components/BookGroup.vue",
    "content": "<template>\n  <el-dialog\n    :title=\"isShowBookGroupSettingDialog ? '设置分组' : '分组管理'\"\n    :visible.sync=\"show\"\n    :width=\"dialogWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    @opened=\"opened\"\n    v-if=\"$store.getters.isNormalPage\"\n    :before-close=\"cancel\"\n  >\n    <div class=\"source-container table-container\">\n      <el-table\n        :data=\"showBookGroupList\"\n        :height=\"dialogContentHeight\"\n        @selection-change=\"bookGroupSelection = $event\"\n        ref=\"bookGroupTableRef\"\n        :key=\"isShowBookGroupSettingDialog\"\n      >\n        <el-table-column\n          type=\"selection\"\n          width=\"25\"\n          v-if=\"isShowBookGroupSettingDialog\"\n        >\n        </el-table-column>\n        <el-table-column property=\"groupName\" label=\"分组名\" min-width=\"100\">\n          <template slot-scope=\"scope\">\n            <div class=\"drag-icon\">\n              <i class=\"el-icon-rank\"></i>\n            </div>\n            <span> {{ displayBookGroupName(scope.row) }}</span>\n          </template>\n        </el-table-column>\n        <el-table-column\n          property=\"show\"\n          label=\"显示\"\n          min-width=\"80\"\n          v-if=\"!isShowBookGroupSettingDialog\"\n        >\n          <template slot-scope=\"scope\">\n            <el-switch\n              v-model=\"scope.row.show\"\n              active-color=\"#13ce66\"\n              inactive-color=\"#ff4949\"\n              :active-value=\"true\"\n              :inactive-value=\"false\"\n              @change=\"toggleBookGroupShow(scope.row, $event)\"\n            >\n            </el-switch>\n          </template>\n        </el-table-column>\n        <el-table-column label=\"操作\" width=\"100px\">\n          <template slot-scope=\"scope\">\n            <el-button type=\"text\" @click=\"saveBookGroup(scope.row)\"\n              >编辑</el-button\n            >\n            <el-button\n              type=\"text\"\n              v-if=\"\n                !isShowBookGroupSettingDialog &&\n                  scope.row.groupId > 0 &&\n                  !getShowShelfBooks(scope.row.groupId).length\n              \"\n              @click=\"deleteBookGroup(scope.row)\"\n              style=\"color: #f56c6c\"\n              >删除</el-button\n            >\n          </template>\n        </el-table-column>\n      </el-table>\n    </div>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"saveBookGroup()\"\n        >添加分组</el-button\n      >\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"saveOrder()\"\n        v-if=\"isShowSaveOrderButton\"\n        >保存排序</el-button\n      >\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        @click=\"setBookGroup\"\n        v-if=\"isShowBookGroupSettingDialog\"\n        >确认</el-button\n      >\n      <el-button size=\"medium\" @click=\"cancel\">取消</el-button>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport Axios from \"../plugins/axios\";\nimport { mapGetters } from \"vuex\";\nimport Dragable from \"sortablejs\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"BookGroup\",\n  data() {\n    return {\n      isShowBookGroupSettingDialog: false,\n      showBookGroupList: [],\n      sortedBookGroupList: [],\n      refreshCounter: 0,\n      bookGroupSelection: []\n    };\n  },\n  props: [\"show\", \"isSet\"],\n  computed: {\n    ...mapGetters([\"dialogWidth\", \"dialogTop\", \"dialogContentHeight\"]),\n    bookGroupList() {\n      return this.$store.state.bookGroupList;\n    },\n    shelfBooks() {\n      return this.$store.getters.shelfBooks;\n    },\n    showBookInfo: {\n      get() {\n        return this.$store.state.showBookInfo;\n      },\n      set(val) {\n        this.$store.commit(\"setShowBookInfo\", val);\n      }\n    },\n    isShowSaveOrderButton() {\n      return (\n        this.showBookGroupList.reduce((p, c) => p + \"\" + c.groupId, \"\") !==\n        this.sortedBookGroupList.reduce((p, c) => p + \"\" + c.groupId, \"\")\n      );\n    }\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        this.isShowBookGroupSettingDialog = this.isSet;\n        this.bookGroupSelection = [];\n        this.computeBookGroupList();\n        if (this.isSet) {\n          this.$nextTick(() => {\n            this.$refs.bookGroupTableRef.clearSelection();\n            this.getBookGroupListForBook(this.showBookInfo.group).forEach(v => {\n              this.$refs.bookGroupTableRef.toggleRowSelection(v, true);\n            });\n          });\n        }\n      }\n    },\n    isSet(value) {\n      if (this.dragableElement) {\n        this.dragableElement.option(\"disabled\", value);\n      }\n    },\n    bookGroupList() {\n      this.computeBookGroupList();\n    }\n  },\n  methods: {\n    cancel() {\n      this.isShowBookGroupSettingDialog = false;\n      this.$emit(\"setShow\", false);\n    },\n    computeBookGroupList() {\n      this.showBookGroupList = [];\n      this.$nextTick(() => {\n        this.showBookGroupList = this.isSet\n          ? this.$store.state.bookGroupList.filter(v => v.groupId > 0)\n          : this.$store.state.bookGroupList;\n        this.sortedBookGroupList = this.showBookGroupList;\n      });\n    },\n    opened() {\n      this.$refs.bookGroupTableRef.doLayout();\n      this.setDragable();\n    },\n    loadBookGroup(refresh) {\n      return this.$root.$children[0].loadBookGroup(refresh);\n    },\n    getShowShelfBooks(bookGroup) {\n      // 处理特殊分组\n      if (bookGroup === -1) {\n        // 全部\n        return this.shelfBooks;\n      } else if (bookGroup === -2) {\n        // 本地\n        return this.shelfBooks.filter(v => v.origin === \"loc_book\");\n      } else if (bookGroup === -3) {\n        // 音频\n        return this.shelfBooks.filter(v => v.type === 1);\n      } else if (bookGroup === -4) {\n        // 未分组\n        return this.shelfBooks.filter(v => v.group === 0);\n      }\n\n      return this.shelfBooks.filter(v =>\n        bookGroup === 0 ? true : v.group & bookGroup\n      );\n    },\n    toggleBookGroupShow(bookGroup, show) {\n      Axios.post(this.api + \"/saveBookGroup\", {\n        ...bookGroup,\n        show\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"修改成功\");\n            this.loadBookGroup(true);\n          }\n        },\n        error => {\n          this.$message.error(\"修改失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async deleteBookGroup(row) {\n      const res = await this.$confirm(`确认要删除该分组吗?`, \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteBookGroup\", {\n        groupId: row.groupId\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"删除分组成功\");\n            this.loadBookGroup(true);\n          }\n        },\n        error => {\n          this.$message.error(\"删除分组失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async saveBookGroup(bookGroup) {\n      const res = await this.$prompt(\n        \"\",\n        `${bookGroup ? \"编辑分组\" : \"添加分组\"}`,\n        {\n          inputValue: bookGroup ? bookGroup.groupName : \"\",\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          inputValidator(v) {\n            if (!v) {\n              return \"分组名不能为空\";\n            }\n            return true;\n          }\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/saveBookGroup\", {\n        ...bookGroup,\n        groupName: res.value\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(bookGroup ? \"修改成功\" : \"添加成功\");\n            this.loadBookGroup(true);\n          }\n        },\n        error => {\n          this.$message.error(\n            (bookGroup ? \"修改失败\" : \"添加失败\") + (error && error.toString())\n          );\n        }\n      );\n    },\n    displayBookGroupName(bookGroup) {\n      return (\n        bookGroup.groupName +\n        (bookGroup.groupId < 0\n          ? \"(\" +\n            this.$store.getters.builtInBookGroupMap[bookGroup.groupId] +\n            \")\"\n          : \"\")\n      );\n    },\n    setBookGroup() {\n      if (!this.bookGroupSelection.length) {\n        this.$message.error(\"请选择书籍分组\");\n        return;\n      }\n      Axios.post(this.api + \"/saveBookGroupId\", {\n        bookUrl: this.showBookInfo.bookUrl,\n        groupId: this.bookGroupSelection.reduce((c, v) => {\n          return c | v.groupId;\n        }, 0)\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"设置成功\");\n            this.cancel();\n            this.showBookInfo = res.data.data;\n            this.$store.commit(\"updateShelfBook\", res.data.data);\n          }\n        },\n        error => {\n          this.$message.error(\"设置失败\" + (error && error.toString()));\n        }\n      );\n    },\n    getBookGroupListForBook(bookGroup) {\n      const groups = [];\n      this.$store.state.bookGroupList.forEach(v => {\n        if (v.groupId > 0 && (v.groupId & bookGroup) !== 0) {\n          groups.push(v);\n        }\n      });\n      return groups;\n    },\n    setDragable() {\n      const el = this.$el.querySelectorAll(\n        \".el-table__body-wrapper > table > tbody\"\n      )[0];\n      this.dragableElement = Dragable.create(el, {\n        handler: \".drag-icon\",\n        setData: function(dataTransfer) {\n          dataTransfer.setData(\"Text\", \"\");\n        },\n        onEnd: evt => {\n          if (evt.oldIndex === evt.newIndex) return;\n          const list = [...this.showBookGroupList];\n          const targetRow = list.splice(evt.oldIndex, 1)[0];\n          list.splice(evt.newIndex, 0, targetRow);\n          this.sortedBookGroupList = list;\n        }\n      });\n      window.bookGroupComp = this;\n      this.dragableElement.option(\"disabled\", this.isSet);\n    },\n    async saveOrder() {\n      Axios.post(this.api + \"/saveBookGroupOrder\", {\n        order: this.sortedBookGroupList.map((v, index) => ({\n          groupId: v.groupId,\n          order: index\n        }))\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"保存成功\");\n            this.loadBookGroup(true);\n          }\n        },\n        error => {\n          this.$message.error(\"保存失败\" + (error && error.toString()));\n        }\n      );\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.qrcode-img {\n  display: block;\n  margin: 0 auto;\n}\n.drag-icon {\n  cursor: move;\n  display: inline-block;\n  margin-right: 5px;\n  user-select: none;\n}\n</style>\n"
  },
  {
    "path": "web/src/components/BookInfo.vue",
    "content": "<template>\n  <el-dialog\n    title=\"书籍信息\"\n    :visible.sync=\"show\"\n    :width=\"dialogSmallWidth\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    :before-close=\"cancel\"\n  >\n    <div class=\"book-info-container\" v-if=\"show\">\n      <div class=\"book-cover\">\n        <div class=\"book-cover-bg\" :style=\"bookCoverBgStyle\"></div>\n        <div class=\"book-cover-bg-image\">\n          <img\n            v-lazy=\"getCover(getBookCoverUrl(showBookInfo))\"\n            :key=\"showBookInfo.name\"\n            alt=\"\"\n            @click=\"triggerBookCoverRefClick\"\n          />\n          <input\n            ref=\"bookCoverRef\"\n            type=\"file\"\n            accept=\"image/jpg, image/png, image/jpeg\"\n            @change=\"onCoverFileChange\"\n            style=\"display:none\"\n          />\n        </div>\n      </div>\n      <div class=\"book-name\">{{ showBookInfo.name }}</div>\n      <div class=\"book-kind\" v-html=\"renderBookKind(showBookInfo.kind)\"></div>\n      <div class=\"book-props\">\n        <div class=\"book-prop book-author\">\n          作者： {{ showBookInfo.author || \"未知\" }}\n        </div>\n        <div class=\"book-prop book-origin\">\n          来源： {{ displayOriginName(showBookInfo.origin) }}\n          <el-button\n            type=\"text\"\n            class=\"book-prop-btn\"\n            v-if=\"showBookInfo.origin === 'loc_book'\"\n            @click=\"refreshLocalBook(showBookInfo)\"\n            >更新</el-button\n          >\n        </div>\n        <div class=\"book-prop book-latest\">\n          <span class=\"latest-title\"\n            >最新： {{ showBookInfo.latestChapterTitle }}\n          </span>\n          <span class=\"book-prop-btn\" v-if=\"isInShelf\">\n            追更\n            <el-switch\n              v-model=\"showBookInfo.canUpdate\"\n              active-color=\"#13ce66\"\n              inactive-color=\"#ff4949\"\n              :active-value=\"true\"\n              :inactive-value=\"false\"\n              @change=\"toggleBookCanUpdate(book)\"\n            >\n            </el-switch>\n          </span>\n        </div>\n        <div class=\"book-prop book-group\" v-if=\"isInShelf\">\n          分组： {{ displayGroupName(showBookInfo.group) }}\n          <el-button\n            type=\"text\"\n            class=\"book-prop-btn\"\n            @click=\"showSetBookGroup()\"\n            >设置分组</el-button\n          >\n        </div>\n        <div class=\"book-prop book-operate-zone\" v-else>\n          <el-tag\n            type=\"success\"\n            :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n            class=\"book-operate-btn\"\n            @click.stop=\"saveBook(showBookInfo, true)\"\n          >\n            加入书架\n          </el-tag>\n        </div>\n      </div>\n      <div class=\"book-intro\" v-html=\"renderBookIntro(showBookInfo)\"></div>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport eventBus from \"../plugins/eventBus\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"BookInfo\",\n  data() {\n    return {};\n  },\n  props: [\"show\"],\n  computed: {\n    ...mapGetters([\"dialogSmallWidth\", \"dialogTop\"]),\n    bookCoverBgStyle() {\n      return {\n        backgroundImage: `url(${this.getCover(\n          this.getBookCoverUrl(this.showBookInfo),\n          true\n        )})`\n      };\n    },\n    bookSourceList() {\n      return this.$store.state.bookSourceList;\n    },\n    showBookInfo: {\n      get() {\n        return this.$store.state.showBookInfo;\n      },\n      set(val) {\n        this.$store.commit(\"setShowBookInfo\", val);\n      }\n    },\n    isInShelf() {\n      return this.$store.getters.shelfBooks.find(\n        v => v.bookUrl === this.showBookInfo.bookUrl\n      );\n    }\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        //\n      }\n    }\n  },\n  methods: {\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    displayGroupName(value) {\n      const groupName = [];\n      let unGroupName = \"\";\n      this.$store.state.bookGroupList.forEach(v => {\n        if (v.groupId > 0 && (v.groupId & value) !== 0) {\n          groupName.push(v.groupName);\n        } else if (v.groupId === -4) {\n          unGroupName = v.groupName;\n        }\n      });\n      return groupName.join(\",\") || unGroupName || \"未分组\";\n    },\n    displayOriginName(value) {\n      if (value === \"loc_book\") return \"本地\";\n      return (\n        (this.bookSourceList.find(v => v.bookSourceUrl === value) || {})\n          .bookSourceName || \"未知书源\"\n      );\n    },\n    renderBookKind(value) {\n      if (!value) {\n        return \"\";\n      }\n      const kindList = value.split(\",\");\n      return kindList\n        .filter(v => v)\n        .map(v => {\n          return `<span>${v}</span>`;\n        })\n        .join(\"\");\n    },\n    renderBookIntro(book) {\n      const intro = (book.intro || \"暂无简介\").split(\"\\n\");\n      return intro\n        .map(v => {\n          return `<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;${v.replace(\n            /^\\s+/g,\n            \"\"\n          )}</p>`;\n        })\n        .join(\"\");\n    },\n    getBookCoverUrl(book) {\n      return book.customCoverUrl || book.coverUrl;\n    },\n    triggerBookCoverRefClick() {\n      this.$refs.bookCoverRef.dispatchEvent(new MouseEvent(\"click\"));\n    },\n    onCoverFileChange(event) {\n      if (!event.target || !event.target.files || !event.target.files.length) {\n        return;\n      }\n      const rawFile = event.target.files && event.target.files[0];\n      // console.log(\"rawFile\", rawFile);\n      let param = new FormData();\n      param.append(\"file\", rawFile);\n      param.append(\"type\", \"covers\");\n      Axios.post(this.api + \"/uploadFile\", param, {\n        headers: { \"Content-Type\": \"multipart/form-data\" }\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            if (!res.data.data.length) {\n              this.$message.error(\"上传文件失败\");\n              return;\n            }\n            this.showBookInfo.customCoverUrl = res.data.data[0];\n            this.saveBook(this.showBookInfo).then(res => {\n              this.showBookInfo = res;\n            });\n          }\n        },\n        error => {\n          this.$message.error(\"上传文件失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    refreshLocalBook(book) {\n      Axios.post(this.api + \"/refreshLocalBook\", {\n        bookUrl: book.bookUrl\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"更新成功\");\n            this.showBookInfo = res.data.data;\n            this.$store.commit(\"updateShelfBook\", res.data.data);\n          }\n        },\n        error => {\n          this.$message.error(\"更新失败\" + (error && error.toString()));\n        }\n      );\n    },\n    toggleBookCanUpdate(book) {\n      return this.saveBook(book);\n    },\n    saveBook(book, isAdd) {\n      return Axios.post(this.api + \"/saveBook\", book).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(isAdd ? \"加入书架成功\" : \"操作成功\");\n            this.showBookInfo = res.data.data;\n            if (isAdd) {\n              this.$root.$children[0].loadBookShelf(true);\n            } else {\n              this.$store.commit(\"updateShelfBook\", res.data.data);\n            }\n          }\n        },\n        error => {\n          this.$message.error(\n            (isAdd ? \"加入书架失败\" : \"操作失败\") + (error && error.toString())\n          );\n        }\n      );\n    },\n    showSetBookGroup() {\n      eventBus.$emit(\"showBookGroupDialog\", true);\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.book-info-container {\n  .book-cover {\n    width: 100%;\n    position: relative;\n    height: 150px;\n\n    .book-cover-bg {\n      position: absolute;\n      height: 100%;\n      width: 100%;\n      filter: blur(50px);\n    }\n\n    .book-cover-bg-image {\n      position: absolute;\n      height: 100%;\n      width: 100%;\n\n      img {\n        margin: 0 auto;\n        display: block;\n        height: 150px;\n      }\n    }\n  }\n  .book-name {\n    display: block;\n    font-size: 16px;\n    font-weight: 500;\n    text-align: center;\n    padding: 10px 0;\n  }\n\n  .book-kind {\n    display: block;\n    color: red;\n    text-align: center;\n    padding: 5px 0;\n  }\n\n  .book-props {\n    padding: 5px 0;\n\n    .book-prop {\n      padding: 3px 0;\n\n      &.book-latest {\n        display: flex;\n        flex-direction: row;\n        justify-content: space-between;\n\n        .latest-title {\n          overflow: hidden;\n          text-overflow: ellipsis;\n          white-space: nowrap;\n        }\n\n        .book-prop-btn {\n          width: 80px;\n          text-align: right;\n        }\n      }\n\n      .book-prop-btn {\n        float: right;\n        height: 19px;\n        padding: 0;\n        cursor: pointer;\n      }\n\n      &.book-operate-zone {\n        text-align: center;\n\n        .book-operate-btn {\n          cursor: pointer;\n        }\n      }\n    }\n  }\n\n  .book-intro {\n    line-height: 1.6;\n    max-height: calc(var(--vh, 1vh) * 70 - 54px - 60px - 150px - 75px - 120px);\n    overflow-y: auto;\n  }\n}\n</style>\n<style lang=\"stylus\">\n.night-theme {\n  .book-info-container {\n    .book-name {\n      color: #eee !important;\n    }\n  }\n}\n@media screen and (max-width: 750px) {\n  .book-info-container {\n    .book-intro {\n      max-height: calc(var(--vh, 1vh) * 100 - 54px - 60px - 150px - 75px - 120px) !important;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "web/src/components/BookManage.vue",
    "content": "<template>\n  <el-dialog\n    title=\"书架管理\"\n    :visible.sync=\"show\"\n    :width=\"dialogWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    v-if=\"$store.getters.isNormalPage\"\n    :before-close=\"cancel\"\n  >\n    <div class=\"custom-dialog-title\" slot=\"title\">\n      <span class=\"el-dialog__title\"\n        >书架管理\n        <span class=\"float-right small-tip\">❗️只能缓存文本内容</span>\n      </span>\n    </div>\n    <div class=\"source-container table-container\">\n      <el-table\n        :data=\"bookList\"\n        :height=\"dialogContentHeight\"\n        @selection-change=\"manageBookSelection = $event\"\n      >\n        <el-table-column\n          type=\"selection\"\n          width=\"25\"\n          :selectable=\"isBookSelectable\"\n          :fixed=\"$store.state.miniInterface\"\n        >\n        </el-table-column>\n        <el-table-column\n          property=\"name\"\n          label=\"书名名\"\n          min-width=\"100\"\n          :fixed=\"$store.state.miniInterface\"\n        >\n          <template slot-scope=\"scope\">\n            <el-button\n              class=\"text-button\"\n              size=\"medium\"\n              type=\"text\"\n              @click=\"showBookInfo(scope.row)\"\n              >{{ scope.row.name }}</el-button\n            >\n          </template>\n        </el-table-column>\n        <el-table-column\n          property=\"author\"\n          label=\"作者\"\n          min-width=\"100\"\n        ></el-table-column>\n        <el-table-column label=\"分组\" min-width=\"120\">\n          <template slot-scope=\"scope\">\n            {{ renderBookGroup(scope.row) }}\n          </template>\n        </el-table-column>\n        <el-table-column label=\"章节\" min-width=\"120\">\n          <template slot-scope=\"scope\">\n            <span>共 {{ scope.row.totalChapterNum }} 章</span><br />\n            <span v-if=\"scope.row.origin !== 'loc_book'\">\n              服务器缓存： {{ scope.row.cachedChapterCount || 0 }} 章 <br\n            /></span>\n            <span>浏览器缓存： {{ scope.row.localCacheCount }} 章</span>\n          </template>\n        </el-table-column>\n        <el-table-column label=\"操作\" width=\"100px\">\n          <template slot-scope=\"scope\">\n            <el-button\n              class=\"text-button\"\n              size=\"medium\"\n              type=\"text\"\n              @click=\"editBook(scope.row)\"\n              >编辑</el-button\n            >\n            <el-button\n              class=\"text-button\"\n              size=\"medium\"\n              type=\"text\"\n              style=\"margin-left: 0\"\n              @click=\"setBookGroup(scope.row)\"\n              >分组</el-button\n            >\n            <el-dropdown @command=\"cacheBook(scope.row, $event)\">\n              <el-button class=\"text-button\" type=\"text\" size=\"medium\">\n                <span v-if=\"isCaching(scope.row)\">\n                  <i class=\"el-icon-loading\"></i> 缓存中\n                </span>\n                <span v-else>\n                  缓存<i class=\"el-icon-arrow-down el-icon--right\"></i>\n                </span>\n              </el-button>\n              <el-dropdown-menu slot=\"dropdown\">\n                <el-dropdown-item\n                  v-if=\"scope.row.origin !== 'loc_book'\"\n                  command=\"cacheBookSSE\"\n                  >缓存到服务器</el-dropdown-item\n                >\n                <el-dropdown-item command=\"cacheBookLocal\"\n                  >缓存到浏览器</el-dropdown-item\n                >\n                <el-dropdown-item\n                  v-if=\"scope.row.origin !== 'loc_book'\"\n                  command=\"deleteBookCache\"\n                  >删除服务器缓存</el-dropdown-item\n                >\n                <el-dropdown-item command=\"deleteBookLocalCache\"\n                  >删除浏览器缓存</el-dropdown-item\n                >\n              </el-dropdown-menu>\n            </el-dropdown>\n            <el-dropdown @command=\"exportBook(scope.row, $event)\">\n              <el-button class=\"text-button\" type=\"text\" size=\"medium\">\n                导出<i class=\"el-icon-arrow-down el-icon--right\"></i>\n              </el-button>\n              <el-dropdown-menu slot=\"dropdown\">\n                <el-dropdown-item command=\"txt\">导出为TXT</el-dropdown-item>\n                <el-dropdown-item command=\"epub\">导出为Epub</el-dropdown-item>\n              </el-dropdown-menu>\n            </el-dropdown>\n          </template>\n        </el-table-column>\n      </el-table>\n    </div>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"deleteBookList\"\n        >批量删除</el-button\n      >\n      <el-dropdown class=\"float-left\" @command=\"addBookGroupMulti\">\n        <el-button type=\"primary\" size=\"medium\">\n          批量添加分组<i class=\"el-icon-arrow-down el-icon--right\"></i>\n        </el-button>\n        <el-dropdown-menu slot=\"dropdown\">\n          <el-dropdown-item\n            v-for=\"(bookGroup, index) in bookGroupList\"\n            :key=\"'bookGroup-' + index\"\n            :command=\"bookGroup\"\n            >{{ bookGroup.groupName }}</el-dropdown-item\n          >\n        </el-dropdown-menu>\n      </el-dropdown>\n      <el-dropdown class=\"float-left\" @command=\"removeBookGroupMulti\">\n        <el-button type=\"primary\" size=\"medium\">\n          批量移除分组<i class=\"el-icon-arrow-down el-icon--right\"></i>\n        </el-button>\n        <el-dropdown-menu slot=\"dropdown\">\n          <el-dropdown-item\n            v-for=\"(bookGroup, index) in bookGroupList\"\n            :key=\"'bookGroup-' + index\"\n            :command=\"bookGroup\"\n            >{{ bookGroup.groupName }}</el-dropdown-item\n          >\n        </el-dropdown-menu>\n      </el-dropdown>\n      <span class=\"check-tip\">已选择 {{ manageBookSelection.length }} 个</span>\n      <el-button size=\"medium\" @click=\"cancel\">取消</el-button>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport eventBus from \"../plugins/eventBus\";\nconst buildURL = require(\"axios/lib/helpers/buildURL\");\nimport { LimitResquest } from \"../plugins/helper\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"BookManage\",\n  data() {\n    return {\n      bookList: [],\n      manageBookSelection: []\n    };\n  },\n  props: [\"show\"],\n  computed: {\n    ...mapGetters([\"dialogWidth\", \"dialogTop\", \"dialogContentHeight\"]),\n    bookGroupList() {\n      return this.$store.state.bookGroupList.filter(v => v.groupId > 0);\n    },\n    cachingBookList: {\n      get() {\n        return this.$store.state.cachingBookList;\n      },\n      set(val) {\n        this.$store.commit(\"setCachingBookList\", val);\n      }\n    },\n    cachingBookMap() {\n      const map = {};\n      this.cachingBookList.map(v => {\n        map[v.bookUrl] = true;\n      });\n      return map;\n    }\n  },\n  created() {\n    window.cacheEventSource = window.cacheEventSource || {};\n    window.cacheRequestHandle = window.cacheRequestHandle || {};\n    window.bookManageComp = this;\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        this.loadBookCacheInfo();\n      } else {\n        this.manageBookSelection = [];\n      }\n    }\n  },\n  methods: {\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    formatTableField(row, column, cellValue) {\n      switch (column.property) {\n        default:\n          return cellValue;\n      }\n    },\n    isBookSelectable() {\n      return true;\n    },\n    async loadBookCacheInfo() {\n      return Axios.get(this.api + \"/getShelfBookWithCacheInfo\").then(\n        res => {\n          if (res.data.isSuccess) {\n            this.computeCachedCata(res.data.data).then(v => {\n              this.bookList = v;\n            });\n          }\n        },\n        error => {\n          this.$message.error(\n            \"获取书架信息失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    async deleteBookList() {\n      if (!this.manageBookSelection.length) {\n        this.$message.error(\"请选择需要删除的书籍\");\n        return;\n      }\n      const res = await this.$confirm(\"确认要删除所选择的书籍吗?\", \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteBooks\", this.manageBookSelection).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.manageBookSelection = [];\n            this.$message.success(\"删除书籍成功\");\n            this.loadBookCacheInfo();\n            this.$root.$children[0].loadBookShelf();\n          }\n        },\n        error => {\n          this.$message.error(\"删除书籍失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async addBookGroupMulti(bookGroup) {\n      return this.operateBookGroupMulti(bookGroup, true);\n    },\n    async removeBookGroupMulti(bookGroup) {\n      return this.operateBookGroupMulti(bookGroup);\n    },\n    async operateBookGroupMulti(bookGroup, isAdd) {\n      const operate = isAdd ? \"添加\" : \"移除\";\n      if (!this.manageBookSelection.length) {\n        this.$message.error(\"请选择需要\" + operate + \"分组的书籍\");\n        return;\n      }\n      const res = await this.$confirm(\n        isAdd\n          ? `确认要将所选择的书籍添加到${bookGroup.groupName}分组吗?`\n          : `确认要将所选择的书籍从${bookGroup.groupName}分组中移除吗?`,\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(\n        this.api + (isAdd ? \"/addBookGroupMulti\" : \"/removeBookGroupMulti\"),\n        {\n          groupId: bookGroup.groupId,\n          bookList: this.manageBookSelection\n        }\n      ).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"操作成功\");\n            this.loadBookCacheInfo();\n            this.$root.$children[0].loadBookShelf();\n          }\n        },\n        error => {\n          this.$message.error(\"操作失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    renderBookGroup(book) {\n      const groups = [];\n      this.$store.state.bookGroupList.forEach(v => {\n        if (v.groupId > 0 && (v.groupId & book.group) !== 0) {\n          groups.push(v.groupName);\n        }\n      });\n      return groups.join(\" \");\n    },\n    showBookInfo(book) {\n      eventBus.$emit(\"showBookInfoDialog\", book);\n    },\n    editBook(book) {\n      eventBus.$emit(\"editBook\", book, false, () => {\n        this.loadBookCacheInfo();\n      });\n    },\n    setBookGroup(book) {\n      this.$store.commit(\"setShowBookInfo\", book);\n      eventBus.$emit(\"showBookGroupDialog\", true);\n    },\n    isCaching(book) {\n      if (this.cachingBookMap[book.bookUrl]) return true;\n      if (window.cacheEventSource && window.cacheEventSource[book.bookUrl])\n        return true;\n      if (window.cacheRequestHandle && window.cacheRequestHandle[book.bookUrl])\n        return true;\n      return false;\n    },\n    cacheBook(book, command) {\n      this[command](book);\n    },\n    async cacheBookSSE(book) {\n      const tryClose = () => {\n        try {\n          if (\n            window.cacheEventSource[book.bookUrl] &&\n            window.cacheEventSource[book.bookUrl].readyState !=\n              window.cacheEventSource[book.bookUrl].CLOSED\n          ) {\n            window.cacheEventSource[book.bookUrl].close();\n          }\n          window.cacheEventSource[book.bookUrl] = null;\n          delete window.cacheEventSource[book.bookUrl];\n          const index = this.cachingBookList.findIndex(\n            v => v.bookUrl === book.bookUrl\n          );\n          this.cachingBookList.splice(index, 1);\n          this.cachingBookList = [].concat(this.cachingBookList);\n        } catch (error) {\n          //\n        }\n      };\n      if (this.isCaching(book)) {\n        // 取消缓存\n        this.$message.info(\"已取消缓存\");\n        if (window.cacheEventSource[book.bookUrl]) {\n          tryClose();\n        }\n        return;\n      }\n\n      const params = {\n        url: book.bookUrl,\n        refresh: 0\n      };\n\n      const url = buildURL(this.api + \"/cacheBookSSE\", params);\n\n      tryClose();\n\n      this.cachingBookList = this.cachingBookList.concat([book]);\n      window.cacheEventSource[book.bookUrl] = new EventSource(url, {\n        withCredentials: true\n      });\n      window.cacheEventSource[book.bookUrl].addEventListener(\"error\", e => {\n        tryClose();\n        try {\n          if (e.data) {\n            const result = JSON.parse(e.data);\n            if (result && result.errorMsg) {\n              this.$message.error(result.errorMsg);\n            }\n          }\n        } catch (error) {\n          //\n        }\n      });\n      window.cacheEventSource[book.bookUrl].addEventListener(\"end\", e => {\n        this.$message.info(book.name + \"缓存到服务器完成\");\n        tryClose();\n        try {\n          if (e.data) {\n            // const result = JSON.parse(e.data);\n            // console.log(result);\n          }\n        } catch (error) {\n          //\n        }\n      });\n      window.cacheEventSource[book.bookUrl].addEventListener(\"message\", e => {\n        try {\n          if (e.data) {\n            const result = JSON.parse(e.data);\n            if (result && result.cachedCount) {\n              const index = this.bookList.findIndex(\n                v => v.bookUrl === book.bookUrl\n              );\n              this.$set(this.bookList, index, {\n                ...book,\n                cachedChapterCount: result.cachedCount\n              });\n            }\n          }\n        } catch (error) {\n          //\n        }\n      });\n    },\n    cacheBookLocal(book) {\n      if (this.isCaching(book)) {\n        // 取消缓存\n        this.$message.info(\"已取消缓存\");\n        if (window.cacheRequestHandle[book.bookUrl]) {\n          window.cacheRequestHandle[book.bookUrl].cancel();\n        }\n        return;\n      }\n      let isComputing = false;\n      const computeCache = () => {\n        this.computeCachedCata([book]).then(bookList => {\n          isComputing = false;\n          const index = this.bookList.findIndex(\n            v => v.bookUrl === book.bookUrl\n          );\n          this.$set(this.bookList, index, {\n            ...book,\n            localCacheCount: bookList[0].localCacheCount\n          });\n        });\n      };\n      this.cachingHandler = LimitResquest(2, handler => {\n        if (!isComputing) {\n          isComputing = true;\n          computeCache();\n        }\n        if (handler.isEnd()) {\n          this.$message.success(\"缓存到浏览器完成\");\n          computeCache();\n        }\n      });\n      for (let i = 0; i < book.totalChapterNum; i++) {\n        this.cachingHandler(() => {\n          return this.$root.$children[0].getBookContent(\n            i,\n            {\n              timeout: 30000,\n              silent: true\n            },\n            false,\n            true,\n            book\n          );\n        });\n      }\n    },\n    exportBook(book, type) {\n      const url = buildURL(this.api + \"/exportBook\", {\n        url: book.bookUrl,\n        isEpub: type === \"epub\" ? 1 : 0\n      });\n\n      window.open(url, \"_target\");\n    },\n    computeCachedCata(bookList, returnCacheMap) {\n      const cachePrefixMap = {};\n      bookList.forEach(book => {\n        cachePrefixMap[book.bookUrl] = {\n          key:\n            \"localCache@\" +\n            book.name +\n            \"_\" +\n            book.author +\n            \"@\" +\n            book.bookUrl +\n            \"@chapterContent-\",\n          map: {}\n        };\n      });\n      return window.$cacheStorage\n        .iterate(function(value, key) {\n          for (const bookUrl in cachePrefixMap) {\n            if (key.startsWith(cachePrefixMap[bookUrl].key)) {\n              try {\n                let index = parseInt(\n                  key.replace(cachePrefixMap[bookUrl].key, \"\")\n                );\n                cachePrefixMap[bookUrl].map[index] = true;\n              } catch (error) {\n                //\n                // console.error(error);\n              }\n              break;\n            }\n          }\n        })\n        .then(() => {\n          if (returnCacheMap) return cachePrefixMap;\n          return bookList.map(v => {\n            const cacheMap = cachePrefixMap[v.bookUrl].map;\n            v.localCacheCount = Object.keys(cacheMap).length;\n            return v;\n          });\n        })\n        .catch(function() {\n          if (returnCacheMap) return cachePrefixMap;\n          return bookList.map(v => {\n            v.localCacheCount = 0;\n            return v;\n          });\n        });\n    },\n    async deleteBookCache(book) {\n      const res = await this.$confirm(\n        `确认要删除服务器上《${book.name}》的缓存章节吗?`,\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteBookCache\", {\n        bookUrl: book.bookUrl\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"删除服务器缓存成功\");\n            this.loadBookCacheInfo();\n          }\n        },\n        error => {\n          this.$message.error(\n            \"删除服务器缓存失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    async deleteBookLocalCache(book) {\n      const res = await this.$confirm(\n        `确认要删除浏览器中《${book.name}》的缓存章节吗?`,\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      this.computeCachedCata([book], true)\n        .then(list => {\n          const op = [];\n          for (const index in list[book.bookUrl].map) {\n            const cacheKey = list[book.bookUrl].key + index;\n            op.push(window.$cacheStorage.removeItem(cacheKey));\n          }\n          return Promise.all(op);\n        })\n        .then(() => {\n          this.$message.success(\"删除浏览器缓存成功\");\n          this.computeCachedCata([].concat(this.bookList)).then(v => {\n            this.bookList = v;\n          });\n        });\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.float-right {\n  float: right;\n}\n.small-tip {\n  font-size: 14px;\n  margin-right: 10px;\n}\n.dialog-footer {\n  .float-left {\n    margin-right: 5px;\n  }\n}\n.source-container {\n  .text-button {\n    padding: 3px 5px;\n  }\n}\n</style>\n"
  },
  {
    "path": "web/src/components/BookShelf.vue",
    "content": "<template>\n  <div class=\"popup-wrapper\" :style=\"popupTheme\">\n    <div class=\"title-zone\">\n      <div class=\"title\">书架({{ shelfBooks.length }})</div>\n      <div :class=\"{ 'title-btn': true, loading: refreshLoading }\">\n        <!-- <span class=\"home-btn\" @click=\"backToHome\">回首页</span> -->\n        <span :class=\"{ loading: refreshLoading }\" @click=\"refreshShelf\">\n          <i class=\"el-icon-loading\" v-if=\"refreshLoading\"></i>\n          {{ refreshLoading ? \"刷新中...\" : \"刷新\" }}\n        </span>\n      </div>\n    </div>\n    <div\n      class=\"data-wrapper\"\n      ref=\"bookList\"\n      :class=\"{ night: $store.getters.isNight, day: !$store.getters.isNight }\"\n    >\n      <div class=\"shelfbook-list\">\n        <div\n          class=\"book-item\"\n          v-for=\"book in shelfBooks\"\n          :class=\"{ selected: isSelected(book) }\"\n          :key=\"book.bookUrl\"\n          @click=\"changeBook(book)\"\n          ref=\"book\"\n        >\n          <div class=\"book-title\">\n            <div class=\"book-name\">\n              {{ book.name }}\n            </div>\n            <div\n              class=\"book-progress\"\n              v-if=\"book.totalChapterNum - 1 - book.durChapterIndex\"\n            >\n              {{ book.totalChapterNum - 1 - book.durChapterIndex }}\n            </div>\n          </div>\n          <div class=\"chapter-text\">\n            {{ book.durChapterTitle }}\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport jump from \"../plugins/jump\";\nimport Axios from \"../plugins/axios\";\nimport { networkFirstRequest } from \"../plugins/helper\";\nexport default {\n  name: \"BookShelf\",\n  data() {\n    return {\n      refreshLoading: false\n    };\n  },\n  props: [\"visible\"],\n  computed: {\n    theme() {\n      return this.$store.getters.config.theme;\n    },\n    popupTheme() {\n      return {\n        background: this.$store.getters.currentThemeConfig.popup\n      };\n    },\n    shelfBooks() {\n      return this.$store.getters.shelfBooks;\n    }\n  },\n  mounted() {},\n  watch: {\n    visible(isVisible) {\n      if (isVisible) {\n        this.getBookshelf();\n      }\n    }\n  },\n  methods: {\n    isSelected(book) {\n      return book.bookUrl == this.$store.getters.readingBook.bookUrl;\n    },\n    getBookshelf(refresh) {\n      if (this.shelfBooks.length) {\n        return;\n      }\n      networkFirstRequest(\n        () =>\n          Axios.get(this.api + `/getBookshelf`, {\n            params: {\n              refresh: refresh ? 1 : 0\n            }\n          }),\n        \"getBookshelf\" +\n          ((this.$store.state.userInfo || {}).username || \"default\")\n      ).then(\n        res => {\n          this.refreshLoading = false;\n          if (res.data.isSuccess) {\n            this.$store.commit(\"setShelfBooks\", res.data.data);\n            if (this.shelfBooks.length) {\n              this.jumpToActive();\n            }\n          }\n        },\n        error => {\n          this.refreshLoading = false;\n          this.$message.error(\n            \"获取书架书籍失败 \" + (error && error.toString())\n          );\n          throw error;\n        }\n      );\n    },\n    changeBook(book) {\n      const readingBook = {\n        name: book.name,\n        bookUrl: book.bookUrl,\n        index: book.index ?? book.durChapterIndex ?? 0,\n        type: book.type,\n        coverUrl: book.customCoverUrl || book.coverUrl,\n        tocUrl: book.tocUrl,\n        author: book.author,\n        origin: book.origin,\n        originName: book.originName,\n        latestChapterTitle: book.latestChapterTitle,\n        intro: book.intro\n      };\n      this.$emit(\"changeBook\", readingBook);\n    },\n    refreshShelf() {\n      if (this.refreshLoading) return;\n      this.refreshLoading = true;\n      this.getBookshelf(true);\n    },\n    jumpToActive() {\n      this.$nextTick(() => {\n        let index = -1;\n        this.shelfBooks.some((v, i) => {\n          if (v.bookUrl == this.$store.getters.readingBook.bookUrl) {\n            index = i;\n            return true;\n          }\n        });\n        if (index < 0) {\n          return;\n        }\n        if (!this.$refs.book || !this.$refs.book[index]) {\n          setTimeout(() => {\n            this.jumpToActive();\n          }, 10);\n          return;\n        }\n        let wrapper = this.$refs.bookList;\n        jump(this.$refs.book[index], {\n          container: wrapper,\n          duration: 0\n        });\n      });\n    },\n    backToHome() {\n      this.$emit(\"toShelf\");\n    }\n  }\n};\n</script>\n\n<style lang=\"stylus\" scoped>\n.popup-wrapper {\n  margin: -16px;\n  margin-bottom: -13px;\n  padding: 24px;\n  padding-top: calc(24px + constant(safe-area-inset-top));\n  padding-top: calc(24px + env(safe-area-inset-top));\n\n  .title-zone {\n    margin: 0 0 20px 0;\n    width: 100%;\n    display: flex;\n    flex-direction: row;\n    flex-wrap: wrap;\n    justify-content: space-between;\n  }\n\n  .title {\n    font-size: 18px;\n    font-weight: 400;\n    font-family: -apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif;\n    color: #ed4259;\n    border-bottom: 1px solid #ed4259;\n    width: fit-content;\n  }\n\n  .title-btn {\n    font-size: 14px;\n    line-height: 26px;\n    color: #ed4259;\n    cursor: pointer;\n    .home-btn {\n      display: inline-block;\n      margin-right: 25px;\n    }\n    &.loading {\n      color: #606266;\n    }\n  }\n\n  .data-wrapper {\n    height: 300px;\n    overflow: auto;\n\n    .shelfbook-list {\n\n      .book-item {\n        width: 100%;\n        cursor: pointer;\n        display: flex;\n        flex-direction: column;\n        max-width: 100%;\n        overflow: hidden;\n        padding: 8px 0;\n\n        .book-title {\n          display: flex;\n          flex-direction: row;\n          flex-wrap: wrap;\n          justify-content: space-between;\n          align-items: center;\n\n          .book-name {\n            font-size: 16px;\n            overflow: hidden;\n            white-space: nowrap;\n            text-overflow: ellipsis;\n          }\n          .book-progress {\n            float: right;\n            font-size: 12px;\n          }\n        }\n\n        .chapter-text {\n          overflow: hidden;\n          white-space: nowrap;\n          text-overflow: ellipsis;\n          color: #888;\n          font-size: 14px;\n          margin-top: 6px;\n        }\n\n        &.selected {\n          .book-name {\n            color: #EB4259;\n          }\n        }\n      }\n    }\n  }\n\n  .data-wrapper::-webkit-scrollbar {\n    width: 0 !important;\n  }\n\n  .night {\n    .shelfbook-list {\n      .book-item {\n        border-bottom: 1px solid #333;\n\n        .book-title {\n          color: #888;\n        }\n\n        .chapter-text {\n          color: #555;\n        }\n      }\n    }\n  }\n\n  .day {\n    >>>.book-item {\n      border-bottom: 1px solid #eee;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "web/src/components/BookSource.vue",
    "content": "<template>\n  <div class=\"popup-wrapper\" :style=\"popupTheme\">\n    <div class=\"title-zone\">\n      <div class=\"title\">来源({{ bookSource.length }})</div>\n      <div :class=\"{ 'title-btn': true, loading: loadingMore }\">\n        <el-select\n          size=\"mini\"\n          v-model=\"bookSourceGroup\"\n          class=\"booksource-group-select\"\n          filterable\n          placeholder=\"全部分组\"\n        >\n          <el-option\n            v-for=\"(item, index) in $store.getters.bookSourceGroupList\"\n            :key=\"'source-group-' + index\"\n            :label=\"item.name + ' (' + item.count + ')'\"\n            :value=\"item.value\"\n          >\n          </el-option>\n        </el-select>\n        <span :class=\"{ loading: loading }\" @click=\"refresh\">\n          <i class=\"el-icon-loading\" v-if=\"loading\"></i>\n          {{ loading ? \"刷新中...\" : \"刷新\" }}\n        </span>\n        <span\n          :class=\"{ loading: loadingMore }\"\n          @click=\"searchBookSourceByEventStream\"\n        >\n          <i class=\"el-icon-loading\" v-if=\"loadingMore\"></i>\n          {{ loadingMore ? \"加载中...\" : \"加载更多\" }}\n        </span>\n      </div>\n    </div>\n    <div\n      class=\"data-wrapper\"\n      ref=\"sourceList\"\n      :class=\"{ night: $store.getters.isNight, day: !$store.getters.isNight }\"\n    >\n      <div class=\"source-list\">\n        <div\n          class=\"source-item\"\n          v-for=\"(searchBook, index) in bookSource\"\n          :class=\"{ selected: isSelected(searchBook) }\"\n          :key=\"index\"\n          @click=\"changeBookSource(searchBook)\"\n          ref=\"source\"\n        >\n          <div class=\"source-title\">\n            <div class=\"source-name\">\n              {{ searchBook.originName }}\n            </div>\n            <div class=\"source-time\">\n              {{ searchBook.time ? \"⏱ \" + searchBook.time + \"ms\" : \"\" }}\n            </div>\n          </div>\n          <div class=\"source-latest-chapter\">\n            {{ searchBook.latestChapterTitle || \"无最新章节\" }}\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport jump from \"../plugins/jump\";\nimport Axios from \"../plugins/axios\";\nconst buildURL = require(\"axios/lib/helpers/buildURL\");\n\nexport default {\n  name: \"BookSource\",\n  data() {\n    return {\n      index: this.$store.getters.readingBook.index,\n      bookSource: [],\n      bookSourceGroup: \"\",\n      bookSourceGroupIndexMap: {},\n      loading: false,\n      loadingMore: false\n    };\n  },\n  props: [\"visible\"],\n  computed: {\n    theme() {\n      return this.$store.getters.config.theme;\n    },\n    popupTheme() {\n      return {\n        background: this.$store.getters.currentThemeConfig.popup\n      };\n    },\n    bookSourceMap() {\n      return this.bookSource.reduce((c, v) => {\n        c[v.bookUrl] = v;\n        return c;\n      }, {});\n    },\n    readingBook() {\n      return this.$store.getters.readingBook || {};\n    }\n  },\n  mounted() {},\n  watch: {\n    visible(isVisible) {\n      if (isVisible) {\n        this.getBookSource();\n      }\n    },\n    readingBook(val, oldVal) {\n      if (val.bookUrl !== oldVal.bookUrl) {\n        this.bookSourceGroupIndexMap = {};\n      }\n    }\n  },\n  methods: {\n    isSelected(searchBook) {\n      return searchBook.bookUrl == this.$store.getters.readingBook.bookUrl;\n    },\n    getBookSource(refresh) {\n      Axios.post(this.api + `/getAvailableBookSource`, {\n        url: this.$store.getters.readingBook.bookUrl,\n        refresh: refresh ? 1 : 0\n      }).then(\n        res => {\n          this.loading = false;\n          if (res.data.isSuccess) {\n            this.bookSource = res.data.data || [];\n            if (this.bookSource.length) {\n              this.bookSourceGroupIndexMap[\"\"] = Math.max(\n                this.bookSourceGroupIndexMap[\"\"] ?? 0,\n                this.bookSource.length\n              );\n              this.jumpToActive();\n            } else {\n              // this.loadMoreSource();\n            }\n          }\n        },\n        error => {\n          this.loading = false;\n          this.$message.error(\n            \"获取书籍来源信息失败 \" + (error && error.toString())\n          );\n          throw error;\n        }\n      );\n    },\n    async changeBookSource(searchBook) {\n      const isInShelf = await this.$root.$children[0].isInShelf(\n        this.$store.getters.readingBook,\n        \"加入书架之后才能切换书源, 是否加入书架?\"\n      );\n      if (!isInShelf) {\n        return;\n      }\n      Axios.post(this.api + `/setBookSource`, {\n        bookUrl: this.$store.getters.readingBook.bookUrl,\n        newUrl: searchBook.bookUrl,\n        bookSourceUrl: searchBook.origin\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.info(\"换源成功\");\n            var book = Object.assign({}, this.$store.getters.readingBook);\n            book.bookUrl = searchBook.bookUrl;\n            book.type =\n              typeof searchBook.type !== \"undefined\"\n                ? searchBook.type\n                : book.type;\n            book.coverUrl =\n              typeof searchBook.coverUrl !== \"undefined\"\n                ? searchBook.coverUrl\n                : book.coverUrl;\n            this.$store.commit(\"setReadingBook\", book);\n            this.$emit(\"changeBookSource\");\n\n            // 重新加载书架\n            Axios.get(this.api + `/getBookshelf`, {}).then(\n              res => {\n                if (res.data.isSuccess) {\n                  this.$store.commit(\"setShelfBooks\", res.data.data);\n                }\n              },\n              () => {\n                //\n              }\n            );\n          }\n        },\n        error => {\n          this.$message.error(\"换源失败 \" + (error && error.toString()));\n          throw error;\n        }\n      );\n    },\n    refresh() {\n      if (this.loadingMore) return;\n      this.loading = true;\n      this.getBookSource(true);\n    },\n    loadMoreSource() {\n      if (this.loadingMore) return;\n      this.loadingMore = true;\n      Axios.post(this.api + `/searchBookSource`, {\n        url: this.$store.getters.readingBook.bookUrl,\n        bookSourceGroup: this.bookSourceGroup,\n        lastIndex: this.bookSourceGroupIndexMap[this.bookSourceGroup]\n      }).then(\n        res => {\n          this.loadingMore = false;\n          if (res.data.isSuccess) {\n            var list = res.data.data.list || [];\n            this.bookSource = [].concat(\n              this.bookSource,\n              list.filter(v => {\n                return !this.bookSourceMap[v.bookUrl];\n              })\n            );\n            if (res.data.data.lastIndex) {\n              this.bookSourceGroupIndexMap[this.bookSourceGroup] =\n                res.data.data.lastIndex;\n            }\n          }\n        },\n        error => {\n          this.loadingMore = false;\n          this.$message.error(\n            \"加载更多书籍来源失败 \" + (error && error.toString())\n          );\n          throw error;\n        }\n      );\n    },\n    searchBookSourceByEventStream() {\n      const tryClose = () => {\n        try {\n          if (\n            this.searchEventSource &&\n            this.searchEventSource.readyState != this.searchEventSource.CLOSED\n          ) {\n            this.searchEventSource.close();\n          }\n          this.searchEventSource = null;\n        } catch (error) {\n          //\n        }\n      };\n      if (this.loadingMore) {\n        tryClose();\n        this.loadingMore = false;\n        return;\n      }\n      const params = {\n        accessToken: this.$store.state.token,\n        concurrentCount: this.$store.state.searchConfig.concurrentCount,\n        url: this.$store.getters.readingBook.bookUrl,\n        bookSourceGroup: this.bookSourceGroup,\n        lastIndex: this.bookSourceGroupIndexMap[this.bookSourceGroup]\n      };\n      this.loadingMore = true;\n\n      const url = buildURL(this.api + \"/searchBookSourceSSE\", params);\n\n      tryClose();\n\n      this.searchEventSource = new EventSource(url, {\n        withCredentials: true\n      });\n      this.searchEventSource.addEventListener(\"error\", e => {\n        this.loadingMore = false;\n        tryClose();\n        try {\n          if (e.data) {\n            const result = JSON.parse(e.data);\n            if (result && result.errorMsg) {\n              this.$message.error(result.errorMsg);\n            }\n          }\n        } catch (error) {\n          //\n        }\n      });\n      let oldBookSourceLength = this.bookSource.length;\n      this.searchEventSource.addEventListener(\"end\", e => {\n        this.loadingMore = false;\n        tryClose();\n        try {\n          if (e.data) {\n            const result = JSON.parse(e.data);\n            if (result && result.lastIndex) {\n              this.bookSourceGroupIndexMap[this.bookSourceGroup] =\n                result.lastIndex;\n            }\n          }\n          if (this.bookSource.length === oldBookSourceLength) {\n            this.$message.error(\"没有更多啦\");\n          }\n        } catch (error) {\n          //\n        }\n      });\n      this.searchEventSource.addEventListener(\"message\", e => {\n        try {\n          if (e.data) {\n            const result = JSON.parse(e.data);\n            if (result && result.lastIndex) {\n              this.bookSourceGroupIndexMap[this.bookSourceGroup] =\n                result.lastIndex;\n            }\n            if (result.data) {\n              this.bookSource = [].concat(\n                this.bookSource,\n                result.data.filter(v => {\n                  return !this.bookSourceMap[v.bookUrl];\n                })\n              );\n            }\n          }\n        } catch (error) {\n          //\n        }\n      });\n    },\n    jumpToActive() {\n      this.$nextTick(() => {\n        let index = -1;\n        this.bookSource.some((v, i) => {\n          if (v.bookUrl == this.$store.getters.readingBook.bookUrl) {\n            index = i;\n            return true;\n          }\n        });\n        if (index < 0) {\n          return;\n        }\n        let wrapper = this.$refs.sourceList;\n        jump(this.$refs.source[index], {\n          container: wrapper,\n          duration: 0\n        });\n      });\n    }\n  }\n};\n</script>\n\n<style lang=\"stylus\" scoped>\n.popup-wrapper {\n  margin: -16px;\n  margin-bottom: -13px;\n  padding: 24px;\n  padding-top: calc(24px + constant(safe-area-inset-top));\n  padding-top: calc(24px + env(safe-area-inset-top));\n\n  .title-zone {\n    margin: 0 0 20px 0;\n    width: 100%;\n    display: flex;\n    flex-direction: row;\n    flex-wrap: wrap;\n    justify-content: space-between;\n  }\n\n  .title {\n    font-size: 18px;\n    font-weight: 400;\n    font-family: -apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif;\n    color: #ed4259;\n    border-bottom: 1px solid #ed4259;\n    width: fit-content;\n  }\n\n  .title-btn {\n    font-size: 14px;\n    line-height: 26px;\n    color: #ed4259;\n    cursor: pointer;\n\n    .booksource-group-select {\n      width: 140px;\n    }\n    .source-count {\n      display: inline-block;\n      color: #606266;\n    }\n    span {\n      margin-left: 15px;\n    }\n    &.loading {\n      color: #606266;\n    }\n  }\n\n  .data-wrapper {\n    height: 300px;\n    overflow: auto;\n\n    .source-list {\n      .source-item {\n        width: 100%;\n        cursor: pointer;\n        display: flex;\n        flex-direction: column;\n        max-width: 100%;\n        overflow: hidden;\n        padding: 8px 0;\n\n        .source-title {\n          display: flex;\n          flex-direction: row;\n          flex-wrap: wrap;\n          justify-content: space-between;\n          align-items: center;\n\n          .source-name {\n            font-size: 16px;\n            overflow: hidden;\n            white-space: nowrap;\n            text-overflow: ellipsis;\n\n          }\n          .source-time {\n            float: right;\n            font-size: 12px;\n          }\n        }\n\n        .source-latest-chapter {\n          overflow: hidden;\n          white-space: nowrap;\n          text-overflow: ellipsis;\n          color: #888;\n          font-size: 14px;\n          margin-top: 6px;\n        }\n\n        &.selected {\n          .source-name {\n            color: #EB4259;\n          }\n        }\n      }\n    }\n  }\n\n  .data-wrapper::-webkit-scrollbar {\n    width: 0 !important;\n  }\n\n  .night {\n    >>>.source-item {\n      border-bottom: 1px solid #333;\n    }\n  }\n\n  .day {\n    >>>.source-item {\n      border-bottom: 1px solid #eee;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "web/src/components/Bookmark.vue",
    "content": "<template>\n  <el-dialog\n    :visible.sync=\"show\"\n    :width=\"dialogWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    v-if=\"$store.getters.isNormalPage\"\n    :before-close=\"cancel\"\n  >\n    <div class=\"custom-dialog-title\" slot=\"title\">\n      <span class=\"el-dialog__title\"\n        >{{ this.book ? this.book.name : \"\" }} 书签管理\n        <span class=\"float-right span-btn\" @click=\"uploadFile\">导入</span>\n        <input\n          ref=\"fileRef\"\n          type=\"file\"\n          @change=\"onFileChange($event)\"\n          style=\"display:none\"\n        />\n      </span>\n    </div>\n    <div class=\"source-container table-container\">\n      <el-table\n        :data=\"bookmarkList\"\n        :height=\"dialogContentHeight\"\n        @selection-change=\"localSelection = $event\"\n      >\n        <el-table-column\n          type=\"selection\"\n          width=\"25\"\n          :fixed=\"$store.state.miniInterface\"\n        >\n        </el-table-column>\n        <el-table-column\n          min-width=\"150px\"\n          label=\"书籍\"\n          :fixed=\"$store.state.miniInterface\"\n        >\n          <template slot-scope=\"scope\">\n            {{ scope.row.bookName }} - {{ scope.row.bookAuthor }}\n          </template>\n        </el-table-column>\n        <el-table-column property=\"chapterName\" label=\"章节\" min-width=\"150px\">\n        </el-table-column>\n        <el-table-column property=\"bookText\" label=\"内容\" min-width=\"150px\">\n        </el-table-column>\n        <el-table-column property=\"content\" label=\"备注\" min-width=\"150px\">\n        </el-table-column>\n        <el-table-column label=\"操作\" width=\"100px\">\n          <template slot-scope=\"scope\">\n            <el-button type=\"text\" @click=\"showBookmark(scope.row)\"\n              >跳转</el-button\n            >\n            <el-button type=\"text\" @click=\"editBookmark(scope.row)\"\n              >编辑</el-button\n            >\n          </template>\n        </el-table-column>\n      </el-table>\n    </div>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"deleteBookmarks\"\n        >批量删除</el-button\n      >\n      <span class=\"check-tip\">已选择 {{ localSelection.length }} 个</span>\n      <el-button size=\"medium\" @click=\"cancel\">取消</el-button>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport eventBus from \"../plugins/eventBus\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"Bookmark\",\n  data() {\n    return {\n      localSelection: []\n    };\n  },\n  computed: {\n    ...mapGetters([\"dialogWidth\", \"dialogTop\", \"dialogContentHeight\"]),\n    bookmarkList() {\n      if (!this.book || !this.book.name) {\n        return this.$store.state.bookmarks;\n      }\n      return this.$store.state.bookmarks.filter(\n        v => v.bookName === this.book.name && v.bookAuthor === this.book.author\n      );\n    }\n  },\n  props: [\"show\", \"book\"],\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        //\n      }\n    }\n  },\n  methods: {\n    formatTableField(row, column, cellValue) {\n      switch (column.property) {\n        default:\n          return cellValue;\n      }\n    },\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    async deleteBookmarks() {\n      if (!this.localSelection.length) {\n        this.$message.error(\"请选择需要删除的书签\");\n        return;\n      }\n      const res = await this.$confirm(\"确认要删除所选择的书签吗?\", \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteBookmarks\", this.localSelection).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.localSelection = [];\n            this.$message.success(\"删除书签成功\");\n            this.$root.$children[0].loadBookmarks(true);\n          }\n        },\n        error => {\n          this.$message.error(\"删除书签失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    editBookmark(row) {\n      eventBus.$emit(\"showBookmarkForm\", { ...row }, false);\n    },\n    uploadFile() {\n      this.$refs.fileRef.dispatchEvent(new MouseEvent(\"click\"));\n    },\n    onFileChange(event) {\n      const rawFile = event.target.files && event.target.files[0];\n      // console.log(\"rawFile\", rawFile);\n      const reader = new FileReader();\n      reader.onload = e => {\n        const data = e.target.result;\n        try {\n          const bookmarkList = JSON.parse(data);\n          if (Array.isArray(bookmarkList) && bookmarkList.length) {\n            this.comfirmImport(bookmarkList);\n          }\n        } catch (error) {\n          this.$message.error(\"书签文件错误\");\n        }\n      };\n      reader.onerror = () => {\n        // console.log(\"FileReader error\", e);\n        // FileReader 读取出错，只能上传读取了\n        let param = new FormData();\n        param.append(\"file\", rawFile);\n        Axios.post(this.api + \"/readSourceFile\", param, {\n          headers: { \"Content-Type\": \"multipart/form-data\" }\n        }).then(\n          res => {\n            if (res.data.isSuccess) {\n              //\n              let bookmarkList = [];\n              res.data.data.forEach(v => {\n                try {\n                  const data = JSON.parse(v);\n                  if (Array.isArray(data)) {\n                    bookmarkList = bookmarkList.concat(data);\n                  }\n                } catch (error) {\n                  //\n                }\n              });\n              if (bookmarkList.length) {\n                this.comfirmImport(bookmarkList);\n              } else {\n                this.$message.error(\"书签文件错误\");\n              }\n            }\n          },\n          error => {\n            this.$message.error(\n              \"读取书签文件内容失败 \" + (error && error.toString())\n            );\n          }\n        );\n      };\n      reader.readAsText(rawFile);\n      this.$refs.fileRef.value = null;\n    },\n    async comfirmImport(bookmarkList) {\n      const res = await this.$confirm(\n        `确认要导入文件中的${bookmarkList.length}条书签吗?`,\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/saveBookmarks\", bookmarkList).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"导入书签成功\");\n            this.$root.$children[0].loadBookmarks(true);\n          }\n        },\n        error => {\n          this.$message.error(\"导入书签失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    showBookmark(bookmark) {\n      eventBus.$emit(\"showBookmark\", bookmark);\n      this.cancel();\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.float-left {\n  float: left;\n}\n</style>\n"
  },
  {
    "path": "web/src/components/BookmarkForm.vue",
    "content": "<template>\n  <el-dialog\n    title=\"书签\"\n    :visible.sync=\"show\"\n    :width=\"dialogWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    v-if=\"$store.getters.isNormalPage\"\n    :before-close=\"cancel\"\n  >\n    <el-form :model=\"bookmarkForm\">\n      <el-form-item label=\"书名\">\n        <el-input v-model=\"bookmarkForm.bookName\" readonly></el-input>\n      </el-form-item>\n      <el-form-item label=\"作者\">\n        <el-input v-model=\"bookmarkForm.bookAuthor\" readonly></el-input>\n      </el-form-item>\n      <el-form-item label=\"章节\">\n        <el-input v-model=\"bookmarkForm.chapterName\" readonly></el-input>\n      </el-form-item>\n      <el-form-item label=\"内容\">\n        <el-input\n          v-model=\"bookmarkForm.bookText\"\n          type=\"textarea\"\n          :rows=\"5\"\n          readonly\n        ></el-input>\n      </el-form-item>\n      <el-form-item label=\"备注\">\n        <el-input\n          v-model=\"bookmarkForm.content\"\n          type=\"textarea\"\n          :rows=\"3\"\n        ></el-input>\n      </el-form-item>\n    </el-form>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button size=\"medium\" @click=\"cancel\">取 消</el-button>\n      <el-button size=\"medium\" type=\"primary\" @click=\"save\">确 定</el-button>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport { defaultBookmark } from \"../plugins/config.js\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"BookmarkForm\",\n  data() {\n    return {\n      bookmarkForm: { ...defaultBookmark }\n    };\n  },\n  props: [\"show\", \"bookmark\", \"isAdd\"],\n  computed: {\n    ...mapGetters([\"dialogWidth\", \"dialogTop\", \"dialogContentHeight\"])\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        this.bookmarkForm = this.bookmark || { ...defaultBookmark };\n      }\n    }\n  },\n  methods: {\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    save() {\n      if (!this.bookmarkForm.bookName && !this.bookmarkForm.bookAuthor) {\n        this.$message.error(\"书籍信息错误\");\n        return;\n      }\n      if (!this.bookmarkForm.bookText) {\n        this.$message.error(\"书籍内容不能为空\");\n        return;\n      }\n      const form = { ...this.bookmarkForm };\n      Axios.post(\"/saveBookmark\", form).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success((this.isAdd ? \"新增\" : \"编辑\") + \"书签成功\");\n            this.$root.$children[0].loadBookmarks(true);\n            this.cancel();\n          }\n        },\n        error => {\n          this.$message.error(\n            (this.isAdd ? \"新增\" : \"编辑\") +\n              \"书签失败 \" +\n              (error && error.toString())\n          );\n        }\n      );\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.float-left {\n  float: left;\n}\n</style>\n"
  },
  {
    "path": "web/src/components/Content.vue",
    "content": "<script>\nimport { loadFont } from \"../plugins/helper\";\nexport default {\n  name: \"Content\",\n  data() {\n    return {\n      currentTime: 0,\n      audioDuration: 0,\n      playing: false,\n      currentSpeed: 1,\n      audioVolume: 100,\n      startTime: 0,\n      iframeStyle: {}\n    };\n  },\n  props: [\n    \"content\",\n    \"error\",\n    \"title\",\n    \"showContent\",\n    \"isScrollRead\",\n    \"showChapterList\",\n    \"currentShowChapter\"\n  ],\n  render() {\n    if (this.showContent) {\n      if (\n        this.$store.getters.currentChapter &&\n        this.$store.getters.currentChapter.isVolume\n      ) {\n        return (\n          <div\n            class=\"content-body chapter-content reading-chapter volume-chapter\"\n            style={this.containerStyle}\n          >\n            <div class=\"volume-content\">\n              <h3 data-pos={0}>{this.title}</h3>\n              <p class=\"volume-tag\">{this.content}</p>\n            </div>\n          </div>\n        );\n      }\n      if (this.isAudio) {\n        // 音频\n        return this.renderAudio();\n      } else if (this.isEpub) {\n        // epub\n        return this.renderEpub();\n      }\n      if (this.isScrollRead) {\n        return this.renderScrollChapterList();\n      }\n      let wordCount = this.title.length + 2; // 2为两个换行符\n      return (\n        <div\n          class=\"content-body chapter-content reading-chapter\"\n          style={this.containerStyle}\n          v-lazy-container={{\n            selector: \"img\"\n          }}\n        >\n          {this.isCbz ? null : <h3 data-pos={0}>{this.title}</h3>}\n          {this.content.split(/\\n+/).map(a => {\n            a = a.replace(/^\\s+/g, \"\");\n            if (!a) {\n              return null;\n            }\n            const pos = wordCount;\n            wordCount += a.length + 2; // 2为两个换行符\n            if (a.indexOf(\"<img\") >= 0) {\n              // 漫画\n              // 将 src 替换为 data-src 懒加载\n              a = a\n                .replace(/src=/g, \"data-src=\")\n                .replace(\"__API_ROOT__\", this.$store.getters.apiRoot);\n              return (\n                <div\n                  style={this.containerStyle}\n                  domPropsInnerHTML={a}\n                  data-pos={pos}\n                ></div>\n              );\n            }\n            // 文本内容\n            return (\n              <p style={this.pStyle} domPropsInnerHTML={a} data-pos={pos} />\n            );\n          })}\n        </div>\n      );\n    } else {\n      return <div />;\n    }\n  },\n  mounted() {\n    if (this.isAudio) {\n      this.play(true);\n    } else if (this.isEpub) {\n      this.initIframe();\n    }\n    window.contentCom = this;\n    this.loadCustomFontFamil();\n  },\n  computed: {\n    readingBook() {\n      return this.$store.getters.readingBook;\n    },\n    chapter() {\n      return (\n        this.$store.getters.readingBook.catalog[\n          this.$store.getters.readingBook.index\n        ] || {}\n      );\n    },\n    show() {\n      return this.$store.state.showContent;\n    },\n    fontSize() {\n      return this.$store.getters.config.fontSize + \"px\";\n    },\n    autoPlay: {\n      get() {\n        return this.$store.state.autoPlay;\n      },\n      set(val) {\n        this.$store.commit(\"setAutoPlay\", val);\n      }\n    },\n    isCarToon() {\n      return (\n        !this.error && !this.isEpub && (this.content || \"\").indexOf(\"<img\") >= 0\n      );\n    },\n    isAudio() {\n      return !this.error && this.readingBook.type === 1;\n    },\n    isEpub() {\n      return (\n        !this.error && this.readingBook.bookUrl.toLowerCase().endsWith(\".epub\")\n      );\n    },\n    isCbz() {\n      return (\n        !this.error && this.readingBook.bookUrl.toLowerCase().endsWith(\".cbz\")\n      );\n    },\n    containerStyle() {\n      return {\n        fontSize: this.$store.getters.config.fontSize + \"px\",\n        fontWeight: this.$store.getters.config.fontWeight || undefined,\n        color:\n          this.$store.getters.config.fontColor ||\n          (this.$store.getters.isNight ? \"#666\" : \"#262626\"),\n        ...this.$store.getters.currentFontFamily,\n        ...(this.$store.getters.config.contentCSS || {})\n      };\n    },\n    pStyle() {\n      return {\n        lineHeight: this.$store.getters.config.lineHeight,\n        marginTop:\n          typeof this.$store.getters.config.paragraphSpace !== \"undefined\"\n            ? this.$store.getters.config.paragraphSpace + \"em\"\n            : null,\n        marginBottom:\n          typeof this.$store.getters.config.paragraphSpace !== \"undefined\"\n            ? this.$store.getters.config.paragraphSpace + \"em\"\n            : null\n      };\n    },\n    windowSize() {\n      return this.$store.state.windowSize;\n    },\n    currentCustomFontFamily() {\n      return this.$store.getters.currentCustomFontFamily;\n    }\n  },\n  watch: {\n    containerStyle() {\n      if (this.isEpub) {\n        this.setIframeStyle();\n      }\n    },\n    pStyle() {\n      if (this.isEpub) {\n        this.setIframeStyle();\n      }\n    },\n    windowSize() {\n      if (this.isEpub) {\n        //\n      }\n    },\n    currentCustomFontFamily() {\n      this.loadCustomFontFamil();\n    }\n  },\n  methods: {\n    renderScrollChapterList() {\n      return (\n        <div\n          class=\"content-body\"\n          style={this.containerStyle}\n          v-lazy-container={{\n            selector: \"img\"\n          }}\n        >\n          {this.showChapterList.map(chapter => {\n            if (chapter.isVolume) {\n              return (\n                <div class=\"content-body chapter-content reading-chapter volume-chapter\">\n                  <div class=\"volume-content\">\n                    <h3 data-pos={0}>{chapter.title}</h3>\n                    <p class=\"volume-tag\">{chapter.content}</p>\n                  </div>\n                </div>\n              );\n            }\n            let wordCount = chapter.title.length + 2; // 2为两个换行符\n            return (\n              <div\n                class={[\n                  \"chapter-content\",\n                  this.readingBook.index === chapter.index\n                    ? \"reading-chapter\"\n                    : \"\"\n                ]}\n                data-index={chapter.index}\n              >\n                {this.isCbz ? null : <h3 data-pos={0}>{chapter.title}</h3>}\n                {chapter.content.split(/\\n+/).map(a => {\n                  a = a.replace(/^\\s+/g, \"\");\n                  if (!a) {\n                    return null;\n                  }\n                  const pos = wordCount;\n                  wordCount += a.length + 2; // 2为两个换行符\n                  if (a.indexOf(\"<img\") >= 0) {\n                    // 漫画\n                    // 将 src 替换为 data-src 懒加载\n                    a = a\n                      .replace(/src=/g, \"data-src=\")\n                      .replace(\"__API_ROOT__\", this.$store.getters.apiRoot);\n                    return (\n                      <div\n                        style={this.containerStyle}\n                        domPropsInnerHTML={a}\n                        data-pos={pos}\n                      ></div>\n                    );\n                  }\n                  // 文本内容\n                  return (\n                    <p\n                      style={this.pStyle}\n                      domPropsInnerHTML={a}\n                      data-pos={pos}\n                    />\n                  );\n                })}\n              </div>\n            );\n          })}\n        </div>\n      );\n    },\n    renderAudio() {\n      return (\n        <div class=\"content-audio\">\n          <audio\n            ref=\"audio\"\n            preload=\"preload\"\n            src={this.content}\n            vOn:loadMetaData={this.audioEvent}\n            vOn:progress={this.onProgress}\n            vOn:playing={this.onProgress}\n            vOn:timeupdate={this.onTimeupdate}\n            vOn:play={this.onPlay}\n            vOn:pause={this.onPause}\n            vOn:ended={this.onEnd}\n            vOn:error={this.onError}\n            vOn:seeked={this.onSeeked}\n            vOn:seeking={this.onSeeking}\n            vOn:stalled={this.audioEvent}\n            vOn:suspend={this.onsuspend}\n            vOn:loadeddata={this.audioEvent}\n            vOn:loadedmetadata={this.audioEvent}\n            vOn:canplay={this.onCanPlay}\n            vOn:canplaythrough={this.audioEvent}\n            vOn:waiting={this.onWaiting}\n          ></audio>\n          <div class=\"book-cover\">\n            <img v-lazy={this.getCover(this.readingBook.coverUrl)} />\n          </div>\n          <div class=\"book-progress\">\n            <div class=\"progress-tip\">{this.formatTime(this.currentTime)}</div>\n            <div class=\"progress-container\">\n              <el-slider\n                vModel={this.currentTime}\n                min={0}\n                max={this.audioDuration}\n                show-tooltip={false}\n                vOn:change={val => {\n                  this.seekTime(val);\n                }}\n              ></el-slider>\n            </div>\n            <div class=\"progress-tip total-time\">\n              {this.formatTime(this.audioDuration)}\n            </div>\n          </div>\n          <div class=\"book-operation\">\n            <i\n              class=\"reader-iconfont reader-icon-jian15s\"\n              vOn:click_stop_prevent={() => {\n                this.seekTime(this.$refs.audio.currentTime - 15);\n              }}\n            ></i>\n            <i\n              class=\"reader-iconfont reader-icon-player-backward-step\"\n              vOn:click_stop_prevent={this.prevChapter}\n            ></i>\n            <i\n              class={[\n                \"reader-iconfont\",\n                this.playing\n                  ? \"reader-icon-player-play\"\n                  : \"reader-icon-player-pause\"\n              ]}\n              vOn:click_stop_prevent={this.toggle}\n            ></i>\n            <i\n              class=\"reader-iconfont reader-icon-player-forward-step\"\n              vOn:click_stop_prevent={this.nextChapter}\n            ></i>\n            <i\n              class=\"reader-iconfont reader-icon-15s\"\n              vOn:click_stop_prevent={() => {\n                this.seekTime(this.$refs.audio.currentTime + 15);\n              }}\n            ></i>\n          </div>\n          <div class=\"book-operation\">\n            <span\n              style={{\n                display: \"flex\",\n                flexDirection: \"row\",\n                alignItems: \"center\"\n              }}\n            >\n              <i\n                class={[\n                  \"reader-iconfont\",\n                  this.audioVolume > 0\n                    ? \"reader-icon-volume\"\n                    : \"reader-icon-volume-off\"\n                ]}\n                vOn:click_stop_prevent={() => {\n                  this.setAudioVolume(this.audioVolume > 0 ? 0 : 100);\n                }}\n                style={{ marginRight: this.audioVolume > 0 ? \"15px\" : \"25px\" }}\n              ></i>\n              <el-slider\n                vModel={this.audioVolume}\n                min={0}\n                max={100}\n                style={{ width: \"180px\" }}\n                show-tooltip={false}\n                vOn:change={val => {\n                  this.setAudioVolume(val);\n                }}\n              ></el-slider>\n            </span>\n          </div>\n          <div\n            class=\"book-info\"\n            style={{\n              background: this.getCover(this.readingBook.coverUrl, true)\n            }}\n          >\n            <div class=\"book-cover\">\n              <img v-lazy={this.getCover(this.readingBook.coverUrl)} />\n            </div>\n            <div class=\"book-intro\">\n              <div class=\"title\">{this.title}</div>\n              <div class=\"subtitle\">\n                {this.readingBook.name}\n                {this.readingBook.author ? \"•\" : \"\"}\n                {this.readingBook.author}\n              </div>\n            </div>\n          </div>\n        </div>\n      );\n    },\n    renderEpub() {\n      return (\n        <iframe\n          class=\"epub-iframe\"\n          ref=\"iframe\"\n          style={this.iframeStyle}\n          src={this.$store.getters.apiRoot + this.content}\n        ></iframe>\n      );\n    },\n    initIframe() {\n      window.addEventListener(\"message\", event => {\n        if (\n          this.$refs.iframe &&\n          event.source === this.$refs.iframe.contentWindow\n        ) {\n          //\n          let message;\n          try {\n            message = JSON.parse(event.data);\n          } catch (error) {\n            return;\n          }\n          if (message.event === \"inited\") {\n            this.iframeStyle = {};\n            // 设置iframe样式\n            this.setIframeStyle();\n            // 同步iframe高度\n            this.syncIframeHeight();\n          } else if (message.event === \"load\") {\n            setTimeout(() => {\n              this.$emit(\"iframeLoad\");\n              this.$emit(\"epubLocationChange\", message.data);\n            }, 100);\n          } else if (message.event === \"setHeight\") {\n            this.iframeStyle = {\n              ...this.iframeStyle,\n              height:\n                Math.max(message.data, this.windowSize.height * 0.8) + \"px\"\n            };\n            this.$emit(\"contentChange\");\n          } else if (message.event === \"click\") {\n            this.$emit(\"epubClick\", message.data);\n          } else if (message.event === \"clickHash\") {\n            this.$emit(\"epubClickHash\", message.data);\n          } else if (message.event === \"keydown\") {\n            this.$emit(\"epubKeydown\", message.data);\n          } else if (message.event === \"previewImageList\") {\n            this.$store.commit(\"setPreviewImageIndex\", message.data.imageIndex);\n            this.$store.commit(\"setPreviewImgList\", message.data.imageList);\n          }\n          // else if (message.event === \"clickA\") {\n          //   this.$emit(\"locationChange\", message.data);\n          // }\n        }\n      });\n    },\n    syncIframeHeight() {\n      this.sendToIframe(\"execute\", {\n        script:\n          \"reader_notify('setHeight', document.documentElement.scrollHeight || document.body.scrollHeight)\"\n      });\n    },\n    setIframeStyle() {\n      let bodyStyle = \"\";\n      for (const i in this.containerStyle) {\n        if (Object.hasOwnProperty.call(this.containerStyle, i)) {\n          bodyStyle +=\n            i.replace(/([A-Z])/g, v => \"-\" + v.toLowerCase()) +\n            \":\" +\n            this.containerStyle[i] +\n            \" !important;\";\n        }\n      }\n      let pStyle = \"\";\n      for (const i in this.pStyle) {\n        if (Object.hasOwnProperty.call(this.pStyle, i)) {\n          pStyle +=\n            i.replace(/([A-Z])/g, v => \"-\" + v.toLowerCase()) +\n            \":\" +\n            this.pStyle[i] +\n            \" !important;\";\n        }\n      }\n      pStyle +=\n        \"font-family: \" + this.containerStyle.fontFamily + \" !important;\";\n      pStyle += \"font-size: \" + this.containerStyle.fontSize + \" !important;\";\n      pStyle +=\n        \"font-weight: \" + this.containerStyle.fontWeight + \" !important;\";\n      pStyle += \"color: \" + this.containerStyle.color + \" !important;\";\n\n      this.sendToIframe(\"setStyle\", {\n        style: `\n        *::-webkit-scrollbar {\n          display: none;\n          width: 0 !important;\n          height: 0 !important;\n        }\n        *:focus {\n          outline: none !important;\n        }\n        html {\n          min-height: 100%;\n        }\n        body {\n          margin: 0 !important;\n          ${bodyStyle}\n        }\n        body p {\n          ${pStyle}\n        }\n        img {\n          display: block;\n          max-width: 100vw !important;\n          height: auto !important;\n        }`\n      });\n    },\n    sendToIframe(event, data) {\n      if (!this.$refs.iframe) {\n        setTimeout(() => {\n          this.sendToIframe(event, data);\n        }, 10);\n        return;\n      }\n      this.$refs.iframe &&\n        this.$refs.iframe.contentWindow &&\n        this.$refs.iframe.contentWindow.postMessage(\n          JSON.stringify({\n            event,\n            ...data\n          }),\n          \"*\"\n        );\n    },\n    formatTime(val) {\n      if (!val) {\n        return \"00:00\";\n      }\n      const pad = v => (v >= 10 ? \"\" + v : \"0\" + v);\n      if (val < 60) {\n        return \"00:\" + pad(val);\n      } else if (val < 3600) {\n        const m = Math.round(val / 60);\n        const s = val % 60;\n        return pad(m) + \":\" + pad(s);\n      } else {\n        const h = Math.round(val / 3600);\n        const m = Math.round(val / 3600 / 60);\n        const s = val % 60;\n        return pad(h) + \":\" + pad(m) + \":\" + pad(s);\n      }\n    },\n    seekTime(val) {\n      if (!isNaN(val) && val !== Infinity) {\n        if (this.$refs.audio) {\n          this.$refs.audio.currentTime = val;\n        }\n      }\n    },\n    setAudioVolume(val) {\n      if (!isNaN(val) && val !== Infinity) {\n        this.audioVolume = val;\n        if (this.$refs.audio) {\n          this.$refs.audio.volume = val / 100;\n        }\n      }\n    },\n    ensureSeekTime(val) {\n      this.startTime = val;\n    },\n    toggle() {\n      if (this.playing) {\n        this.$refs.audio && this.$refs.audio.pause();\n      } else {\n        this.play();\n      }\n    },\n    play(init) {\n      if (!this.$refs.audio) {\n        setTimeout(() => {\n          this.play(init);\n        }, 10);\n        return;\n      }\n      if (init) {\n        this.$refs.audio.load();\n        this.computeDuration();\n      }\n      if (!init || this.autoPlay) {\n        this.$refs.audio.play();\n      }\n    },\n    prevChapter() {\n      this.autoPlay = true;\n      this.$emit(\"prevChapter\");\n    },\n    nextChapter() {\n      this.autoPlay = true;\n      this.$emit(\"nextChapter\");\n    },\n    computeDuration() {\n      if (!this.$refs.audio) {\n        setTimeout(() => {\n          this.computeDuration();\n        }, 100);\n        return;\n      }\n      let duration = this.$refs.audio.duration;\n      if (\n        this.$refs.audio.readyState >= 1 &&\n        !isNaN(duration) &&\n        duration !== Infinity &&\n        duration\n      ) {\n        this.audioDuration = parseInt(duration);\n        this.$refs.audio.playbackRate = this.currentSpeed;\n        this.$refs.audio.currentTime = this.startTime;\n        // 有时会失败（看浏览器）\n        if (this.autoPlay) {\n          this.$refs.audio.play();\n        }\n      } else {\n        setTimeout(() => {\n          this.computeDuration();\n        }, 50);\n      }\n    },\n    onProgress() {\n      // 记录缓存进度。触发事件包括缓存数据更新时的 progress 事件，以及各种播放动作会触发的 playing 事件\n    },\n    onTimeupdate() {\n      if (this.$refs.audio) {\n        this.currentTime = this.$refs.audio.currentTime | 0;\n      }\n      this.$emit(\"updateProgress\");\n    },\n    onPlay() {\n      this.playing = true;\n    },\n    onPause() {\n      this.playing = false;\n    },\n    onEnd() {\n      this.playing = false;\n      this.currentTime = 0;\n      this.audioDuration = 0;\n      this.autoPlay = true;\n      this.$emit(\"nextChapter\");\n    },\n    onError(event) {\n      // console.log(arguments);\n      this.$message.error(event.toString());\n      this.playing = false;\n    },\n    onSeeked() {},\n    onSeeking() {},\n    audioEvent() {\n      // console.log(\"audioEvent\", arguments);\n    },\n    onsuspend() {\n      // console.log(\"onsuspend\", arguments);\n    },\n    onCanPlay() {\n      // console.log(\"onCanPlay\", arguments);\n    },\n    onWaiting() {\n      // console.log(\"onWaiting\", arguments);\n    },\n    loadCustomFontFamil() {\n      if (this.currentCustomFontFamily) {\n        loadFont(\n          this.currentCustomFontFamily.name,\n          this.currentCustomFontFamily.url\n        );\n      }\n    }\n  }\n};\n</script>\n\n<style lang=\"stylus\" scoped>\np {\n  display: block;\n  word-wrap: break-word;\n  word-break: break-all;\n  text-indent: 2em;\n}\np.reading {\n  color: red !important;\n}\nh3 {\n  font-size: 28px;\n  line-height: 1.2;\n  margin: 1em 0;\n  text-align: center;\n}\nh3.reading {\n  color: red !important;\n}\n.volume-chapter {\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n\n  .volume-content {\n    text-align: center;\n  }\n\n  .volume-tag {\n    text-align: right;\n  }\n}\n.content-audio {\n  margin: 0 auto;\n  width: 100%;\n\n  .book-cover {\n\n    img {\n      max-width: 200px;\n      margin: 0 auto;\n      display: block;\n    }\n  }\n\n  .book-progress {\n    padding: 25px 15px;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n\n    .progress-tip {\n      padding-top: 5px;\n      padding-bottom: 5px;\n      font-size: 14px;\n      width: 45px;\n    }\n\n    .progress-container {\n      flex: 1;\n      margin-left: 10px;\n      margin-right: 10px;\n    }\n\n    .total-time {\n      text-align: right;\n    }\n  }\n\n  .book-operation {\n    padding: 0px 15px 25px;\n    display: flex;\n    flex-direction: row;\n    justify-content: space-around;\n\n    i {\n      display: inline-block;\n      cursor: pointer;\n      font-size: 24px;\n      line-height: 1;\n    }\n  }\n\n  .book-info {\n    padding: 10px 15px;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n\n    .book-cover {\n      width: 50px;\n\n      img {\n        width: 100%;\n        max-height: 100%;\n      }\n    }\n\n    .book-intro {\n      flex: 1;\n      padding-left: 15px;\n\n      .title {\n        font-size: 16px;\n      }\n\n      .subtitle {\n        margin-top: 5px;\n        font-size: 14px;\n      }\n    }\n  }\n}\n.epub-iframe {\n  border: none;\n  width: 100%;\n  min-height: calc(var(--vh, 1vh) * 50);\n  // pointer-events: none;\n}\n</style>\n<style lang=\"stylus\">\n.content-body {\n  img {\n    width: 100%;\n    max-width: 100vw;\n    display: block;\n  }\n}\n.day {\n  .content-audio {\n    .book-operation {\n      color: #222;\n    }\n\n    .book-intro {\n      .title {\n        color: #121212;\n      }\n      .subtitle {\n        color: #666;\n      }\n    }\n  }\n}\n.night {\n  .content-audio {\n    .book-operation {\n      color: #888;\n    }\n\n    .book-intro {\n      .title {\n        color: #888;\n      }\n      .subtitle {\n        color: #666;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "web/src/components/Explore.vue",
    "content": "<template>\n  <div class=\"popup-wrapper\" :style=\"popupTheme\">\n    <div class=\"title-zone\">\n      <div class=\"title\">书海</div>\n      <div :class=\"{ 'title-btn': true }\">\n        <span class=\"source-count\">\n          共{{ bookSourceShowLength }}个可用书源\n        </span>\n        <i\n          class=\"el-icon-close close-btn\"\n          v-if=\"$store.state.miniInterface\"\n          @click.stop=\"close\"\n        ></i>\n      </div>\n    </div>\n    <div class=\"source-group-wrapper\">\n      <el-tag\n        type=\"info\"\n        :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n        class=\"source-group-btn\"\n        :class=\"sourceGroup === name ? 'selected' : ''\"\n        v-for=\"name in bookSourceGroup\"\n        :key=\"'sourceGroup-' + name\"\n        @click=\"setSourceGroup(name)\"\n      >\n        {{ name }}\n      </el-tag>\n    </div>\n    <div\n      class=\"data-wrapper\"\n      ref=\"sourceList\"\n      :class=\"{ night: $store.getters.isNight, day: !$store.getters.isNight }\"\n      @scroll=\"scrollHandler\"\n    >\n      <div class=\"cata\">\n        <el-collapse\n          class=\"source-collapse\"\n          ref=\"sourceList\"\n          v-model=\"showCollapse\"\n        >\n          <el-collapse-item\n            v-for=\"(source, index) in bookSourceListNew\"\n            :title=\"source.bookSourceName\"\n            :name=\"index\"\n            :key=\"'source-' + index\"\n            ref=\"source\"\n            v-show=\"\n              !sourceGroup ||\n                (sourceGroup === '未分组' && source.bookSourceGroup === '') ||\n                source.bookSourceGroup === sourceGroup\n            \"\n          >\n            <template v-if=\"showCollapse.includes(index)\">\n              <div\n                class=\"explore-group\"\n                v-for=\"(group, indexG) in source.exploreGroup\"\n                :key=\"'group-' + indexG\"\n              >\n                <el-tag\n                  type=\"info\"\n                  :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n                  class=\"explore-btn\"\n                  v-for=\"(item, indexI) in group\"\n                  :key=\"'group-' + indexI\"\n                  @click=\"exploreBookSource(item.url, source.bookSourceUrl, 1)\"\n                >\n                  {{ item.name }}\n                </el-tag>\n              </div>\n            </template>\n          </el-collapse-item>\n        </el-collapse>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport jump from \"../plugins/jump\";\nimport Axios from \"../plugins/axios\";\nexport default {\n  name: \"Explore\",\n  data() {\n    return {\n      page: 1,\n      ruleFindUrl: \"\",\n      bookSourceUrl: \"\",\n      exploreList: [],\n      showCollapse: [],\n      sourceGroup: \"\"\n    };\n  },\n  props: [\"visible\", \"bookSourceList\"],\n  computed: {\n    theme() {\n      return this.$store.getters.config.theme;\n    },\n    popupTheme() {\n      return {\n        background: this.$store.getters.currentThemeConfig.popup\n      };\n    },\n    bookSourceListNew() {\n      const list = [];\n      this.bookSourceList.forEach(source => {\n        const exploreGroup = this.getExploreGroup(source);\n        if (exploreGroup.length) {\n          list.push({\n            bookSourceName: source.bookSourceName,\n            bookSourceGroup: source.bookSourceGroup,\n            exploreGroup,\n            bookSourceUrl: source.bookSourceUrl\n          });\n        }\n      });\n      // console.log(list);\n      return list;\n    },\n    bookSourceGroup() {\n      const groups = new Set();\n      this.bookSourceListNew.forEach(v => {\n        v.bookSourceGroup && groups.add(v.bookSourceGroup);\n      });\n      groups.add(\"未分组\");\n      return Array.from(groups);\n    },\n    bookSourceShowLength() {\n      if (!this.sourceGroup) {\n        return this.bookSourceListNew.length;\n      }\n      return this.bookSourceListNew.filter(v =>\n        this.sourceGroup === \"未分组\"\n          ? !v.bookSourceGroup\n          : v.bookSourceGroup === this.sourceGroup\n      ).length;\n    }\n  },\n  mounted() {\n    window.explorePop = this;\n  },\n  watch: {\n    visible(isVisible) {\n      if (isVisible) {\n        this.$nextTick(() => {\n          this.lastScrollTop &&\n            (this.$refs.sourceList.scrollTop = this.lastScrollTop);\n        });\n      }\n    }\n  },\n  methods: {\n    getExploreGroup(bookSource) {\n      if (!bookSource.exploreUrl) {\n        return [];\n      }\n      const result = [];\n      let zone = [];\n      let exploreUrlList = [];\n      try {\n        // 由于是在客户端解析，所以不支持解析 <js> 和 @js: 开头的 exploreUrl\n        // [{\\\"title\\\":\\\"玄幻奇幻\\\",\\\"url\\\":\\\"\\/xuanhuan\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}},{\\\"title\\\":\\\"武侠仙侠\\\",\\\"url\\\":\\\"\\/wuxia\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}},{\\\"title\\\":\\\"都市生活\\\",\\\"url\\\":\\\"\\/dushi\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}},{\\\"title\\\":\\\"历史军事\\\",\\\"url\\\":\\\"\\/lishi\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}},{\\\"title\\\":\\\"游戏竞技\\\",\\\"url\\\":\\\"\\/youxi\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}},{\\\"title\\\":\\\"科幻未来\\\",\\\"url\\\":\\\"\\/kehuan\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}},{\\\"title\\\":\\\"幻想奇缘\\\",\\\"url\\\":\\\"\\/huanxiang\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}},{\\\"title\\\":\\\"古代言情\\\",\\\"url\\\":\\\"\\/gudai\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}},{\\\"title\\\":\\\"二次い元\\\",\\\"url\\\":\\\"\\/erciyuan\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}},{\\\"title\\\":\\\"现代言情\\\",\\\"url\\\":\\\"\\/xiandai\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}},{\\\"title\\\":\\\"浪漫青春\\\",\\\"url\\\":\\\"\\/qingchun\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}},{\\\"title\\\":\\\"其他类型\\\",\\\"url\\\":\\\"\\/qita\\/{{page}}.html\\\",\\\"style\\\":{\\\"layout_flexGrow\\\":0.25}}]\n        // 尝试解析为 JSON\n        exploreUrlList = JSON.parse(bookSource.exploreUrl);\n      } catch (error) {\n        // 有些源的 key 是单引号的，尝试用 JS 解析\n        try {\n          exploreUrlList = new Function(\"return \" + bookSource.exploreUrl)();\n        } catch (error) {\n          //\n          // console.log(error);\n        }\n      }\n      if (Array.isArray(exploreUrlList) && exploreUrlList.length) {\n        let percent = 0;\n        exploreUrlList.forEach(v => {\n          // 只考虑 layout_flexBasisPercent 样式\n          const basisPercent =\n            (v.style && v.style.layout_flexBasisPercent) || 0.25;\n          zone.push({\n            name: v.title,\n            url: v.url\n          });\n          percent += basisPercent;\n          if (percent >= 1) {\n            // percent 超过1, 分割为一块\n            result.push(zone);\n            zone = [];\n            percent = 0;\n          }\n        });\n      } else {\n        // 解析为 字符串\n        bookSource.exploreUrl\n          .replace(/\\r\\n/g, \"\\n\")\n          .split(\"\\n\")\n          .forEach(v => {\n            if (!v) {\n              if (zone.length) {\n                // 出现空行，分割为一块\n                result.push(zone);\n                zone = [];\n              }\n            } else {\n              v = v.split(\"::\");\n              zone.push({\n                name: v[0],\n                url: v[1]\n              });\n            }\n          });\n      }\n\n      if (zone.length) {\n        result.push(zone);\n      }\n      // console.log(bookSource.bookSourceName, result);\n      return result;\n    },\n    exploreBookSource(url, sourceUrl, page) {\n      this.page = page || 1;\n      this.ruleFindUrl = url;\n      this.bookSourceUrl = sourceUrl;\n      Axios.post(this.api + `/exploreBook`, {\n        ruleFindUrl: url,\n        bookSourceUrl: sourceUrl,\n        page\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            //\n            if (page === 1) {\n              this.exploreList = res.data.data;\n            } else {\n              // this.exploreList = [].concat(this.exploreList, res.data.data);\n              var data = [].concat(this.exploreList);\n              var map = data.reduce((c, v) => {\n                c[v.bookUrl] = v;\n                return c;\n              }, {});\n              var length = data.length;\n              res.data.data.forEach(v => {\n                if (!map[v.bookUrl]) {\n                  data.push(v);\n                }\n              });\n              this.exploreList = data;\n              if (data.length === length) {\n                this.$message.error(\"没有更多啦\");\n              }\n            }\n            // console.log(this.exploreList);\n            this.$emit(\"showSearchList\", this.exploreList);\n          }\n        },\n        error => {\n          this.$message.error(\"探索失败 \" + (error && error.toString()));\n          throw error;\n        }\n      );\n    },\n    loadMore() {\n      this.page = this.page + 1;\n      this.exploreBookSource(this.ruleFindUrl, this.bookSourceUrl, this.page);\n    },\n    jumpToActive() {\n      this.$nextTick(() => {\n        let index = -1;\n        this.bookSourceListNew.some((v, i) => {\n          if (v.bookSourceUrl == this.bookSourceUrl) {\n            index = i;\n            return true;\n          }\n        });\n        if (index < 0) {\n          return;\n        }\n        let wrapper = this.$refs.sourceList;\n        jump(this.$refs.source[index], {\n          container: wrapper,\n          duration: 0\n        });\n      });\n    },\n    close() {\n      this.$emit(\"close\");\n    },\n    scrollHandler() {\n      this.lastScrollTop = this.$refs.sourceList.scrollTop;\n    },\n    setSourceGroup(group) {\n      if (this.sourceGroup === group) {\n        this.sourceGroup = \"\";\n      } else {\n        this.sourceGroup = group;\n      }\n    }\n  }\n};\n</script>\n\n<style lang=\"stylus\" scoped>\n.popup-wrapper {\n  margin: -16px;\n  margin-bottom: -13px;\n  padding: 24px;\n  padding-top: calc(24px + constant(safe-area-inset-top));\n  padding-top: calc(24px + env(safe-area-inset-top));\n\n  .title-zone {\n    margin: 0 0 20px 0;\n    width: 100%;\n    display: flex;\n    flex-direction: row;\n    flex-wrap: wrap;\n    justify-content: space-between;\n  }\n\n  .title {\n    font-size: 18px;\n    font-weight: 400;\n    font-family: -apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif;\n    color: #ed4259;\n    border-bottom: 1px solid #ed4259;\n    width: fit-content;\n  }\n\n  .title-btn {\n    font-size: 14px;\n    line-height: 26px;\n    color: #ed4259;\n    .source-count {\n      display: inline-block;\n      margin-right: 25px;\n      color: #606266;\n    }\n    .close-btn {\n      font-size: 20px;\n      vertical-align: middle;\n      cursor: pointer;\n    }\n    &.loading {\n      color: #606266;\n    }\n  }\n\n  .source-group-wrapper {\n    display: flex;\n    flex-direction: row;\n    overflow-x: auto;\n    padding: 5px 0;\n\n    .source-group-btn {\n      margin-right: 10px;\n      cursor: pointer;\n    }\n\n    .source-group-btn.selected {\n      color: #ed4259;\n    }\n  }\n\n  .data-wrapper {\n    height: 300px;\n    overflow: auto;\n\n    .cata {\n      display: flex;\n      flex-direction: row;\n      flex-wrap: wrap;\n      justify-content: space-between;\n\n      .source-collapse {\n        width: 100%;\n        border: none;\n      }\n\n      .explore-group {\n        display: flex;\n        justify-content: space-between;\n        margin-bottom: 2px;\n        padding-top: 2px;\n        overflow-x: auto;\n      }\n\n      .explore-btn {\n        margin-right: 15px;\n        margin-bottom: 5px;\n        cursor: pointer;\n      }\n    }\n  }\n\n  .data-wrapper::-webkit-scrollbar {\n    width: 0 !important;\n  }\n\n  >>>.el-collapse-item__header {\n    background: transparent;\n    color: #606266;\n  }\n\n  >>>.el-collapse-item__wrap {\n    background: transparent;\n    color: #606266;\n  }\n\n  >>>.el-collapse-item__content {\n    color: #606266;\n    padding: 10px;\n  }\n\n  .night {\n    >>>.el-collapse-item__header {\n      border-bottom: 1px solid #666;\n    }\n    >>>.el-collapse-item__wrap {\n      border-bottom: 1px solid #666;\n    }\n    >>>.explore-group {\n      border-bottom: 1px dashed #333;\n    }\n  }\n\n  .day {\n    >>>.el-collapse-item__header {\n      border-bottom: 1px solid #EBEEF5;\n    }\n    >>>.el-collapse-item__wrap {\n      border-bottom: 1px solid #EBEEF5;\n    }\n    >>>.explore-group {\n      border-bottom: 1px dashed #efefef;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "web/src/components/LocalStore.vue",
    "content": "<template>\n  <el-dialog\n    title=\"书仓文件管理\"\n    :visible.sync=\"show\"\n    :width=\"dialogWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    v-if=\"$store.getters.isNormalPage\"\n    :before-close=\"cancel\"\n  >\n    <div class=\"source-container table-container\">\n      <el-table\n        :data=\"localFileList\"\n        :height=\"dialogContentHeight\"\n        @selection-change=\"localFileSelection = $event\"\n      >\n        <el-table-column\n          type=\"selection\"\n          width=\"25\"\n          :fixed=\"$store.state.miniInterface\"\n          :selectable=\"row => !row.toParent\"\n        >\n        </el-table-column>\n        <el-table-column\n          property=\"name\"\n          min-width=\"150px\"\n          label=\"文件名\"\n          :fixed=\"$store.state.miniInterface\"\n        >\n          <template slot-scope=\"scope\">\n            <span v-if=\"!scope.row.isDirectory\">{{ scope.row.name }}</span>\n            <el-link\n              type=\"primary\"\n              v-if=\"scope.row.isDirectory\"\n              @click=\"showLocalStoreFile(scope.row.path)\"\n              >{{ scope.row.name }}</el-link\n            >\n          </template>\n        </el-table-column>\n        <el-table-column\n          property=\"size\"\n          label=\"大小\"\n          :formatter=\"formatTableField\"\n          min-width=\"100px\"\n        ></el-table-column>\n        <el-table-column\n          property=\"lastModified\"\n          label=\"修改时间\"\n          :formatter=\"formatTableField\"\n          width=\"120px\"\n        ></el-table-column>\n        <el-table-column label=\"操作\" width=\"100px\">\n          <template slot-scope=\"scope\">\n            <el-button\n              type=\"text\"\n              @click=\"deleteLocalStoreFile(scope.row)\"\n              style=\"color: #f56c6c\"\n              v-if=\"!scope.row.toParent\"\n              >删除</el-button\n            >\n            <el-button\n              type=\"text\"\n              @click=\"importFromLocalStore(scope.row)\"\n              v-if=\"canImport(scope.row)\"\n              >加入书架</el-button\n            >\n          </template>\n        </el-table-column>\n      </el-table>\n    </div>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"deleteLocalStoreFileList\"\n        >批量删除</el-button\n      >\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"importFromLocalStore(true)\"\n        >批量加入书架</el-button\n      >\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"uploadToLocalStore\"\n      >\n        上传书籍\n      </el-button>\n      <input\n        ref=\"bookRef\"\n        type=\"file\"\n        multiple=\"multiple\"\n        @change=\"onBookFileChange\"\n        style=\"display:none\"\n      />\n      <span class=\"check-tip\">已选择 {{ localFileSelection.length }} 个</span>\n      <el-button size=\"medium\" @click=\"cancel\">取消</el-button>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport { formatSize } from \"../plugins/helper\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"LocalStore\",\n  data() {\n    return {\n      localCurrentPath: \"/\",\n      localFileList: [],\n\n      localFileSelection: []\n    };\n  },\n  props: [\"show\"],\n  computed: {\n    ...mapGetters([\"dialogWidth\", \"dialogTop\", \"dialogContentHeight\"])\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        this.showLocalStoreFile(\"/\");\n      }\n    }\n  },\n  methods: {\n    formatTableField(row, column, cellValue) {\n      switch (column.property) {\n        case \"createdAt\":\n        case \"lastLoginAt\":\n        case \"lastModified\":\n          return cellValue ? new Date(cellValue).format(\"yy-MM-dd hh:mm\") : \"\";\n        case \"size\":\n          return row.isDirectory ? \"\" : formatSize(cellValue);\n        default:\n          return cellValue;\n      }\n    },\n    canImport(row) {\n      const path = row.path.toLowerCase();\n      return (\n        path.endsWith(\".txt\") ||\n        path.endsWith(\".epub\") ||\n        path.endsWith(\".umd\") ||\n        path.endsWith(\".cbz\")\n      );\n    },\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    showLocalStoreFile(path) {\n      this.localCurrentPath = path || \"/\";\n      Axios.get(this.api + \"/getLocalStoreFileList\", {\n        params: {\n          path: this.localCurrentPath\n        }\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            res.data.data = res.data.data || [];\n            if (this.localCurrentPath !== \"/\") {\n              const paths = this.localCurrentPath.split(\"/\").filter(v => v);\n              paths.pop();\n              res.data.data.unshift({\n                name: \"..\",\n                isDirectory: true,\n                toParent: true,\n                path: \"/\" + paths.join(\"/\")\n              });\n            }\n            this.localFileList = res.data.data;\n            this.showLocalStoreManageDialog = true;\n          }\n        },\n        error => {\n          this.$message.error(\n            \"加载书仓文件列表失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    async deleteLocalStoreFileList() {\n      if (!this.localFileSelection.length) {\n        this.$message.error(\"请选择需要删除的文件\");\n        return;\n      }\n      const res = await this.$confirm(\"确认要删除所选择的文件吗?\", \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteLocalStoreFileList\", {\n        path: this.localFileSelection.map(v => v.path)\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.localFileSelection = [];\n            this.$message.success(\"删除文件成功\");\n            this.showLocalStoreFile(this.localCurrentPath);\n          }\n        },\n        error => {\n          this.$message.error(\"删除文件失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async deleteLocalStoreFile(row) {\n      const res = await this.$confirm(\n        `确认要删除该${row.isDirectory ? \"文件夹\" : \"文件\"}吗?`,\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteLocalStoreFile\", {\n        path: row.path\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"删除文件成功\");\n            this.showLocalStoreFile(this.localCurrentPath);\n          }\n        },\n        error => {\n          this.$message.error(\"删除文件失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async importFromLocalStore(row) {\n      if (row === true) {\n        if (!this.localFileSelection.length) {\n          this.$message.error(\"请选择需要加入书架的书籍\");\n          return;\n        }\n      }\n      Axios.post(this.api + \"/importFromLocalPathPreview\", {\n        path:\n          row === true ? this.localFileSelection.map(v => v.path) : [row.path]\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            if (!res.data.data || !res.data.data.length) {\n              this.$message.error(\"没有选择可导入的书籍\");\n              return;\n            }\n            // this.cancel();\n            setTimeout(() => {\n              this.$emit(\"importFromLocalPathPreview\", res.data.data);\n            }, 0);\n          }\n        },\n        error => {\n          this.$message.error(\"请求失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    uploadToLocalStore() {\n      this.$refs.bookRef.dispatchEvent(new MouseEvent(\"click\"));\n    },\n    onBookFileChange(event) {\n      if (!event.target || !event.target.files || !event.target.files.length) {\n        return;\n      }\n      let param = new FormData();\n      for (let i = 0; i < event.target.files.length; i++) {\n        const file = event.target.files[i];\n        param.append(\"file\" + i, file);\n      }\n      param.append(\"path\", this.localCurrentPath);\n      Axios.post(this.api + \"/uploadFileToLocalStore\", param, {\n        headers: { \"Content-Type\": \"multipart/form-data\" }\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"上传书籍成功\");\n            this.showLocalStoreFile(this.localCurrentPath);\n          }\n        },\n        error => {\n          this.$message.error(\"上传书籍 \" + (error && error.toString()));\n        }\n      );\n      this.$refs.bookRef.value = null;\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.float-left {\n  float: left;\n}\n</style>\n"
  },
  {
    "path": "web/src/components/MPCode.vue",
    "content": "<template>\n  <el-dialog\n    title=\"关注公众号【假装大佬】\"\n    :visible.sync=\"show\"\n    :width=\"dialogSmallWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    :before-close=\"cancel\"\n  >\n    <el-image\n      :src=\"require('../assets/imgs/mpcode.jpg')\"\n      class=\"qrcode-img\"\n      fit=\"cover\"\n      lazy\n    />\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"ReplaceRuleForm\",\n  data() {\n    return {};\n  },\n  props: [\"show\"],\n  computed: {\n    ...mapGetters([\"dialogSmallWidth\", \"dialogTop\"])\n  },\n  methods: {\n    cancel() {\n      this.$emit(\"setShow\", false);\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.qrcode-img {\n  display: block;\n  margin: 0 auto;\n}\n</style>\n"
  },
  {
    "path": "web/src/components/PopCatalog.vue",
    "content": "<template>\n  <div class=\"popup-wrapper\" :style=\"popupTheme\">\n    <div class=\"title-zone\">\n      <div class=\"title\">\n        目录\n        <span v-if=\"catalog.length\">({{ catalog.length }})</span>\n      </div>\n      <div :class=\"{ 'title-btn': true }\">\n        <span class=\"span-btn\" v-if=\"catalog.length\" @click=\"asc = !asc\">{{\n          asc ? \"倒序\" : \"顺序\"\n        }}</span>\n        <span class=\"span-btn\" v-if=\"catalog.length\" @click=\"toTop\">顶部</span>\n        <span class=\"span-btn\" v-if=\"catalog.length\" @click=\"toBottom\"\n          >底部</span\n        >\n        <span\n          class=\"span-btn\"\n          v-if=\"book.origin === 'loc_book'\"\n          @click=\"changeRule\"\n        >\n          修改规则\n        </span>\n        <span\n          :class=\"{ loading: refreshLoading, 'refresh-btn': true }\"\n          @click=\"refreshChapter\"\n        >\n          <i class=\"el-icon-loading\" v-if=\"refreshLoading\"></i>\n          {{ refreshLoading ? \"刷新中...\" : \"刷新\" }}\n        </span>\n      </div>\n    </div>\n    <div\n      class=\"data-wrapper\"\n      ref=\"cataData\"\n      :class=\"{ night: $store.getters.isNight, day: !$store.getters.isNight }\"\n    >\n      <div class=\"cata\">\n        <div\n          class=\"log\"\n          v-for=\"(note, index) in cataList\"\n          :class=\"{ selected: isSelected(index) }\"\n          :key=\"note.index\"\n          @click=\"gotoChapter(note)\"\n          ref=\"cata\"\n        >\n          <div\n            :class=\"{\n              'log-text': true,\n              cached: cachedCataMap[note.index]\n            }\"\n          >\n            {{ note.title }}\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport jump from \"../plugins/jump\";\nimport Axios from \"../plugins/axios\";\nimport Vue from \"vue\";\n\nexport default {\n  name: \"PopCata\",\n  data() {\n    return {\n      refreshLoading: false,\n      asc: true,\n      cachedCataMap: {},\n      tocUrl: \"\"\n    };\n  },\n  props: [\"visible\"],\n  computed: {\n    book() {\n      return this.$store.getters.readingBook;\n    },\n    index() {\n      return this.$store.getters.readingBook.index;\n    },\n    catalog() {\n      return this.$store.getters.readingBook.catalog || [];\n    },\n    cataList() {\n      if (this.asc) {\n        return this.catalog;\n      } else {\n        return [].concat(this.catalog).reverse();\n      }\n    },\n    theme() {\n      return this.$store.getters.config.theme;\n    },\n    popupTheme() {\n      return {\n        background: this.$store.getters.currentThemeConfig.popup\n      };\n    },\n    tocRuleList() {\n      if (!this.book || !this.book.originName) {\n        return [];\n      }\n      if (this.book.originName.toLowerCase().endsWith(\".txt\")) {\n        // txt\n        return this.$store.state.txtTocRules;\n      } else {\n        // epub\n        return [\n          { name: \"根据 Spin 获取章节，使用 Toc 补充章节名\", rule: \"spin+toc\" },\n          { name: \"根据 Spin 获取章节，强制使用 Toc 章节名\", rule: \"spin<toc\" },\n          { name: \"根据 Spin 获取章节\", rule: \"spin\" },\n          { name: \"根据 Toc 获取章节，使用 Spin 补充章节名\", rule: \"toc+spin\" },\n          { name: \"根据 Toc 获取章节，强制使用 Spin 章节名\", rule: \"toc<spin\" },\n          { name: \"根据 Toc 获取章节\", rule: \"toc\" }\n        ];\n      }\n    }\n  },\n  mounted() {\n    window.popcatalogComp = this;\n  },\n  watch: {\n    visible(isVisible) {\n      if (isVisible) {\n        this.computeCachedCata();\n        this.$nextTick(() => {\n          this.jumpToCurrent();\n        });\n      }\n    },\n    catalog() {\n      this.refreshLoading = false;\n      this.computeCachedCata();\n    },\n    index() {\n      this.computeCachedCata();\n    },\n    asc() {\n      this.jumpToCurrent();\n    }\n  },\n  methods: {\n    isSelected(index) {\n      if (this.asc) {\n        return index == this.$store.getters.readingBook.index;\n      } else {\n        return (\n          this.catalog.length - 1 - index ==\n          this.$store.getters.readingBook.index\n        );\n      }\n    },\n    gotoChapter(note) {\n      const index = this.catalog.indexOf(note);\n      this.$emit(\"close\");\n      this.$emit(\"getContent\", index);\n    },\n    refreshChapter() {\n      this.refreshLoading = true;\n      this.$emit(\"refresh\");\n    },\n    async changeRule() {\n      const res = await this.$msgbox({\n        title: \"修改目录规则\",\n        message: this.renderComp(),\n        showCancelButton: true,\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\"\n      }).catch(action => {\n        return action === \"close\" ? \"close\" : false;\n      });\n      if (res === \"confirm\") {\n        //\n        if (this.tocUrl === this.book.tocUrl) {\n          this.$message.error(\"未修改规则\");\n          return;\n        }\n        const shelfBook = this.$store.getters.shelfBooks.find(\n          v => v.bookUrl === this.book.bookUrl\n        );\n        shelfBook.tocUrl = this.tocUrl;\n        return Axios.post(this.api + \"/saveBook\", shelfBook).then(\n          res => {\n            if (res.data.isSuccess) {\n              this.$message.success(\"操作成功\");\n              this.$store.commit(\"updateShelfBook\", res.data.data);\n              this.refreshLoading = true;\n              this.$emit(\"refresh\");\n            }\n          },\n          error => {\n            this.$message.error(\"操作失败\" + (error && error.toString()));\n          }\n        );\n      } else {\n        return false;\n      }\n    },\n    renderComp() {\n      var tocRuleList = this.tocRuleList;\n      this.tocUrl = this.book.tocUrl;\n      var catalog = this;\n      Vue.component(\"custComp2\", {\n        render() {\n          window.custComp2 = this;\n          return (\n            <div style={{ textAlign: \"center\" }}>\n              <span>请选择规则：</span>\n              <el-select\n                size=\"mini\"\n                vModel={this.selectedRule}\n                filterable={true}\n                placeholder=\"未分组\"\n                vOn:change={this.change}\n              >\n                {tocRuleList.map((rule, index) => {\n                  return (\n                    <el-option\n                      key={\"rule-\" + index}\n                      label={rule.name}\n                      value={rule.rule}\n                    ></el-option>\n                  );\n                })}\n              </el-select>\n              <el-input\n                type=\"textarea\"\n                rows={3}\n                style={{ marginTop: \"10px\" }}\n                vModel={this.selectedRule}\n                size=\"small\"\n                vOn:change={this.change}\n              />\n            </div>\n          );\n        },\n        data() {\n          return {\n            selectedRule: catalog.tocUrl\n          };\n        },\n        methods: {\n          change() {\n            catalog.tocUrl = this.selectedRule;\n          }\n        }\n      });\n      var custComp2 = Vue.component(\"custComp2\");\n      return this.$createElement(custComp2);\n    },\n    jumpToCurrent(index) {\n      if (typeof index === \"undefined\") {\n        index = this.asc\n          ? this.$store.getters.readingBook.index\n          : this.catalog.length - 1 - this.$store.getters.readingBook.index;\n      }\n      if (!this.$refs.cata || !this.$refs.cata[index]) {\n        setTimeout(() => {\n          this.jumpToCurrent(index);\n        }, 10);\n        return;\n      }\n      let wrapper = this.$refs.cataData;\n      jump(this.$refs.cata[index], { container: wrapper, duration: 0 });\n    },\n    toTop() {\n      this.jumpToCurrent(0);\n    },\n    toBottom() {\n      this.jumpToCurrent(this.catalog.length - 1);\n    },\n    computeCachedCata() {\n      const cacheMap = {};\n      const cachePrefix =\n        \"localCache@\" +\n        this.$store.getters.readingBook.name +\n        \"_\" +\n        this.$store.getters.readingBook.author +\n        \"@\" +\n        this.$store.getters.readingBook.bookUrl +\n        \"@chapterContent-\";\n      window.$cacheStorage\n        .iterate(function(value, key) {\n          if (key.startsWith(cachePrefix)) {\n            try {\n              let index = parseInt(key.replace(cachePrefix, \"\"));\n              cacheMap[index] = true;\n            } catch (error) {\n              //\n              // console.error(error);\n            }\n          }\n        })\n        .then(() => {\n          this.cachedCataMap = cacheMap;\n        })\n        .catch(function() {});\n    }\n  }\n};\n</script>\n\n<style lang=\"stylus\" scoped>\n.popup-wrapper {\n  margin: -16px;\n  margin-bottom: -13px;\n  padding: 24px;\n  padding-top: calc(24px + constant(safe-area-inset-top));\n  padding-top: calc(24px + env(safe-area-inset-top));\n\n  .title-zone {\n    margin: 0 0 20px 0;\n    width: 100%;\n    display: flex;\n    flex-direction: row;\n    flex-wrap: wrap;\n    justify-content: space-between;\n  }\n\n  .title {\n    font-size: 18px;\n    font-weight: 400;\n    font-family: -apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif;\n    color: #ed4259;\n    border-bottom: 1px solid #ed4259;\n    width: fit-content;\n  }\n\n  .title-btn {\n    font-size: 14px;\n    line-height: 26px;\n    color: #606266;\n    .progress-percent {\n      display: inline-block;\n      margin-right: 25px;\n    }\n    .span-btn {\n      display: inline-block;\n      color: #ed4259;\n      margin-left: 15px;\n      cursor: pointer;\n    }\n    .refresh-btn {\n      display: inline-block;\n      margin-left: 15px;\n      color: #ed4259;\n      cursor: pointer;\n      &.loading {\n        color: #606266;\n      }\n    }\n  }\n\n  .data-wrapper {\n    height: 300px;\n    overflow: auto;\n\n    .cata {\n      display: flex;\n      flex-direction: row;\n      flex-wrap: wrap;\n      justify-content: space-between;\n\n      .cached {\n        color: #444;\n      }\n\n      .selected .log-text {\n        color: #EB4259 !important;\n      }\n\n      .log {\n        width: 50%;\n        height: 40px;\n        cursor: pointer;\n        float: left;\n        font: 16px / 40px PingFangSC-Regular, HelveticaNeue-Light, 'Helvetica Neue Light', 'Microsoft YaHei', sans-serif;\n\n        .log-text {\n          margin-right: 26px;\n          overflow: hidden;\n          white-space: nowrap;\n          text-overflow: ellipsis;\n        }\n      }\n    }\n  }\n\n  .data-wrapper::-webkit-scrollbar {\n    width: 0 !important;\n  }\n\n  .night {\n    >>>.log {\n      border-bottom: 1px solid #333;\n    }\n  }\n\n  .day {\n    >>>.log {\n      border-bottom: 1px solid #eee;\n    }\n\n    >>>.cached {\n      color: #bbb !important;\n    }\n  }\n}\n@media screen and (max-width: 500px) {\n  .popup-wrapper .data-wrapper .cata .log {\n    width: 100%;\n\n    .log-text {\n      margin-right: 0;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "web/src/components/ReadSettings.vue",
    "content": "<template>\n  <div\n    class=\"settings-wrapper\"\n    :style=\"popupTheme\"\n    :class=\"{ night: $store.getters.isNight, day: !$store.getters.isNight }\"\n  >\n    <div class=\"settings-title\">\n      设置\n      <div class=\"title-btn\" @click=\"resetConfig\">重置为默认配置</div>\n    </div>\n    <div class=\"setting-list\">\n      <ul>\n        <li>\n          <span class=\"setting-item-title\">特殊模式</span>\n          <div class=\"selection-zone\">\n            <span\n              class=\"span-item\"\n              v-for=\"(type, index) in pageTypes\"\n              :key=\"index\"\n              :class=\"{ selected: config.pageType == type }\"\n              @click=\"setPageType(type)\"\n              >{{ type === \"Kindle\" ? \"简洁\" : \"正常\" }}</span\n            >\n            <span class=\"small-tip\"\n              >❗️开启简洁模式会关闭动画以及首页的部分功能</span\n            >\n          </div>\n        </li>\n        <el-divider></el-divider>\n        <li>\n          <span class=\"setting-item-title\">配置方案</span>\n          <div class=\"selection-zone\">\n            <span\n              class=\"span-item\"\n              v-for=\"(customConfig, index) in $store.state.customConfigList\"\n              :key=\"index\"\n              :class=\"{\n                selected:\n                  $store.getters.config.customConfig === customConfig.name\n              }\"\n              @click=\"setCustomConfig(customConfig)\"\n            >\n              <span>{{ customConfig.name }}</span>\n              <i\n                class=\"el-icon-close delete-custom-config-icon\"\n                v-if=\"\n                  index > 1 &&\n                    $store.getters.config.customConfig !== customConfig.name\n                \"\n                @click.stop=\"deleteCustomConfig(index, customConfig.name)\"\n              ></i>\n            </span>\n            <span\n              class=\"span-item\"\n              :key=\"'addNewCustomConfig'\"\n              @click=\"addNewCustomConfig\"\n              >新增方案</span\n            >\n            <span\n              class=\"span-item\"\n              :key=\"'autoTheme'\"\n              ref=\"themes\"\n              @click=\"setAutoTheme\"\n              :class=\"{ selected: $store.getters.config.autoTheme }\"\n              >自动切换</span\n            >\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">方案类型</span>\n          <div class=\"selection-zone\">\n            <span\n              class=\"span-item\"\n              v-for=\"(configDefaultType, index) in configDefaultTypeList\"\n              :key=\"index\"\n              :class=\"{\n                selected:\n                  currentCustomConfig.configDefaultType === configDefaultType\n              }\"\n              @click=\"setConfigDefaultType(configDefaultType)\"\n              >{{ configDefaultType }}</span\n            >\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">阅读主题</span>\n          <div class=\"selection-zone\">\n            <span\n              class=\"theme-item\"\n              v-for=\"(themeColor, index) in themeColors\"\n              :key=\"index\"\n              :style=\"themeColor\"\n              ref=\"themes\"\n              @click=\"setConfig('theme', index)\"\n              :class=\"{ selected: config.theme === index }\"\n              ><em v-if=\"index != 6\" class=\"iconfont\">&#58980;</em\n              ><em v-else class=\"moon-icon\">{{ moonIcon }}</em></span\n            >\n            <span\n              class=\"span-item\"\n              :key=\"'custom'\"\n              ref=\"themes\"\n              @click=\"setConfig('theme', 'custom')\"\n              :class=\"{ selected: config.theme === 'custom' }\"\n              >自定义</span\n            >\n          </div>\n        </li>\n        <li v-if=\"config.theme === 'custom'\">\n          <span class=\"setting-item-title\">自定义</span>\n          <div class=\"custom-theme\">\n            <div class=\"custom-theme-title\">\n              <span class=\"custom-theme-title\">主题模式</span>\n              <span\n                class=\"span-item\"\n                v-for=\"(type, index) in themeTypes\"\n                :key=\"index\"\n                :class=\"{ selected: themeType == type }\"\n                @click=\"setConfig('themeType', type)\"\n                >{{ type === \"day\" ? \"白天\" : \"黑夜\" }}</span\n              >\n            </div>\n            <span class=\"custom-theme-title\"\n              >页面背景颜色\n              <el-color-picker v-model=\"config.bodyColor\"></el-color-picker>\n            </span>\n            <span class=\"custom-theme-title\"\n              >浮窗背景颜色\n              <el-color-picker v-model=\"config.popupColor\"></el-color-picker\n            ></span>\n            <span class=\"custom-theme-title\"\n              >阅读背景颜色\n              <el-color-picker v-model=\"config.contentColor\"></el-color-picker\n            ></span>\n            <span class=\"custom-theme-title\"\n              >阅读背景图片\n              <img\n                class=\"content-bg-preview\"\n                v-for=\"(item, index) in builtinBG\"\n                :key=\"index\"\n                :class=\"{\n                  selected: $store.getters.config.contentBGImg == item.src\n                }\"\n                :src=\"item.src\"\n                alt=\"\"\n                @click=\"setBGImg(item.src)\"\n              />\n              <div\n                class=\"content-bg-preview\"\n                v-for=\"item in $store.getters.config.customBGImgList || []\"\n                :key=\"item\"\n                :class=\"{\n                  selected: $store.getters.config.contentBGImg == item\n                }\"\n              >\n                <img\n                  :src=\"getCustomBGImgURL(item)\"\n                  alt=\"\"\n                  @click=\"setBGImg(item)\"\n                />\n                <i\n                  class=\"el-icon-close delete-bg-icon\"\n                  @click.stop=\"deleteCustomBGImg(item)\"\n                ></i>\n              </div>\n\n              <span class=\"upload-bg-btn\" @click=\"uploadBGFile\">上传</span>\n              <input\n                ref=\"bgFileRef\"\n                type=\"file\"\n                @change=\"onBGFileChange\"\n                style=\"display:none\"\n              />\n            </span>\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">正文字体</span>\n          <div class=\"selection-zone\">\n            <span\n              class=\"span-item\"\n              v-for=\"(font, index) in fonts\"\n              :key=\"index\"\n              :class=\"{ selected: config.font == index }\"\n              @click=\"setConfig('font', index)\"\n              >{{ font }}\n              <i\n                :class=\"{\n                  'el-icon-upload': true,\n                  'upload-font-icon': true,\n                  active:\n                    config.customFontsMap &&\n                    config.customFontsMap[customFonts[index]]\n                }\"\n                @click.stop=\"uploadFontFile(customFonts[index], font)\"\n              ></i>\n            </span>\n            <input\n              ref=\"fontFileRef\"\n              type=\"file\"\n              @change=\"onFontFileChange\"\n              style=\"display:none\"\n            />\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">简繁转换</span>\n          <div class=\"selection-zone\">\n            <span\n              class=\"span-item\"\n              v-for=\"(chineseFont, index) in chineseFonts\"\n              :key=\"index\"\n              :class=\"{ selected: config.chineseFont == chineseFont }\"\n              @click=\"setConfig('chineseFont', chineseFont)\"\n              >{{ chineseFont }}</span\n            >\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">字体大小</span>\n          <div class=\"resize\">\n            <span class=\"less\" @click=\"decConfig('fontSize')\"\n              ><em class=\"iconfont\">&#58966;</em></span\n            ><b></b>\n            <span class=\"lang\">\n              <el-input\n                class=\"setting-input\"\n                v-model=\"config.fontSize\"\n                size=\"mini\"\n              ></el-input></span\n            ><b></b>\n            <span class=\"more\" @click=\"incConfig('fontSize')\"\n              ><em class=\"iconfont\">&#58976;</em></span\n            >\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">字体粗细</span>\n          <div class=\"resize\">\n            <span class=\"less\" @click=\"decConfig('fontWeight')\"\n              ><i class=\"el-icon-minus\"></i></span\n            ><b></b>\n            <span class=\"lang\">\n              <el-input\n                class=\"setting-input\"\n                v-model=\"config.fontWeight\"\n                size=\"mini\"\n              ></el-input></span\n            ><b></b>\n            <span class=\"less\" @click=\"incConfig('fontWeight')\"\n              ><i class=\"el-icon-plus\"></i\n            ></span>\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">段落行高</span>\n          <div class=\"resize\">\n            <span class=\"less\" @click=\"decConfig('lineHeight')\"\n              ><i class=\"el-icon-minus\"></i></span\n            ><b></b>\n            <span class=\"lang\">\n              <el-input\n                class=\"setting-input\"\n                v-model=\"config.lineHeight\"\n                size=\"mini\"\n              ></el-input></span\n            ><b></b>\n            <span class=\"less\" @click=\"incConfig('lineHeight')\"\n              ><i class=\"el-icon-plus\"></i\n            ></span>\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">段落间距</span>\n          <div class=\"resize\">\n            <span class=\"less\" @click=\"decConfig('paragraphSpace')\"\n              ><i class=\"el-icon-minus\"></i></span\n            ><b></b>\n            <span class=\"lang\">\n              <el-input\n                class=\"setting-input\"\n                v-model=\"config.paragraphSpace\"\n                size=\"mini\"\n              ></el-input></span\n            ><b></b>\n            <span class=\"less\" @click=\"incConfig('paragraphSpace')\"\n              ><i class=\"el-icon-plus\"></i\n            ></span>\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title font-color-title\">字体颜色</span>\n          <el-color-picker v-model=\"config.fontColor\"></el-color-picker>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">页面模式</span>\n          <div class=\"selection-zone\">\n            <span\n              class=\"span-item\"\n              v-for=\"(mode, index) in pageModes\"\n              :key=\"index\"\n              :class=\"{ selected: config.pageMode == mode }\"\n              @click=\"setPageMode(mode)\"\n              >{{ mode }}</span\n            >\n          </div>\n        </li>\n        <li v-if=\"!$store.state.miniInterface\">\n          <span class=\"setting-item-title\">页面宽度</span>\n          <div class=\"resize\">\n            <span class=\"less\" @click=\"decConfig('readWidth')\"\n              ><em class=\"iconfont\">&#58965;</em></span\n            ><b></b> <span class=\"lang\">{{ config.readWidth }}</span\n            ><b></b>\n            <span class=\"more\" @click=\"incConfig('readWidth')\"\n              ><em class=\"iconfont\">&#58975;</em></span\n            >\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">翻页方式</span>\n          <div class=\"selection-zone\">\n            <span\n              class=\"span-item\"\n              v-for=\"(method, index) in readMethods\"\n              :key=\"index\"\n              :class=\"{ selected: config.readMethod == method }\"\n              @click=\"setReadMethod(method)\"\n              v-show=\"\n                (!$store.state.miniInterface && method !== '左右滑动') ||\n                  $store.state.miniInterface\n              \"\n              >{{ method }}</span\n            >\n            <span class=\"small-tip\"\n              >❗️上下滚动2会自动隐藏看过的章节，但是可能会抖动</span\n            >\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">动画时长</span>\n          <div class=\"resize\">\n            <span class=\"less\" @click=\"decConfig('animateMSTime')\"\n              ><i class=\"el-icon-minus\"></i></span\n            ><b></b>\n            <span class=\"lang\">\n              <el-input\n                class=\"setting-input\"\n                v-model=\"config.animateMSTime\"\n                size=\"mini\"\n              ></el-input></span\n            ><b></b>\n            <span class=\"less\" @click=\"incConfig('animateMSTime')\"\n              ><i class=\"el-icon-plus\"></i\n            ></span>\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">自动翻页</span>\n          <div class=\"selection-zone\">\n            <span\n              class=\"span-item\"\n              v-for=\"(method, index) in autoReadingMethods\"\n              :key=\"index\"\n              :class=\"{ selected: config.autoReadingMethod === method }\"\n              @click=\"setConfig('autoReadingMethod', method)\"\n              >{{ method }}</span\n            >\n          </div>\n        </li>\n        <li v-if=\"config.autoReadingMethod === '像素滚动'\">\n          <span class=\"setting-item-title\">滚动像素</span>\n          <div class=\"resize\">\n            <span class=\"less\" @click=\"decConfig('autoReadingPixel')\"\n              ><i class=\"el-icon-minus\"></i></span\n            ><b></b>\n            <span class=\"lang\">\n              <el-input\n                class=\"setting-input\"\n                v-model=\"config.autoReadingPixel\"\n                size=\"mini\"\n              ></el-input> </span\n            ><b></b>\n            <span class=\"less\" @click=\"incConfig('autoReadingPixel')\"\n              ><i class=\"el-icon-plus\"></i\n            ></span>\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">翻页速度</span>\n          <div class=\"resize\">\n            <span class=\"less\" @click=\"decConfig('autoReadingLineTime')\"\n              ><i class=\"el-icon-minus\"></i></span\n            ><b></b>\n            <span class=\"lang\"\n              ><el-input\n                class=\"setting-input\"\n                v-model=\"config.autoReadingLineTime\"\n                size=\"mini\"\n              ></el-input></span\n            ><b></b>\n            <span class=\"less\" @click=\"incConfig('autoReadingLineTime')\"\n              ><i class=\"el-icon-plus\"></i\n            ></span>\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">全屏点击</span>\n          <div class=\"selection-zone\">\n            <span\n              class=\"span-item\"\n              v-for=\"(method, index) in clickMethods\"\n              :key=\"index\"\n              :class=\"{ selected: config.clickMethod == method }\"\n              @click=\"setConfig('clickMethod', method)\"\n              >{{ method }}</span\n            >\n          </div>\n        </li>\n        <li>\n          <span class=\"setting-item-title\">选择文字</span>\n          <div class=\"selection-zone\">\n            <span\n              class=\"span-item\"\n              v-for=\"(action, index) in selectionActions\"\n              :key=\"index\"\n              :class=\"{ selected: config.selectionAction == action }\"\n              @click=\"setConfig('selectionAction', action)\"\n              >{{ action }}</span\n            >\n          </div>\n        </li>\n        <el-divider></el-divider>\n        <li class=\"operation-zone\">\n          <span class=\"span-btn\" @click=\"showClickZone\">显示翻页区域</span>\n          <span class=\"span-btn\" @click=\"showRuleEditor\">过滤规则管理</span>\n        </li>\n      </ul>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Axios from \"../plugins/axios\";\nimport settings from \"../plugins/config\";\nimport eventBus from \"../plugins/eventBus\";\nimport { isMiniInterface, removeFont } from \"../plugins/helper\";\nimport { setCache, getCache } from \"../plugins/cache\";\nimport { customFonts } from \"../plugins/config\";\n\nexport default {\n  name: \"ReadSettings\",\n  data() {\n    return {\n      themeColors: [\n        {\n          background: \"rgba(250, 245, 235, 0.8)\"\n        },\n        {\n          background: \"rgba(245, 234, 204, 0.8)\"\n        },\n        {\n          background: \"rgba(230, 242, 230, 0.8)\"\n        },\n        {\n          background: \"rgba(228, 241, 245, 0.8)\"\n        },\n        {\n          background: \"rgba(245, 228, 228, 0.8)\"\n        },\n        {\n          background: \"rgba(224, 224, 224, 0.8)\"\n        },\n        {\n          background: \"rgba(0, 0, 0, 0.5)\"\n        },\n        {\n          background: \"rgba(255, 255, 255, 0.8)\"\n        }\n      ],\n      builtinBG: [\n        { src: \"bg/山水画.jpg\" },\n        { src: \"bg/山水墨影.jpg\" },\n        { src: \"bg/羊皮纸1.jpg\" },\n        { src: \"bg/护眼漫绿.jpg\" },\n        { src: \"bg/羊皮纸2.jpg\" },\n        { src: \"bg/新羊皮纸.jpg\" },\n        { src: \"bg/羊皮纸3.jpg\" },\n        { src: \"bg/明媚倾城.jpg\" },\n        { src: \"bg/羊皮纸4.jpg\" },\n        { src: \"bg/深宫魅影.jpg\" },\n        { src: \"bg/午后沙滩.jpg\" },\n        { src: \"bg/清新时光.jpg\" },\n        { src: \"bg/宁静夜色.jpg\" },\n        { src: \"bg/边彩画布.jpg\" }\n      ],\n      fonts: [\"系统\", \"黑体\", \"楷体\", \"宋体\", \"仿宋\"],\n      readMethods: [\"上下滑动\", \"左右滑动\", \"上下滚动\", \"上下滚动2\"],\n      clickMethods: [\"下一页\", \"自动\", \"不翻页\"],\n      selectionActions: [\"操作弹窗\", \"忽略\"],\n      pageModes: [\"自适应\", \"手机模式\"],\n      pageTypes: [\"正常\", \"Kindle\"],\n      themeTypes: [\"day\", \"night\"],\n      configDefaultTypeList: [\"白天默认\", \"黑夜默认\"],\n      autoReadingMethods: [\"像素滚动\", \"段落滚动\"],\n      chineseFonts: [\"简体\", \"繁体\"],\n\n      customFontName: \"\",\n      customFonts: customFonts,\n\n      config: this.$store.state.config,\n\n      configRules: {\n        fontSize: { min: 8, delta: 1 },\n        fontWeight: { min: 100, max: 900, delta: 100 },\n        animateMSTime: { min: 0, max: 500, delta: 50 },\n        autoReadingPixel: { min: 1, delta: 5 },\n        autoReadingLineTime: { min: 10, delta: 50 },\n        lineHeight: { min: 1, max: 5, delta: 0.2 },\n        paragraphSpace: { min: 0, max: 5, delta: 0.2 },\n        readWidth: {\n          min: Math.min(Math.floor(window.innerWidth / 160), 4) * 160,\n          max: Math.floor(window.innerWidth / 160) * 160,\n          delta: 160\n        }\n      }\n    };\n  },\n  mounted() {\n    this.config = {\n      ...settings.config,\n      ...this.config,\n      selectionAction:\n        this.$store.state.config.selectionAction === \"过滤弹窗\"\n          ? \"操作弹窗\"\n          : \"忽略\"\n    };\n  },\n  computed: {\n    moonIcon() {\n      return this.$store.getters.isSystemNight ? \"\" : \"\";\n    },\n    popupTheme() {\n      return {\n        background: this.$store.getters.currentThemeConfig.popup\n      };\n    },\n    currentCustomConfig() {\n      return this.$store.state.customConfigList.find(\n        v => v.name === this.$store.state.config.customConfig\n      );\n    }\n  },\n  watch: {\n    config: {\n      deep: true,\n      handler(val) {\n        this.$store.commit(\"setConfig\", { ...val });\n      }\n    }\n  },\n  methods: {\n    setPageType(type) {\n      if (type === this.config.pageType) {\n        return;\n      }\n      let lastConfig = {};\n      if (type === \"Kindle\") {\n        setCache(\"lastNormalConfig\", this.config);\n\n        lastConfig = getCache(\"lastKindleConfig\");\n        lastConfig = lastConfig || {\n          animateMSTime: 0,\n          fontSize: Math.min(this.fontSize, 20),\n          theme: 7,\n          readMethod: \"左右滑动\",\n          selectionAction: \"忽略\",\n          pageMode: \"手机模式\"\n        };\n      } else {\n        setCache(\"lastKindleConfig\", this.config);\n        lastConfig = getCache(\"lastNormalConfig\") || {};\n      }\n\n      this.config = { ...this.config, ...(lastConfig || {}), pageType: type };\n\n      this.$emit(\"readMethodChange\");\n      this.$emit(\"pageModeChange\");\n      if (this.config.pageMode === \"手机模式\") {\n        this.$store.commit(\"setMiniInterface\", true);\n      } else {\n        this.$store.commit(\"setMiniInterface\", isMiniInterface());\n      }\n    },\n    setPageMode(pageMode) {\n      this.$emit(\"pageModeChange\");\n      this.config = { ...this.config, pageMode };\n      if (this.config.pageMode === \"手机模式\") {\n        this.$store.commit(\"setMiniInterface\", true);\n      } else {\n        this.$store.commit(\"setMiniInterface\", isMiniInterface());\n      }\n    },\n    setReadMethod(readMethod) {\n      this.$emit(\"readMethodChange\");\n      this.config = { ...this.config, readMethod };\n    },\n    setConfig(name, value) {\n      const data = {};\n      data[name] = value;\n      this.config = { ...this.config, ...data };\n    },\n    setAutoTheme() {\n      this.config = { ...this.config, autoTheme: !this.config.autoTheme };\n    },\n    incConfig(name) {\n      const data = {};\n      const rule = this.configRules[name];\n      const val = +this.config[name];\n      data[name] =\n        \"max\" in rule ? Math.min(rule.max, val + rule.delta) : val + rule.delta;\n      this.config = {\n        ...this.config,\n        ...data\n      };\n    },\n    decConfig(name) {\n      const data = {};\n      const rule = this.configRules[name];\n      const val = +this.config[name];\n      data[name] =\n        \"min\" in rule ? Math.max(rule.min, val - rule.delta) : val - rule.delta;\n      this.config = {\n        ...this.config,\n        ...data\n      };\n    },\n    getCustomBGImgURL(src) {\n      return this.api.replace(/\\/reader3\\/?/, \"\") + src;\n    },\n    setBGImg(src) {\n      let config = { ...this.config };\n      if (config.contentBGImg === src) {\n        delete config.contentBGImg;\n      } else {\n        config.contentBGImg = src;\n      }\n      this.config = config;\n    },\n    uploadBGFile() {\n      this.$refs.bgFileRef.dispatchEvent(new MouseEvent(\"click\"));\n    },\n    onBGFileChange(event) {\n      const rawFile = event.target.files && event.target.files[0];\n      // console.log(\"rawFile\", rawFile);\n      let param = new FormData();\n      param.append(\"file\", rawFile);\n      param.append(\"type\", \"background\");\n      Axios.post(this.api + \"/uploadFile\", param, {\n        headers: { \"Content-Type\": \"multipart/form-data\" }\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            if (!res.data.data.length) {\n              this.$message.error(\"上传文件失败\");\n              return;\n            }\n            let config = { ...this.config };\n            config.customBGImgList = config.customBGImgList || [];\n            if (!config.customBGImgList.includes(res.data.data[0])) {\n              config.customBGImgList.push(res.data.data[0]);\n            }\n            config.contentBGImg = res.data.data[0];\n            this.config = config;\n          }\n        },\n        error => {\n          this.$message.error(\"上传文件失败 \" + (error && error.toString()));\n        }\n      );\n      this.$refs.bgFileRef.value = null;\n    },\n    async uploadFontFile(customFontName, fontName) {\n      if (\n        this.config.customFontsMap &&\n        this.config.customFontsMap[customFontName]\n      ) {\n        const res = await this.$confirm(\n          `已上传自定义的${fontName}字体?`,\n          \"提示\",\n          {\n            confirmButtonText: \"继续上传\",\n            cancelButtonText: \"恢复默认\",\n            type: \"warning\",\n            closeOnClickModal: false,\n            closeOnPressEscape: false,\n            distinguishCancelAndClose: true\n          }\n        ).catch(action => {\n          return action === \"close\" ? \"close\" : false;\n        });\n        if (res === \"close\") {\n          return;\n        }\n        if (!res) {\n          Axios.post(this.api + \"/deleteFile\", {\n            url: this.config.customFontsMap[customFontName]\n          }).then(\n            res => {\n              if (res.data.isSuccess) {\n                let config = { ...this.config };\n                delete config.customFontsMap[customFontName];\n                this.config = config;\n                removeFont(customFontName);\n              }\n            },\n            error => {\n              this.$message.error(\n                \"删除自定义字体文件失败 \" + (error && error.toString())\n              );\n            }\n          );\n          return;\n        }\n      }\n      this.customFontName = customFontName;\n      this.$refs.fontFileRef.dispatchEvent(new MouseEvent(\"click\"));\n    },\n    onFontFileChange(event) {\n      const rawFile = event.target.files && event.target.files[0];\n      // console.log(\"rawFile\", rawFile);\n      if (!rawFile.name.toLowerCase().endsWith(\".ttf\")) {\n        this.$message.error(\"只支持 TTF 字体文件\");\n        return;\n      }\n      let param = new FormData();\n      param.append(\"file\", rawFile);\n      param.append(\"type\", \"fonts\");\n      Axios.post(this.api + \"/uploadFile\", param, {\n        headers: { \"Content-Type\": \"multipart/form-data\" }\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            if (!res.data.data.length) {\n              this.$message.error(\"上传文件失败\");\n              return;\n            }\n            let config = { ...this.config };\n            config.customFontsMap = config.customFontsMap || {};\n            config.customFontsMap[this.customFontName] = res.data.data[0];\n            this.config = config;\n          }\n        },\n        error => {\n          this.$message.error(\"上传文件失败 \" + (error && error.toString()));\n        }\n      );\n      this.$refs.fontFileRef.value = null;\n    },\n    deleteCustomBGImg(src) {\n      Axios.post(this.api + \"/deleteFile\", {\n        url: src\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            let config = { ...this.config };\n            config.customBGImgList = config.customBGImgList || [];\n            var index = config.customBGImgList.indexOf(src);\n            if (index != -1) {\n              config.customBGImgList.splice(index, 1);\n            }\n            if (config.contentBGImg === src) {\n              config.contentBGImg = this.builtinBG[0].src;\n            }\n            this.config = config;\n          }\n        },\n        error => {\n          this.$message.error(\"删除文件失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    resetConfig() {\n      this.config = { ...settings.config };\n    },\n    showClickZone() {\n      this.$emit(\"close\");\n      this.$emit(\"showClickZone\");\n    },\n    showRuleEditor() {\n      this.$emit(\"close\");\n      eventBus.$emit(\"showReplaceRuleDialog\");\n    },\n    async addNewCustomConfig() {\n      const res = await this.$prompt(\"请输入方案名称\", `添加配置方案`, {\n        inputValue: \"\",\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        inputValidator(v) {\n          if (!v) {\n            return \"方案名不能为空\";\n          }\n          return true;\n        }\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      const name = res.value.replace(/^\\s+/, \"\").replace(/\\s+$/, \"\");\n      if (!name) {\n        return \"方案名不能为空\";\n      }\n      const isExist = this.$store.state.customConfigList.find(\n        v => v.name === name\n      );\n      if (isExist) {\n        return \"方案名不能重复\";\n      }\n      const newConfig = { ...this.$store.state.customConfigList[0] };\n      newConfig.name = name;\n      this.$store.commit(\n        \"setCustomConfigList\",\n        [].concat(this.$store.state.customConfigList).concat([newConfig])\n      );\n    },\n    setCustomConfig(customConfig) {\n      this.config = {\n        ...this.config,\n        customConfig: customConfig.name,\n        ...customConfig\n      };\n    },\n    async deleteCustomConfig(index, name) {\n      const customConfigList = [].concat(this.$store.state.customConfigList);\n      if (index <= 1) {\n        this.$message.error(\"内置方案不能删除\");\n        return;\n      }\n      if (customConfigList.length <= index) {\n        this.$message.error(\"方案不存在\");\n        return;\n      }\n      if (this.$store.state.config.customConfig === name) {\n        this.$message.error(\"方案正在使用，无法删除\");\n        return;\n      }\n      const res = await this.$confirm(`确认要删除${name}方案吗？`, \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      customConfigList.splice(index, 1);\n\n      this.$store.commit(\"setCustomConfigList\", [].concat(customConfigList));\n    },\n    async setConfigDefaultType(configDefaultType) {\n      const res = await this.$confirm(\n        `确认要设置当前方案为${configDefaultType}吗？继续操作将替换现有的${configDefaultType}方案`,\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      const customConfigList = [].concat(this.$store.state.customConfigList);\n      customConfigList.forEach(v => {\n        if (v.name === this.$store.state.config.customConfig) {\n          v.configDefaultType = configDefaultType;\n        } else if (v.configDefaultType === configDefaultType) {\n          v.configDefaultType = \"\";\n        }\n      });\n      this.$store.commit(\"setCustomConfigList\", customConfigList);\n    }\n  }\n};\n</script>\n\n<style lang=\"stylus\" scoped>\n>>>.iconfont {\n  font-family: iconfont;\n  font-style: normal;\n}\n\n>>>.moon-icon {\n  font-family: iconfont;\n  font-style: normal;\n}\n\n.settings-wrapper {\n  user-select: none;\n  margin: -16px;\n  margin-bottom: -13px;\n  text-align: left;\n  padding: 24px;\n  padding-top: calc(24px + constant(safe-area-inset-top));\n  padding-top: calc(24px + env(safe-area-inset-top));\n\n  .settings-title {\n    font-size: 18px;\n    line-height: 22px;\n    margin-bottom: 28px;\n    font-family: -apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif;\n    font-weight: 400;\n\n    .title-btn {\n      float: right;\n      font-size: 14px;\n      color: #ed4259;\n      cursor: pointer;\n    }\n  }\n\n  .setting-list {\n    max-height: 45vh;\n    overflow-y: auto;\n    ul {\n      list-style: none outside none;\n      margin: 0;\n      padding: 0;\n\n      li:not(:first-child) {\n        margin-top: 20px;\n      }\n\n      li {\n        list-style: none outside none;\n\n        .setting-item-title {\n          display: inline-block;\n          width: 56px;\n          margin-right: 16px;\n          vertical-align: top;\n          line-height: 36px;\n          color: #666;\n        }\n        .font-color-title {\n          line-height: 40px;\n        }\n        .selection-zone {\n          display: inline-block;\n          width: calc(100% - 72px);\n          word-wrap: break-word;\n\n          span {\n            margin-bottom: 5px;\n          }\n        }\n\n        .span-item {\n          width: 78px;\n          height: 34px;\n          cursor: pointer;\n          margin-right: 16px;\n          border-radius: 2px;\n          text-align: center;\n          vertical-align: middle;\n          display: inline-block;\n          font: 14px / 34px PingFangSC-Regular, HelveticaNeue-Light, 'Helvetica Neue Light', 'Microsoft YaHei', sans-serif;\n          position: relative;\n\n          .delete-custom-config-icon {\n            display: inline-block;\n            cursor: pointer;\n            position: absolute;\n            top: -10px;\n            right: -10px;\n            font-size: 20px;\n            color: #ed4259;\n            z-index: 10;\n          }\n\n          .upload-font-icon {\n            display: inline-block;\n            cursor: pointer;\n            position: absolute;\n            top: -10px;\n            right: -10px;\n            font-size: 20px;\n            z-index: 10;\n            color: #606266;\n\n            &.active {\n              color: #ed4259;\n            }\n          }\n        }\n\n        .span-item.selected  {\n          border: 1px solid #ed4259;\n          color: #ed4259;\n        }\n\n        .custom-theme {\n          width: calc(100% - 72px);\n          display: inline-block;\n\n          .custom-theme-title {\n            display: inline-block;\n            margin-right: 28px;\n            margin-bottom: 5px;\n          }\n\n          .content-bg-preview {\n            width: 36px;\n            height: 36px;\n            display: inline-block;\n            vertical-align: middle;\n            margin-left: 10px;\n            margin-bottom: 8px;\n            position: relative;\n            box-sizing: border-box;\n\n            img {\n              width: 100%;\n              height: 100%;\n              display: inline-block;\n              vertical-align: middle;\n            }\n\n            .delete-bg-icon {\n              position: absolute;\n              top: -6px;\n              right: -6px;\n              font-size: 18px;\n              color: #ed4259;\n            }\n          }\n          .selected {\n            color: #ed4259;\n            border: 1px solid #ed4259;\n          }\n          .upload-bg-btn {\n            display: inline-block;\n            margin-left: 10px;\n            color: #ed4259;\n            cursor: pointer;\n          }\n        }\n\n        .theme-item {\n          line-height: 32px;\n          width: 34px;\n          height: 34px;\n          margin-right: 16px;\n          border-radius: 100%;\n          display: inline-block;\n          cursor: pointer;\n          text-align: center;\n          vertical-align: middle;\n\n          .iconfont {\n            display: none;\n          }\n        }\n\n        .selected {\n          color: #ed4259;\n\n          .iconfont {\n            display: inline;\n          }\n        }\n      }\n\n      li {\n\n        .resize {\n          display: inline-block;\n          height: 34px;\n          vertical-align: middle;\n          border-radius: 2px;\n\n          span {\n            min-width: 72px;\n            height: 34px;\n            line-height: 34px;\n            display: inline-block;\n            cursor: pointer;\n            text-align: center;\n            vertical-align: middle;\n\n            em {\n              font-style: normal;\n            }\n          }\n\n          .lang {\n            color: #a6a6a6;\n            font-weight: 400;\n            font-family: -apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif;\n          }\n\n          b {\n            display: inline-block;\n            height: 20px;\n            vertical-align: middle;\n          }\n        }\n      }\n\n      .operation-zone {\n        display: flex;\n        flex-direction: row;\n        justify-content: space-between;\n\n        .span-btn {\n          cursor: pointer;\n          color: #ed4259;\n        }\n      }\n    }\n  }\n  .setting-list::-webkit-scrollbar {\n    width: 0 !important;\n  }\n  .el-color-picker {\n    vertical-align: middle;\n  }\n}\n\n.night {\n  >>>.theme-item {\n    border: 1px solid #666;\n  }\n\n  >>>.selected {\n    border: 1px solid #666;\n  }\n\n  >>>.moon-icon {\n    color: #ed4259;\n  }\n\n  .span-item {\n    border: 1px solid #666;\n    background: rgba(45, 45, 45, 0.5);\n  }\n\n  >>>.resize {\n    border: 1px solid #666;\n    background: rgba(45, 45, 45, 0.5);\n\n    b {\n      border-right: 1px solid #666;\n    }\n  }\n}\n\n.day {\n  >>>.theme-item {\n    border: 1px solid #e5e5e5;\n  }\n\n  >>>.selected {\n    border: 1px solid #ed4259;\n  }\n\n  >>>.moon-icon {\n    display: inline;\n    color: rgba(255, 255, 255, 0.2);\n  }\n\n  .span-item {\n    background: rgba(255, 255, 255, 0.5);\n    border: 1px solid rgba(0, 0, 0, 0.1);\n  }\n\n  >>>.resize {\n    border: 1px solid #e5e5e5;\n    background: rgba(255, 255, 255, 0.5);\n\n    b {\n      border-right: 1px solid #e5e5e5;\n    }\n  }\n}\n\n@media (hover: hover) {\n  .span-item:hover {\n    border: 1px solid #ed4259;\n    color: #ed4259;\n  }\n  li {\n    .less:hover, .more:hover {\n      color: #ed4259;\n    }\n  }\n}\n</style>\n<style lang=\"stylus\">\n.setting-input {\n  .el-input__inner {\n    background: transparent;\n    border: none !important;\n    text-align: center;\n    width: 72px;\n    font-size: 14px;\n    color: #a6a6a6;\n  }\n}\n</style>\n"
  },
  {
    "path": "web/src/components/ReplaceRule.vue",
    "content": "<template>\n  <el-dialog\n    title=\"替换规则管理\"\n    :visible.sync=\"show\"\n    :width=\"dialogWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    v-if=\"$store.getters.isNormalPage\"\n    :before-close=\"cancel\"\n  >\n    <div class=\"custom-dialog-title\" slot=\"title\">\n      <span class=\"el-dialog__title\"\n        >替换规则管理\n        <span class=\"float-right span-btn\" @click=\"uploadFile\">导入</span>\n        <input\n          ref=\"fileRef\"\n          type=\"file\"\n          @change=\"onFileChange($event)\"\n          style=\"display:none\"\n        />\n      </span>\n    </div>\n    <div class=\"source-container table-container\">\n      <el-table\n        :data=\"$store.state.filterRules\"\n        :height=\"dialogContentHeight\"\n        @selection-change=\"localSelection = $event\"\n      >\n        <el-table-column\n          type=\"selection\"\n          width=\"25\"\n          :fixed=\"$store.state.miniInterface\"\n        >\n        </el-table-column>\n        <el-table-column\n          property=\"name\"\n          min-width=\"150px\"\n          label=\"规则名称\"\n          :fixed=\"$store.state.miniInterface\"\n        >\n        </el-table-column>\n        <el-table-column property=\"scope\" label=\"替换范围\" min-width=\"150px\">\n        </el-table-column>\n        <el-table-column property=\"isEnabled\" label=\"是否启用\" min-width=\"80\">\n          <template slot-scope=\"scope\">\n            <el-switch\n              v-model=\"scope.row.isEnabled\"\n              active-color=\"#13ce66\"\n              inactive-color=\"#ff4949\"\n              :active-value=\"true\"\n              :inactive-value=\"false\"\n              @change=\"toggleRuleEnabled(scope.row, $event)\"\n            >\n            </el-switch>\n          </template>\n        </el-table-column>\n        <el-table-column label=\"操作\" width=\"100px\">\n          <template slot-scope=\"scope\">\n            <el-button type=\"text\" @click=\"editReplaceRule(scope.row)\"\n              >编辑</el-button\n            >\n          </template>\n        </el-table-column>\n      </el-table>\n    </div>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"deleteReplaceRules\"\n        >批量删除</el-button\n      >\n      <span class=\"check-tip\">已选择 {{ localSelection.length }} 个</span>\n      <el-button size=\"medium\" @click=\"cancel\">取消</el-button>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport eventBus from \"../plugins/eventBus\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"ReplaceRule\",\n  data() {\n    return {\n      localSelection: []\n    };\n  },\n  computed: {\n    ...mapGetters([\"dialogWidth\", \"dialogTop\", \"dialogContentHeight\"])\n  },\n  props: [\"show\"],\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        //\n      }\n    }\n  },\n  methods: {\n    formatTableField(row, column, cellValue) {\n      switch (column.property) {\n        default:\n          return cellValue;\n      }\n    },\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    async deleteReplaceRules() {\n      if (!this.localSelection.length) {\n        this.$message.error(\"请选择需要删除的替换规则\");\n        return;\n      }\n      const res = await this.$confirm(\"确认要删除所选择的替换规则吗?\", \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteReplaceRules\", this.localSelection).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.localSelection = [];\n            this.$message.success(\"删除替换规则成功\");\n            this.$root.$children[0].loadReplaceRules(true);\n          }\n        },\n        error => {\n          this.$message.error(\n            \"删除替换规则失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    toggleRuleEnabled(rule, isEnabled) {\n      Axios.post(\"/saveReplaceRule\", { ...rule, isEnabled }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"修改成功\");\n            this.$root.$children[0].loadReplaceRules(true);\n          }\n        },\n        error => {\n          this.$message.error(\"修改失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    editReplaceRule(row) {\n      eventBus.$emit(\"showReplaceRuleForm\", { ...row }, false);\n    },\n    uploadFile() {\n      this.$refs.fileRef.dispatchEvent(new MouseEvent(\"click\"));\n    },\n    onFileChange(event) {\n      const rawFile = event.target.files && event.target.files[0];\n      // console.log(\"rawFile\", rawFile);\n      const reader = new FileReader();\n      reader.onload = e => {\n        const data = e.target.result;\n        try {\n          const ruleList = JSON.parse(data);\n          if (Array.isArray(ruleList) && ruleList.length) {\n            this.comfirmImport(ruleList);\n          }\n        } catch (error) {\n          this.$message.error(\"替换规则文件错误\");\n        }\n      };\n      reader.onerror = () => {\n        // console.log(\"FileReader error\", e);\n        // FileReader 读取出错，只能上传读取了\n        let param = new FormData();\n        param.append(\"file\", rawFile);\n        Axios.post(this.api + \"/readSourceFile\", param, {\n          headers: { \"Content-Type\": \"multipart/form-data\" }\n        }).then(\n          res => {\n            if (res.data.isSuccess) {\n              //\n              let ruleList = [];\n              res.data.data.forEach(v => {\n                try {\n                  const data = JSON.parse(v);\n                  if (Array.isArray(data)) {\n                    ruleList = ruleList.concat(data);\n                  }\n                } catch (error) {\n                  //\n                }\n              });\n              if (ruleList.length) {\n                this.comfirmImport(ruleList);\n              } else {\n                this.$message.error(\"替换规则文件错误\");\n              }\n            }\n          },\n          error => {\n            this.$message.error(\n              \"读取替换规则文件内容失败 \" + (error && error.toString())\n            );\n          }\n        );\n      };\n      reader.readAsText(rawFile);\n      this.$refs.fileRef.value = null;\n    },\n    async comfirmImport(ruleList) {\n      const res = await this.$confirm(\n        `确认要导入文件中的${ruleList.length}条替换规则吗?`,\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/saveReplaceRules\", ruleList).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"导入替换规则成功\");\n            this.$root.$children[0].loadReplaceRules(true);\n          }\n        },\n        error => {\n          this.$message.error(\n            \"导入替换规则失败 \" + (error && error.toString())\n          );\n        }\n      );\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.float-left {\n  float: left;\n}\n</style>\n"
  },
  {
    "path": "web/src/components/ReplaceRuleForm.vue",
    "content": "<template>\n  <el-dialog\n    title=\"替换规则\"\n    :visible.sync=\"show\"\n    :width=\"dialogWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    v-if=\"$store.getters.isNormalPage\"\n    :before-close=\"cancel\"\n  >\n    <el-form :model=\"ruleForm\">\n      <el-form-item label=\"名称\">\n        <el-input v-model=\"ruleForm.name\"></el-input>\n      </el-form-item>\n      <el-form-item label=\"规则\">\n        <el-input v-model=\"ruleForm.pattern\"></el-input>\n      </el-form-item>\n      <el-form-item label=\"替换为\">\n        <el-input v-model=\"ruleForm.replacement\"></el-input>\n      </el-form-item>\n      <el-form-item label=\"替换范围\">\n        <el-input v-model=\"ruleForm.scope\"></el-input>\n      </el-form-item>\n      <el-checkbox v-model=\"ruleForm.isRegex\">使用正则表达式</el-checkbox>\n      <el-checkbox v-model=\"ruleForm.isEnabled\">是否启用</el-checkbox>\n    </el-form>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button size=\"medium\" @click=\"cancel\">取 消</el-button>\n      <el-button size=\"medium\" type=\"primary\" @click=\"save\">确 定</el-button>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport { defaultReplaceRule } from \"../plugins/config.js\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"ReplaceRuleForm\",\n  data() {\n    return {\n      ruleForm: { ...defaultReplaceRule }\n    };\n  },\n  props: [\"show\", \"rule\", \"isAdd\"],\n  computed: {\n    ...mapGetters([\"dialogWidth\", \"dialogTop\", \"dialogContentHeight\"])\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        this.ruleForm = this.rule || { ...defaultReplaceRule };\n      }\n    }\n  },\n  methods: {\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    save() {\n      if (!this.ruleForm.name) {\n        this.$message.error(\"规则名不能为空\");\n        return;\n      }\n      if (!this.ruleForm.pattern) {\n        this.$message.error(\"规则不能为空\");\n        return;\n      }\n      if (!this.ruleForm.scope) {\n        this.$message.error(\"替换范围不能为空\");\n        return;\n      }\n      if (this.isAdd) {\n        // 判断 name 是否唯一\n        const isExisted = this.$store.state.filterRules.find(\n          v => v.name === this.ruleForm.name\n        );\n        if (isExisted) {\n          this.$message.error(\"规则名不能重复\");\n          return;\n        }\n      }\n      const rule = { ...this.ruleForm };\n      // this.$store.commit(\"addFilterRule\", rule);\n      Axios.post(\"/saveReplaceRule\", rule).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\n              (this.isAdd ? \"新增\" : \"编辑\") + \"替换规则成功\"\n            );\n            this.$root.$children[0].loadReplaceRules(true);\n            this.cancel();\n          }\n        },\n        error => {\n          this.$message.error(\n            (this.isAdd ? \"新增\" : \"编辑\") +\n              \"替换规则失败 \" +\n              (error && error.toString())\n          );\n        }\n      );\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.float-left {\n  float: left;\n}\n</style>\n"
  },
  {
    "path": "web/src/components/RssArticle.vue",
    "content": "<template>\n  <el-dialog\n    :title=\"rssArticleInfo.title\"\n    :visible.sync=\"show\"\n    :width=\"dialogSmallWidth\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    :before-close=\"cancel\"\n  >\n    <div class=\"rss-article-info-container\" v-if=\"show\">\n      <div\n        class=\"rss-article-content\"\n        ref=\"rssArticleContentRef\"\n        v-html=\"rssArticleInfo.content || rssArticleInfo.description\"\n        @click=\"rssArticleClickHandler\"\n      ></div>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"RssArticle\",\n  data() {\n    return {\n      rssArticleList: [],\n      sourceUrl: \"\",\n      sortUrls: [],\n      sortName: \"\",\n      hasMoreRssArticles: true,\n      page: 1\n    };\n  },\n  props: [\"show\", \"rssArticleInfo\"],\n  computed: {\n    ...mapGetters([\"dialogSmallWidth\", \"dialogTop\"]),\n    rssArticleImageList() {\n      return this.rssArticleList\n        .filter(v => v.image)\n        .map(v => this.getImage(v.image, true, true));\n    }\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        //\n      }\n    }\n  },\n  methods: {\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    rssArticleClickHandler(e) {\n      if (\n        e.target &&\n        e.target.nodeName &&\n        e.target.nodeName.toLowerCase() === \"img\"\n      ) {\n        const imgList = this.$refs.rssArticleContentRef.querySelectorAll(\"img\");\n        if (imgList.length) {\n          const imgUrlList = [];\n          let index = 0;\n          for (let i = 0; i < imgList.length; i++) {\n            imgUrlList.push(imgList[i].src);\n            if (imgList[i] === e.target) {\n              index = i;\n            }\n          }\n          this.$store.commit(\"setPreviewImageIndex\", index);\n          this.$store.commit(\"setPreviewImgList\", imgUrlList);\n        }\n      }\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.rss-article-info-container {\n  max-height: calc(var(--vh, 1vh) * 70 - 54px - 60px);\n  overflow-y: auto;\n}\n</style>\n<style lang=\"stylus\">\n.night-theme {\n  .rss-article-content {\n    color: #aaa;\n  }\n}\n@media screen and (max-width: 750px) {\n  .rss-article-info-container {\n    max-height: calc(var(--vh, 1vh) * 100 - 54px - 40px) !important;\n  }\n}\n.rss-article-info-container img {\n  max-width: 100%;\n}\n.rss-article-info-container video {\n  max-width: 100%;\n}\n</style>\n"
  },
  {
    "path": "web/src/components/RssArticleList.vue",
    "content": "<template>\n  <el-dialog\n    :title=\"rssSource.sourceName\"\n    :visible.sync=\"show\"\n    :width=\"dialogSmallWidth\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    :before-close=\"cancel\"\n  >\n    <el-tabs\n      v-if=\"sortUrls.length > 1\"\n      v-model=\"sortName\"\n      @tab-click=\"onTabClick\"\n    >\n      <el-tab-pane\n        v-for=\"(sort, index) in sortUrls\"\n        :label=\"sort.name\"\n        :name=\"sort.name\"\n        :key=\"'sort-' + index + sort.name\"\n      ></el-tab-pane>\n    </el-tabs>\n    <div class=\"rss-article-list-container\" ref=\"rssArticleListRef\">\n      <div\n        class=\"rss-article\"\n        v-for=\"(article, index) in rssArticleList\"\n        :key=\"'rss-article-' + index\"\n        @click=\"getRssArticleContent(article)\"\n      >\n        <div class=\"rss-article-info\">\n          <div class=\"rss-article-title\">{{ article.title }}</div>\n          <div class=\"rss-article-date\">{{ article.pubDate }}</div>\n        </div>\n        <div class=\"rss-article-image\" v-if=\"article.image\">\n          <div class=\"image-wrapper\">\n            <el-image\n              class=\"rss-article-img\"\n              :src=\"getCover(article.image, true, true)\"\n              :preview-src-list=\"rssArticleImageList\"\n              fit=\"cover\"\n              lazy\n              @click.stop=\"noop\"\n            >\n            </el-image>\n          </div>\n        </div>\n      </div>\n      <div\n        class=\"load-more-rss\"\n        @click=\"hasMoreRssArticles && getRssArticles(page + 1)\"\n      >\n        {{ hasMoreRssArticles ? \"加载更多\" : \"没有更多啦\" }}\n      </div>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport eventBus from \"../plugins/eventBus\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"RssArticleList\",\n  data() {\n    return {\n      rssArticleList: [],\n      sourceUrl: \"\",\n      sortUrls: [],\n      sortName: \"\",\n      sortUrl: \"\",\n      hasMoreRssArticles: true,\n      page: 1\n    };\n  },\n  props: [\"show\", \"rssSource\"],\n  computed: {\n    ...mapGetters([\"dialogSmallWidth\", \"dialogTop\"]),\n    rssArticleImageList() {\n      return this.rssArticleList\n        .filter(v => v.image)\n        .map(v => this.getImage(v.image, true, true));\n    }\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        //\n        try {\n          this.parseSourceUrl();\n          this.getRssArticles();\n        } catch (error) {\n          // console.log(error);\n          this.$message.error(\"解析失败: \" + error);\n        }\n      } else {\n        this.sortUrls = [];\n        this.rssArticleList = [];\n        this.page = 1;\n        this.sortName = \"\";\n        this.sortUrl = \"\";\n        this.hasMoreRssArticles = true;\n      }\n    }\n  },\n  methods: {\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    parseSourceUrl() {\n      this.sourceUrl = this.rssSource.sourceUrl;\n      if (!this.rssSource.singleUrl && this.rssSource.sortUrl) {\n        // 由于是在客户端解析，所以不支持解析 <js> 和 @js: 开头的 sortUrl\n        const sortUrls = [];\n        this.rssSource.sortUrl\n          .replace(/\\r\\n/g, \"\\n\")\n          .split(\"\\n\")\n          .forEach(v => {\n            if (v) {\n              v = v.split(\"::\");\n              sortUrls.push({\n                name: v[0],\n                url: v[1]\n              });\n            }\n          });\n        this.sortUrls = sortUrls;\n        this.sortName = sortUrls[0].name;\n        this.sortUrl = sortUrls[0].url;\n      }\n    },\n    onTabClick(sort) {\n      this.sortUrl = this.sortUrls.find(v => v.name === sort.name).url;\n      this.page = 1;\n      this.hasMoreRssArticles = true;\n      this.getRssArticles();\n    },\n    getRssArticles(page) {\n      this.page = page || 1;\n      Axios.post(this.api + \"/getRssArticles\", {\n        sourceUrl: this.sourceUrl,\n        sortName: this.sortName,\n        sortUrl: this.sortUrl,\n        page: this.page\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            //\n            const articles = res.data.data.first;\n            // const nextPageUrl = res.data.data.second;\n            if (!articles.length) {\n              this.$message.error(\"没有数据\");\n              this.hasMoreRssArticles = false;\n              return;\n            }\n            if (this.page > 1) {\n              this.rssArticleList = []\n                .concat(this.rssArticleList)\n                .concat(articles);\n            } else {\n              this.rssArticleList = articles;\n            }\n          }\n        },\n        error => {\n          this.$message.error(\n            \"加载RSS文章列表失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    noop() {},\n    showRssArticle() {},\n    getRssArticleContent(article) {\n      Axios.post(this.api + \"/getRssContent\", {\n        sourceUrl: this.rssSource.sourceUrl,\n        link: article.link,\n        origin: article.origin\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            eventBus.$emit(\"showRssArticleDialog\", {\n              ...article,\n              content: res.data.data\n            });\n          }\n        },\n        error => {\n          this.$message.error(\n            \"加载RSS文章内容失败 \" + (error && error.toString())\n          );\n        }\n      );\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.rss-article-list-container {\n  max-height: calc(var(--vh, 1vh) * 70 - 54px - 60px);\n  overflow-y: auto;\n\n  .rss-article {\n    display: flex;\n    flex-direction: row;\n    padding: 15px 10px;\n    border-bottom: 1px solid #eee;\n    cursor: pointer;\n\n    .rss-article-info {\n      display: flex;\n      flex-direction: column;\n      justify-content: space-between;\n      flex: 1;\n      padding-right: 5px;\n\n      .rss-article-title {\n        font-weight: 600;\n        font-size: 14px;\n      }\n\n      .rss-article-date {\n        font-size: 12px;\n        margin-top: 10px;\n      }\n    }\n    .rss-article-image {\n      width: 120px;\n      display: flex;\n      align-items: center;\n\n      .image-wrapper {\n        width: 100%;\n        height: 0;\n        padding-bottom: 62.5%;\n        overflow: hidden;\n\n        .rss-article-img {\n          width: 120px;\n          height: 75px;\n        }\n      }\n    }\n  }\n\n  .load-more-rss {\n    text-align: center;\n    padding: 10px;\n    cursor: pointer;\n  }\n}\n</style>\n<style lang=\"stylus\">\n.night-theme {\n  .rss-article-list-container {\n    .rss-article {\n      border-color: #333;\n      color: #aaa;\n\n      .rss-article-date {\n        color: #666;\n      }\n    }\n  }\n}\n@media screen and (max-width: 750px) {\n  .rss-article-list-container {\n    max-height: calc(var(--vh, 1vh) * 100 - 54px - 40px) !important;\n\n    .rss-article {\n      padding: 15px 5px;\n\n      .rss-article-image {\n        width: 100px;\n        .rss-article-img {\n          width: 100px;\n          height: 62.5px;\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "web/src/components/RssSourceList.vue",
    "content": "<template>\n  <el-dialog\n    :visible.sync=\"show\"\n    :width=\"dialogSmallWidth\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    :before-close=\"cancel\"\n  >\n    <div class=\"custom-dialog-title\" slot=\"title\">\n      <span class=\"el-dialog__title\"\n        >RSS订阅({{ rssSourceList.length }})\n        <span\n          class=\"float-right span-btn\"\n          @click=\"showRssSourceEditButton = !showRssSourceEditButton\"\n          >{{ showRssSourceEditButton ? \"取消\" : \"编辑\" }}</span\n        >\n        <span class=\"float-right span-btn\" @click=\"uploadRssSource\">导入</span>\n        <span class=\"float-right span-btn\" @click=\"editRssSource(false)\"\n          >新增</span\n        >\n      </span>\n      <input\n        ref=\"rssInputRef\"\n        type=\"file\"\n        @change=\"onSourceFileChange($event, true)\"\n        style=\"display:none\"\n      />\n    </div>\n    <div class=\"rss-source-list-container\">\n      <div\n        class=\"rss-source\"\n        v-for=\"(source, index) in rssSourceList\"\n        :key=\"'rss-' + index\"\n        @click=\"showRssArticleListDialog(source)\"\n      >\n        <i\n          class=\"el-icon-close\"\n          v-if=\"showRssSourceEditButton\"\n          @click.stop=\"deleteRssSource(source)\"\n        ></i>\n        <i\n          class=\"el-icon-edit\"\n          v-if=\"showRssSourceEditButton\"\n          @click.stop=\"editRssSource(source)\"\n        ></i>\n        <el-image\n          :src=\"getImage(source.sourceIcon, true)\"\n          class=\"rss-icon\"\n          fit=\"cover\"\n          lazy\n        />\n        <div class=\"rss-title\">{{ source.sourceName }}</div>\n      </div>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport eventBus from \"../plugins/eventBus\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"RssSourceList\",\n  data() {\n    return {\n      showRssSourceEditButton: false\n    };\n  },\n  props: [\"show\", \"rssSource\"],\n  computed: {\n    ...mapGetters([\"dialogSmallWidth\", \"dialogTop\"]),\n    rssSourceList() {\n      return []\n        .concat(this.$store.state.rssSourceList)\n        .sort((a, b) => a.customOrder - b.customOrder);\n    }\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        //\n      }\n    }\n  },\n  methods: {\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    async deleteRssSource(source) {\n      const res = await this.$confirm(`确认要删除该RSS订阅源吗?`, \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteRssSource\", source).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"删除成功\");\n            this.loadRssSources(true);\n          }\n        },\n        error => {\n          this.$message.error(\"删除失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    uploadRssSource() {\n      this.$refs.rssInputRef.dispatchEvent(new MouseEvent(\"click\"));\n    },\n    onSourceFileChange(event, isRssSource) {\n      eventBus.$emit(\"onSourceFileChange\", event, isRssSource);\n    },\n    showRssArticleListDialog(source) {\n      eventBus.$emit(\"showRssArticleListDialog\", source);\n    },\n    editRssSource(rssSource) {\n      rssSource = rssSource || {\n        sourceName: \"新增RSS源\",\n        sourceUrl: \"\",\n        sourceIcon: \"\",\n        sourceGroup: \"\",\n        enabled: true,\n        singleUrl: true,\n        articleStyle: 0,\n        ruleArticles: \"\",\n        ruleTitle: \"\",\n        rulePubDate: \"\",\n        ruleImage: \"\",\n        ruleLink: \"\",\n        ruleContent: \"\",\n        enableJs: true\n      };\n      eventBus.$emit(\n        \"showEditor\",\n        \"编辑RSS源\",\n        JSON.stringify(rssSource, null, 4),\n        (content, close) => {\n          try {\n            const source = JSON.parse(content);\n            if (!source.sourceName) {\n              this.$message.error(\"RSS源名称不能为空\");\n              return;\n            }\n            if (!source.sourceUrl) {\n              this.$message.error(\"RSS源链接不能为空\");\n              return;\n            }\n            Axios.post(this.api + \"/saveRssSource\", source).then(\n              res => {\n                if (res.data.isSuccess) {\n                  //\n                  close();\n                  this.$message.success(\"保存RSS源成功\");\n                  this.loadRssSources(true);\n                }\n              },\n              error => {\n                this.$message.error(\n                  \"保存RSS源失败 \" + (error && error.toString())\n                );\n              }\n            );\n          } catch (e) {\n            this.$message.error(\"RSS源必须是JSON格式\");\n          }\n        }\n      );\n    },\n    loadRssSources(refresh) {\n      return this.$root.$children[0].loadRssSources(refresh);\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.rss-source-list-container {\n  max-height: calc(var(--vh, 1vh) * 70 - 54px - 60px);\n  overflow-y: auto;\n\n  .rss-source {\n    display: inline-block;\n    width: 25%;\n    box-sizing: border-box;\n    padding: 10px;\n    position: relative;\n    text-align: center;\n    vertical-align: top;\n    margin-bottom: 10px;\n    cursor: pointer;\n    position: relative;\n\n    .el-icon-close {\n      position: absolute;\n      right: 6px;\n      top: 8px;\n      font-size: 18px;\n    }\n\n    .el-icon-edit {\n      position: absolute;\n      right: 6px;\n      top: 42px;\n      font-size: 18px;\n    }\n\n    .rss-icon {\n      display: inline-block;\n      width: 50px;\n      height: 50px;\n      border-radius: 5px;\n    }\n    .rss-title {\n      margin-top: 5px;\n      text-align: center;\n    }\n  }\n}\n</style>\n<style lang=\"stylus\">\n.night-theme {\n  .rss-source-list-container {\n    .rss-source {\n      .rss-title {\n        color: #aaa;\n      }\n    }\n  }\n}\n@media screen and (max-width: 750px) {\n  .rss-source-list-container {\n    max-height: calc(var(--vh, 1vh) * 100 - 54px - 40px) !important;;\n  }\n}\n@media screen and (max-width: 480px) {\n  .rss-source-list-container {\n    .rss-source {\n      .el-icon-close {\n        right: -5px;\n      }\n\n      .el-icon-edit {\n        right: -5px;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "web/src/components/SearchBookContent.vue",
    "content": "<template>\n  <el-dialog\n    :visible.sync=\"show\"\n    :width=\"dialogWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    v-if=\"$store.getters.isNormalPage\"\n    :before-close=\"cancel\"\n    @opened=\"opend\"\n  >\n    <div class=\"custom-dialog-title\" slot=\"title\">\n      <span class=\"el-dialog__title\">\n        <span class=\"title-input\">\n          <el-input\n            size=\"mini\"\n            placeholder=\"搜索书籍内容\"\n            v-model=\"keyword\"\n            class=\"search-input\"\n            @keyup.enter.native=\"searchBookContent(-1)\"\n          >\n            <i slot=\"prefix\" class=\"el-input__icon el-icon-search\"></i>\n          </el-input>\n        </span>\n      </span>\n    </div>\n    <div class=\"source-container table-container\">\n      <el-table\n        ref=\"resultTable\"\n        :data=\"searchResultList\"\n        :height=\"dialogContentHeight\"\n        @row-click=\"clickRow\"\n      >\n        <el-table-column property=\"chapterTitle\" min-width=\"100px\" label=\"章节\">\n        </el-table-column>\n        <el-table-column\n          property=\"resultText\"\n          min-width=\"250px\"\n          label=\"搜索结果\"\n        >\n        </el-table-column>\n      </el-table>\n    </div>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        :disabled=\"loading\"\n        @click=\"searchBookContent(lastIndex)\"\n        >{{ loading ? \"加载中\" : \"加载更多\" }}</el-button\n      >\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        v-if=\"lastScrollTop > 0\"\n        @click=\"restoreScrollTop\"\n        >跳转上次位置</el-button\n      >\n      <el-button size=\"medium\" @click=\"cancel\">取消</el-button>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport eventBus from \"../plugins/eventBus\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"SearchBookContent\",\n  data() {\n    return {\n      lastScrollTop: 0,\n      keyword: \"\",\n      lastIndex: 0,\n      searchResultList: [],\n      loading: false\n    };\n  },\n  computed: {\n    ...mapGetters([\"dialogWidth\", \"dialogTop\", \"dialogContentHeight\"])\n  },\n  props: [\"show\", \"book\"],\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        //\n      }\n    },\n    book: {\n      deep: true,\n      handler(newVal, oldVal) {\n        if (newVal.bookUrl !== oldVal.bookUrl) {\n          this.keyword = \"\";\n          this.lastIndex = -1;\n          this.searchResultList = [];\n        }\n      }\n    }\n  },\n  created() {\n    window.searchBookComp = this;\n  },\n  methods: {\n    formatTableField(row, column, cellValue) {\n      switch (column.property) {\n        default:\n          return cellValue;\n      }\n    },\n    opend() {\n      this.$nextTick(() => {\n        this.restoreScrollTop();\n      });\n    },\n    restoreScrollTop() {\n      if (!this.$refs.resultTable || !this.$refs.resultTable.$ready) {\n        this.$nextTick(() => {\n          this.restoreScrollTop();\n        });\n        return;\n      }\n      try {\n        this.$refs.resultTable.bodyWrapper.scrollTop = this.lastScrollTop;\n      } catch (error) {\n        // console.error(error);\n        setTimeout(() => {\n          this.restoreScrollTop();\n        }, 10);\n      }\n    },\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    async searchBookContent(lastIndex) {\n      if (this.loading) {\n        return;\n      }\n      this.loading = true;\n      Axios.post(this.api + \"/searchBookContent\", {\n        url: this.book.bookUrl,\n        keyword: this.keyword,\n        lastIndex: lastIndex\n      }).then(\n        res => {\n          this.loading = false;\n          if (res.data.isSuccess) {\n            this.lastIndex = res.data.data.lastIndex;\n            if (lastIndex === -1) {\n              this.searchResultList = res.data.data.list;\n            } else {\n              this.searchResultList = []\n                .concat(this.searchResultList)\n                .concat(res.data.data.list);\n            }\n          }\n        },\n        error => {\n          this.loading = false;\n          this.$message.error(\"加载失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    clickRow(row) {\n      this.lastScrollTop = this.$refs.resultTable.bodyWrapper.scrollTop;\n      eventBus.$emit(\"showSearchContent\", row);\n      this.cancel();\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.float-left {\n  float: left;\n}\n.title-input {\n  display: inline-block;\n  width: 75%;\n  margin: 0 auto;\n  transform: translateX(20%);\n}\n</style>\n"
  },
  {
    "path": "web/src/components/UserManage.vue",
    "content": "<template>\n  <el-dialog\n    title=\"用户管理\"\n    :visible.sync=\"show\"\n    :width=\"dialogWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    v-if=\"$store.getters.isNormalPage\"\n    :before-close=\"cancel\"\n  >\n    <div class=\"custom-dialog-title\" slot=\"title\">\n      <span class=\"el-dialog__title\"\n        >用户管理\n        <span class=\"float-right span-btn\" @click=\"showAddUserDialog()\"\n          >新增</span\n        >\n      </span>\n    </div>\n    <div class=\"source-container table-container\">\n      <el-table\n        :data=\"userList\"\n        :height=\"dialogContentHeight\"\n        @selection-change=\"manageUserSelection = $event\"\n      >\n        <el-table-column\n          type=\"selection\"\n          width=\"25\"\n          :selectable=\"isUserSelectable\"\n          :fixed=\"$store.state.miniInterface\"\n        >\n        </el-table-column>\n        <el-table-column\n          property=\"username\"\n          label=\"用户名\"\n          min-width=\"100\"\n          :fixed=\"$store.state.miniInterface\"\n        ></el-table-column>\n        <el-table-column\n          property=\"lastLoginAt\"\n          label=\"上次登录\"\n          :formatter=\"formatTableField\"\n          min-width=\"120\"\n        ></el-table-column>\n        <el-table-column\n          property=\"createdAt\"\n          label=\"注册时间\"\n          :formatter=\"formatTableField\"\n          min-width=\"120\"\n        ></el-table-column>\n        <el-table-column property=\"enableWebdav\" label=\"WebDAV\" min-width=\"80\">\n          <template slot-scope=\"scope\">\n            <el-switch\n              v-if=\"scope.row.userNS !== 'default'\"\n              v-model=\"scope.row.enableWebdav\"\n              active-color=\"#13ce66\"\n              inactive-color=\"#ff4949\"\n              :active-value=\"true\"\n              :inactive-value=\"false\"\n              @change=\"toggleUserWebdav(scope.row, $event)\"\n            >\n            </el-switch>\n          </template>\n        </el-table-column>\n        <el-table-column\n          property=\"enableLocalStore\"\n          label=\"书仓\"\n          min-width=\"80\"\n        >\n          <template slot-scope=\"scope\">\n            <el-switch\n              v-if=\"scope.row.userNS !== 'default'\"\n              v-model=\"scope.row.enableLocalStore\"\n              active-color=\"#13ce66\"\n              inactive-color=\"#ff4949\"\n              :active-value=\"true\"\n              :inactive-value=\"false\"\n              @change=\"toggleUserLocalStore(scope.row, $event)\"\n            >\n            </el-switch>\n          </template>\n        </el-table-column>\n        <el-table-column label=\"操作\" width=\"100px\">\n          <template slot-scope=\"scope\">\n            <el-button type=\"text\" @click=\"resetPassword(scope.row)\"\n              >重置密码</el-button\n            >\n            <el-button type=\"text\" @click=\"setAsDefaultBookSources(scope.row)\"\n              >设为默认书源</el-button\n            >\n          </template>\n        </el-table-column>\n      </el-table>\n    </div>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"deleteUserList\"\n        >批量删除</el-button\n      >\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"deleteUserBookSource\"\n        >删除用户书源</el-button\n      >\n      <span class=\"check-tip\">已选择 {{ manageUserSelection.length }} 个</span>\n      <el-button size=\"medium\" @click=\"cancel\">取消</el-button>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport eventBus from \"../plugins/eventBus\";\nimport { formatSize } from \"../plugins/helper\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"UserManage\",\n  data() {\n    return {\n      manageUserSelection: []\n    };\n  },\n  props: [\"show\"],\n  computed: {\n    ...mapGetters([\"dialogWidth\", \"dialogTop\", \"dialogContentHeight\"]),\n    userList: {\n      get() {\n        return this.$store.state.userList;\n      },\n      set(val) {\n        this.$store.commit(\"setUserList\", val);\n      }\n    }\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        this.manageUserSelection = [];\n      }\n    }\n  },\n  methods: {\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    showAddUserDialog() {\n      eventBus.$emit(\"showAddUserDialog\");\n    },\n    formatTableField(row, column, cellValue) {\n      switch (column.property) {\n        case \"createdAt\":\n        case \"lastLoginAt\":\n        case \"lastModified\":\n          return cellValue ? new Date(cellValue).format(\"yy-MM-dd hh:mm\") : \"\";\n        case \"size\":\n          return row.isDirectory ? \"\" : formatSize(cellValue);\n        default:\n          return cellValue;\n      }\n    },\n    isUserSelectable(user) {\n      return user.userNS !== \"default\";\n    },\n    async deleteUserList() {\n      if (!this.manageUserSelection.length) {\n        this.$message.error(\"请选择需要删除的用户\");\n        return;\n      }\n      const res = await this.$confirm(\"确认要删除所选择的用户吗?\", \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(\n        this.api + \"/deleteUsers\",\n        this.manageUserSelection.map(v => v.username)\n      ).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.manageUserSelection = [];\n            this.$message.success(\"删除用户成功\");\n            this.userList = res.data.data.map(v => ({\n              ...v,\n              userNS: v.username\n            }));\n          }\n        },\n        error => {\n          this.$message.error(\"删除用户失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async deleteUserBookSource() {\n      if (!this.manageUserSelection.length) {\n        this.$message.error(\"请选择需要删除书源的用户\");\n        return;\n      }\n      const res = await this.$confirm(\"确认要删除所选择的用户书源吗?\", \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(\n        this.api + \"/deleteUserBookSource\",\n        this.manageUserSelection.map(v => v.username)\n      ).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.manageUserSelection = [];\n            this.$message.success(\"操作成功\");\n          }\n        },\n        error => {\n          this.$message.error(\"操作失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    toggleUserWebdav(user, enableWebdav) {\n      Axios.post(this.api + \"/updateUser\", {\n        username: user.username,\n        enableWebdav\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"修改成功\");\n            this.userList = res.data.data.map(v => ({\n              ...v,\n              userNS: v.username\n            }));\n          }\n        },\n        error => {\n          this.$message.error(\"修改失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    toggleUserLocalStore(user, enableLocalStore) {\n      Axios.post(this.api + \"/updateUser\", {\n        username: user.username,\n        enableLocalStore\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"修改成功\");\n            this.userList = res.data.data.map(v => ({\n              ...v,\n              userNS: v.username\n            }));\n          }\n        },\n        error => {\n          this.$message.error(\"修改失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async setAsDefaultBookSources(user) {\n      const res = await this.$confirm(\n        `确认要将用户${user.username}的书源设为默认书源（新用户有效）吗?`,\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      return Axios.post(this.api + \"/setAsDefaultBookSources\", {\n        username: user.username\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"设置成功\");\n          }\n        },\n        error => {\n          this.$message.error(\"设置失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async resetPassword(user) {\n      const res = await this.$prompt(\"\", \"重置密码\", {\n        inputValue: \"\",\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        inputValidator(v) {\n          if (!v) {\n            return \"密码不能为空\";\n          }\n          return true;\n        }\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/resetPassword\", {\n        username: user.username,\n        password: res.value\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"重置密码成功\");\n          }\n        },\n        error => {\n          this.$message.error(\"重置密码失败 \" + (error && error.toString()));\n        }\n      );\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.float-right {\n  float: right;\n}\n</style>\n"
  },
  {
    "path": "web/src/components/WebDAV.vue",
    "content": "<template>\n  <el-dialog\n    title=\"WebDAV文件管理\"\n    :visible.sync=\"show\"\n    :width=\"dialogWidth\"\n    :top=\"dialogTop\"\n    :fullscreen=\"$store.state.miniInterface\"\n    :class=\"\n      isWebApp && !$store.getters.isNight ? 'status-bar-light-bg-dialog' : ''\n    \"\n    v-if=\"$store.getters.isNormalPage\"\n    :before-close=\"cancel\"\n  >\n    <div class=\"source-container table-container\">\n      <el-table\n        :data=\"fileList\"\n        :height=\"dialogContentHeight\"\n        @selection-change=\"fileSelection = $event\"\n      >\n        <el-table-column\n          type=\"selection\"\n          width=\"25\"\n          :fixed=\"$store.state.miniInterface\"\n          :selectable=\"row => !row.toParent\"\n        >\n        </el-table-column>\n        <el-table-column\n          property=\"name\"\n          min-width=\"150px\"\n          label=\"文件名\"\n          :fixed=\"$store.state.miniInterface\"\n        >\n          <template slot-scope=\"scope\">\n            <span v-if=\"!scope.row.isDirectory\">{{ scope.row.name }}</span>\n            <el-link\n              type=\"primary\"\n              v-if=\"scope.row.isDirectory\"\n              @click=\"showWebdavFile(scope.row.path)\"\n              >{{ scope.row.name }}</el-link\n            >\n          </template>\n        </el-table-column>\n        <el-table-column\n          property=\"size\"\n          label=\"大小\"\n          :formatter=\"formatTableField\"\n          min-width=\"100px\"\n        ></el-table-column>\n        <el-table-column\n          property=\"lastModified\"\n          label=\"修改时间\"\n          :formatter=\"formatTableField\"\n          width=\"120px\"\n        ></el-table-column>\n        <el-table-column label=\"操作\" width=\"100px\">\n          <template slot-scope=\"scope\">\n            <el-button\n              type=\"text\"\n              @click=\"restoreFromWebdav(scope.row)\"\n              v-if=\"!scope.row.isDirectory && scope.row.name.endsWith('.zip')\"\n              >还原</el-button\n            >\n            <el-button\n              type=\"text\"\n              @click=\"downloadFromWebdav(scope.row)\"\n              v-if=\"!scope.row.isDirectory\"\n              >下载</el-button\n            >\n            <el-button\n              type=\"text\"\n              @click=\"importFromWebdav(scope.row)\"\n              v-if=\"canImport(scope.row)\"\n              >加入书架</el-button\n            >\n            <el-button\n              type=\"text\"\n              @click=\"deleteWebdavFile(scope.row)\"\n              style=\"color: #f56c6c\"\n              v-if=\"!scope.row.toParent\"\n              >删除</el-button\n            >\n          </template>\n        </el-table-column>\n      </el-table>\n    </div>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"deleteWebdavFileList\"\n        >批量删除</el-button\n      >\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"importFromWebdav(true)\"\n        >批量加入书架</el-button\n      >\n      <el-button\n        type=\"primary\"\n        size=\"medium\"\n        class=\"float-left\"\n        @click=\"uploadToWebDAV\"\n      >\n        上传文件\n      </el-button>\n      <input\n        ref=\"fileRef\"\n        type=\"file\"\n        multiple=\"multiple\"\n        @change=\"onFileChange\"\n        style=\"display:none\"\n      />\n      <span class=\"check-tip\">已选择 {{ fileSelection.length }} 个</span>\n      <el-button size=\"medium\" @click=\"cancel\">取消</el-button>\n    </div>\n  </el-dialog>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Axios from \"../plugins/axios\";\nimport { formatSize } from \"../plugins/helper\";\n\nexport default {\n  model: {\n    prop: \"show\",\n    event: \"setShow\"\n  },\n  name: \"WebDAV\",\n  data() {\n    return {\n      currentPath: \"/\",\n      fileList: [],\n\n      fileSelection: []\n    };\n  },\n  props: [\"show\"],\n  computed: {\n    ...mapGetters([\"dialogWidth\", \"dialogTop\", \"dialogContentHeight\"])\n  },\n  watch: {\n    show(isVisible) {\n      if (isVisible) {\n        this.showWebdavFile(\"/\");\n      }\n    }\n  },\n  methods: {\n    formatTableField(row, column, cellValue) {\n      switch (column.property) {\n        case \"createdAt\":\n        case \"lastLoginAt\":\n        case \"lastModified\":\n          return cellValue ? new Date(cellValue).format(\"yy-MM-dd hh:mm\") : \"\";\n        case \"size\":\n          return row.isDirectory ? \"\" : formatSize(cellValue);\n        default:\n          return cellValue;\n      }\n    },\n    canImport(row) {\n      const path = row.path.toLowerCase();\n      return (\n        path.endsWith(\".txt\") || path.endsWith(\".epub\") || path.endsWith(\".umd\")\n      );\n    },\n    cancel() {\n      this.$emit(\"setShow\", false);\n    },\n    showWebdavFile(path) {\n      this.currentPath = path || \"/\";\n      Axios.get(this.api + \"/getWebdavFileList\", {\n        params: {\n          path: this.currentPath\n        }\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            res.data.data = res.data.data || [];\n            if (this.currentPath !== \"/\") {\n              const paths = this.currentPath.split(\"/\").filter(v => v);\n              paths.pop();\n              res.data.data.unshift({\n                name: \"..\",\n                isDirectory: true,\n                toParent: true,\n                path: \"/\" + paths.join(\"/\")\n              });\n            }\n            this.fileList = res.data.data;\n          }\n        },\n        error => {\n          this.$message.error(\n            \"加载 WebDAV 文件列表失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    async deleteWebdavFileList() {\n      if (!this.fileSelection.length) {\n        this.$message.error(\"请选择需要删除的文件\");\n        return;\n      }\n      const res = await this.$confirm(\"确认要删除所选择的文件吗?\", \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteWebdavFileList\", {\n        path: this.fileSelection.map(v => v.path)\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.fileSelection = [];\n            this.$message.success(\"删除文件成功\");\n            this.showWebdavFile(this.currentPath);\n          }\n        },\n        error => {\n          this.$message.error(\"删除文件失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async deleteWebdavFile(row) {\n      const res = await this.$confirm(\n        `确认要删除该${row.isDirectory ? \"文件夹\" : \"文件\"}吗?`,\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteWebdavFile\", {\n        path: row.path\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"删除文件成功\");\n            this.showWebdavFile(this.currentPath);\n          }\n        },\n        error => {\n          this.$message.error(\"删除文件失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async restoreFromWebdav(row) {\n      const res = await this.$confirm(\n        `确认要从该压缩文件恢复书源、书架、分组和RSS订阅数据吗?`,\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/restoreFromWebdav\", {\n        path: row.path\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"恢复成功\");\n            this.init(true);\n          }\n        },\n        error => {\n          this.$message.error(\"恢复失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    downloadFromWebdav(row) {\n      window.open(\n        this.api +\n          \"/getWebdavFile?path=\" +\n          escape(row.path) +\n          \"&accessToken=\" +\n          this.$store.state.token,\n        \"__blank\"\n      );\n    },\n    uploadToWebDAV() {\n      this.$refs.fileRef.dispatchEvent(new MouseEvent(\"click\"));\n    },\n    onFileChange(event) {\n      if (!event.target || !event.target.files || !event.target.files.length) {\n        return;\n      }\n      let param = new FormData();\n      for (let i = 0; i < event.target.files.length; i++) {\n        const file = event.target.files[i];\n        param.append(\"file\" + i, file);\n      }\n      param.append(\"path\", this.currentPath);\n      Axios.post(this.api + \"/uploadFileToWebdav\", param, {\n        headers: { \"Content-Type\": \"multipart/form-data\" }\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"上传文件成功\");\n            this.showWebdavFile(this.currentPath);\n          }\n        },\n        error => {\n          this.$message.error(\"上传文件失败 \" + (error && error.toString()));\n        }\n      );\n      this.$refs.fileRef.value = null;\n    },\n    async importFromWebdav(row) {\n      if (row === true) {\n        if (!this.fileSelection.length) {\n          this.$message.error(\"请选择需要加入书架的书籍\");\n          return;\n        }\n      }\n      Axios.post(this.api + \"/importFromLocalPathPreview\", {\n        path: row === true ? this.fileSelection.map(v => v.path) : [row.path],\n        webdav: true\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            if (!res.data.data || !res.data.data.length) {\n              this.$message.error(\"没有选择可导入的书籍\");\n              return;\n            }\n            // this.cancel();\n            setTimeout(() => {\n              this.$emit(\"importFromLocalPathPreview\", res.data.data);\n            }, 0);\n          }\n        },\n        error => {\n          this.$message.error(\"请求失败 \" + (error && error.toString()));\n        }\n      );\n    }\n  }\n};\n</script>\n<style lang=\"stylus\" scoped>\n.float-left {\n  float: left;\n}\n</style>\n"
  },
  {
    "path": "web/src/main.js",
    "content": "import Vue from \"vue\";\nimport App from \"./App.vue\";\nimport router from \"./router\";\nimport \"./plugins/element.js\";\nimport store from \"./plugins/vuex.js\";\nimport \"./plugins/md5.js\";\nimport { registerServiceWorker } from \"./registerServiceWorker\";\nimport noCover from \"./assets/imgs/noCover.jpeg\";\nimport noImage from \"./assets/imgs/noImage.png\";\nimport VueLazyload from \"vue-lazyload\";\nimport { jsonEncode } from \"./plugins/safe-json-stringify\";\nimport localforage from \"localforage\";\n\ntry {\n  // 设置全局错误收集\n  if (window.location.href.indexOf(\"errorAlert\") > 0) {\n    window.errorAlert = true;\n  }\n  window.onerror = function(event, source, lineno, colno, error) {\n    if (window.errorAlert) {\n      window.alert(\n        jsonEncode({\n          event: event,\n          source,\n          lineno,\n          colno,\n          error: error\n        })\n      );\n    }\n  };\n  window.addEventListener(\"unhandledrejection\", e => {\n    if (window.errorAlert) {\n      window.alert(jsonEncode(e));\n    }\n  });\n\n  Vue.config.errorHandler = e => {\n    if (window.errorAlert) {\n      window.alert(jsonEncode(e));\n    }\n  };\n\n  window.$cacheStorage = localforage.createInstance({\n    name: \"cacheStorage\"\n  });\n\n  registerServiceWorker();\n\n  Vue.config.productionTip = false;\n\n  Vue.use(VueLazyload, {\n    observer: true\n  });\n\n  Vue.mixin({\n    computed: {\n      api() {\n        return this.$store.getters.api;\n      },\n      isWebApp() {\n        return window.navigator.standalone;\n      },\n      isPWA() {\n        return [\"fullscreen\", \"standalone\", \"minimal-ui\"].some(\n          displayMode =>\n            window.matchMedia(\"(display-mode: \" + displayMode + \")\").matches\n        );\n      },\n      isNightTheme() {\n        return this.$store.getters.isNight;\n      },\n      currentUserName() {\n        return this.$store.getters.currentUserName;\n      }\n    },\n    methods: {\n      getImagePath(url, useSW) {\n        if (\n          url &&\n          (url.startsWith(\"http://\") ||\n            url.startsWith(\"https://\") ||\n            url.startsWith(\"//\"))\n        ) {\n          if (useSW && window.serviceWorkerReady) {\n            return url;\n          }\n          return this.api + \"/cover?path=\" + url;\n        }\n        if (!url) return false;\n        // 默认是接口服务器上的资源\n        return this.$store.getters.apiRoot + url;\n      },\n      getCover(coverUrl, normal, useSW) {\n        coverUrl = this.getImagePath(coverUrl, useSW);\n        if (coverUrl) {\n          return normal\n            ? coverUrl\n            : {\n                src: coverUrl,\n                error: noCover\n              };\n        }\n        return noCover;\n      },\n      getImage(imageUrl, normal, useSW) {\n        imageUrl = this.getImagePath(imageUrl, useSW);\n        if (imageUrl) {\n          return normal\n            ? imageUrl\n            : {\n                src: imageUrl,\n                error: noCover\n              };\n        }\n        return noImage;\n      }\n    }\n  });\n\n  new Vue({\n    router,\n    store,\n    render: h => h(App)\n  }).$mount(\"#app\");\n} catch (error) {\n  alert(error.stack);\n}\n"
  },
  {
    "path": "web/src/plugins/animate.js",
    "content": "if (!window.requestAnimationFrame) {\n  window.requestAnimationFrame = function(callback) {\n    return setTimeout(callback, 1000 / 60);\n  };\n}\n\n// 动画执行函数\nfunction Animate(options) {\n  var start = Date.now();\n\n  window.requestAnimationFrame(function _animate() {\n    // timeFraction 从 0 逐渐增加到 1\n    var timeFraction = (Date.now() - start) / options.duration;\n    if (timeFraction > 1) timeFraction = 1;\n\n    var progress = options.timing(timeFraction); // 动画当前进度\n    options.draw(progress); // 绘制动画\n\n    if (timeFraction < 1) {\n      window.requestAnimationFrame(_animate);\n    } else {\n      options.onEnd && options.onEnd();\n    }\n  });\n}\n\n// 时序函数\nAnimate.Timings = {\n  // 线性函数\n  linear: function(timeFraction) {\n    return timeFraction;\n  },\n  // 圆弧函数\n  circle: function(timeFraction) {\n    return 1 - Math.sin(Math.acos(timeFraction));\n  },\n  // 圆弧函数（与上一个相同）\n  circle2: function(timeFraction) {\n    return 1 - (1 - timeFraction ** 2) ** 0.5;\n  },\n  // 反-弹跳函数\n  bounce: function(timeFraction) {\n    for (var a = 0, b = 1; (a += b), (b /= 2); ) {\n      if (timeFraction >= (7 - 4 * a) / 11) {\n        return (\n          -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)\n        );\n      }\n    }\n  },\n\n  // 幂函数，x 为指数\n  power: function(x, timeFraction) {\n    return Math.pow(timeFraction, x);\n  },\n  // 反弹函数，x 为弹性系数\n  back: function(x, timeFraction) {\n    return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x);\n  },\n  // 伸缩函数，x 为初始范围\n  elastic: function(x, timeFraction) {\n    return (\n      Math.pow(2, 10 * (timeFraction - 1)) *\n      Math.cos(((20 * Math.PI * x) / 3) * timeFraction)\n    );\n  }\n};\n\n// 工具函数\nAnimate.Utils = {\n  // 接受时序函数，返回时序函数的反函数\n  makeEaseOut: function(timing) {\n    return function easeOut(timeFraction) {\n      return 1 - timing(1 - timeFraction);\n    };\n  },\n  // 接受时序函数，返回时序函数的 easeInOut 变体\n  makeEaseInOut: function(timing) {\n    return function easeInOut(timeFraction) {\n      if (timeFraction < 0.5) return timing(2 * timeFraction) / 2;\n      else return 1 - timing(2 * (1 - timeFraction)) / 2;\n    };\n  }\n};\n\nexport default Animate;\n"
  },
  {
    "path": "web/src/plugins/axios.js",
    "content": "import Axios from \"axios\";\nimport { Message, MessageBox } from \"element-ui\";\nimport { errorTypeList } from \"./config\";\nimport store from \"./vuex\";\n\nconst service = Axios.create({\n  baseURL: store.getters.api,\n  withCredentials: true,\n  timeout: 5 * 60 * 1000\n});\n\nstore.watch(\n  state => state.api,\n  () => {\n    service.defaults.baseURL = store.getters.api;\n  }\n);\n\nservice.interceptors.request.use(\n  config => config,\n  error => {\n    // console.log(error); // for debug\n    return Promise.reject(error);\n  }\n);\n\nservice.interceptors.response.use(\n  response => {\n    return response;\n  },\n  error => {\n    return Promise.reject(error);\n  }\n);\n\nlet isShowLoginTip = false;\n\nexport const request = async ({\n  url,\n  method = \"get\",\n  params = {},\n  data = {},\n  headers = {},\n  options = {},\n  alert\n}) => {\n  // post 默认显示返回的信息\n  if (alert === undefined) {\n    alert = method === \"post\";\n  }\n  if (store.state.token) {\n    params.accessToken = store.state.token;\n  }\n  if (store.state.isManagerMode && store.state.secureKey) {\n    params.secureKey = store.state.secureKey;\n    params.userNS = store.state.userNS;\n  }\n  // 防止 ie 缓存 GET 请求\n  params.v = new Date().getTime();\n  const query = {\n    url,\n    method,\n    headers,\n    params,\n    data,\n    ...options\n  };\n  const response = await service(query).catch(e => {\n    if (params.bookSourceUrl && store.state.failureIncludeTimeout) {\n      // 判断是否失效书源\n      const errorMsg = e.toString();\n      window.errorMsgList = window.errorMsgList || [];\n      window.errorMsgList.push(errorMsg);\n      if (errorMsg.indexOf(\"timeout\") >= 0) {\n        store.commit(\"addFailureBookSource\", {\n          bookSourceUrl: params.bookSourceUrl,\n          errorMsg\n        });\n      }\n    }\n    throw e;\n  });\n  if (!response) {\n    return;\n  }\n  const res = response.data;\n\n  const { isSuccess, errorMsg } = res;\n  if (!isSuccess) {\n    let result;\n    switch (res.data) {\n      case \"NEED_LOGIN\":\n        // 需要登录\n        store.commit(\"setShowLogin\", true);\n        if (!isShowLoginTip) {\n          isShowLoginTip = true;\n          setTimeout(() => {\n            errorMsg && Message.error({ message: errorMsg, duration: 2000 });\n            setTimeout(() => {\n              isShowLoginTip = false;\n            }, 2000);\n          }, 200);\n        }\n        break;\n      case \"NEED_SECURE_KEY\":\n        result = await MessageBox.prompt(\n          \"请输入管理密码后继续操作\",\n          \"操作确认\"\n        );\n        if (result && result.action === \"confirm\" && result.value) {\n          params.secureKey = result.value;\n          store.commit(\"setSecureKey\", result.value);\n          return await request({\n            url,\n            method,\n            params,\n            data,\n            headers,\n            alert\n          });\n        }\n        break;\n      default:\n        if (params.bookSourceUrl) {\n          // 判断是否失效书源\n          if (errorMsg) {\n            window.errorMsgList = window.errorMsgList || [];\n            window.errorMsgList.push(errorMsg);\n            for (let index = 0; index < errorTypeList.length; index++) {\n              if (errorMsg.indexOf(errorTypeList[index]) >= 0) {\n                store.commit(\"addFailureBookSource\", {\n                  bookSourceUrl: params.bookSourceUrl,\n                  errorMsg\n                });\n                break;\n              }\n            }\n          }\n        }\n        if (!options.silent) {\n          errorMsg && Message.error({ message: errorMsg, duration: 2000 });\n        }\n        break;\n    }\n  } else {\n    alert && errorMsg && Message.success({ message: errorMsg, duration: 1500 });\n  }\n\n  return response;\n};\n\nrequest.get = async (url, options) => {\n  options = options || {};\n  return await request({\n    url,\n    method: \"get\",\n    params: options.params || {},\n    options\n  });\n};\n\nrequest.post = async (url, data, options) => {\n  return await request({\n    url,\n    data,\n    method: \"post\",\n    options\n  });\n};\n\nexport default request;\n"
  },
  {
    "path": "web/src/plugins/cache.js",
    "content": "export const setCache = (key, value) => {\n  value = typeof value === \"string\" ? value : JSON.stringify(value);\n  window.localStorage && window.localStorage.setItem(key, value);\n};\n\nexport const getCache = (key, defaultVal = null) => {\n  let val = defaultVal;\n  try {\n    val = window.localStorage && window.localStorage.getItem(key);\n    if (val === null) {\n      return defaultVal;\n    }\n    if (val) {\n      const parseVal = JSON.parse(val);\n      if (parseVal !== null) {\n        return parseVal;\n      }\n    }\n    return val;\n  } catch (error) {\n    return val;\n  }\n};\n"
  },
  {
    "path": "web/src/plugins/chinese.js",
    "content": "let scStr =\n  \"皑蔼碍爱翱袄奥坝罢摆败颁办绊帮绑镑谤剥饱宝报鲍辈贝钡狈备惫绷笔毕毙闭边编贬变辩辫鳖瘪濒滨宾摈饼拨钵铂驳卜补参蚕残惭惨灿苍舱仓沧厕侧册测层诧搀掺蝉馋谗缠铲产阐颤场尝长偿肠厂畅钞车彻尘陈衬撑称惩诚骋痴迟驰耻齿炽冲虫宠畴踌筹绸丑橱厨锄雏础储触处传疮闯创锤纯绰辞词赐聪葱囱从丛凑窜错达带贷担单郸掸胆惮诞弹当挡党荡档捣岛祷导盗灯邓敌涤递缔点垫电淀钓调迭谍叠钉顶锭订东动栋冻斗犊独读赌镀锻断缎兑队对吨顿钝夺鹅额讹恶饿儿尔饵贰发罚阀珐矾钒烦范贩饭访纺飞废费纷坟奋愤粪丰枫锋风疯冯缝讽凤肤辐抚辅赋复负讣妇缚该钙盖干赶秆赣冈刚钢纲岗皋镐搁鸽阁铬个给龚宫巩贡钩沟构购够蛊顾剐关观馆惯贯广规硅归龟闺轨诡柜贵刽辊滚锅国过骇韩汉阂鹤贺横轰鸿红后壶护沪户哗华画划话怀坏欢环还缓换唤痪焕涣黄谎挥辉毁贿秽会烩汇讳诲绘荤浑伙获货祸击机积饥讥鸡绩缉极辑级挤几蓟剂济计记际继纪夹荚颊贾钾价驾歼监坚笺间艰缄茧检碱硷拣捡简俭减荐槛鉴践贱见键舰剑饯渐溅涧浆蒋桨奖讲酱胶浇骄娇搅铰矫侥脚饺缴绞轿较秸阶节茎惊经颈静镜径痉竞净纠厩旧驹举据锯惧剧鹃绢杰洁结诫届紧锦仅谨进晋烬尽劲荆觉决诀绝钧军骏开凯颗壳课垦恳抠库裤夸块侩宽矿旷况亏岿窥馈溃扩阔蜡腊莱来赖蓝栏拦篮阑兰澜谰揽览懒缆烂滥捞劳涝乐镭垒类泪篱离里鲤礼丽厉励砾历沥隶俩联莲连镰怜涟帘敛脸链恋炼练粮凉两辆谅疗辽镣猎临邻鳞凛赁龄铃凌灵岭领馏刘龙聋咙笼垄拢陇楼娄搂篓芦卢颅庐炉掳卤虏鲁赂禄录陆驴吕铝侣屡缕虑滤绿峦挛孪滦乱抡轮伦仑沦纶论萝罗逻锣箩骡骆络妈玛码蚂马骂吗买麦卖迈脉瞒馒蛮满谩猫锚铆贸么霉没镁门闷们锰梦谜弥觅绵缅庙灭悯闽鸣铭谬谋亩钠纳难挠脑恼闹馁腻撵捻酿鸟聂啮镊镍柠狞宁拧泞钮纽脓浓农疟诺欧鸥殴呕沤盘庞国爱赔喷鹏骗飘频贫苹凭评泼颇扑铺朴谱脐齐骑岂启气弃讫牵扦钎铅迁签谦钱钳潜浅谴堑枪呛墙蔷强抢锹桥乔侨翘窍窃钦亲轻氢倾顷请庆琼穷趋区躯驱龋颧权劝却鹊让饶扰绕热韧认纫荣绒软锐闰润洒萨鳃赛伞丧骚扫涩杀纱筛晒闪陕赡缮伤赏烧绍赊摄慑设绅审婶肾渗声绳胜圣师狮湿诗尸时蚀实识驶势释饰视试寿兽枢输书赎属术树竖数帅双谁税顺说硕烁丝饲耸怂颂讼诵擞苏诉肃虽绥岁孙损笋缩琐锁獭挞抬摊贪瘫滩坛谭谈叹汤烫涛绦腾誊锑题体屉条贴铁厅听烃铜统头图涂团颓蜕脱鸵驮驼椭洼袜弯湾顽万网韦违围为潍维苇伟伪纬谓卫温闻纹稳问瓮挝蜗涡窝呜钨乌诬无芜吴坞雾务误锡牺袭习铣戏细虾辖峡侠狭厦锨鲜纤咸贤衔闲显险现献县馅羡宪线厢镶乡详响项萧销晓啸蝎协挟携胁谐写泻谢锌衅兴汹锈绣虚嘘须许绪续轩悬选癣绚学勋询寻驯训讯逊压鸦鸭哑亚讶阉烟盐严颜阎艳厌砚彦谚验鸯杨扬疡阳痒养样瑶摇尧遥窑谣药爷页业叶医铱颐遗仪彝蚁艺亿忆义诣议谊译异绎荫阴银饮樱婴鹰应缨莹萤营荧蝇颖哟拥佣痈踊咏涌优忧邮铀犹游诱舆鱼渔娱与屿语吁御狱誉预驭鸳渊辕园员圆缘远愿约跃钥岳粤悦阅云郧匀陨运蕴酝晕韵杂灾载攒暂赞赃脏凿枣灶责择则泽贼赠扎札轧铡闸诈斋债毡盏斩辗崭栈战绽张涨帐账胀赵蛰辙锗这贞针侦诊镇阵挣睁狰帧郑证织职执纸挚掷帜质钟终种肿众诌轴皱昼骤猪诸诛烛瞩嘱贮铸筑驻专砖转赚桩庄装妆壮状锥赘坠缀谆浊兹资渍踪综总纵邹诅组钻致钟么为只凶准启板里雳余链泄\";\nlet tcStr =\n  \"皚藹礙愛翺襖奧壩罷擺敗頒辦絆幫綁鎊謗剝飽寶報鮑輩貝鋇狽備憊繃筆畢斃閉邊編貶變辯辮鼈癟瀕濱賓擯餅撥缽鉑駁蔔補參蠶殘慚慘燦蒼艙倉滄廁側冊測層詫攙摻蟬饞讒纏鏟産闡顫場嘗長償腸廠暢鈔車徹塵陳襯撐稱懲誠騁癡遲馳恥齒熾沖蟲寵疇躊籌綢醜櫥廚鋤雛礎儲觸處傳瘡闖創錘純綽辭詞賜聰蔥囪從叢湊竄錯達帶貸擔單鄲撣膽憚誕彈當擋黨蕩檔搗島禱導盜燈鄧敵滌遞締點墊電澱釣調叠諜疊釘頂錠訂東動棟凍鬥犢獨讀賭鍍鍛斷緞兌隊對噸頓鈍奪鵝額訛惡餓兒爾餌貳發罰閥琺礬釩煩範販飯訪紡飛廢費紛墳奮憤糞豐楓鋒風瘋馮縫諷鳳膚輻撫輔賦複負訃婦縛該鈣蓋幹趕稈贛岡剛鋼綱崗臯鎬擱鴿閣鉻個給龔宮鞏貢鈎溝構購夠蠱顧剮關觀館慣貫廣規矽歸龜閨軌詭櫃貴劊輥滾鍋國過駭韓漢閡鶴賀橫轟鴻紅後壺護滬戶嘩華畫劃話懷壞歡環還緩換喚瘓煥渙黃謊揮輝毀賄穢會燴彙諱誨繪葷渾夥獲貨禍擊機積饑譏雞績緝極輯級擠幾薊劑濟計記際繼紀夾莢頰賈鉀價駕殲監堅箋間艱緘繭檢堿鹼揀撿簡儉減薦檻鑒踐賤見鍵艦劍餞漸濺澗漿蔣槳獎講醬膠澆驕嬌攪鉸矯僥腳餃繳絞轎較稭階節莖驚經頸靜鏡徑痙競淨糾廄舊駒舉據鋸懼劇鵑絹傑潔結誡屆緊錦僅謹進晉燼盡勁荊覺決訣絕鈞軍駿開凱顆殼課墾懇摳庫褲誇塊儈寬礦曠況虧巋窺饋潰擴闊蠟臘萊來賴藍欄攔籃闌蘭瀾讕攬覽懶纜爛濫撈勞澇樂鐳壘類淚籬離裏鯉禮麗厲勵礫曆瀝隸倆聯蓮連鐮憐漣簾斂臉鏈戀煉練糧涼兩輛諒療遼鐐獵臨鄰鱗凜賃齡鈴淩靈嶺領餾劉龍聾嚨籠壟攏隴樓婁摟簍蘆盧顱廬爐擄鹵虜魯賂祿錄陸驢呂鋁侶屢縷慮濾綠巒攣孿灤亂掄輪倫侖淪綸論蘿羅邏鑼籮騾駱絡媽瑪碼螞馬罵嗎買麥賣邁脈瞞饅蠻滿謾貓錨鉚貿麽黴沒鎂門悶們錳夢謎彌覓綿緬廟滅憫閩鳴銘謬謀畝鈉納難撓腦惱鬧餒膩攆撚釀鳥聶齧鑷鎳檸獰甯擰濘鈕紐膿濃農瘧諾歐鷗毆嘔漚盤龐國愛賠噴鵬騙飄頻貧蘋憑評潑頗撲鋪樸譜臍齊騎豈啓氣棄訖牽扡釺鉛遷簽謙錢鉗潛淺譴塹槍嗆牆薔強搶鍬橋喬僑翹竅竊欽親輕氫傾頃請慶瓊窮趨區軀驅齲顴權勸卻鵲讓饒擾繞熱韌認紉榮絨軟銳閏潤灑薩鰓賽傘喪騷掃澀殺紗篩曬閃陝贍繕傷賞燒紹賒攝懾設紳審嬸腎滲聲繩勝聖師獅濕詩屍時蝕實識駛勢釋飾視試壽獸樞輸書贖屬術樹豎數帥雙誰稅順說碩爍絲飼聳慫頌訟誦擻蘇訴肅雖綏歲孫損筍縮瑣鎖獺撻擡攤貪癱灘壇譚談歎湯燙濤縧騰謄銻題體屜條貼鐵廳聽烴銅統頭圖塗團頹蛻脫鴕馱駝橢窪襪彎灣頑萬網韋違圍爲濰維葦偉僞緯謂衛溫聞紋穩問甕撾蝸渦窩嗚鎢烏誣無蕪吳塢霧務誤錫犧襲習銑戲細蝦轄峽俠狹廈鍁鮮纖鹹賢銜閑顯險現獻縣餡羨憲線廂鑲鄉詳響項蕭銷曉嘯蠍協挾攜脅諧寫瀉謝鋅釁興洶鏽繡虛噓須許緒續軒懸選癬絢學勳詢尋馴訓訊遜壓鴉鴨啞亞訝閹煙鹽嚴顔閻豔厭硯彥諺驗鴦楊揚瘍陽癢養樣瑤搖堯遙窯謠藥爺頁業葉醫銥頤遺儀彜蟻藝億憶義詣議誼譯異繹蔭陰銀飲櫻嬰鷹應纓瑩螢營熒蠅穎喲擁傭癰踴詠湧優憂郵鈾猶遊誘輿魚漁娛與嶼語籲禦獄譽預馭鴛淵轅園員圓緣遠願約躍鑰嶽粵悅閱雲鄖勻隕運蘊醞暈韻雜災載攢暫贊贓髒鑿棗竈責擇則澤賊贈紮劄軋鍘閘詐齋債氈盞斬輾嶄棧戰綻張漲帳賬脹趙蟄轍鍺這貞針偵診鎮陣掙睜猙幀鄭證織職執紙摯擲幟質鍾終種腫衆謅軸皺晝驟豬諸誅燭矚囑貯鑄築駐專磚轉賺樁莊裝妝壯狀錐贅墜綴諄濁茲資漬蹤綜總縱鄒詛組鑽緻鐘麼為隻兇準啟闆裡靂餘鍊洩\";\n\n// 构建映射字典\nconst scToTcMap = {};\nconst tcToScMap = {};\nfor (let i = 0; i < scStr.length; i++) {\n  scToTcMap[scStr[i]] = tcStr[i];\n  tcToScMap[tcStr[i]] = scStr[i];\n}\n\n// 释放内存\nscStr = undefined;\ntcStr = undefined;\n\nexport const traditionalized = function(orgStr) {\n  let str = \"\";\n  for (let i = 0; i < orgStr.length; i++) {\n    const char = orgStr[i];\n    if (char.charCodeAt(0) > 10000) {\n      str += scToTcMap[char] || char;\n    } else {\n      str += char;\n    }\n  }\n  return str;\n};\n\nexport const simplized = function(orgStr) {\n  let str = \"\";\n  for (let i = 0; i < orgStr.length; i++) {\n    const char = orgStr[i];\n    if (char.charCodeAt(0) > 10000) {\n      str += tcToScMap[char] || char;\n    } else {\n      str += char;\n    }\n  }\n  return str;\n};\n"
  },
  {
    "path": "web/src/plugins/config.js",
    "content": "import body_0 from \"../assets/imgs/themes/body_0.png\";\nimport content_0 from \"../assets/imgs/themes/content_0.png\";\nimport popup_0 from \"../assets/imgs/themes/popup_0.png\";\nimport body_1 from \"../assets/imgs/themes/body_1.png\";\nimport content_1 from \"../assets/imgs/themes/content_1.png\";\nimport popup_1 from \"../assets/imgs/themes/popup_1.png\";\nimport body_2 from \"../assets/imgs/themes/body_2.png\";\nimport content_2 from \"../assets/imgs/themes/content_2.png\";\nimport popup_2 from \"../assets/imgs/themes/popup_2.png\";\nimport body_3 from \"../assets/imgs/themes/body_3.png\";\nimport content_3 from \"../assets/imgs/themes/content_3.png\";\nimport popup_3 from \"../assets/imgs/themes/popup_3.png\";\nimport body_5 from \"../assets/imgs/themes/body_5.png\";\nimport content_5 from \"../assets/imgs/themes/content_5.png\";\nimport popup_5 from \"../assets/imgs/themes/popup_5.png\";\nimport body_6 from \"../assets/imgs/themes/body_6.png\";\nimport content_6 from \"../assets/imgs/themes/content_6.png\";\n// import popup_6 from \"../assets/imgs/themes/popup_6.png\";\n\nconst defaultDayConfig = {\n  configDefaultType: \"白天默认\",\n  name: \"内置白天\",\n  theme: 0,\n  font: 0,\n  chineseFont: \"简体\",\n  fontSize: 18,\n  fontWeight: 400,\n  fontColor: \"#262626\",\n  bodyColor: \"#eadfca\",\n  contentColor: \"#fff\",\n  popupColor: \"#ede7da\",\n  themeType: \"day\",\n  readMethod: \"上下滑动\",\n  clickMethod: \"自动\",\n  animateMSTime: 300, // 翻页动画时长\n  readWidth: 800,\n  lineHeight: 1.8, // 行高\n  paragraphSpace: 0.2, // 段间距\n  autoReadingMethod: \"像素滚动\",\n  autoReadingPixel: 1,\n  autoReadingLineTime: 1000,\n  pageMode: \"自适应\",\n  selectionAction: \"操作弹窗\"\n};\nconst defaultNightConfig = {\n  configDefaultType: \"黑夜默认\",\n  name: \"内置黑夜\",\n  theme: 6,\n  font: 0,\n  chineseFont: \"简体\",\n  fontSize: 18,\n  fontWeight: 400,\n  fontColor: \"#666666\",\n  bodyColor: \"#121212\",\n  contentColor: \"#171717\",\n  popupColor: \"#121212\",\n  themeType: \"night\",\n  readMethod: \"上下滑动\",\n  clickMethod: \"自动\",\n  animateMSTime: 300, // 翻页动画时长\n  readWidth: 800,\n  lineHeight: 1.8, // 行高\n  paragraphSpace: 0.2, // 段间距\n  autoReadingMethod: \"像素滚动\",\n  autoReadingPixel: 1,\n  autoReadingLineTime: 1000,\n  pageMode: \"自适应\",\n  selectionAction: \"操作弹窗\"\n};\nconst settings = {\n  shelfConfig: {\n    showBookGroup: -1\n  },\n  searchConfig: {\n    searchType: \"multi\",\n    bookSourceGroup: \"\",\n    bookSourceUrl: \"\",\n    concurrentCount: 24\n  },\n  customConfigList: [defaultDayConfig, defaultNightConfig],\n  config: {\n    ...defaultDayConfig,\n    customConfig: \"内置白天\",\n    autoTheme: true, // 自动切换主题\n    pageType: \"正常\"\n  },\n  speechVoiceConfig: {\n    voiceName: \"\",\n    speechRate: 1,\n    speechPitch: 1\n  },\n  defaultNightTheme: 6,\n  themes: [\n    {\n      body: \"url(\" + body_0 + \") repeat\",\n      content: \"url(\" + content_0 + \") repeat\",\n      popup: \"url(\" + popup_0 + \") repeat\"\n    },\n    {\n      body: \"url(\" + body_1 + \") repeat\",\n      content: \"url(\" + content_1 + \") repeat\",\n      popup: \"url(\" + popup_1 + \") repeat\"\n    },\n    {\n      body: \"url(\" + body_2 + \") repeat\",\n      content: \"url(\" + content_2 + \") repeat\",\n      popup: \"url(\" + popup_2 + \") repeat\"\n    },\n    {\n      body: \"url(\" + body_3 + \") repeat\",\n      content: \"url(\" + content_3 + \") repeat\",\n      popup: \"url(\" + popup_3 + \") repeat\"\n    },\n    {\n      body: \"#ebcece repeat\",\n      content: \"#f5e4e4 repeat\",\n      popup: \"#faeceb repeat\"\n    },\n    {\n      body: \"url(\" + body_5 + \") repeat\",\n      content: \"url(\" + content_5 + \") repeat\",\n      popup: \"url(\" + popup_5 + \") repeat\"\n    },\n    {\n      body: \"url(\" + body_6 + \") repeat\",\n      content: \"url(\" + content_6 + \") repeat\",\n      popup: \"#121212\"\n    },\n    {\n      body: \"#f7f7f7 repeat\",\n      content: \"#fff repeat\",\n      popup: \"#f7f7f7 repeat\"\n    }\n  ],\n  fonts: [\n    {\n      fontFamily: \"custom-system\"\n    },\n    // 黑体\n    {\n      // fontFamily:\n      //   '-apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif'\n      fontFamily: \"custom-ht, reader-ht\"\n    },\n    // 楷体\n    {\n      // fontFamily:\n      // 'Baskerville, Georgia, \"Liberation Serif\", \"Kaiti SC\", STKaiti, \"AR PL UKai CN\", \"AR PL UKai HK\", \"AR PL UKai TW\", \"AR PL UKai TW MBE\", \"AR PL KaitiM GB\", KaiTi, KaiTi_GB2312, DFKai-SB, \"TW-Kai\", serif',\n      fontFamily: \"custom-kt, reader-kt\"\n      // fontFamily: \"STKaiti\",\n      // \"-fx-font-family\": \"STKaiti\"\n    },\n    // 宋体\n    {\n      // fontFamily:\n      // 'Georgia, \"Nimbus Roman No9 L\", \"Songti SC\", \"Noto Serif CJK SC\", \"Source Han Serif SC\", \"Source Han Serif CN\", STSong, \"AR PL New Sung\", \"AR PL SungtiL GB\", NSimSun, SimSun, \"TW-Sung\", \"WenQuanYi Bitmap Song\", \"AR PL UMing CN\", \"AR PL UMing HK\", \"AR PL UMing TW\", \"AR PL UMing TW MBE\", PMingLiU, MingLiU, serif',\n      fontFamily: \"custom-st, reader-st\"\n      // fontFamily: \"'Source Han Serif CN'\",\n      // \"-fx-font-family\": \"'Source Han Serif CN'\"\n    },\n    // 仿宋\n    {\n      // fontFamily:\n      //   'Baskerville, \"Times New Roman\", \"Liberation Serif\", STFangsong, FangSong, FangSong_GB2312, \"CWTEX-F\", serif',\n      fontFamily: \"custom-fs, reader-fs\"\n      // fontFamily: \"STFangsong\",\n      // \"-fx-font-family\": \"STFangsong\"\n    }\n  ]\n};\nexport const errorTypeList = [\n  \"UnknownHostException\",\n  \"ConnectException: Failed to connect\",\n  \"SocketException: Connection reset\",\n  \"SSLHandshakeException\",\n  \"responseCode: 307\",\n  \"responseCode: 400\",\n  \"responseCode: 403\",\n  \"responseCode: 404\",\n  \"responseCode: 500\",\n  \"responseCode: 502\",\n  \"responseCode: 503\",\n  \"responseCode: 504\",\n  \"responseCode: 513\"\n];\nexport const defaultReplaceRule = {\n  name: \"\",\n  pattern: \"\",\n  replacement: \"\",\n  scope: \"\",\n  isRegex: false,\n  isEnabled: true\n};\nexport const defaultBookmark = {\n  bookName: \"\",\n  bookAuthor: \"\",\n  chapterIndex: 0,\n  chapterPos: 0,\n  chapterName: \"\",\n  bookText: \"\",\n  content: \"\"\n};\nexport const syncConfigFiled = Object.keys(defaultDayConfig).concat([\n  \"contentBGImg\"\n]);\nexport const customFonts = [\n  \"custom-system\",\n  \"custom-ht\",\n  \"custom-kt\",\n  \"custom-st\",\n  \"custom-fs\"\n];\nexport default settings;\n"
  },
  {
    "path": "web/src/plugins/element.js",
    "content": "import Vue from \"vue\";\nimport {\n  Button,\n  Divider,\n  MessageBox,\n  Message,\n  Breadcrumb,\n  BreadcrumbItem,\n  Table,\n  TableColumn,\n  Popover,\n  Loading,\n  Input,\n  Select,\n  Option,\n  Tag,\n  Collapse,\n  CollapseItem,\n  Dialog,\n  Checkbox,\n  CheckboxGroup,\n  ColorPicker,\n  Slider,\n  Form,\n  FormItem,\n  Switch,\n  Link,\n  RadioGroup,\n  RadioButton,\n  Pagination,\n  InputNumber,\n  Image,\n  Badge,\n  Tabs,\n  TabPane,\n  Dropdown,\n  DropdownItem,\n  DropdownMenu\n} from \"element-ui\";\n\nVue.use(Button);\nVue.use(Divider);\nVue.use(Breadcrumb);\nVue.use(BreadcrumbItem);\nVue.use(Table);\nVue.use(TableColumn);\nVue.use(Popover);\nVue.use(Input);\nVue.use(Select);\nVue.use(Option);\nVue.use(Tag);\nVue.use(Loading.directive);\nVue.use(Collapse);\nVue.use(CollapseItem);\nVue.use(Dialog);\nVue.use(Checkbox);\nVue.use(CheckboxGroup);\nVue.use(ColorPicker);\nVue.use(Slider);\nVue.use(Form);\nVue.use(FormItem);\nVue.use(Switch);\nVue.use(Link);\nVue.use(RadioGroup);\nVue.use(RadioButton);\nVue.use(Pagination);\nVue.use(InputNumber);\nVue.use(Image);\nVue.use(Badge);\nVue.use(Tabs);\nVue.use(TabPane);\nVue.use(Dropdown);\nVue.use(DropdownItem);\nVue.use(DropdownMenu);\n\nVue.prototype.$msgbox = MessageBox;\nVue.prototype.$message = Object.assign({}, Message, {\n  info(message, duration) {\n    const options = typeof message === \"string\" ? { message } : message;\n    options.duration = duration || 1000;\n    Message.info(options);\n  },\n  error(message, duration) {\n    const options = typeof message === \"string\" ? { message } : message;\n    options.duration = duration || 2000;\n    Message.error(options);\n  },\n  success(message, duration) {\n    const options = typeof message === \"string\" ? { message } : message;\n    options.duration = duration || 1000;\n    Message.success(options);\n  }\n});\nVue.prototype.$ELEMENT = {\n  zIndex: 2100\n};\nVue.prototype.$alert = MessageBox.alert;\nVue.prototype.$confirm = MessageBox.confirm;\nVue.prototype.$prompt = MessageBox.prompt;\nVue.prototype.$loading = Loading.service;\n"
  },
  {
    "path": "web/src/plugins/eventBus.js",
    "content": "import Vue from \"vue\";\nexport default new Vue();\n"
  },
  {
    "path": "web/src/plugins/helper.js",
    "content": "// import { Message } from \"element-ui\";\nimport { getCache } from \"../plugins/cache\";\n\nexport const formatSize = function(value, scale) {\n  if (value == null || value == \"\") {\n    return \"0 Bytes\";\n  }\n  var unitArr = new Array(\n    \"Bytes\",\n    \"KB\",\n    \"MB\",\n    \"GB\",\n    \"TB\",\n    \"PB\",\n    \"EB\",\n    \"ZB\",\n    \"YB\"\n  );\n  var index = 0;\n  index = Math.floor(Math.log(value) / Math.log(1024));\n  var size = value / Math.pow(1024, index);\n  size = size.toFixed(scale || 2);\n  return size + \" \" + unitArr[index];\n};\n\nexport const LimitResquest = function(limit, process) {\n  let currentSum = 0;\n  let requests = [];\n\n  async function run() {\n    let err, result;\n    try {\n      ++currentSum;\n      handler.leftCount = requests.length;\n      const fn = requests.shift();\n      result = await fn();\n    } catch (error) {\n      err = error;\n      // console.log(\"Error\", err);\n      handler.errorCount++;\n    } finally {\n      --currentSum;\n      handler.requestCount++;\n      handler.leftCount = requests.length;\n      process && process(handler, result, err);\n      if (requests.length > 0) {\n        run();\n      }\n    }\n  }\n\n  const handler = reqFn => {\n    if (!reqFn || !(reqFn instanceof Function)) {\n      return;\n    }\n    requests.push(reqFn);\n    handler.leftCount = requests.length;\n    if (currentSum < limit) {\n      run();\n    }\n  };\n\n  handler.requestCount = 0;\n  handler.leftCount = 0;\n  handler.errorCount = 0;\n  handler.cancel = () => {\n    requests = [];\n  };\n  handler.isEnd = () => {\n    return !handler.leftCount && !currentSum;\n  };\n\n  return handler;\n};\n\nexport const networkFirstRequest = async function(requestFunc, cacheKey) {\n  cacheKey = \"localCache@\" + cacheKey;\n  const res = await requestFunc().catch(() => {\n    // 请求出错，使用缓存\n    // 使用新的异步存储\n    return window.$cacheStorage\n      .getItem(cacheKey)\n      .then(cacheResponse => {\n        if (cacheResponse) {\n          return { data: cacheResponse };\n        }\n      })\n      .catch(err => {\n        // 兼容旧逻辑\n        const cacheResponse = getCache(cacheKey);\n        if (cacheResponse) {\n          return { data: cacheResponse };\n        }\n        throw err;\n      });\n  });\n  if (res.data && res.data.isSuccess) {\n    // 使用新的异步存储\n    window.$cacheStorage.setItem(cacheKey, res.data).catch(() => {});\n  }\n  return res;\n};\n\nexport const cacheFirstRequest = async function(\n  requestFunc,\n  cacheKey,\n  validateCache\n) {\n  cacheKey = \"localCache@\" + cacheKey;\n  // validateCache === true 时，直接刷新缓存\n  if (validateCache !== true) {\n    let cacheResponse = await window.$cacheStorage\n      .getItem(cacheKey)\n      .then(cacheResponse => {\n        if (cacheResponse) {\n          return cacheResponse;\n        }\n        // console.log(\"Cache not found in new cache\");\n        throw new Error(\"Cache not found\");\n      })\n      .catch(() => {\n        // 兼容旧逻辑\n        const cacheResponse = getCache(cacheKey);\n        return cacheResponse;\n      });\n    if (cacheResponse) {\n      if (!validateCache || (validateCache && validateCache(cacheResponse))) {\n        return { data: cacheResponse };\n      }\n    }\n  }\n  const res = await requestFunc();\n  if (res.data && res.data.isSuccess) {\n    // 使用新的异步存储\n    window.$cacheStorage.setItem(cacheKey, res.data).catch(() => {});\n  }\n  return res;\n};\n\nexport const isMiniInterface = () => window.innerWidth <= 750;\n\nexport const editDistance = function(strA, strB) {\n  // Levenshtein Edit Distance\n  if (strA === strB) {\n    return 1.0;\n  }\n  if (!strA || !strB) {\n    return 0.0;\n  }\n  const arr = new Array(strA.length + 1);\n  for (let i1 = 0; i1 <= strA.length; i1++) {\n    arr[i1] = new Array(strB.length + 1);\n  }\n  for (let i1 = 0; i1 <= strA.length; i1++) {\n    for (let i2 = 0; i2 <= strB.length; i2++) {\n      if (i1 === 0) {\n        arr[0][i2] = i2;\n      } else if (i2 === 0) {\n        arr[i1][0] = i1;\n      } else if (strA.charAt(i1 - 1) === strB.charAt(i2 - 1)) {\n        arr[i1][i2] = arr[i1 - 1][i2 - 1];\n      } else {\n        arr[i1][i2] =\n          1 +\n          Math.min(\n            arr[i1 - 1][i2 - 1],\n            Math.min(arr[i1][i2 - 1], arr[i1 - 1][i2])\n          );\n      }\n    }\n  }\n  return 1 - arr[strA.length][strB.length] / Math.max(strA.length, strB.length);\n};\n\nexport const loadFont = function(fontName, fontUrl) {\n  window.customFonts = window.customFonts || {};\n  if (\n    !window.customFonts[fontName] ||\n    window.customFonts[fontName] !== fontUrl\n  ) {\n    // 动态插入CSS\n    const style = document.createElement(\"style\");\n    style.textContent = `\n    @font-face {\n      font-family: \"${fontName}\";\n      src: url(\"${fontUrl}\");\n    }`;\n    style.id = \"custom-font-\" + fontName;\n    document.head.appendChild(style);\n    window.customFonts[fontName] = fontUrl;\n  }\n};\n\nexport const removeFont = function(fontName) {\n  window.customFonts = window.customFonts || {};\n  delete window.customFonts[fontName];\n  const nodeList = document.querySelectorAll(\"#custom-font-\" + fontName);\n  for (let i = 0; i < nodeList.length; i++) {\n    const node = nodeList[i];\n    node.remove();\n  }\n};\n"
  },
  {
    "path": "web/src/plugins/jump.js",
    "content": "const easeInOutQuad = (t, b, c, d) => {\n  t /= d / 2;\n  if (t < 1) return (c / 2) * t * t + b;\n  t--;\n  return (-c / 2) * (t * (t - 2) - 1) + b;\n};\n\nconst jumper = () => {\n  // private variable cache\n  // no variables are created during a jump, preventing memory leaks\n\n  let container; // container element to be scrolled       (node)\n  let element; // element to scroll to                   (node)\n\n  let start; // where scroll starts                    (px)\n  let stop; // where scroll stops                     (px)\n\n  let offset; // adjustment from the stop position      (px)\n  let easing; // easing function                        (function)\n  let a11y; // accessibility support flag             (boolean)\n\n  let distance; // distance of scroll                     (px)\n  let duration; // scroll duration                        (ms)\n\n  let timeStart; // time scroll started                    (ms)\n  let timeElapsed; // time spent scrolling thus far          (ms)\n\n  let next; // next scroll position                   (px)\n\n  let callback; // to call when done scrolling            (function)\n\n  // scroll position helper\n\n  function location() {\n    let top = container.scrollTop || container.scrollY || container.pageYOffset;\n    top = typeof top === \"undefined\" ? 0 : top;\n    return top;\n  }\n\n  // element offset helper\n\n  function top(element) {\n    const elementTop = element.getBoundingClientRect().top;\n    const containerTop = container.getBoundingClientRect\n      ? container.getBoundingClientRect().top\n      : 0;\n\n    return elementTop - containerTop + start;\n  }\n\n  // scrollTo helper\n\n  function scrollTo(top) {\n    container.scrollTo\n      ? container.scrollTo(0, top) // window\n      : (container.scrollTop = top); // custom container\n  }\n\n  // rAF loop helper\n\n  function loop(timeCurrent) {\n    // store time scroll started, if not started already\n    if (!timeStart) {\n      timeStart = timeCurrent;\n    }\n\n    // determine time spent scrolling so far\n    timeElapsed = timeCurrent - timeStart;\n\n    // calculate next scroll position\n    next = easing(timeElapsed, start, distance, duration);\n\n    // scroll to it\n    scrollTo(next);\n\n    // check progress\n    timeElapsed < duration\n      ? requestAnimationFrame(loop) // continue scroll loop\n      : done(); // scrolling is done\n  }\n\n  // scroll finished helper\n\n  function done() {\n    // account for rAF time rounding inaccuracies\n    scrollTo(start + distance);\n\n    // if scrolling to an element, and accessibility is enabled\n    if (element && a11y) {\n      // add tabindex indicating programmatic focus\n      element.setAttribute(\"tabindex\", \"-1\");\n\n      // focus the element\n      element.focus();\n    }\n\n    // if it exists, fire the callback\n    if (typeof callback === \"function\") {\n      callback();\n    }\n\n    // reset time for next jump\n    timeStart = false;\n  }\n\n  // API\n\n  function jump(target, options = {}) {\n    // resolve options, or use defaults\n    duration = options.duration || 1000;\n    offset = options.offset || 0;\n    callback = options.callback; // \"undefined\" is a suitable default, and won't be called\n    easing = options.easing || easeInOutQuad;\n    a11y = options.a11y || false;\n\n    // resolve container\n    switch (typeof options.container) {\n      case \"object\":\n        // we assume container is an HTML element (Node)\n        container = options.container;\n        break;\n\n      case \"string\":\n        container = document.querySelector(options.container);\n        break;\n\n      default:\n        container = window;\n    }\n\n    // cache starting position\n    start = location();\n\n    // resolve target\n    switch (typeof target) {\n      // scroll from current position\n      case \"number\":\n        element = undefined; // no element to scroll to\n        a11y = false; // make sure accessibility is off\n        stop = start + target;\n        break;\n\n      // scroll to element (node)\n      // bounding rect is relative to the viewport\n      case \"object\":\n        element = target;\n        stop = top(element);\n        break;\n\n      // scroll to element (selector)\n      // bounding rect is relative to the viewport\n      case \"string\":\n        element = document.querySelector(target);\n        stop = top(element);\n        break;\n    }\n\n    // resolve scroll distance, accounting for offset\n    distance = stop - start + offset;\n\n    // resolve duration\n    switch (typeof options.duration) {\n      // number in ms\n      case \"number\":\n        duration = options.duration;\n        break;\n\n      // function passed the distance of the scroll\n      case \"function\":\n        duration = options.duration(distance);\n        break;\n    }\n\n    // start the loop\n    requestAnimationFrame(loop);\n  }\n\n  // expose only the jump method\n  return jump;\n};\n\n// export singleton\n\nconst singleton = jumper();\n\nexport default singleton;\n"
  },
  {
    "path": "web/src/plugins/md5.js",
    "content": "/*\nmd5.js\n*/\nString.prototype.MD5 = function(bit) {\n  var sMessage = this;\n  function RotateLeft(lValue, iShiftBits) {\n    return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));\n  }\n  function AddUnsigned(lX, lY) {\n    var lX4, lY4, lX8, lY8, lResult;\n    lX8 = lX & 0x80000000;\n    lY8 = lY & 0x80000000;\n    lX4 = lX & 0x40000000;\n    lY4 = lY & 0x40000000;\n    lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff);\n    if (lX4 & lY4) return lResult ^ 0x80000000 ^ lX8 ^ lY8;\n    if (lX4 | lY4) {\n      if (lResult & 0x40000000) return lResult ^ 0xc0000000 ^ lX8 ^ lY8;\n      else return lResult ^ 0x40000000 ^ lX8 ^ lY8;\n    } else return lResult ^ lX8 ^ lY8;\n  }\n  function F(x, y, z) {\n    return (x & y) | (~x & z);\n  }\n  function G(x, y, z) {\n    return (x & z) | (y & ~z);\n  }\n  function H(x, y, z) {\n    return x ^ y ^ z;\n  }\n  function I(x, y, z) {\n    return y ^ (x | ~z);\n  }\n  function FF(a, b, c, d, x, s, ac) {\n    a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));\n    return AddUnsigned(RotateLeft(a, s), b);\n  }\n  function GG(a, b, c, d, x, s, ac) {\n    a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));\n    return AddUnsigned(RotateLeft(a, s), b);\n  }\n  function HH(a, b, c, d, x, s, ac) {\n    a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));\n    return AddUnsigned(RotateLeft(a, s), b);\n  }\n  function II(a, b, c, d, x, s, ac) {\n    a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));\n    return AddUnsigned(RotateLeft(a, s), b);\n  }\n  function ConvertToWordArray(sMessage) {\n    var lWordCount;\n    var lMessageLength = sMessage.length;\n    var lNumberOfWords_temp1 = lMessageLength + 8;\n    var lNumberOfWords_temp2 =\n      (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;\n    var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;\n    var lWordArray = Array(lNumberOfWords - 1);\n    var lBytePosition = 0;\n    var lByteCount = 0;\n    while (lByteCount < lMessageLength) {\n      lWordCount = (lByteCount - (lByteCount % 4)) / 4;\n      lBytePosition = (lByteCount % 4) * 8;\n      lWordArray[lWordCount] =\n        lWordArray[lWordCount] |\n        (sMessage.charCodeAt(lByteCount) << lBytePosition);\n      lByteCount++;\n    }\n    lWordCount = (lByteCount - (lByteCount % 4)) / 4;\n    lBytePosition = (lByteCount % 4) * 8;\n    lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);\n    lWordArray[lNumberOfWords - 2] = lMessageLength << 3;\n    lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;\n    return lWordArray;\n  }\n  function WordToHex(lValue) {\n    var WordToHexValue = \"\",\n      WordToHexValue_temp = \"\",\n      lByte,\n      lCount;\n    for (lCount = 0; lCount <= 3; lCount++) {\n      lByte = (lValue >>> (lCount * 8)) & 255;\n      WordToHexValue_temp = \"0\" + lByte.toString(16);\n      WordToHexValue =\n        WordToHexValue +\n        WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2);\n    }\n    return WordToHexValue;\n  }\n  var x = Array();\n  var k, AA, BB, CC, DD, a, b, c, d;\n  var S11 = 7,\n    S12 = 12,\n    S13 = 17,\n    S14 = 22;\n  var S21 = 5,\n    S22 = 9,\n    S23 = 14,\n    S24 = 20;\n  var S31 = 4,\n    S32 = 11,\n    S33 = 16,\n    S34 = 23;\n  var S41 = 6,\n    S42 = 10,\n    S43 = 15,\n    S44 = 21;\n  // Steps 1 and 2. Append padding bits and length and convert to words\n  x = ConvertToWordArray(sMessage);\n  // Step 3. Initialise\n  a = 0x67452301;\n  b = 0xefcdab89;\n  c = 0x98badcfe;\n  d = 0x10325476;\n  // Step 4. Process the message in 16-word blocks\n  for (k = 0; k < x.length; k += 16) {\n    AA = a;\n    BB = b;\n    CC = c;\n    DD = d;\n    a = FF(a, b, c, d, x[k + 0], S11, 0xd76aa478);\n    d = FF(d, a, b, c, x[k + 1], S12, 0xe8c7b756);\n    c = FF(c, d, a, b, x[k + 2], S13, 0x242070db);\n    b = FF(b, c, d, a, x[k + 3], S14, 0xc1bdceee);\n    a = FF(a, b, c, d, x[k + 4], S11, 0xf57c0faf);\n    d = FF(d, a, b, c, x[k + 5], S12, 0x4787c62a);\n    c = FF(c, d, a, b, x[k + 6], S13, 0xa8304613);\n    b = FF(b, c, d, a, x[k + 7], S14, 0xfd469501);\n    a = FF(a, b, c, d, x[k + 8], S11, 0x698098d8);\n    d = FF(d, a, b, c, x[k + 9], S12, 0x8b44f7af);\n    c = FF(c, d, a, b, x[k + 10], S13, 0xffff5bb1);\n    b = FF(b, c, d, a, x[k + 11], S14, 0x895cd7be);\n    a = FF(a, b, c, d, x[k + 12], S11, 0x6b901122);\n    d = FF(d, a, b, c, x[k + 13], S12, 0xfd987193);\n    c = FF(c, d, a, b, x[k + 14], S13, 0xa679438e);\n    b = FF(b, c, d, a, x[k + 15], S14, 0x49b40821);\n    a = GG(a, b, c, d, x[k + 1], S21, 0xf61e2562);\n    d = GG(d, a, b, c, x[k + 6], S22, 0xc040b340);\n    c = GG(c, d, a, b, x[k + 11], S23, 0x265e5a51);\n    b = GG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa);\n    a = GG(a, b, c, d, x[k + 5], S21, 0xd62f105d);\n    d = GG(d, a, b, c, x[k + 10], S22, 0x2441453);\n    c = GG(c, d, a, b, x[k + 15], S23, 0xd8a1e681);\n    b = GG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8);\n    a = GG(a, b, c, d, x[k + 9], S21, 0x21e1cde6);\n    d = GG(d, a, b, c, x[k + 14], S22, 0xc33707d6);\n    c = GG(c, d, a, b, x[k + 3], S23, 0xf4d50d87);\n    b = GG(b, c, d, a, x[k + 8], S24, 0x455a14ed);\n    a = GG(a, b, c, d, x[k + 13], S21, 0xa9e3e905);\n    d = GG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8);\n    c = GG(c, d, a, b, x[k + 7], S23, 0x676f02d9);\n    b = GG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a);\n    a = HH(a, b, c, d, x[k + 5], S31, 0xfffa3942);\n    d = HH(d, a, b, c, x[k + 8], S32, 0x8771f681);\n    c = HH(c, d, a, b, x[k + 11], S33, 0x6d9d6122);\n    b = HH(b, c, d, a, x[k + 14], S34, 0xfde5380c);\n    a = HH(a, b, c, d, x[k + 1], S31, 0xa4beea44);\n    d = HH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9);\n    c = HH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60);\n    b = HH(b, c, d, a, x[k + 10], S34, 0xbebfbc70);\n    a = HH(a, b, c, d, x[k + 13], S31, 0x289b7ec6);\n    d = HH(d, a, b, c, x[k + 0], S32, 0xeaa127fa);\n    c = HH(c, d, a, b, x[k + 3], S33, 0xd4ef3085);\n    b = HH(b, c, d, a, x[k + 6], S34, 0x4881d05);\n    a = HH(a, b, c, d, x[k + 9], S31, 0xd9d4d039);\n    d = HH(d, a, b, c, x[k + 12], S32, 0xe6db99e5);\n    c = HH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8);\n    b = HH(b, c, d, a, x[k + 2], S34, 0xc4ac5665);\n    a = II(a, b, c, d, x[k + 0], S41, 0xf4292244);\n    d = II(d, a, b, c, x[k + 7], S42, 0x432aff97);\n    c = II(c, d, a, b, x[k + 14], S43, 0xab9423a7);\n    b = II(b, c, d, a, x[k + 5], S44, 0xfc93a039);\n    a = II(a, b, c, d, x[k + 12], S41, 0x655b59c3);\n    d = II(d, a, b, c, x[k + 3], S42, 0x8f0ccc92);\n    c = II(c, d, a, b, x[k + 10], S43, 0xffeff47d);\n    b = II(b, c, d, a, x[k + 1], S44, 0x85845dd1);\n    a = II(a, b, c, d, x[k + 8], S41, 0x6fa87e4f);\n    d = II(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0);\n    c = II(c, d, a, b, x[k + 6], S43, 0xa3014314);\n    b = II(b, c, d, a, x[k + 13], S44, 0x4e0811a1);\n    a = II(a, b, c, d, x[k + 4], S41, 0xf7537e82);\n    d = II(d, a, b, c, x[k + 11], S42, 0xbd3af235);\n    c = II(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb);\n    b = II(b, c, d, a, x[k + 9], S44, 0xeb86d391);\n    a = AddUnsigned(a, AA);\n    b = AddUnsigned(b, BB);\n    c = AddUnsigned(c, CC);\n    d = AddUnsigned(d, DD);\n  }\n  if (bit == 32) {\n    return WordToHex(a) + WordToHex(b) + WordToHex(c) + WordToHex(d);\n  } else {\n    return WordToHex(b) + WordToHex(c);\n  }\n};\n"
  },
  {
    "path": "web/src/plugins/safe-json-stringify.js",
    "content": "var hasProp = Object.prototype.hasOwnProperty;\n\nfunction throwsMessage(err) {\n  return \"[Throws: \" + (err ? err.message : \"?\") + \"]\";\n}\n\nfunction safeGetValueFromPropertyOnObject(obj, property) {\n  if (hasProp.call(obj, property)) {\n    try {\n      return obj[property];\n    } catch (err) {\n      return throwsMessage(err);\n    }\n  }\n\n  return obj[property];\n}\n\nfunction ensureProperties(obj) {\n  var seen = []; // store references to objects we have seen before\n\n  function visit(obj) {\n    if (obj === null || typeof obj !== \"object\") {\n      return obj;\n    }\n\n    if (seen.indexOf(obj) !== -1) {\n      return \"[Circular]\";\n    }\n    seen.push(obj);\n\n    if (typeof obj.toJSON === \"function\") {\n      try {\n        var fResult = visit(obj.toJSON());\n        seen.pop();\n        return fResult;\n      } catch (err) {\n        return throwsMessage(err);\n      }\n    }\n\n    if (Array.isArray(obj)) {\n      var aResult = obj.map(visit);\n      seen.pop();\n      return aResult;\n    }\n\n    var result = Object.keys(obj).reduce(function(result, prop) {\n      // prevent faulty defined getter properties\n      result[prop] = visit(safeGetValueFromPropertyOnObject(obj, prop));\n      return result;\n    }, {});\n    seen.pop();\n    return result;\n  }\n\n  return visit(obj);\n}\n\nexport const jsonEncode = function(data, replacer, space) {\n  return JSON.stringify(ensureProperties(data), replacer, space);\n};\n"
  },
  {
    "path": "web/src/plugins/vuex.js",
    "content": "import Vue from \"vue\";\nimport Vuex from \"vuex\";\nimport settings, { customFonts, syncConfigFiled } from \"./config\";\nimport { setCache, getCache } from \"../plugins/cache\";\nimport { Message } from \"element-ui\";\n\nconst defaultNS = [{ username: \"默认\", userNS: \"default\" }];\nconst builtInBookGroup = [\n  { groupId: -1, groupName: \"全部\", order: -10, show: true },\n  { groupId: -2, groupName: \"本地\", order: -9, show: true },\n  { groupId: -3, groupName: \"音频\", order: -8, show: true },\n  { groupId: -4, groupName: \"未分组\", order: -7, show: true }\n];\nVue.use(Vuex);\n\nconst getCurrentUserName = state => {\n  return state.isManagerMode\n    ? state.userNS\n    : (state.userInfo || {}).username || \"default\";\n};\n\nexport default new Vuex.Store({\n  state: {\n    connected: false,\n    api: getCache(\"api_prefix\") || location.host + \"/reader3\",\n    shelfBooks: [],\n    readingBook: {},\n    config: { ...settings.config },\n    miniInterface: false,\n    windowSize: {\n      width: window.innerWidth,\n      height: window.innerHeight\n    },\n    touchable: \"ontouchstart\" in document,\n    showLogin: false,\n    loginAuth: true,\n    token: getCache(\"api_token\") || \"\",\n    bookSourceList: [],\n    isSecureMode: false,\n    isManagerMode: false,\n    secureKey: \"\",\n    userInfo: {},\n    userList: [].concat(defaultNS),\n    userNS: \"default\",\n    showManagerMode: false,\n    version: process.env.VUE_APP_BUILD_VERSION,\n    filterRules: [],\n    speechVoiceConfig: { ...settings.speechVoiceConfig },\n    safeArea: {\n      top: 0,\n      bottom: 0,\n      left: 0,\n      right: 0\n    },\n    autoPlay: false,\n    failureIncludeTimeout: false,\n    failureBookSource: [],\n    bookGroupList: [],\n    rssSourceList: [],\n    shelfConfig: { ...settings.shelfConfig },\n    showImageViewer: false,\n    previewImageIndex: 0,\n    previewImgList: [],\n    searchConfig: { ...settings.searchConfig },\n    txtTocRules: [],\n    customConfigList: [].concat(settings.customConfigList),\n    showBookInfo: {},\n    cachingBookList: [],\n    bookmarks: []\n  },\n  mutations: {\n    setShelfBooks(state, books) {\n      // 过滤一下不用的字段，省点内存\n      window.shelfBooks = books;\n      state.shelfBooks = books.map(v => {\n        return {\n          author: v.author,\n          bookUrl: v.bookUrl,\n          coverUrl: v.coverUrl,\n          tocUrl: v.tocUrl,\n          charset: v.charset,\n          customCoverUrl: v.customCoverUrl,\n          canUpdate: v.canUpdate,\n          durChapterIndex: v.durChapterIndex,\n          durChapterPos: v.durChapterPos,\n          durChapterTime: v.durChapterTime,\n          durChapterTitle: v.durChapterTitle,\n          kind: v.kind,\n          intro: v.intro,\n          lastCheckTime: v.lastCheckTime,\n          latestChapterTitle: v.latestChapterTitle,\n          name: v.name,\n          origin: v.origin,\n          originName: v.originName,\n          totalChapterNum: v.totalChapterNum,\n          type: v.type,\n          group: v.group\n        };\n      });\n    },\n    updateShelfBook(state, book) {\n      const index = state.shelfBooks.findIndex(v => v.bookUrl === book.bookUrl);\n      if (index >= 0) {\n        state.shelfBooks[index] = {\n          ...state.shelfBooks[index],\n          ...{\n            author: book.author || state.shelfBooks[index].author,\n            bookUrl: book.bookUrl || state.shelfBooks[index].bookUrl,\n            coverUrl: book.coverUrl || state.shelfBooks[index].coverUrl,\n            tocUrl: book.tocUrl || state.shelfBooks[index].tocUrl,\n            charset: book.charset || state.shelfBooks[index].charset,\n            customCoverUrl:\n              book.customCoverUrl || state.shelfBooks[index].customCoverUrl,\n            canUpdate:\n              typeof book.canUpdate === \"undefined\"\n                ? state.shelfBooks[index].canUpdate\n                : book.canUpdate,\n            durChapterIndex:\n              book.durChapterIndex || state.shelfBooks[index].durChapterIndex,\n            durChapterPos:\n              book.durChapterPos || state.shelfBooks[index].durChapterPos,\n            durChapterTime:\n              book.durChapterTime || state.shelfBooks[index].durChapterTime,\n            durChapterTitle:\n              book.durChapterTitle || state.shelfBooks[index].durChapterTitle,\n            kind: book.kind || state.shelfBooks[index].kind,\n            intro: book.intro || state.shelfBooks[index].intro,\n            lastCheckTime:\n              book.lastCheckTime || state.shelfBooks[index].lastCheckTime,\n            latestChapterTitle:\n              book.latestChapterTitle ||\n              state.shelfBooks[index].latestChapterTitle,\n            name: book.name || state.shelfBooks[index].name,\n            origin: book.origin || state.shelfBooks[index].origin,\n            originName: book.originName || state.shelfBooks[index].originName,\n            totalChapterNum:\n              book.totalChapterNum || state.shelfBooks[index].totalChapterNum,\n            type: book.type || state.shelfBooks[index].type,\n            group: book.group || state.shelfBooks[index].group\n          }\n        };\n        state.shelfBooks = [].concat(state.shelfBooks);\n      }\n    },\n    setReadingBook(state, readingBook) {\n      state.readingBook = readingBook;\n      // 更新书架信息\n      setTimeout(() => {\n        for (let i = 0; i < state.shelfBooks.length; i++) {\n          if (state.shelfBooks[i].bookUrl === readingBook.bookUrl) {\n            const title = ((readingBook.catalog || [])[readingBook.index] || {})\n              .title;\n            state.shelfBooks[i] = {\n              ...state.shelfBooks[i],\n              durChapterTime: new Date().getTime(),\n              durChapterIndex: readingBook.index,\n              ...(title\n                ? {\n                    durChapterTitle: title\n                  }\n                : {})\n            };\n            break;\n          }\n        }\n        state.shelfBooks = [].concat(state.shelfBooks);\n      }, 100);\n      // eslint-disable-next-line no-unused-vars\n      const { catalog, latestChapterTitle, intro, ...info } = readingBook;\n      setCache(\n        getCurrentUserName(state) + \"@readingRecent\",\n        JSON.stringify(info)\n      );\n    },\n    setConfig(state, config) {\n      delete config.name;\n      delete config.configDefaultType;\n      if (\n        config.theme !== settings.defaultNightTheme &&\n        config.theme !== \"custom\"\n      ) {\n        config.themeType = \"day\";\n      } else if (config.theme === settings.defaultNightTheme) {\n        config.themeType = \"night\";\n      }\n      state.config = config;\n      // 同步设置到 customConfig\n      if (config.customConfig) {\n        const index = state.customConfigList.findIndex(\n          v => v.name === config.customConfig\n        );\n        if (index >= 0) {\n          const oldCustomConfig = { ...state.customConfigList[index] };\n          syncConfigFiled.forEach(field => {\n            if (\n              typeof config[field] !== \"undefined\" &&\n              field !== \"name\" &&\n              field !== \"configDefaultType\"\n            ) {\n              oldCustomConfig[field] = config[field];\n            }\n          });\n          state.customConfigList[index] = oldCustomConfig;\n          state.customConfigList = [].concat(state.customConfigList);\n          setCache(\"customConfigList\", JSON.stringify(state.customConfigList));\n        }\n      }\n      setCache(\"config\", JSON.stringify(config));\n    },\n    setMiniInterface(state, mini) {\n      if (state.config.pageMode === \"自适应\") {\n        state.miniInterface = mini;\n      } else {\n        state.miniInterface = true;\n      }\n    },\n    setWindowSize(state, size) {\n      state.windowSize = size;\n    },\n    setTouchable(state, touchable) {\n      state.touchable = touchable;\n    },\n    setApi(state, api) {\n      state.api = api;\n    },\n    setConnected(state, connected) {\n      state.connected = connected;\n    },\n    setShowLogin(state, showLogin) {\n      state.showLogin = showLogin;\n      if (showLogin) {\n        state.loginAuth = false;\n      }\n    },\n    setLoginAuth(state, loginAuth) {\n      state.loginAuth = loginAuth;\n    },\n    setToken(state, token) {\n      state.token = token;\n      setCache(\"api_token\", token);\n    },\n    setBookSourceList(state, list) {\n      // 过滤一下不用的字段，省点内存\n      state.bookSourceList = list.map(v => {\n        return {\n          bookSourceGroup: v.bookSourceGroup,\n          bookSourceName: v.bookSourceName,\n          bookSourceType: v.bookSourceType,\n          bookSourceUrl: v.bookSourceUrl,\n          exploreUrl: v.exploreUrl\n        };\n      });\n    },\n    setUserNS(state, userNS) {\n      state.userNS = userNS;\n    },\n    setIsSecureMode(state, isSecureMode) {\n      state.isSecureMode = isSecureMode;\n    },\n    setSecureKey(state, secureKey) {\n      state.secureKey = secureKey;\n    },\n    setIsManagerMode(state, isManagerMode) {\n      state.isManagerMode = isManagerMode;\n    },\n    setShowManagerMode(state, showManagerMode) {\n      state.showManagerMode = showManagerMode;\n    },\n    setUserInfo(state, userInfo) {\n      state.userInfo = userInfo;\n    },\n    setUserList(state, userList) {\n      if (userList.length) {\n        state.userList = []\n          .concat([{ username: \"系统默认\", userNS: \"default\" }])\n          .concat(userList);\n      } else {\n        state.userList = [].concat(defaultNS);\n      }\n    },\n    setFilterRules(state, filterRules) {\n      state.filterRules = filterRules;\n      setCache(\"filterRules\", JSON.stringify(filterRules));\n    },\n    addFilterRule(state, rule) {\n      let filterRules = [].concat(state.filterRules);\n      if (typeof rule.index !== \"undefined\" && rule.index >= 0) {\n        filterRules[rule.index] = rule;\n        state.filterRules = filterRules;\n      } else {\n        filterRules = filterRules.concat([rule]);\n        state.filterRules = filterRules;\n      }\n      // setCache(\"filterRules\", JSON.stringify(filterRules));\n    },\n    setNightTheme(state, isNight) {\n      let config = { ...state.config };\n      let themeConfig;\n      if (isNight) {\n        // 设置为默认黑夜方案\n        themeConfig = state.customConfigList.find(\n          v => v.configDefaultType === \"黑夜默认\"\n        );\n      } else {\n        // 设置为默认白天方案\n        themeConfig = state.customConfigList.find(\n          v => v.configDefaultType === \"白天默认\"\n        );\n      }\n      if (!themeConfig) {\n        Message.error(\"未配置\" + (isNight ? \"黑夜默认\" : \"白天默认\") + \"方案\");\n        return;\n      }\n      config = { ...config, ...themeConfig };\n      config.customConfig = themeConfig.name;\n      // let config = { ...state.config };\n      // if (config.theme !== \"custom\") {\n      //   config.theme = parseInt(config.theme);\n      // }\n      // if (isNight) {\n      //   if (\n      //     config.theme !== settings.defaultNightTheme &&\n      //     config.themeType !== \"night\"\n      //   ) {\n      //     setCache(\"lastDayTheme\", config.theme);\n      //   }\n      //   const lastNightTheme = getCache(\"lastNightTheme\") || 6;\n      //   config.themeType = \"night\";\n      //   config.theme = lastNightTheme;\n      // } else if (\n      //   config.theme === settings.defaultNightTheme ||\n      //   config.themeType === \"night\"\n      // ) {\n      //   setCache(\"lastNightTheme\", config.theme);\n      //   const lastDayTheme = getCache(\"lastDayTheme\") || 0;\n      //   config.themeType = \"day\";\n      //   config.theme = lastDayTheme;\n      // }\n      // if (config.theme !== \"custom\") {\n      //   config.theme = parseInt(config.theme);\n      // }\n      state.config = config;\n      setCache(\"config\", JSON.stringify(config));\n    },\n    setSpeechVoiceConfig(state, voiceConfig) {\n      state.speechVoiceConfig = voiceConfig;\n      setCache(\"speechVoiceConfig\", JSON.stringify(voiceConfig));\n    },\n    setSafeArea(state, safeArea) {\n      state.safeArea = { ...state.safeArea, ...safeArea };\n    },\n    setAutoPlay(state, autoPlay) {\n      state.autoPlay = autoPlay;\n    },\n    addFailureBookSource(state, { bookSourceUrl, errorMsg }) {\n      const index = state.failureBookSource.findIndex(\n        v => v.bookSourceUrl === bookSourceUrl\n      );\n      if (index >= 0) {\n        return;\n      }\n      const bookSource = state.bookSourceList.find(\n        v => v.bookSourceUrl === bookSourceUrl\n      );\n      if (bookSource) {\n        state.failureBookSource = state.failureBookSource.concat([\n          { ...bookSource, errorMsg }\n        ]);\n      }\n    },\n    removeFailureBookSource(state, bookSourceList) {\n      for (let i = 0; i < bookSourceList.length; i++) {\n        const index = state.failureBookSource.findIndex(\n          v => v.bookSourceUrl === bookSourceList[i].bookSourceUrl\n        );\n        if (index >= 0) {\n          state.failureBookSource.splice(index, 1);\n        }\n      }\n    },\n    setFailureIncludeTimeout(state, failureIncludeTimeout) {\n      state.failureIncludeTimeout = failureIncludeTimeout;\n    },\n    setBookGroupList(state, bookGroupList) {\n      const _bookGroupList = [];\n\n      builtInBookGroup.forEach(group => {\n        if (!bookGroupList.some(v => v.groupId === group.groupId)) {\n          _bookGroupList.push(group);\n        }\n      });\n      state.bookGroupList = _bookGroupList\n        .concat(bookGroupList)\n        .sort((a, b) => a.order - b.order);\n    },\n    setRssSourceList(state, rssSources) {\n      state.rssSourceList = [].concat(rssSources);\n    },\n    setShelfConfig(state, shelfConfig) {\n      state.shelfConfig = shelfConfig;\n      setCache(\"shelfConfig\", JSON.stringify(shelfConfig));\n    },\n    setPreviewImageIndex(state, previewImageIndex) {\n      state.previewImageIndex = previewImageIndex;\n    },\n    setPreviewImgList(state, previewImgList) {\n      if (previewImgList === false) {\n        state.showImageViewer = false;\n        state.previewImgList = [];\n        state.previewImageIndex = 0;\n      } else {\n        state.previewImgList = previewImgList;\n        state.showImageViewer = true;\n      }\n    },\n    setSearchConfig(state, searchConfig) {\n      state.searchConfig = searchConfig;\n      setCache(\"searchConfig\", JSON.stringify(searchConfig));\n    },\n    setTxtTocRules(state, tocRules) {\n      state.txtTocRules = [].concat(tocRules);\n    },\n    setCustomConfigList(state, customConfigList) {\n      state.customConfigList = [].concat(customConfigList);\n      setCache(\"customConfigList\", JSON.stringify(customConfigList));\n    },\n    setShowBookInfo(state, book) {\n      state.showBookInfo = book;\n    },\n    setCachingBookList(state, cachingBookList) {\n      state.cachingBookList = [].concat(cachingBookList);\n    },\n    setBookmarks(state, bookmarks) {\n      state.bookmarks = bookmarks;\n    }\n  },\n  getters: {\n    api: state => {\n      if (\n        state.api.startsWith(\"http://\") ||\n        state.api.startsWith(\"https://\") ||\n        state.api.startsWith(\"//\")\n      ) {\n        return state.api;\n      }\n      return \"//\" + state.api;\n    },\n    apiRoot: (state, getters) => {\n      return getters.api.replace(/\\/reader3\\/?/, \"\");\n    },\n    isSlideRead: state => {\n      return state.miniInterface && state.config.readMethod === \"左右滑动\";\n    },\n    isSystemNight: state => {\n      return state.config.theme === settings.defaultNightTheme;\n    },\n    isNight: state => {\n      return state.config.themeType === \"night\";\n    },\n    isKindlePage: state => {\n      return state.config.pageType === \"Kindle\";\n    },\n    isNormalPage: state => {\n      return state.config.pageType === \"正常\";\n    },\n    currentContentBGImg: (state, getters) => {\n      if (state.config.contentBGImg) {\n        return state.config.contentBGImg.startsWith(\"bg/\") ||\n          state.config.contentBGImg.startsWith(\"http://\") ||\n          state.config.contentBGImg.startsWith(\"https://\") ||\n          state.config.contentBGImg.startsWith(\"//\")\n          ? state.config.contentBGImg\n          : getters.apiRoot + state.config.contentBGImg;\n      }\n      return undefined;\n    },\n    customCSSUrl: (_, getters) => {\n      if (getters.api) {\n        return getters.apiRoot + \"/assets/reader.css\";\n      }\n      return false;\n    },\n    currentFontFamily: state => {\n      return settings.fonts[state.config.font];\n    },\n    currentCustomFontFamily: (state, getters) => {\n      const customFontName = customFonts[state.config.font];\n      if (\n        state.config.customFontsMap &&\n        state.config.customFontsMap[customFontName]\n      ) {\n        let url = state.config.customFontsMap[customFontName];\n        return {\n          name: customFontName,\n          url:\n            url.startsWith(\"http://\") ||\n            url.startsWith(\"https://\") ||\n            url.startsWith(\"//\")\n              ? url\n              : getters.apiRoot + url\n        };\n      }\n      return null;\n    },\n    currentThemeConfig: (state, getters) => {\n      if (state.config.theme === \"custom\") {\n        return {\n          body:\n            (state.miniInterface && state.config.isNormalPage\n              ? \"linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 56px), \"\n              : \"\") + (state.config.bodyColor || \"#eadfca\"),\n          content: {\n            backgroundImage: getters.currentContentBGImg\n              ? `${\n                  state.miniInterface && state.config.isNormalPage\n                    ? \"linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 56px), \"\n                    : \"\"\n                }url(${getters.currentContentBGImg})`\n              : null,\n            backgroundPosition: \"center\",\n            backgroundRepeat: \"no-repeat\",\n            backgroundAttachment: \"fixed\",\n            backgroundColor: state.config.contentColor || \"#ede7da\",\n            backgroundSize: \"cover\"\n          },\n          popupPure: state.config.popupColor || \"#ede7da\",\n          popup:\n            (state.miniInterface && state.config.isNormalPage\n              ? \"linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 36px), \"\n              : \"\") + (state.config.popupColor || \"#ede7da\")\n        };\n      } else {\n        const config = {\n          ...settings.themes[state.config.theme]\n        };\n        config.popupPure = config.popup;\n        if (state.miniInterface && state.config.isNormalPage) {\n          config.body =\n            \"linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 36px), \" +\n            config.body;\n          config.content =\n            \"linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 36px), \" +\n            config.content;\n          config.popup =\n            \"linear-gradient(to bottom,rgba(0,0,0,.2) 0,transparent 36px), \" +\n            config.popup;\n        }\n        return config;\n      }\n    },\n    shelfBooks: state => {\n      return state.shelfBooks.sort(function(a, b) {\n        var x = a[\"durChapterTime\"] || 0;\n        var y = b[\"durChapterTime\"] || 0;\n        return y - x;\n      });\n    },\n    bookSourceGroupList: state => {\n      const groupsMap = {};\n      state.bookSourceList.forEach(v => {\n        if (v.bookSourceGroup) {\n          groupsMap[v.bookSourceGroup] = (groupsMap[v.bookSourceGroup] | 0) + 1;\n        }\n      });\n      const groups = [\n        {\n          name: \"全部分组\",\n          value: \"\",\n          count: state.bookSourceList.length\n        }\n      ];\n      for (const i in groupsMap) {\n        if (Object.hasOwnProperty.call(groupsMap, i)) {\n          groups.push({\n            name: i,\n            value: i,\n            count: groupsMap[i]\n          });\n        }\n      }\n      return groups;\n    },\n    builtInBookGroupMap: () => {\n      return builtInBookGroup.reduce((c, v) => {\n        c[v.groupId] = v.groupName;\n        return c;\n      }, {});\n    },\n    config: state => {\n      return state.config;\n    },\n    collapseMenu: state => {\n      return state.miniInterface;\n    },\n    dialogWidth: state => {\n      return state.miniInterface\n        ? \"85%\"\n        : Math.min(Math.max(state.windowSize.width * 0.7, 750), 1000) + \"px\";\n    },\n    dialogSmallWidth: state => {\n      return state.miniInterface ? \"85%\" : \"500px\";\n    },\n    dialogTop: (state, getters) => {\n      return (\n        (state.windowSize.height - getters.dialogContentHeight - 70 - 54 - 60) /\n          2 +\n        \"px\"\n      );\n    },\n    dialogContentHeight: state => {\n      if (state.miniInterface) {\n        return state.windowSize.height - 54 - 60 - 70;\n      }\n      return Math.min(0.7 * state.windowSize.height - 70 - 54 - 60, 400);\n    },\n    popupWidth: state => {\n      return state.miniInterface ? state.windowSize.width : \"600\";\n    },\n    currentUserName: state => {\n      return getCurrentUserName(state);\n    },\n    currentChapter: state => {\n      return state.readingBook && state.readingBook.catalog\n        ? state.readingBook.catalog[state.readingBook.index]\n        : {};\n    },\n    readingBook: state => {\n      return {\n        ...state.readingBook,\n        ...(state.shelfBooks.find(\n          v => v.bookUrl === state.readingBook.bookUrl\n        ) || {})\n      };\n    }\n  },\n  actions: {\n    syncFromLocalStorage({ commit, getters }) {\n      try {\n        // 获取配置\n        const config = getCache(\"config\");\n        if (config && typeof config === \"object\") {\n          commit(\"setConfig\", { ...settings.config, ...config });\n        }\n      } catch (error) {\n        //\n      }\n      try {\n        // 获取最近阅读书籍\n        const readingRecent = getCache(\n          getters.currentUserName + \"@readingRecent\"\n        );\n        if (readingRecent && typeof readingRecent === \"object\") {\n          if (typeof readingRecent.index == \"undefined\") {\n            readingRecent.index = 0;\n          }\n          commit(\"setReadingBook\", readingRecent);\n        }\n      } catch (error) {\n        //\n      }\n      // try {\n      //   // 获取过滤规则\n      //   const filterRules = getCache(\"filterRules\");\n      //   if (filterRules && Array.isArray(filterRules)) {\n      //     commit(\"setFilterRules\", filterRules);\n      //   }\n      // } catch (error) {\n      //   //\n      // }\n      try {\n        // 获取自定义配置方案\n        const customConfigList = getCache(\"customConfigList\");\n        if (customConfigList && Array.isArray(customConfigList)) {\n          commit(\"setCustomConfigList\", customConfigList);\n        }\n      } catch (error) {\n        //\n      }\n      try {\n        // 获取听书配置\n        const speechVoiceConfig = getCache(\"speechVoiceConfig\");\n        if (speechVoiceConfig && typeof speechVoiceConfig === \"object\") {\n          commit(\"setSpeechVoiceConfig\", {\n            ...settings.speechVoiceConfig,\n            ...speechVoiceConfig\n          });\n        }\n      } catch (error) {\n        //\n      }\n      try {\n        // 获取书架设置\n        const shelfConfig = getCache(\"shelfConfig\");\n        if (shelfConfig && typeof shelfConfig === \"object\") {\n          commit(\"setShelfConfig\", {\n            ...settings.shelfConfig,\n            ...shelfConfig\n          });\n        }\n      } catch (error) {\n        //\n      }\n      try {\n        // 获取搜索设置\n        const searchConfig = getCache(\"searchConfig\");\n        if (searchConfig && typeof searchConfig === \"object\") {\n          commit(\"setSearchConfig\", {\n            ...settings.searchConfig,\n            ...searchConfig\n          });\n        }\n      } catch (error) {\n        //\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "web/src/registerServiceWorker.js",
    "content": "/* eslint-disable no-console */\n\nimport { register } from \"register-service-worker\";\n\nexport function registerServiceWorker() {\n  try {\n    if (\n      process.env.NODE_ENV === \"production\" &&\n      !window.getQueryString(\"nopwa\")\n    ) {\n      register(`${process.env.BASE_URL}service-worker.js`, {\n        ready() {\n          // console.log(\n          //   \"App is being served from cache by a service worker.\\n\" +\n          //     \"For more details, visit https://goo.gl/AFskqB\"\n          // );\n          window.serviceWorkerReady = true;\n        },\n        registered(registration) {\n          // console.log(\"Service worker has been registered.\");\n          if (window.localStorage) {\n            const currentVersion = window.localStorage.getItem(\n              \"READER_APP_BUILD_VERSION\"\n            );\n            const newVersion = process.env.VUE_APP_BUILD_VERSION;\n            if (currentVersion !== newVersion) {\n              registration.active.postMessage({ type: \"SKIP_WAITING\" });\n              window.localStorage.setItem(\n                \"READER_APP_BUILD_VERSION\",\n                newVersion\n              );\n            }\n          }\n        }\n        // cached() {\n        //   console.log(\"Content has been cached for offline use.\");\n        // },\n        // updatefound() {\n        //   console.log(\"New content is downloading.\");\n        // },\n        // updated() {\n        //   console.log(\"New content is available; please refresh.\");\n        // },\n        // offline() {\n        //   console.log(\n        //     \"No internet connection found. App is running in offline mode.\"\n        //   );\n        // },\n        // error(error) {\n        //   console.error(\"Error during service worker registration:\", error);\n        // }\n      });\n    }\n  } catch (error) {\n    //\n  }\n}\n"
  },
  {
    "path": "web/src/router/index.js",
    "content": "import Vue from \"vue\";\nimport VueRouter from \"vue-router\";\n\nVue.use(VueRouter);\n\nconst originalPush = VueRouter.prototype.push;\n\nVueRouter.prototype.push = function push(location) {\n  return originalPush.call(this, location).catch(err => err);\n};\n\nconst routes = [\n  {\n    path: \"/\",\n    name: \"index\",\n    component: () =>\n      import(/* webpackChunkName: \"index\" */ \"../views/Index.vue\")\n  },\n  {\n    path: \"/reader\",\n    name: \"Reader\",\n    component: () =>\n      import(/* webpackChunkName: \"reader\" */ \"../views/Reader.vue\")\n  }\n];\n\nconst router = new VueRouter({\n  // mode: \"history\",\n  base: process.env.BASE_URL,\n  routes\n});\n\nexport default router;\n"
  },
  {
    "path": "web/src/views/Index.vue",
    "content": "<template>\n  <div\n    class=\"index-wrapper\"\n    :class=\"{\n      night: isNight,\n      day: !isNight\n    }\"\n  >\n    <div\n      class=\"navigation-wrapper\"\n      :class=\"[\n        navigationClass,\n        isWebApp && !isNight ? 'status-bar-light-bg' : ''\n      ]\"\n      :style=\"navigationStyle\"\n      @touchstart=\"handleTouchStart\"\n      @touchmove=\"handleTouchMove\"\n      @touchend=\"handleTouchEnd\"\n      v-if=\"$store.getters.isNormalPage\"\n    >\n      <div class=\"navigation-inner-wrapper\">\n        <div class=\"navigation-title\">\n          阅读\n          <span class=\"version-text\" @click=\"updateForce\">{{\n            $store.state.version\n          }}</span>\n        </div>\n        <div class=\"navigation-sub-title\">\n          清风不识字，何故乱翻书\n        </div>\n        <div class=\"search-wrapper\">\n          <el-input\n            size=\"mini\"\n            placeholder=\"搜索书籍\"\n            v-model=\"search\"\n            class=\"search-input\"\n            @keyup.enter.native=\"searchBook(1)\"\n          >\n            <i slot=\"prefix\" class=\"el-input__icon el-icon-search\"></i>\n          </el-input>\n        </div>\n        <div class=\"setting-wrapper search-setting\">\n          <div class=\"setting-title\">\n            搜索设置\n          </div>\n          <div class=\"setting-item\">\n            <el-select\n              size=\"mini\"\n              v-model=\"searchConfig.searchType\"\n              class=\"setting-select\"\n              filterable\n              placeholder=\"请选择搜索方式\"\n            >\n              <el-option\n                v-for=\"(item, index) in searchTypeList\"\n                :key=\"'search-type-' + index\"\n                :label=\"item.name\"\n                :value=\"item.value\"\n              >\n              </el-option>\n            </el-select>\n          </div>\n          <div\n            class=\"setting-item\"\n            v-show=\"searchConfig.searchType === 'single'\"\n          >\n            <el-select\n              size=\"mini\"\n              v-model=\"searchConfig.bookSourceUrl\"\n              class=\"setting-select\"\n              filterable\n              placeholder=\"请选择搜索书源\"\n            >\n              <el-option\n                v-for=\"(item, index) in bookSourceList\"\n                :key=\"'source-' + index\"\n                :label=\"item.bookSourceName\"\n                :value=\"item.bookSourceUrl\"\n              >\n              </el-option>\n            </el-select>\n          </div>\n          <div\n            class=\"setting-item\"\n            v-show=\"searchConfig.searchType !== 'single'\"\n          >\n            <el-select\n              size=\"mini\"\n              v-model=\"searchConfig.bookSourceGroup\"\n              class=\"setting-select\"\n              filterable\n              placeholder=\"请选择搜索书源分组\"\n            >\n              <el-option\n                v-for=\"(item, index) in bookSourceGroupList\"\n                :key=\"'source-group-' + index\"\n                :label=\"item.name + ' (' + item.count + ')'\"\n                :value=\"item.value\"\n              >\n              </el-option>\n            </el-select>\n          </div>\n          <div\n            class=\"setting-item\"\n            v-show=\"searchConfig.searchType !== 'single'\"\n          >\n            <el-select\n              size=\"mini\"\n              v-model=\"searchConfig.concurrentCount\"\n              class=\"setting-select\"\n              filterable\n              placeholder=\"请选择并发线程\"\n            >\n              <el-option\n                v-for=\"(item, index) in concurrentList\"\n                :key=\"'source-' + index\"\n                :label=\"item + '并发线程'\"\n                :value=\"item\"\n              >\n              </el-option>\n            </el-select>\n          </div>\n        </div>\n        <div class=\"recent-wrapper\">\n          <div class=\"recent-title\">\n            最近阅读\n          </div>\n          <div class=\"reading-recent\">\n            <el-tag\n              type=\"warning\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"recent-book\"\n              @click=\"toDetail(readingRecent)\"\n              :class=\"{ 'no-point': readingRecent.bookUrl == '' }\"\n            >\n              {{ readingRecent.name }}\n            </el-tag>\n          </div>\n        </div>\n        <div class=\"setting-wrapper\">\n          <div class=\"setting-title\">\n            后端设定\n          </div>\n          <div class=\"setting-item\">\n            <el-tag\n              :type=\"connectType\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-connect\"\n              :class=\"{ 'no-point': connecting }\"\n              @click=\"setIP\"\n            >\n              {{ connectStatus }}\n            </el-tag>\n          </div>\n        </div>\n        <div class=\"setting-wrapper\">\n          <div class=\"setting-title\">\n            书源设置\n          </div>\n          <div class=\"setting-item\">\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"showBookSourceManageDialog = true\"\n            >\n              书源管理\n            </el-tag>\n            <el-popover\n              placement=\"right\"\n              :width=\"popupWidth\"\n              trigger=\"click\"\n              :visible-arrow=\"false\"\n              v-model=\"popExploreVisible\"\n              popper-class=\"popper-component\"\n            >\n              <Explore\n                ref=\"popExplore\"\n                class=\"popup\"\n                :visible=\"popExploreVisible\"\n                :bookSourceList=\"bookSourceList\"\n                @showSearchList=\"showSearchList\"\n                @close=\"popExploreVisible = false\"\n              />\n              <el-tag\n                type=\"info\"\n                :effect=\"isNight ? 'dark' : 'light'\"\n                slot=\"reference\"\n                ref=\"exploreBtn\"\n                class=\"setting-btn\"\n                @click=\"showNavigation = false\"\n              >\n                探索书源\n              </el-tag>\n            </el-popover>\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"uploadBookSource\"\n            >\n              导入书源\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"loadRemoteBookSource\"\n            >\n              远程书源\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"showFailureBookSource()\"\n            >\n              失效书源\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"debugBookSource()\"\n            >\n              调试书源\n            </el-tag>\n            <input\n              ref=\"fileRef\"\n              type=\"file\"\n              @change=\"onSourceFileChange\"\n              style=\"display:none\"\n            />\n          </div>\n        </div>\n        <div class=\"setting-wrapper\">\n          <div class=\"setting-title\">\n            书架设置\n          </div>\n          <div class=\"setting-item\">\n            <el-tag\n              type=\"info\"\n              :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"showBookManage\"\n            >\n              书籍管理\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"showManageBookGroup\"\n            >\n              分组管理\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"importLocalBook\"\n            >\n              导入书籍\n            </el-tag>\n            <input\n              ref=\"bookRef\"\n              type=\"file\"\n              multiple=\"multiple\"\n              @change=\"onBookFileChange\"\n              style=\"display:none\"\n            />\n            <el-tag\n              type=\"info\"\n              :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"showLocalStoreManageDialog = true\"\n              v-if=\"\n                !$store.state.isSecureMode ||\n                  $store.state.userInfo.enableLocalStore\n              \"\n            >\n              浏览书仓\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"init(true)\"\n            >\n              刷新缓存\n            </el-tag>\n          </div>\n        </div>\n\n        <div class=\"setting-wrapper\">\n          <div class=\"setting-title\">\n            用户空间\n            <span\n              class=\"right-text\"\n              v-if=\"$store.state.isSecureMode && $store.state.userInfo.username\"\n              @click=\"logout()\"\n              >注销</span\n            >\n            <span\n              class=\"right-text\"\n              v-else\n              @click=\"$store.commit('setShowLogin', true)\"\n              >登录</span\n            >\n          </div>\n          <div class=\"setting-item\" v-if=\"$store.state.showManagerMode\">\n            <el-select\n              size=\"mini\"\n              v-model=\"userNS\"\n              class=\"setting-select\"\n              filterable\n              placeholder=\"请选择用户空间\"\n            >\n              <el-option\n                v-for=\"(item, index) in userList\"\n                :key=\"'source-' + index\"\n                :label=\"item.username\"\n                :value=\"item.userNS\"\n              >\n              </el-option>\n            </el-select>\n          </div>\n          <div class=\"setting-item\">\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"saveUserConfig\"\n              v-if=\"localStorageAvaliable\"\n            >\n              备份用户配置\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"restoreUserConfig\"\n              v-if=\"localStorageAvaliable\"\n            >\n              同步用户配置\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"loadUserList\"\n              v-if=\"$store.state.showManagerMode\"\n            >\n              加载用户空间\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              v-if=\"$store.state.isManagerMode\"\n              @click=\"showUserManageDialog()\"\n            >\n              管理用户空间\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              v-if=\"$store.state.isManagerMode\"\n              @click=\"exitSecureMode\"\n            >\n              退出管理模式\n            </el-tag>\n          </div>\n        </div>\n        <div\n          class=\"setting-wrapper\"\n          v-if=\"\n            !$store.state.isSecureMode || $store.state.userInfo.enableWebdav\n          \"\n        >\n          <div class=\"setting-title\">\n            WebDAV\n          </div>\n          <div class=\"setting-item\">\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"showWebDAVManageDialog = true\"\n            >\n              文件管理\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"backupToWebdav\"\n            >\n              保存备份\n            </el-tag>\n          </div>\n        </div>\n        <div class=\"setting-wrapper\">\n          <div class=\"setting-title\">\n            其它\n          </div>\n          <div class=\"setting-item\">\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"showMPCode\"\n            >\n              关注公众号【假装大佬】\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"joinTGChannel\"\n            >\n              加入TG频道【假装大佬】\n            </el-tag>\n          </div>\n        </div>\n        <div class=\"setting-wrapper\">\n          <div class=\"setting-title\">\n            本地缓存\n            <span class=\"right-text\">{{ localCacheStats.total }}</span>\n          </div>\n          <div class=\"setting-item\">\n            <el-tag\n              type=\"info\"\n              :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"clearCache('bookSourceList')\"\n            >\n              清空书源缓存\n              <span>{{ localCacheStats.bookSourceList }}</span>\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"clearCache('rssSources')\"\n            >\n              清空RSS源缓存\n              <span>{{ localCacheStats.rssSources }}</span>\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"clearCache('chapterList')\"\n            >\n              清空章节列表缓存\n              <span>{{ localCacheStats.chapterList }}</span>\n            </el-tag>\n            <el-tag\n              type=\"info\"\n              :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n              class=\"setting-btn\"\n              @click=\"clearCache('chapterContent')\"\n            >\n              清空章节内容缓存\n              <span>{{ localCacheStats.chapterContent }}</span>\n            </el-tag>\n          </div>\n        </div>\n      </div>\n      <div class=\"bottom-icons\">\n        <a href=\"https://github.com/hectorqin/reader\" target=\"_blank\">\n          <div class=\"bottom-icon\">\n            <img\n              v-if=\"isNight\"\n              :src=\"require('../assets/imgs/github.png')\"\n              alt=\"\"\n            />\n            <img v-else :src=\"require('../assets/imgs/github2.png')\" alt=\"\" />\n          </div>\n        </a>\n        <span\n          class=\"theme-item\"\n          :style=\"themeColor\"\n          ref=\"themes\"\n          @click=\"toogleNight\"\n        >\n          <i class=\"el-icon-moon\" v-if=\"!isNight\"></i>\n          <i class=\"el-icon-sunny\" v-else></i>\n        </span>\n      </div>\n    </div>\n    <div\n      class=\"shelf-wrapper\"\n      :class=\"isWebApp && !isNight ? 'status-bar-light-bg' : ''\"\n      ref=\"shelfWrapper\"\n      @click=\"showNavigation = false\"\n    >\n      <div class=\"shelf-title\">\n        <i\n          class=\"el-icon-menu\"\n          v-if=\"$store.getters.isNormalPage && collapseMenu\"\n          @click.stop=\"toggleMenu\"\n        ></i>\n        {{ isSearchResult ? (isExploreResult ? \"探索\" : \"搜索\") : \"书架\" }}\n        ({{ bookList.length }})\n        <div\n          class=\"title-btn\"\n          v-if=\"$store.getters.isNormalPage && isSearchResult\"\n          @click=\"backToShelf\"\n        >\n          书架\n        </div>\n        <div\n          class=\"title-btn\"\n          v-if=\"$store.getters.isNormalPage && isSearchResult\"\n          @click=\"loadMore\"\n        >\n          <i class=\"el-icon-loading\" v-if=\"loadingMore\"></i>\n          {{ loadingMore ? \"加载中...\" : \"加载更多\" }}\n        </div>\n        <div\n          class=\"title-btn\"\n          v-if=\"$store.getters.isNormalPage && !isSearchResult\"\n          @click=\"showBookEditButton = !showBookEditButton\"\n        >\n          {{ showBookEditButton ? \"取消\" : \"编辑\" }}\n        </div>\n        <div class=\"title-btn\" v-if=\"!isSearchResult\" @click=\"refreshShelf\">\n          <i class=\"el-icon-loading\" v-if=\"refreshLoading\"></i>\n          {{ refreshLoading ? \"刷新中...\" : \"刷新\" }}\n        </div>\n        <div\n          class=\"title-btn\"\n          v-if=\"$store.getters.isNormalPage && !isSearchResult\"\n          @click=\"showRssDialog\"\n        >\n          RSS\n        </div>\n        <div\n          class=\"title-btn\"\n          @click=\"showExplorePop\"\n          v-if=\"\n            $store.getters.isNormalPage && !(isSearchResult && !isExploreResult)\n          \"\n        >\n          书海\n        </div>\n      </div>\n      <div class=\"book-group-wrapper\" v-if=\"!isSearchResult\">\n        <el-tabs class=\"book-group-tabs\" v-model=\"showBookGroupString\" stretch>\n          <el-tab-pane\n            v-for=\"group in bookGroupDisplayList\"\n            :label=\"group.groupName\"\n            :name=\"'' + group.groupId\"\n            :key=\"'bookGroup-' + group.groupId\"\n          ></el-tab-pane>\n        </el-tabs>\n      </div>\n      <div\n        class=\"books-wrapper\"\n        ref=\"bookList\"\n        @touchstart=\"handleTouchStart\"\n        @touchmove=\"handleTouchMove\"\n        @touchend=\"handleTouchEnd\"\n        @scroll=\"scrollHandler\"\n      >\n        <div class=\"wrapper\">\n          <div\n            class=\"book\"\n            :style=\"showNavigation ? { minWidth: '360px !important' } : {}\"\n            v-for=\"book in bookList\"\n            :key=\"book.bookUrl\"\n            @click=\"toDetail(book)\"\n          >\n            <div class=\"cover-img\" @click.stop=\"showBookInfoDialog(book)\">\n              <!-- <img class=\"cover\" v-lazy=\"getCover(book.coverUrl)\" alt=\"\" /> -->\n              <el-image\n                class=\"cover\"\n                ref=\"bookCoverList\"\n                :src=\"getCover(getBookCoverUrl(book), true)\"\n                fit=\"cover\"\n                lazy\n              >\n              </el-image>\n            </div>\n            <div class=\"info\" @click=\"toDetail(book)\">\n              <div class=\"book-operation\">\n                <i\n                  class=\"el-icon-close\"\n                  v-if=\"!isSearchResult && showBookEditButton\"\n                  @click.stop=\"deleteBook(book)\"\n                ></i>\n                <i\n                  class=\"el-icon-edit\"\n                  v-if=\"!isSearchResult && showBookEditButton\"\n                  @click.stop=\"editBook(book)\"\n                ></i>\n                <i\n                  class=\"el-icon-edit\"\n                  v-if=\"isSearchResult\"\n                  @click.stop=\"editBook(book, true)\"\n                ></i>\n                <el-badge\n                  class=\"unread-num-badge\"\n                  :max=\"99\"\n                  :value=\"book.totalChapterNum - 1 - book.durChapterIndex\"\n                  v-if=\"\n                    !isSearchResult &&\n                      !showBookEditButton &&\n                      book.totalChapterNum - 1 - book.durChapterIndex > 0\n                  \"\n                />\n              </div>\n              <div\n                class=\"name\"\n                slot=\"reference\"\n                :class=\"showBookEditButton ? 'edit' : ''\"\n              >\n                {{ book.name }}\n              </div>\n              <div class=\"sub\">\n                <div class=\"author\">\n                  {{ book.author || \"\" }}\n                </div>\n                <div class=\"dot\" v-if=\"book.totalChapterNum\">•</div>\n                <div class=\"size\" v-if=\"book.totalChapterNum\">\n                  共{{ book.totalChapterNum }}章\n                </div>\n              </div>\n              <div\n                class=\"dur-chapter\"\n                v-if=\"!isSearchResult && book.durChapterTitle\"\n              >\n                已读：{{ book.durChapterTitle }}\n              </div>\n              <div class=\"last-chapter\" v-if=\"book.latestChapterTitle\">\n                {{\n                  book.lastCheckTime ? dateFormat(book.lastCheckTime) : \"最新\"\n                }}：{{ book.latestChapterTitle }}\n              </div>\n              <div v-if=\"isSearchResult\">\n                <el-tag\n                  type=\"success\"\n                  :effect=\"isNight ? 'dark' : 'light'\"\n                  class=\"setting-connect\"\n                  @click.stop=\"addBookToShelf(book)\"\n                >\n                  加入书架\n                </el-tag>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n    <el-dialog\n      :title=\"isImportRssSource ? '导入RSS源' : '导入书源'\"\n      :visible.sync=\"showImportSourceDialog\"\n      :width=\"dialogWidth\"\n      :top=\"this.collapseMenu ? '0' : '15vh'\"\n      :fullscreen=\"collapseMenu\"\n      :class=\"isWebApp && !isNight ? 'status-bar-light-bg' : ''\"\n      v-if=\"$store.getters.isNormalPage\"\n    >\n      <div class=\"source-container source-list-container\">\n        <el-checkbox-group\n          v-model=\"checkedSourceIndex\"\n          @change=\"handleCheckedSourcesChange\"\n        >\n          <el-checkbox\n            v-for=\"(source, index) in importSourceList\"\n            :label=\"index\"\n            :key=\"index\"\n            class=\"source-checkbox\"\n            >{{ isImportRssSource ? source.sourceName : source.bookSourceName }}\n            {{ isImportRssSource ? source.sourceUrl : source.bookSourceUrl }}\n            {{ getSourceTag(source) }}</el-checkbox\n          >\n        </el-checkbox-group>\n      </div>\n      <div slot=\"footer\" class=\"dialog-footer\">\n        <el-checkbox\n          :indeterminate=\"isIndeterminate\"\n          v-model=\"checkAll\"\n          @change=\"handleCheckAllChange\"\n          border\n          size=\"medium\"\n          class=\"float-left\"\n          >全选</el-checkbox\n        >\n        <span class=\"check-tip\">已选择 {{ checkedSourceIndex.length }} 个</span>\n        <el-button\n          size=\"medium\"\n          @click=\"\n            showImportSourceDialog = false;\n            checkedSourceIndex = [];\n          \"\n          >取消</el-button\n        >\n        <el-button size=\"medium\" type=\"primary\" @click=\"saveSourceList\"\n          >确定</el-button\n        >\n      </div>\n    </el-dialog>\n    <el-dialog\n      :visible.sync=\"showBookSourceManageDialog\"\n      :width=\"dialogWidth\"\n      :top=\"dialogTop\"\n      @closed=\"\n        isShowFailureBookSource = false;\n        showSourceGroup = '';\n      \"\n      :fullscreen=\"collapseMenu\"\n      :class=\"isWebApp && !isNight ? 'status-bar-light-bg-dialog' : ''\"\n      v-if=\"$store.getters.isNormalPage\"\n    >\n      <div class=\"custom-dialog-title\" slot=\"title\">\n        <span class=\"el-dialog__title\"\n          >{{ isShowFailureBookSource ? \"失效书源管理\" : \"书源管理\" }}\n          <span\n            v-if=\"!isShowFailureBookSource\"\n            class=\"float-right span-btn\"\n            @click=\"deleteAllBookSource()\"\n            >清空</span\n          >\n          <span\n            v-if=\"!isShowFailureBookSource\"\n            class=\"float-right span-btn\"\n            @click=\"deleteBookSourceFile()\"\n            >恢复默认</span\n          >\n          <span\n            v-if=\"!isShowFailureBookSource\"\n            class=\"float-right span-btn\"\n            @click=\"exportBookSource()\"\n            >导出</span\n          >\n          <span\n            v-if=\"!isShowFailureBookSource\"\n            class=\"float-right span-btn\"\n            @click=\"editBookSource(false)\"\n            >新增</span\n          >\n        </span>\n      </div>\n      <div class=\"source-container table-container\">\n        <div class=\"check-form\" v-if=\"isShowFailureBookSource\">\n          <span class=\"check-form-label\">搜索词：</span>\n          <el-input v-model=\"checkBookSourceConfig.keyword\" size=\"small\">\n          </el-input>\n          <span class=\"check-form-label\" style=\"min-width: 68px;\">\n            超时(ms)：\n          </span>\n          <el-input-number\n            v-model=\"checkBookSourceConfig.timeout\"\n            :min=\"1000\"\n            :max=\"15000\"\n            :step=\"500\"\n            size=\"small\"\n          >\n          </el-input-number>\n          <span class=\"check-form-label\">并发数：</span>\n          <el-input-number\n            v-model=\"checkBookSourceConfig.concurrent\"\n            :min=\"3\"\n            :max=\"15\"\n            :step=\"1\"\n            size=\"small\"\n          >\n          </el-input-number>\n        </div>\n        <div class=\"source-group-wrapper\">\n          <el-tag\n            type=\"info\"\n            :effect=\"$store.getters.isNight ? 'dark' : 'light'\"\n            class=\"source-group-btn\"\n            :class=\"showSourceGroup === name ? 'selected' : ''\"\n            v-for=\"name in bookSourceShowGroup\"\n            :key=\"'sourceGroup-' + name\"\n            @click=\"setShowSourceGroup(name)\"\n          >\n            {{ name }}\n          </el-tag>\n        </div>\n        <el-table\n          :data=\"bookSourceShowResultPageList\"\n          :height=\"\n            dialogContentHeight - 42 - 42 - (isShowFailureBookSource ? 32 : 0)\n          \"\n          @selection-change=\"manageSourceSelection = $event\"\n          :key=\"isShowFailureBookSource\"\n        >\n          <el-table-column\n            type=\"selection\"\n            width=\"25\"\n            :fixed=\"$store.state.miniInterface\"\n            :selectable=\"isBookSourceSelectable\"\n          >\n          </el-table-column>\n          <el-table-column\n            property=\"bookSourceName\"\n            label=\"书源名称\"\n            min-width=\"120\"\n            :fixed=\"$store.state.miniInterface\"\n          ></el-table-column>\n          <el-table-column\n            property=\"bookSourceUrl\"\n            label=\"书源链接\"\n            min-width=\"120\"\n          >\n            <template slot-scope=\"scope\">\n              <el-link\n                type=\"primary\"\n                :href=\"scope.row.bookSourceUrl\"\n                target=\"_blank\"\n                >{{ scope.row.bookSourceUrl }}</el-link\n              >\n            </template>\n          </el-table-column>\n          <el-table-column\n            property=\"errorMsg\"\n            label=\"错误信息\"\n            min-width=\"120\"\n            v-if=\"isShowFailureBookSource\"\n          ></el-table-column>\n          <el-table-column label=\"书架书籍\" min-width=\"120\">\n            <template slot-scope=\"scope\">\n              <pre>{{ showSourceBook(scope.row) }}</pre>\n            </template>\n          </el-table-column>\n          <el-table-column\n            label=\"操作\"\n            width=\"100px\"\n            v-if=\"!isShowFailureBookSource\"\n          >\n            <template slot-scope=\"scope\">\n              <el-button type=\"text\" @click=\"editBookSource(scope.row)\"\n                >编辑</el-button\n              >\n            </template>\n          </el-table-column>\n        </el-table>\n        <div class=\"source-pagination\">\n          <el-pagination\n            :current-page.sync=\"bookSourcePagination.page\"\n            :page-sizes=\"[25, 50, 100, 200, 300, 400]\"\n            :page-size.sync=\"bookSourcePagination.size\"\n            layout=\"total, sizes, prev, pager, next\"\n            :total=\"bookSourceShowLength\"\n            :pager-count=\"collapseMenu ? 5 : 7\"\n          >\n          </el-pagination>\n        </div>\n      </div>\n      <div slot=\"footer\" class=\"dialog-footer\">\n        <el-button\n          type=\"primary\"\n          class=\"float-left\"\n          size=\"medium\"\n          @click=\"deleteBookSourceList\"\n          >批量删除</el-button\n        >\n        <span class=\"check-tip\"\n          >已选择 {{ manageSourceSelection.length }} 个</span\n        >\n        <el-button\n          @click=\"checkBookSource\"\n          v-if=\"isShowFailureBookSource\"\n          size=\"medium\"\n          style=\"margin-bottom: 5px;\"\n          :disabled=\"isCheckingBookSource\"\n          >{{ isCheckingBookSource ? \"正在\" : \"\" }}检测书源\n          {{ checkBookSourceTip }}</el-button\n        >\n        <el-button @click=\"showBookSourceManageDialog = false\" size=\"medium\"\n          >取消</el-button\n        >\n      </div>\n    </el-dialog>\n\n    <el-dialog\n      :title=\"'导入本地书籍' + importMultiBookTip\"\n      :visible.sync=\"showImportBookDialog\"\n      :width=\"dialogSmallWidth\"\n      :top=\"dialogTop\"\n      @closed=\"importBookDialogClosed\"\n      :fullscreen=\"collapseMenu\"\n      :class=\"isWebApp && !isNight ? 'status-bar-light-bg-dialog' : ''\"\n      v-if=\"$store.getters.isNormalPage\"\n    >\n      <div class=\"source-container table-container\">\n        <div class=\"check-form\">\n          <div class=\"book-cover\">\n            <el-image\n              class=\"cover\"\n              :src=\"getCover(getBookCoverUrl(importBookInfo), true)\"\n              :key=\"getBookCoverUrl(importBookInfo)\"\n              fit=\"cover\"\n              lazy\n            >\n            </el-image>\n          </div>\n          <div class=\"book-info\">\n            <div>\n              <span>书名：</span>\n              <el-input v-model=\"importBookInfo.name\" size=\"small\"> </el-input>\n            </div>\n            <div>\n              <span>作者：</span>\n              <el-input v-model=\"importBookInfo.author\" size=\"small\">\n              </el-input>\n            </div>\n            <div>\n              <span>分组：</span>\n              <el-select\n                size=\"mini\"\n                v-model=\"importBookGroup\"\n                filterable\n                multiple\n                placeholder=\"未分组\"\n              >\n                <el-option\n                  v-for=\"(bookGroup, index) in bookGroupSetList\"\n                  :key=\"'bookGroup-' + index\"\n                  :label=\"bookGroup.groupName\"\n                  :value=\"bookGroup.groupId\"\n                >\n                </el-option>\n              </el-select>\n            </div>\n            <div v-if=\"isShowTocRule\">\n              <span>规则：</span>\n              <el-select\n                size=\"mini\"\n                v-model=\"importUsedTxtRule\"\n                filterable\n                placeholder=\"内置规则\"\n              >\n                <el-option\n                  v-for=\"(rule, index) in tocRuleList\"\n                  :key=\"'txtTocRule-' + index\"\n                  :label=\"rule.name\"\n                  :value=\"rule.rule\"\n                >\n                </el-option>\n              </el-select>\n              <el-button\n                class=\"toc-refresh-btn\"\n                type=\"text\"\n                @click=\"getChapterListByRule()\"\n                >刷新目录</el-button\n              >\n            </div>\n            <div v-if=\"isShowTocRule\">\n              <el-input\n                type=\"textarea\"\n                :rows=\"2\"\n                v-model=\"importBookInfo.tocUrl\"\n                size=\"small\"\n              >\n              </el-input>\n            </div>\n          </div>\n        </div>\n        <div class=\"chapter-title\">\n          章节列表({{ importBookChapters.length }})\n        </div>\n        <div\n          class=\"chapter-list\"\n          :style=\"{ maxHeight: dialogContentHeight - 40 - 35 + 'px' }\"\n        >\n          <p v-for=\"(chapter, index) in importBookChapters\" :key=\"index\">\n            {{ index + 1 }}. {{ chapter.title }}\n          </p>\n        </div>\n      </div>\n      <div slot=\"footer\" class=\"dialog-footer\">\n        <el-button\n          type=\"primary\"\n          size=\"medium\"\n          @click=\"saveBook(importBookInfo, true)\"\n          >确定导入</el-button\n        >\n        <el-button size=\"medium\" @click=\"showImportBookDialog = false\"\n          >取消</el-button\n        >\n      </div>\n    </el-dialog>\n\n    <LocalStore\n      v-model=\"showLocalStoreManageDialog\"\n      @importFromLocalPathPreview=\"importMultiBooks\"\n    ></LocalStore>\n\n    <WebDAV\n      v-model=\"showWebDAVManageDialog\"\n      @importFromLocalPathPreview=\"importMultiBooks\"\n    ></WebDAV>\n  </div>\n</template>\n\n<script>\nimport { mapGetters } from \"vuex\";\nimport Explore from \"../components/Explore.vue\";\nimport LocalStore from \"../components/LocalStore.vue\";\nimport WebDAV from \"../components/WebDAV.vue\";\nimport Axios from \"../plugins/axios\";\nimport { errorTypeList } from \"../plugins/config\";\nimport { setCache, getCache } from \"../plugins/cache\";\nimport eventBus from \"../plugins/eventBus\";\nimport { formatSize, LimitResquest } from \"../plugins/helper\";\nconst buildURL = require(\"axios/lib/helpers/buildURL\");\nimport { isInContainer } from \"element-ui/src/utils/dom\";\nimport Vue from \"vue\";\n\nexport default {\n  components: {\n    Explore,\n    LocalStore,\n    WebDAV\n  },\n  data() {\n    return {\n      search: \"\",\n      searchTypeList: [\n        { name: \"单源搜索\", value: \"single\" },\n        { name: \"多源搜索(过滤书名/作者名)\", value: \"multi\" }\n      ],\n      isSearchResult: false,\n      isExploreResult: false,\n      searchResult: [],\n      searchPage: 1,\n      refreshLoading: false,\n      searchLastIndex: -1,\n\n      showBookEditButton: false,\n\n      popExploreVisible: false,\n      loadingMore: false,\n\n      importSourceList: [],\n      showImportSourceDialog: false,\n      isImportRssSource: false,\n      checkAll: false,\n      isIndeterminate: false,\n      checkedSourceIndex: [],\n\n      showBookSourceManageDialog: false,\n      manageSourceSelection: [],\n      isShowFailureBookSource: false,\n      checkBookSourceTip: \"\",\n      isCheckingBookSource: false,\n\n      showNavigation: false,\n\n      navigationClass: \"\",\n      navigationStyle: {},\n\n      popIntroVisible: {},\n\n      connecting: false,\n\n      lastScrollTop: 0,\n\n      localStorageAvaliable:\n        window.localStorage &&\n        window.localStorage.getItem &&\n        window.localStorage.setItem,\n\n      showSourceGroup: \"\",\n      bookSourcePagination: {\n        page: 1,\n        size: 25\n      },\n      checkBookSourceConfig: {\n        keyword: \"斗罗大陆\",\n        timeout: 5000,\n        concurrent: 5\n      },\n      importBookInfo: {},\n      importBookGroup: [],\n      importBookChapters: [],\n      showImportBookDialog: false,\n\n      importMultiBookTip: \"\",\n\n      rssSource: {},\n\n      concurrentList: [12, 18, 24, 30, 36, 42, 48, 54, 60],\n\n      localCacheStats: {\n        total: \"0 Bytes\",\n        bookSourceList: \"0 Bytes\",\n        rssSources: \"0 Bytes\",\n        chapterList: \"0 Bytes\",\n        chapterContent: \"0 Bytes\"\n      },\n\n      showLocalStoreManageDialog: false,\n\n      showWebDAVManageDialog: false,\n\n      importUsedTxtRule: \"\",\n\n      showAddUser: false,\n      addUserForm: {\n        username: \"\",\n        password: \"\"\n      }\n    };\n  },\n  watch: {\n    searchConfig: {\n      handler(val) {\n        this.$store.commit(\"setSearchConfig\", val);\n        if (this.isSearchResult) {\n          this.searchBook(1);\n        }\n      },\n      deep: true\n    },\n    searchResult(val) {\n      if (this.isSearchResult && val.length) {\n        this.$nextTick(() => {\n          this.$refs.bookList.scrollTop = this.lastScrollTop;\n        });\n      }\n    },\n    collapseMenu(val) {\n      if (!val) {\n        this.navigationClass = \"\";\n      } else if (!this.showNavigation) {\n        this.navigationClass = \"navigation-hidden\";\n      }\n    },\n    showNavigation(val) {\n      if (!val) {\n        this.navigationClass = \"navigation-out\";\n        setTimeout(() => {\n          this.navigationClass = \"navigation-hidden\";\n        }, 300);\n      } else {\n        this.navigationClass = \"navigation-in\";\n      }\n    },\n    loginAuth() {\n      this.init(true);\n    },\n    userNS() {\n      this.init(true);\n    },\n    importUsedTxtRule(val) {\n      if (val) {\n        this.importBookInfo.tocUrl = val;\n      }\n    },\n    importBookGroup(val) {\n      if (val && this.showImportBookDialog) {\n        let groupId = 0;\n        val.forEach(v => {\n          groupId |= v;\n        });\n        this.importBookInfo.group = groupId;\n      }\n    },\n    showBookGroup() {\n      this.$nextTick(() => {\n        // 手动处理 el-image 图片加载\n        setTimeout(this.ensureLoadBookCover);\n      });\n    }\n  },\n  mounted() {\n    document.title = \"阅读\";\n    this.navigationClass =\n      this.collapseMenu && !this.showNavigation ? \"navigation-hidden\" : \"\";\n    window.shelfPage = this;\n    this.init();\n    eventBus.$on(\"onSourceFileChange\", (event, isRssSource) => {\n      if (this._inactive) {\n        return;\n      }\n      this.onSourceFileChange(event, isRssSource);\n    });\n    eventBus.$on(\"editBook\", (book, isAdd, onSuccess) => {\n      if (this._inactive) {\n        return;\n      }\n      this.editBook(book, isAdd, onSuccess);\n    });\n  },\n  activated() {\n    document.title = \"阅读\";\n    this.scanCacheStorage();\n  },\n  methods: {\n    init(refresh) {\n      this.$root.$children[0].init(refresh);\n    },\n    setIP() {\n      this.$prompt(\"请输入接口地址 ( 如：localhost:8080/reader3 )\", \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        inputValue: this.api,\n        // inputPattern: /^((2[0-4]\\d|25[0-5]|[1]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[1]?\\d\\d?):([1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-9][0-9][0-9][0-9]|[1-6][0-5][0-5][0-3][0-5])$/,\n        // inputErrorMessage: \"url 形式不正确\",\n        beforeClose: (action, instance, done) => {\n          if (action === \"confirm\") {\n            this.connecting = true;\n            instance.confirmButtonLoading = true;\n            instance.confirmButtonText = \"校验中……\";\n            var inputUrl = instance.inputValue.replace(/\\/*$/g, \"\");\n            this.loadBookshelf(inputUrl)\n              .then(() => {\n                this.connecting = false;\n                instance.confirmButtonLoading = false;\n                done();\n                setCache(\"api_prefix\", inputUrl);\n                this.$store.commit(\"setApi\", inputUrl);\n                // 初始化\n                this.init();\n              })\n              .catch(() => {\n                instance.confirmButtonLoading = false;\n                instance.confirmButtonText = \"确定\";\n              });\n          } else {\n            done();\n          }\n        }\n      })\n        .then(({ value }) => {\n          this.$message({\n            type: \"success\",\n            message: \"与\" + value + \"连接成功\"\n          });\n        })\n        .catch(() => {});\n    },\n    loadBookshelf(api, refresh) {\n      api = api || this.api;\n      if (!api) {\n        this.$message.error(\"请先设置后端接口地址\");\n        this.$store.commit(\"setConnected\", false);\n        return Promise.reject(false);\n      }\n\n      if (!this.loading || !this.loading.visible) {\n        this.loading = this.$loading({\n          target: this.$refs.bookList,\n          lock: true,\n          text: refresh ? \"正在刷新书籍信息\" : \"正在获取书籍信息\",\n          spinner: \"el-icon-loading\",\n          background: this.isNight ? \"#222\" : \"#fff\"\n        });\n      }\n\n      if (\n        !api.startsWith(\"http://\") &&\n        !api.startsWith(\"https://\") &&\n        !api.startsWith(\"//\")\n      ) {\n        api = \"//\" + api;\n      }\n\n      return this.$root.$children[0].loadBookShelf(refresh, api).then(() => {\n        this.loading.close();\n      });\n    },\n    refreshShelf() {\n      return this.loadBookshelf(null, true);\n    },\n    loadBookGroup(refresh) {\n      return this.$root.$children[0].loadBookGroup(refresh);\n    },\n    loadBookSource(refresh) {\n      return this.$root.$children[0].loadBookSource(refresh);\n    },\n    searchBook(page) {\n      if (!this.$store.state.connected) {\n        this.$message.error(\"后端未连接\");\n        return;\n      }\n      if (!this.search) {\n        this.$message.error(\"请输入关键词进行搜索\");\n        return;\n      }\n      if (\n        this.searchConfig.searchType === \"single\" &&\n        !this.searchConfig.bookSourceUrl\n      ) {\n        this.$message.error(\"请选择书源进行搜索\");\n        return;\n      }\n      if (page) {\n        this.searchPage = page;\n      }\n      page = this.searchPage;\n      if (page === 1) {\n        // 重新搜索\n        this.searchLastIndex = -1;\n      }\n      if (this.searchConfig.searchType === \"multi\" && window.EventSource) {\n        this.searchBookByEventStream(page);\n        return;\n      }\n      if (this.loadingMore) {\n        return;\n      }\n      this.isSearchResult = true;\n      this.isExploreResult = false;\n      this.loadingMore = true;\n      if (page === 1) {\n        this.searchResult = [];\n      }\n      Axios.post(\n        this.api +\n          (this.searchConfig.searchType === \"single\"\n            ? \"/searchBook\"\n            : \"/searchBookMulti\"),\n        {\n          key: this.search,\n          bookSourceUrl: this.searchConfig.bookSourceUrl,\n          bookSourceGroup: this.searchConfig.bookSourceGroup,\n          concurrentCount: this.searchConfig.concurrentCount,\n          lastIndex: this.searchLastIndex, // 多源搜索时的索引\n          page: page // 单源搜索时的page\n        },\n        {\n          timeout: this.searchConfig.searchType === \"single\" ? 30000 : 180000\n        }\n      ).then(\n        res => {\n          this.loadingMore = false;\n          if (res.data.isSuccess) {\n            //\n            let resultList = [];\n            if (this.searchConfig.searchType === \"single\") {\n              resultList = res.data.data;\n            } else {\n              this.searchLastIndex = res.data.data.lastIndex;\n              resultList = res.data.data.list;\n            }\n            var data = [].concat(this.searchResult);\n            var length = data.length;\n            resultList.forEach(v => {\n              if (!this.searchResultMap[v.bookUrl]) {\n                data.push(v);\n              }\n            });\n            this.searchResult = data;\n            if (data.length === length) {\n              this.$message.error(\"没有更多啦\");\n            }\n          }\n        },\n        error => {\n          this.$message.error(\"搜索书籍失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    searchBookByEventStream(page) {\n      const tryClose = () => {\n        try {\n          if (\n            this.searchEventSource &&\n            this.searchEventSource.readyState != this.searchEventSource.CLOSED\n          ) {\n            this.searchEventSource.close();\n          }\n          this.searchEventSource = null;\n        } catch (error) {\n          //\n        }\n      };\n      if (this.loadingMore) {\n        tryClose();\n        this.loadingMore = false;\n        // page === 1 是重新搜索\n        if (page !== 1) {\n          // 停止搜索\n          return;\n        }\n      }\n      const params = {\n        accessToken: this.$store.state.token,\n        key: this.search,\n        bookSourceUrl: this.searchConfig.bookSourceUrl,\n        bookSourceGroup: this.searchConfig.bookSourceGroup,\n        concurrentCount: this.searchConfig.concurrentCount,\n        lastIndex: this.searchLastIndex, // 多源搜索时的索引\n        page: page // 单源搜索时的page\n      };\n\n      this.isSearchResult = true;\n      this.isExploreResult = false;\n      this.loadingMore = true;\n      if (page === 1) {\n        this.searchResult = [];\n      }\n      const url = buildURL(this.api + \"/searchBookMultiSSE\", params);\n\n      tryClose();\n\n      this.searchEventSource = new EventSource(url, {\n        withCredentials: true\n      });\n      this.searchEventSource.addEventListener(\"error\", e => {\n        this.loadingMore = false;\n        tryClose();\n        try {\n          if (e.data) {\n            const result = JSON.parse(e.data);\n            if (result && result.errorMsg) {\n              this.$message.error(result.errorMsg);\n            }\n          }\n        } catch (error) {\n          //\n        }\n      });\n      let oldSearchResultLength = this.searchResult.length;\n      this.searchEventSource.addEventListener(\"end\", e => {\n        this.loadingMore = false;\n        tryClose();\n        try {\n          if (e.data) {\n            const result = JSON.parse(e.data);\n            if (result && result.lastIndex) {\n              this.searchLastIndex = result.lastIndex;\n            }\n          }\n          if (this.searchResult.length === oldSearchResultLength) {\n            this.$message.error(\"没有更多啦\");\n          }\n        } catch (error) {\n          //\n        }\n      });\n      this.searchEventSource.addEventListener(\"message\", e => {\n        try {\n          if (e.data) {\n            const result = JSON.parse(e.data);\n            if (result && result.lastIndex) {\n              this.searchLastIndex = result.lastIndex;\n            }\n            if (result.data) {\n              var data = [].concat(this.searchResult);\n              result.data.forEach(v => {\n                if (!this.searchResultMap[v.bookUrl]) {\n                  data.push(v);\n                }\n              });\n              this.searchResult = data;\n            }\n          }\n        } catch (error) {\n          //\n        }\n      });\n    },\n    toDetail(book) {\n      if (!book.bookUrl) {\n        return;\n      }\n      if (this.isSearchResult) {\n        // this.$message.error(\"请先加入书架\");\n        // return;\n      }\n      this.$store.commit(\"setReadingBook\", {\n        name: book.name,\n        bookUrl: book.bookUrl,\n        index: book.index ?? book.durChapterIndex ?? 0,\n        type: book.type,\n        coverUrl: this.getBookCoverUrl(book),\n        tocUrl: book.tocUrl,\n        author: book.author,\n        origin: book.origin,\n        originName: book.originName,\n        latestChapterTitle: book.latestChapterTitle,\n        intro: book.intro\n      });\n      this.$router.push({\n        path: \"/reader\" + (this.isSearchResult ? \"?search=1\" : \"\")\n      });\n    },\n    async addBookToShelf(book) {\n      const customImportBookInfo = await this.customImportBookInfo({\n        title: \"设置分组\",\n        cancelButtonText: \"暂不加入\"\n      });\n      if (customImportBookInfo === false) {\n        return;\n      }\n      this.saveBook({ ...book, ...customImportBookInfo });\n    },\n    saveBook(book, isImport, isEdit) {\n      if (!book || !book.bookUrl || !book.origin) {\n        this.$message.error(\"书籍信息错误\");\n        return Promise.reject(false);\n      }\n      return Axios.post(this.api + \"/saveBook\", book).then(\n        res => {\n          if (res.data.isSuccess) {\n            //\n            if (isImport) {\n              this.showImportBookDialog = false;\n            }\n            this.$message.success(\n              isImport\n                ? \"导入书籍成功\"\n                : isEdit\n                ? \"修改书籍成功\"\n                : \"加入书架成功\"\n            );\n            if (!isEdit) {\n              this.loadBookshelf();\n            } else {\n              this.$store.commit(\"updateShelfBook\", res.data.data);\n            }\n            return res.data.data;\n          }\n        },\n        error => {\n          this.$message.error(\n            (isImport\n              ? \"导入书籍失败\"\n              : isEdit\n              ? \"修改书籍失败\"\n              : \"加入书架失败 \") + (error && error.toString())\n          );\n        }\n      );\n    },\n    async deleteBook(book) {\n      if (!book || (!book.name && !book.bookUrl)) {\n        this.$message.error(\"书籍信息错误\");\n        return;\n      }\n      const res = await this.$confirm(\n        \"此操作将删除书籍信息以及阅读进度, 是否继续?\",\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteBook\", book).then(\n        res => {\n          if (res.data.isSuccess) {\n            //\n            this.$message.success(\"删除成功\");\n            this.loadBookshelf();\n          }\n        },\n        error => {\n          this.$message.error(\"删除失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    editBook(book, isAdd, onSuccess) {\n      if (!book || !book.name || !book.bookUrl || !book.origin) {\n        this.$message.error(\"书籍信息错误\");\n        return;\n      }\n      const bookInfo = { ...book };\n      delete bookInfo[\"variableMap$delegate\"];\n      eventBus.$emit(\n        \"showEditor\",\n        isAdd ? \"保存书籍\" : \"编辑书籍\",\n        JSON.stringify(bookInfo, null, 4),\n        async (content, close) => {\n          try {\n            const newBook = JSON.parse(content);\n            if (!newBook.name) {\n              this.$message.error(\"书籍名称不能为空\");\n              return;\n            }\n            if (!newBook.bookUrl) {\n              this.$message.error(\"书籍链接不能为空\");\n              return;\n            }\n            if (!newBook.origin) {\n              this.$message.error(\"书籍来源不能为空\");\n              return;\n            }\n            if (isAdd) {\n              const res = await this.$confirm(\n                \"加入书架之后才能编辑书籍信息, 是否加入书架?\",\n                \"提示\",\n                {\n                  confirmButtonText: \"确定\",\n                  cancelButtonText: \"取消\",\n                  type: \"warning\"\n                }\n              ).catch(() => {\n                return false;\n              });\n              if (!res) {\n                return;\n              }\n            }\n            this.saveBook(newBook, false, true).then(() => {\n              close();\n              if (onSuccess) {\n                onSuccess();\n              }\n            });\n          } catch (e) {\n            this.$message.error(\"书籍信息必须是JSON格式\");\n          }\n        }\n      );\n    },\n    currentDateTime() {\n      const now = new Date();\n      const pad = a => (a < 10 ? \"0\" + a : a);\n      return (\n        now.getFullYear() +\n        pad(now.getMonth() + 1) +\n        pad(now.getDate()) +\n        \"_\" +\n        pad(now.getHours()) +\n        pad(now.getMinutes()) +\n        pad(now.getSeconds())\n      );\n    },\n    dateFormat(t) {\n      let time = new Date().getTime();\n      let int = parseInt((time - t) / 1000);\n      let str = \"\";\n\n      if (int <= 30) {\n        str = \"刚刚\";\n      } else if (int < 60) {\n        str = int + \"秒前\";\n      } else if (int < 3600) {\n        str = parseInt(int / 60) + \"分钟前\";\n      } else if (int < 86400) {\n        str = parseInt(int / 3600) + \"小时前\";\n      } else if (int < 2592000) {\n        str = parseInt(int / 86400) + \"天前\";\n      } else if (int < 31536000) {\n        str = parseInt(int / 2592000) + \"月前\";\n      } else {\n        str = parseInt(int / 31536000) + \"年前\";\n      }\n      return str;\n    },\n    backToShelf() {\n      this.isSearchResult = false;\n      this.isExploreResult = false;\n      this.searchResult = [];\n      this.loadingMore = false;\n    },\n    toogleNight() {\n      if (this.isNight) {\n        this.$store.commit(\"setNightTheme\", false);\n      } else {\n        this.$store.commit(\"setNightTheme\", true);\n      }\n    },\n    showSearchList(data) {\n      this.isSearchResult = true;\n      this.isExploreResult = true;\n      this.loadingMore = false;\n      this.searchResult = data;\n    },\n    loadMore() {\n      this.lastScrollTop = this.$refs.bookList.scrollTop;\n      if (this.isExploreResult) {\n        this.loadingMore = true;\n        this.$refs.popExplore.loadMore();\n      } else {\n        this.searchBook(this.searchPage + 1);\n      }\n    },\n    uploadBookSource() {\n      this.$refs.fileRef.dispatchEvent(new MouseEvent(\"click\"));\n    },\n    onSourceFileChange(event, isRssSource) {\n      const rawFile = event.target.files && event.target.files[0];\n      // console.log(\"rawFile\", rawFile);\n      const reader = new FileReader();\n      const sourceTypeName = isRssSource ? \"RSS源\" : \"书源\";\n      reader.onload = e => {\n        const data = e.target.result;\n        try {\n          const sourceList = JSON.parse(data);\n          if (Array.isArray(sourceList) && sourceList.length) {\n            this.importSourceList = sourceList.map(v => {\n              if (v.headerMap) {\n                if (!v.header) {\n                  v.header =\n                    typeof v.headerMap === \"string\"\n                      ? v.headerMap\n                      : JSON.stringify(v.headerMap);\n                }\n                delete v.headerMap;\n              }\n              return v;\n            });\n            this.showImportSourceDialog = true;\n            this.isImportRssSource = !!isRssSource;\n          } else {\n            this.$message.error(sourceTypeName + \"文件错误\");\n          }\n        } catch (error) {\n          this.$message.error(sourceTypeName + \"文件错误\");\n        }\n      };\n      reader.onerror = () => {\n        // console.log(\"FileReader error\", e);\n        // FileReader 读取出错，只能上传读取了\n        let param = new FormData();\n        param.append(\"file\", rawFile);\n        Axios.post(this.api + \"/readSourceFile\", param, {\n          headers: { \"Content-Type\": \"multipart/form-data\" }\n        }).then(\n          res => {\n            if (res.data.isSuccess) {\n              //\n              let sourceList = [];\n              res.data.data.forEach(v => {\n                try {\n                  const data = JSON.parse(v);\n                  if (Array.isArray(data)) {\n                    sourceList = sourceList.concat(data);\n                  }\n                } catch (error) {\n                  //\n                }\n              });\n              if (sourceList.length) {\n                this.importSourceList = sourceList.map(v => {\n                  if (v.headerMap) {\n                    if (!v.header) {\n                      v.header =\n                        typeof v.headerMap === \"string\"\n                          ? v.headerMap\n                          : JSON.stringify(v.headerMap);\n                    }\n                    delete v.headerMap;\n                  }\n                  return v;\n                });\n                this.showImportSourceDialog = true;\n                this.isImportRssSource = !!isRssSource;\n              } else {\n                this.$message.error(sourceTypeName + \"文件错误\");\n              }\n            }\n          },\n          error => {\n            this.$message.error(\n              \"读取\" +\n                sourceTypeName +\n                \"文件内容失败 \" +\n                (error && error.toString())\n            );\n          }\n        );\n      };\n      reader.readAsText(rawFile);\n      if (this.isRssSource) {\n        this.$refs.rssInputRef.value = null;\n      } else {\n        this.$refs.fileRef.value = null;\n      }\n    },\n    async loadRemoteBookSource() {\n      const lastRemoteSourceUrl = getCache(\n        this.currentUserName + \"@lastRemoteSourceUrl\",\n        \"\"\n      );\n      const res = await this.$prompt(\"请输入远程书源链接\", \"导入远程书源文件\", {\n        inputValue: lastRemoteSourceUrl || \"\",\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res || !res.value) {\n        return;\n      }\n      Axios.post(this.api + \"/readRemoteSourceFile\", {\n        url: res.value\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            setCache(this.currentUserName + \"@lastRemoteSourceUrl\", res.value);\n            //\n            let sourceList = [];\n            res.data.data.forEach(v => {\n              try {\n                const data = JSON.parse(v);\n                if (Array.isArray(data)) {\n                  sourceList = sourceList.concat(data);\n                }\n              } catch (error) {\n                //\n              }\n            });\n            if (sourceList.length) {\n              this.importSourceList = sourceList;\n              this.showImportSourceDialog = true;\n              this.isImportRssSource = false;\n            } else {\n              this.$message.error(\"远程书源文件错误\");\n            }\n          }\n        },\n        error => {\n          this.$message.error(\n            \"读取远程书源文件内容失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    handleCheckAllChange(val) {\n      let hasFilterd = false;\n      this.checkedSourceIndex = val\n        ? this.importSourceList\n            .map((v, i) => {\n              // 不勾选使用了 js，webview的书源\n              const source = JSON.stringify(v);\n              if (\n                source.indexOf(\"@js:\") !== -1 ||\n                source.indexOf(\"webView:\") !== -1\n              ) {\n                hasFilterd = true;\n                return false;\n              }\n              return i;\n            })\n            .filter(v => v)\n        : [];\n      if (val && hasFilterd) {\n        this.$message.info(\"部分使用了Javascript和Webview的书源未勾选\");\n      }\n      this.isIndeterminate = false;\n    },\n    handleCheckedSourcesChange(value) {\n      let checkedCount = value.length;\n      this.checkAll = checkedCount === this.importSourceList.length;\n      this.isIndeterminate =\n        checkedCount > 0 && checkedCount < this.importSourceList.length;\n    },\n    getSourceTag(source) {\n      const sourceStr = JSON.stringify(source);\n      const tags = [];\n      if (sourceStr.indexOf(\"@js:\") !== -1) {\n        tags.push(\"@Javascript\");\n      }\n\n      if (sourceStr.indexOf(\"webView:\") !== -1) {\n        tags.push(\"@WebView\");\n      }\n\n      return \"   \" + tags.join(\"  \");\n    },\n    saveSourceList() {\n      if (!this.$store.state.connected) {\n        this.$message.error(\"后端未连接\");\n        return;\n      }\n      if (!this.checkedSourceIndex.length) {\n        this.$message.error(\"请选择需要导入的源\");\n        return;\n      }\n      const sourceList = this.checkedSourceIndex.map(\n        v => this.importSourceList[v]\n      );\n      Axios.post(\n        this.api +\n          (this.isImportRssSource ? \"/saveRssSources\" : \"/saveBookSources\"),\n        sourceList\n      ).then(\n        res => {\n          if (res.data.isSuccess) {\n            //\n            this.$message.success(\n              this.isImportRssSource ? \"导入RSS源成功\" : \"导入书源成功\"\n            );\n            if (this.isImportRssSource) {\n              this.loadRssSources(true);\n            } else {\n              this.loadBookSource(true);\n            }\n            this.showImportSourceDialog = false;\n            this.isImportRssSource = false;\n            this.checkedSourceIndex = [];\n          }\n        },\n        error => {\n          this.$message.error(\n            (this.isImportRssSource ? \"导入RSS源失败 \" : \"导入书源失败 \") +\n              (error && error.toString())\n          );\n        }\n      );\n    },\n    isBookSourceSelectable(bookSource) {\n      const res = [];\n      (this.$store.state.shelfBooks || []).forEach(v => {\n        if (v.origin === bookSource.bookSourceUrl) {\n          res.push(v.name);\n        }\n      });\n      return !res.length;\n    },\n    showSourceBook(bookSource) {\n      const res = [];\n      (this.$store.state.shelfBooks || []).forEach(v => {\n        if (v.origin === bookSource.bookSourceUrl) {\n          res.push(v.name);\n        }\n      });\n      return res.join(\"\\n\");\n    },\n    getInvalidBookSources() {\n      if (!this.$store.state.connected) {\n        this.$message.error(\"后端未连接\");\n        return;\n      }\n      Axios.post(this.api + \"/getInvalidBookSources\").then(\n        res => {\n          if (res.data.isSuccess) {\n            //\n            res.data.data.forEach(v => {\n              this.$store.commit(\"addFailureBookSource\", {\n                bookSourceUrl: v.sourceUrl,\n                errorMsg: v.error\n              });\n            });\n          }\n        },\n        () => {\n          //\n        }\n      );\n    },\n    async checkBookSource() {\n      if (!this.checkBookSourceConfig.keyword) {\n        this.$message.error(\"请输入搜索关键词\");\n        return;\n      }\n      this.isCheckingBookSource = true;\n      this.$store.commit(\"setFailureIncludeTimeout\", true);\n      const limitFunc = LimitResquest(\n        this.checkBookSourceConfig.concurrent,\n        handler => {\n          this.checkBookSourceTip =\n            handler.requestCount + \"/\" + this.bookSourceList.length;\n          if (handler.isEnd()) {\n            this.isCheckingBookSource = false;\n            this.$store.commit(\"setFailureIncludeTimeout\", false);\n          }\n        }\n      );\n      this.bookSourceList.forEach(v => {\n        limitFunc(() => {\n          return Axios.post(\n            this.api + \"/searchBook\",\n            {\n              key: this.checkBookSourceConfig.keyword,\n              bookSourceUrl: v.bookSourceUrl\n            },\n            {\n              timeout: this.checkBookSourceConfig.timeout,\n              silent: true\n            }\n          );\n        });\n      });\n    },\n    async deleteBookSourceList() {\n      if (!this.manageSourceSelection.length) {\n        this.$message.error(\"请选择需要删除的源\");\n        return;\n      }\n      const res = await this.$confirm(\"确认要删除所选择的书源吗?\", \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(\n        this.api + \"/deleteBookSources\",\n        this.manageSourceSelection\n      ).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$store.commit(\n              \"removeFailureBookSource\",\n              this.manageSourceSelection\n            );\n            this.manageSourceSelection = [];\n            this.$message.success(\"删除书源成功\");\n            this.loadBookSource(true);\n          }\n        },\n        error => {\n          this.$message.error(\"删除书源失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    toggleMenu() {\n      if (this.collapseMenu) {\n        this.showNavigation = !this.showNavigation;\n      }\n    },\n    showExplorePop() {\n      setTimeout(() => {\n        this.popExploreVisible = true;\n      }, 100);\n    },\n    showBookInfoDialog(book) {\n      eventBus.$emit(\"showBookInfoDialog\", book);\n    },\n    async saveUserConfig() {\n      if (!window.localStorage) {\n        this.$message.error(\"当前终端不支持localStorage\");\n        return;\n      }\n      const res = await this.$confirm(\n        \"确认要备份当前终端的阅读配置、书架设置、搜索设置、自定义配置方案吗?\",\n        \"提示\"\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      const userConfig = {};\n      [\"config\", \"shelfConfig\", \"searchConfig\", \"customConfigList\"].forEach(\n        key => {\n          const val = getCache(key);\n          if (val) {\n            userConfig[key] = val;\n          }\n        }\n      );\n      Axios.post(this.api + \"/saveUserConfig\", userConfig).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"备份成功\");\n          }\n        },\n        error => {\n          this.$message.error(\"备份失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async restoreUserConfig() {\n      if (!window.localStorage) {\n        this.$message.error(\"当前终端不支持localStorage\");\n        return;\n      }\n      const res = await this.$confirm(\n        \"确认要从备份文件中恢复当前终端的阅读配置、书架设置、搜索设置、自定义配置方案吗?\",\n        \"提示\"\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.get(this.api + \"/getUserConfig\").then(\n        res => {\n          if (res.data.isSuccess) {\n            for (const key in res.data.data) {\n              if (Object.hasOwnProperty.call(res.data.data, key)) {\n                setCache(key, res.data.data[key]);\n              }\n            }\n            this.$store.dispatch(\"syncFromLocalStorage\");\n            this.$message.success(\"恢复成功\");\n          }\n        },\n        error => {\n          this.$message.error(\"恢复失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    loadUserList() {\n      if (!this.$store.state.connected) {\n        this.$message.error(\"后端未连接\");\n        return;\n      }\n      Axios.get(this.api + \"/getUserList\").then(\n        res => {\n          if (res.data.isSuccess) {\n            this.userNS = this.$store.state.userInfo.username;\n            this.userList = res.data.data.map(v => ({\n              ...v,\n              userNS: v.username\n            }));\n            this.$store.commit(\"setIsManagerMode\", true);\n          }\n        },\n        error => {\n          this.$message.error(\n            \"加载用户空间失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    formatTableField(row, column, cellValue) {\n      switch (column.property) {\n        case \"createdAt\":\n        case \"lastLoginAt\":\n        case \"lastModified\":\n          return cellValue ? new Date(cellValue).format(\"yy-MM-dd hh:mm\") : \"\";\n        case \"size\":\n          return row.isDirectory ? \"\" : formatSize(cellValue);\n        default:\n          return cellValue;\n      }\n    },\n    exitSecureMode() {\n      this.userNS = \"default\";\n      this.userList = [];\n      this.$store.commit(\"setIsManagerMode\", false);\n      this.init(true);\n    },\n    async backupToWebdav() {\n      const res = await this.$confirm(\n        `确认要用当前书源和书架信息覆盖备份文件中的书源、书架、分组和RSS订阅数据吗?`,\n        \"提示\",\n        {\n          confirmButtonText: \"确定\",\n          cancelButtonText: \"取消\",\n          type: \"warning\"\n        }\n      ).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/backupToWebdav\").then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$message.success(\"备份成功\");\n          }\n        },\n        error => {\n          this.$message.error(\"备份失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    handleTouchStart(e) {\n      this.lastTouch = false;\n      this.lastMoveX = false;\n      this.touchMoveTimes = 0;\n      // 边缘 20px 以内禁止触摸\n      if (\n        e.touches &&\n        e.touches[0] &&\n        e.touches[0].clientX > 20 &&\n        e.touches[0].clientX < window.innerWidth - 20 &&\n        e.touches[0].clientY > 20 &&\n        e.touches[0].clientY < window.innerHeight - 20\n      ) {\n        this.lastTouch = e.touches[0];\n      }\n    },\n    handleTouchMove(e) {\n      if (e.touches && e.touches[0] && this.lastTouch) {\n        if (this.collapseMenu) {\n          const moveX = e.touches[0].clientX - this.lastTouch.clientX;\n          const moveY = e.touches[0].clientY - this.lastTouch.clientY;\n          if (Math.abs(moveY) > Math.abs(moveX)) {\n            this.navigationStyle = {};\n            this.lastMoveX = 0;\n            return;\n          }\n          e.preventDefault();\n          e.stopPropagation();\n          if (!this.showNavigation && moveX > 0 && moveX <= 270) {\n            // 往右拉，打开目录\n            if (this.touchMoveTimes % 3 === 0) {\n              this.navigationStyle = {\n                marginLeft: moveX - 270 + \"px\"\n              };\n            }\n            this.lastMoveX = moveX;\n          } else if (this.showNavigation && moveX < 0 && moveX >= -270) {\n            // 往左拉，关闭目录\n            if (this.touchMoveTimes % 3 === 0) {\n              this.navigationStyle = {\n                marginLeft: moveX + \"px\"\n              };\n            }\n            this.lastMoveX = moveX;\n          }\n          this.touchMoveTimes++;\n        }\n      }\n    },\n    handleTouchEnd() {\n      if (this.collapseMenu) {\n        if (this.lastMoveX > 0) {\n          this.showNavigation = true;\n          this.navigationStyle = {};\n        } else if (this.lastMoveX < 0) {\n          this.showNavigation = false;\n          this.navigationStyle = {};\n        }\n      }\n    },\n    showFailureBookSource() {\n      this.getInvalidBookSources();\n      this.isShowFailureBookSource = true;\n      this.showBookSourceManageDialog = true;\n    },\n    debugBookSource() {\n      window.open(\n        window.location.origin +\n          window.location.pathname +\n          \"bookSourceDebug/#domain=\" +\n          this.api,\n        \"_target\"\n      );\n    },\n    setShowSourceGroup(group) {\n      if (this.showSourceGroup === group) {\n        this.showSourceGroup = \"\";\n      } else {\n        this.showSourceGroup = group;\n      }\n    },\n    importLocalBook() {\n      this.$refs.bookRef.dispatchEvent(new MouseEvent(\"click\"));\n    },\n    onBookFileChange(event) {\n      if (!event.target || !event.target.files || !event.target.files.length) {\n        return;\n      }\n      let param = new FormData();\n      for (let i = 0; i < event.target.files.length; i++) {\n        const file = event.target.files[i];\n        param.append(\"file\" + i, file);\n      }\n      Axios.post(this.api + \"/importBookPreview\", param, {\n        headers: { \"Content-Type\": \"multipart/form-data\" }\n      }).then(\n        res => {\n          if (res.data.isSuccess && res.data.data.length) {\n            if (res.data.data.length > 1) {\n              // 批量导入\n              this.importMultiBooks(res.data.data);\n            } else {\n              //\n              this.importBookInfo = res.data.data[0].book;\n              this.importBookGroup = [];\n              this.importBookChapters = res.data.data[0].chapters;\n              this.showImportBookDialog = true;\n            }\n          }\n        },\n        error => {\n          this.$message.error(\"上传书籍 \" + (error && error.toString()));\n        }\n      );\n      this.$refs.bookRef.value = null;\n    },\n    async importMultiBooks(books) {\n      if (!books || !books.length) {\n        return;\n      }\n      if (books.length == 1) {\n        this.importBookInfo = books[0].book;\n        this.importBookGroup = [];\n        this.importBookChapters = books[0].chapters;\n        this.showImportBookDialog = true;\n        return;\n      }\n      const res = await this.$confirm(\n        `你选择导入多本书籍，请选择导入方式?`,\n        \"提示\",\n        {\n          confirmButtonText: \"批量导入\",\n          cancelButtonText: \"逐一确认导入\",\n          type: \"warning\",\n          closeOnClickModal: false,\n          closeOnPressEscape: false,\n          distinguishCancelAndClose: true\n        }\n      ).catch(action => {\n        return action === \"close\" ? \"close\" : false;\n      });\n      if (res === \"close\") {\n        return;\n      }\n      if (res) {\n        const customImportBookInfo = await this.customImportBookInfo();\n        if (customImportBookInfo === false) {\n          return;\n        }\n        for (let i = 0; i < books.length; i++) {\n          const book = books[i];\n          await this.saveBook(\n            { ...book.book, ...customImportBookInfo },\n            true\n          ).catch(() => {});\n        }\n      } else {\n        for (let i = 0; i < books.length; i++) {\n          const book = books[i];\n          this.importMultiBookTip = `（${i + 1}/${books.length}）`;\n          await this.waitForImportBook(book);\n        }\n        this.importMultiBookTip = \"\";\n      }\n    },\n    waitForImportBook(bookInfo) {\n      return new Promise(resolve => {\n        this.importBookInfo = bookInfo.book;\n        this.importBookGroup = [];\n        this.importBookChapters = bookInfo.chapters;\n        this.showImportBookDialog = true;\n        this.$once(\"importEnd\", resolve);\n      });\n    },\n    importBookDialogClosed() {\n      const url = this.importBookInfo.bookUrl;\n      this.importBookInfo = {};\n      this.importBookGroup = [];\n      this.importBookChapters = [];\n      this.importUsedTxtRule = \"\";\n      this.$nextTick(() => {\n        this.$emit(\"importEnd\");\n      });\n\n      Axios.post(\n        this.api + \"/deleteFile\",\n        {\n          url\n        },\n        {\n          silent: true\n        }\n      ).then(\n        () => {\n          //\n        },\n        () => {\n          //\n        }\n      );\n    },\n    async customImportBookInfo(options) {\n      this.importBookGroup = [];\n      const res = await this.$msgbox({\n        title: \"统一设置分组\",\n        message: this.renderComp(),\n        showCancelButton: true,\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消导入\",\n        ...(options || {})\n      }).catch(action => {\n        return action === \"close\" ? \"close\" : false;\n      });\n      if (res === \"confirm\") {\n        return {\n          group: this.importBookGroup.reduce((v, c) => v | c, 0)\n        };\n      } else {\n        return false;\n      }\n    },\n    renderComp() {\n      var bookGroupList = this.bookGroupSetList;\n      var shelf = this;\n      Vue.component(\"custComp\", {\n        render() {\n          return (\n            <div style={{ textAlign: \"center\" }}>\n              <span>请选择分组：</span>\n              <el-select\n                size=\"mini\"\n                vModel={this.importBookGroup}\n                ref=\"bookGroupSelect\"\n                filterable={true}\n                multiple={true}\n                placeholder=\"未分组\"\n                vOn:change={this.change}\n              >\n                {bookGroupList.map((bookGroup, index) => {\n                  return (\n                    <el-option\n                      key={\"bookGroup-\" + index}\n                      label={bookGroup.groupName}\n                      value={bookGroup.groupId}\n                    ></el-option>\n                  );\n                })}\n              </el-select>\n            </div>\n          );\n        },\n        data() {\n          return {\n            importBookGroup: []\n          };\n        },\n        methods: {\n          change() {\n            shelf.importBookGroup = this.importBookGroup;\n          }\n        }\n      });\n      var custComp = Vue.component(\"custComp\");\n      return this.$createElement(custComp);\n    },\n    showBookManage() {\n      eventBus.$emit(\"showBookManageDialog\");\n    },\n    showManageBookGroup() {\n      this.loadBookGroup(true);\n      eventBus.$emit(\"showBookGroupDialog\", false);\n    },\n    getShowShelfBooks(bookGroup) {\n      // 处理特殊分组\n      if (bookGroup === -1) {\n        // 全部\n        return this.shelfBooks;\n      } else if (bookGroup === -2) {\n        // 本地\n        return this.shelfBooks.filter(v => v.origin === \"loc_book\");\n      } else if (bookGroup === -3) {\n        // 音频\n        return this.shelfBooks.filter(v => v.type === 1);\n      } else if (bookGroup === -4) {\n        // 未分组\n        return this.shelfBooks.filter(v => v.group === 0);\n      }\n\n      return this.shelfBooks.filter(v =>\n        bookGroup === 0 ? true : v.group & bookGroup\n      );\n    },\n    loadRssSources(refresh) {\n      return this.$root.$children[0].loadRssSources(refresh);\n    },\n    showRssDialog() {\n      eventBus.$emit(\"showRssSourceListDialog\");\n    },\n    showRssArticleListDialog(source) {\n      eventBus.$emit(\"showRssArticleListDialog\", source);\n    },\n    noop() {},\n    exportBookSource() {\n      Axios.get(this.api + \"/getBookSources\").then(\n        res => {\n          if (res.data.isSuccess) {\n            const aEle = document.createElement(\"a\");\n            const blob = new Blob([\n              JSON.stringify(res.data.data || [], null, 4)\n            ]);\n\n            aEle.download = \"reader书源-\" + this.currentDateTime() + \".json\";\n            aEle.href = URL.createObjectURL(blob);\n            aEle.click();\n          }\n        },\n        error => {\n          this.$message.error(\"导出书源失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async deleteAllBookSource() {\n      const res = await this.$confirm(`确认要清空所有书源吗?`, \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteAllBookSources\").then(\n        res => {\n          if (res.data.isSuccess) {\n            //\n            this.$message.success(\"清空书源成功\");\n            this.loadBookSource(true);\n          }\n        },\n        error => {\n          this.$message.error(\"清空书源失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    async deleteBookSourceFile() {\n      const res = await this.$confirm(`确认要恢复默认书源吗?`, \"提示\", {\n        confirmButtonText: \"确定\",\n        cancelButtonText: \"取消\",\n        type: \"warning\"\n      }).catch(() => {\n        return false;\n      });\n      if (!res) {\n        return;\n      }\n      Axios.post(this.api + \"/deleteBookSourcesFile\").then(\n        res => {\n          if (res.data.isSuccess) {\n            //\n            this.$message.success(\"恢复默认书源成功\");\n            this.loadBookSource(true);\n          }\n        },\n        error => {\n          this.$message.error(\"操作失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    editBookSource(bookSource) {\n      const editHandler = data => {\n        eventBus.$emit(\n          \"showEditor\",\n          \"编辑书源\",\n          JSON.stringify(data, null, 4),\n          (content, close) => {\n            try {\n              const source = JSON.parse(content);\n              if (!source.bookSourceName) {\n                this.$message.error(\"书源名称不能为空\");\n                return;\n              }\n              if (!source.bookSourceUrl) {\n                this.$message.error(\"书源链接不能为空\");\n                return;\n              }\n              Axios.post(this.api + \"/saveBookSource\", source).then(\n                res => {\n                  if (res.data.isSuccess) {\n                    //\n                    close();\n                    this.$message.success(\"保存书源成功\");\n                    this.loadBookSource(true);\n                  }\n                },\n                error => {\n                  this.$message.error(\n                    \"保存书源失败 \" + (error && error.toString())\n                  );\n                }\n              );\n            } catch (e) {\n              this.$message.error(\"书源必须是JSON格式\");\n            }\n          }\n        );\n      };\n      if (!bookSource) {\n        editHandler({\n          bookSourceComment: \"\",\n          bookSourceGroup: \"\",\n          bookSourceName: \"新增书源\",\n          bookSourceType: 0,\n          bookSourceUrl: \"\",\n          bookUrlPattern: \"\",\n          enabled: true,\n          enabledExplore: true,\n          exploreUrl: \"\",\n          ruleBookInfo: {},\n          ruleContent: {\n            content: \"\"\n          },\n          ruleExplore: {},\n          ruleSearch: {\n            author: \"\",\n            bookList: \"\",\n            bookUrl: \"\",\n            coverUrl: \"\",\n            intro: \"\",\n            kind: \"\",\n            lastChapter: \"\",\n            name: \"\"\n          },\n          ruleToc: {\n            chapterList: \"\",\n            chapterName: \"\",\n            chapterUrl: \"\"\n          },\n          searchUrl: \"\"\n        });\n        return;\n      }\n      Axios.post(this.api + \"/getBookSource\", {\n        bookSourceUrl: bookSource.bookSourceUrl\n      }).then(\n        res => {\n          if (res.data.isSuccess) {\n            //\n            editHandler(res.data.data);\n          }\n        },\n        error => {\n          this.$message.error(\n            \"加载书源信息失败 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    updateForce() {\n      if (\"serviceWorker\" in navigator) {\n        navigator.serviceWorker\n          .getRegistrations()\n          .then(async function(registrations) {\n            /* eslint-disable-next-line no-console */\n            console.log(\"registrations\", registrations);\n            for (let i = 0; i < registrations.length; i++) {\n              await registrations[i].update();\n            }\n\n            /* eslint-disable-next-line no-console */\n            console.log(\"Try to clear home cache\");\n            navigator.serviceWorker.controller &&\n              navigator.serviceWorker.controller.postMessage({\n                type: \"CLEAR_HOME_CACHE\"\n              });\n\n            /* eslint-disable-next-line no-console */\n            console.log(\"Try to skip waiting\");\n            navigator.serviceWorker.controller &&\n              navigator.serviceWorker.controller.postMessage({\n                type: \"SKIP_WAITING\"\n              });\n\n            setTimeout(() => {\n              /* eslint-disable-next-line no-console */\n              console.log(\"Try to reload force\");\n              window.location.reload(true);\n            }, 50);\n          });\n      }\n    },\n    async scanCacheStorage() {\n      this.localCacheStats = {\n        total: (await this.analyseLocalStorage()).totalBytes,\n        bookSourceList: (await this.analyseLocalStorage(\"bookSourceList\"))\n          .totalBytes,\n        rssSources: (await this.analyseLocalStorage(\"rssSources\")).totalBytes,\n        chapterList: (await this.analyseLocalStorage(\"chapterList\")).totalBytes,\n        chapterContent: (await this.analyseLocalStorage(\"chapterContent\"))\n          .totalBytes\n      };\n    },\n    analyseLocalStorage(match) {\n      let totalBytes = 0;\n      let cacheBytes = 0;\n      return window.$cacheStorage\n        .iterate(function(value, key) {\n          if (!match || key.indexOf(match) >= 0) {\n            totalBytes += JSON.stringify(value).getBytesLength();\n            if (key.startsWith(\"localCache@\")) {\n              cacheBytes += JSON.stringify(value).getBytesLength();\n            }\n          }\n        })\n        .then(() => {\n          return {\n            totalBytes: formatSize(totalBytes),\n            cacheBytes: formatSize(cacheBytes)\n          };\n        })\n        .catch(function() {\n          // 当出错时，此处代码运行\n          // console.log(err);\n        });\n    },\n    clearCache(match) {\n      let cacheBytes = 0;\n      window.$cacheStorage\n        .iterate(function(value, key) {\n          if (!match || key.indexOf(match) >= 0) {\n            if (key.startsWith(\"localCache@\")) {\n              cacheBytes += JSON.stringify(value).getBytesLength();\n              window.$cacheStorage.removeItem(key);\n            }\n          }\n        })\n        .then(() => {\n          this.scanCacheStorage();\n\n          return {\n            cacheBytes: formatSize(cacheBytes)\n          };\n        })\n        .catch(function() {\n          // 当出错时，此处代码运行\n          // console.log(err);\n        });\n    },\n    scrollHandler() {\n      this.lastScrollTop = this.$refs.bookList.scrollTop;\n    },\n    getBookCoverUrl(book) {\n      return book.customCoverUrl || book.coverUrl;\n    },\n    logout() {\n      Axios.post(this.api + \"/logout\").then(\n        res => {\n          if (res.data.isSuccess) {\n            this.$store.commit(\"setToken\", \"\");\n            window.location.reload(true);\n          }\n        },\n        error => {\n          this.$message.error(\"注销失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    getChapterListByRule() {\n      return Axios.post(\"/getChapterListByRule\", this.importBookInfo).then(\n        res => {\n          if (res.data.isSuccess && res.data.data.book) {\n            this.importBookInfo = res.data.data.book;\n            this.importBookChapters = res.data.data.chapters;\n          }\n        },\n        error => {\n          this.$message.error(\"注销失败 \" + (error && error.toString()));\n        }\n      );\n    },\n    showUserManageDialog() {\n      eventBus.$emit(\"showUserManageDialog\");\n    },\n    showMPCode() {\n      eventBus.$emit(\"showMPCodeDialog\");\n    },\n    joinTGChannel() {\n      window.open(\"https://t.me/facker_channel\", \"_target\");\n    },\n    ensureLoadBookCover() {\n      // 手动触发滚动事件，显示书籍封面图片\n      this.$refs.bookList.dispatchEvent(new MouseEvent(\"scroll\"));\n\n      // 上面一步应该能搞定，下面再确认一下\n      this.$refs.bookCoverList.forEach(v => {\n        if (!v.show && isInContainer(v.$el, this.$refs.bookList)) {\n          // console.log(\"not show \", v);\n          v.show = true;\n        }\n      });\n    }\n  },\n  computed: {\n    ...mapGetters([\n      \"collapseMenu\",\n      \"dialogWidth\",\n      \"dialogSmallWidth\",\n      \"dialogTop\",\n      \"dialogContentHeight\",\n      \"popupWidth\"\n    ]),\n    config() {\n      return this.$store.getters.config;\n    },\n    isNight() {\n      return this.$store.getters.isNight;\n    },\n    themeColor() {\n      if (this.$store.getters.isNight) {\n        return {\n          background: \"#f7f7f7\"\n        };\n      } else {\n        return {\n          background: \"#222\"\n        };\n      }\n    },\n    bookList() {\n      return this.isSearchResult ? this.searchResult : this.showShelfBooks;\n    },\n    bookCoverList() {\n      return this.bookList\n        .filter(v => this.getBookCoverUrl(v))\n        .map(v => this.getCover(this.getBookCoverUrl(v), true));\n    },\n    shelfBooks() {\n      return this.$store.getters.shelfBooks;\n    },\n    showShelfBooks() {\n      return this.getShowShelfBooks(this.showBookGroup);\n    },\n    searchResultMap() {\n      return this.searchResult.reduce((c, v) => {\n        c[v.bookUrl] = v;\n        return c;\n      }, {});\n    },\n    connectStatus() {\n      return this.$store.state.connected\n        ? `后端已连接`\n        : this.connecting\n        ? \"正在连接后端服务器……\"\n        : \"点击设置后端接口前缀\";\n    },\n    connectType() {\n      return this.$store.state.connected ? \"success\" : \"danger\";\n    },\n    readingRecent() {\n      return this.$store.getters.readingBook &&\n        this.$store.getters.readingBook.name\n        ? this.$store.getters.readingBook\n        : {\n            name: \"尚无阅读记录\",\n            bookUrl: \"\",\n            index: 0\n          };\n    },\n    loginAuth() {\n      return this.$store.state.loginAuth;\n    },\n    bookSourceList() {\n      return this.$store.state.bookSourceList;\n    },\n    userNS: {\n      get() {\n        return this.$store.state.userNS;\n      },\n      set(val) {\n        this.$store.commit(\"setUserNS\", val);\n        if (val) {\n          this.$store.commit(\"setIsManagerMode\", true);\n        }\n      }\n    },\n    userList: {\n      get() {\n        return this.$store.state.userList;\n      },\n      set(val) {\n        this.$store.commit(\"setUserList\", val);\n      }\n    },\n    bookSourceShowList() {\n      return this.isShowFailureBookSource\n        ? this.$store.state.failureBookSource\n        : this.bookSourceList;\n    },\n    bookSourceGroupList() {\n      const groupsMap = {};\n      this.bookSourceList.forEach(v => {\n        if (v.bookSourceGroup) {\n          groupsMap[v.bookSourceGroup] = (groupsMap[v.bookSourceGroup] | 0) + 1;\n        }\n      });\n      const groups = [\n        {\n          name: \"全部分组\",\n          value: \"\",\n          count: this.bookSourceList.length\n        }\n      ];\n      for (const i in groupsMap) {\n        if (Object.hasOwnProperty.call(groupsMap, i)) {\n          groups.push({\n            name: i,\n            value: i,\n            count: groupsMap[i]\n          });\n        }\n      }\n      return groups;\n    },\n    bookSourceShowGroup() {\n      if (!this.isShowFailureBookSource) {\n        const groups = new Set();\n        this.bookSourceShowList.forEach(v => {\n          v.bookSourceGroup && groups.add(v.bookSourceGroup);\n        });\n        groups.add(\"未分组\");\n        return Array.from(groups);\n      } else {\n        return [].concat(errorTypeList).concat([\"timeout\"]);\n      }\n    },\n    bookSourceShowLength() {\n      return this.bookSourceShowResult.length;\n    },\n    bookSourceShowResult() {\n      if (!this.showSourceGroup) {\n        return this.bookSourceShowList;\n      }\n      if (this.isShowFailureBookSource) {\n        return this.bookSourceShowList.filter(v =>\n          this.showSourceGroup\n            ? v.errorMsg.indexOf(this.showSourceGroup) >= 0\n            : true\n        );\n      } else {\n        return this.bookSourceShowList.filter(v =>\n          this.showSourceGroup === \"未分组\"\n            ? !v.bookSourceGroup\n            : v.bookSourceGroup === this.showSourceGroup\n        );\n      }\n    },\n    bookSourceShowResultPageList() {\n      const start =\n        (this.bookSourcePagination.page - 1) * this.bookSourcePagination.size;\n      if (start > this.bookSourceShowResult.length) {\n        return [];\n      }\n      return this.bookSourceShowResult.slice(\n        start,\n        Math.min(\n          start + this.bookSourcePagination.size,\n          this.bookSourceShowResult.length\n        )\n      );\n    },\n    showBookGroup: {\n      get() {\n        if (!this.bookGroupDisplayList.length) return -1;\n        return this.$store.state.shelfConfig.showBookGroup;\n      },\n      set(val) {\n        this.$store.commit(\"setShelfConfig\", {\n          ...this.$store.state.shelfConfig,\n          showBookGroup: val\n        });\n      }\n    },\n    showBookGroupString: {\n      get() {\n        return \"\" + this.showBookGroup;\n      },\n      set(val) {\n        this.showBookGroup = +val;\n      }\n    },\n    bookGroupSetList() {\n      return this.$store.state.bookGroupList.filter(v => v.groupId > 0);\n    },\n    bookGroupDisplayList() {\n      return this.$store.state.bookGroupList\n        .filter(v => this.getShowShelfBooks(v.groupId).length && v.show)\n        .sort((a, b) => a.order - b.order);\n    },\n    searchConfig: {\n      get() {\n        return this.$store.state.searchConfig;\n      },\n      set(val) {\n        this.$store.commit(\"setSearchConfig\", val);\n      }\n    },\n    isShowTocRule() {\n      try {\n        return (\n          this.importBookInfo &&\n          this.importBookInfo.originName &&\n          (this.importBookInfo.originName.toLowerCase().endsWith(\".txt\") ||\n            this.importBookInfo.originName.toLowerCase().endsWith(\".epub\"))\n        );\n      } catch (e) {\n        // console.log(e);\n      }\n      return false;\n    },\n    tocRuleList() {\n      if (!this.importBookInfo || !this.importBookInfo.originName) {\n        return [];\n      }\n      if (this.importBookInfo.originName.toLowerCase().endsWith(\".txt\")) {\n        // txt\n        return this.$store.state.txtTocRules;\n      } else {\n        // epub\n        return [\n          { name: \"根据 Spin 获取章节，使用 Toc 补充章节名\", rule: \"spin+toc\" },\n          { name: \"根据 Spin 获取章节，强制使用 Toc 章节名\", rule: \"spin<toc\" },\n          { name: \"根据 Spin 获取章节\", rule: \"spin\" },\n          { name: \"根据 Toc 获取章节，使用 Spin 补充章节名\", rule: \"toc+spin\" },\n          { name: \"根据 Toc 获取章节，强制使用 Spin 章节名\", rule: \"toc<spin\" },\n          { name: \"根据 Toc 获取章节\", rule: \"toc\" }\n        ];\n      }\n    }\n  }\n};\n</script>\n\n<style lang=\"stylus\" scoped>\n.index-wrapper {\n  height: 100%;\n  width: 100%;\n  display: flex;\n  flex-direction: row;\n\n  .navigation-wrapper {\n    width: 260px;\n    min-width: 260px;\n    height: 100%;\n    box-sizing: border-box;\n    background-color: #F7F7F7;\n    position: relative;\n    padding-top: 0;\n    padding-top: constant(safe-area-inset-top) !important;\n    padding-top: env(safe-area-inset-top) !important;\n\n    .navigation-inner-wrapper {\n      padding: 48px 36px 66px 36px;\n      height: 100%;\n      overflow-y: auto;\n      box-sizing: border-box;\n    }\n\n    .navigation-title {\n      font-size: 24px;\n      font-weight: 600;\n      font-family: -apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif;\n\n      .version-text {\n        float: right;\n        font-size: 14px;\n        line-height: 33px;\n        font-weight: 400;\n        color: #b1b1b1;\n        display: inline-block;\n        cursor: pointer;\n      }\n    }\n\n    .navigation-sub-title {\n      font-size: 16px;\n      font-weight: 500;\n      font-family: -apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif;\n      margin-top: 16px;\n      color: #b1b1b1;\n    }\n\n    .search-wrapper {\n      .search-input {\n        border-radius: 50%;\n        margin-top: 24px;\n\n        >>> .el-input__inner {\n          border-radius: 50px;\n          border-color: #E3E3E3;\n        }\n      }\n    }\n\n    .recent-wrapper {\n      margin-top: 36px;\n\n      .recent-title {\n        font-size: 14px;\n        color: #b1b1b1;\n        font-family: -apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif;\n      }\n\n      .reading-recent {\n        margin: 18px 0;\n\n        .recent-book {\n          cursor: pointer;\n          max-width: 100%;\n          overflow: hidden;\n          text-overflow: ellipsis;\n        }\n      }\n    }\n\n    .setting-wrapper {\n      margin-top: 36px;\n\n      .setting-title {\n        font-size: 14px;\n        color: #b1b1b1;\n        font-family: -apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif;\n\n        .right-text {\n          float: right;\n          display: inline-block;\n          height: 20px;\n          line-height: 20px;\n          cursor: pointer;\n          user-select: none;\n        }\n      }\n\n      .no-point {\n        pointer-events: none;\n      }\n\n      .setting-connect {\n        cursor: pointer;\n        max-width: 100%;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n\n      .setting-item {\n        padding-top: 16px;\n      }\n\n      .setting-btn {\n        margin-right: 15px;\n        margin-bottom: 15px;\n        cursor: pointer;\n      }\n\n      .setting-select {\n        width: 100%;\n      }\n    }\n\n    .search-setting {\n      margin-top: 28px;\n    }\n\n    .bottom-icons {\n      position: absolute;\n      bottom: 30px;\n      width: 188px;\n      left: 36px;\n      align-items: center;\n      display: flex;\n      flex-direction: row;\n      justify-content: space-between;\n      pointer-events: none;\n\n      .bottom-icon {\n        height: 36px;\n        pointer-events: all;\n        img {\n          width: 36px;\n          height: 36px;\n        }\n      }\n\n      .theme-item {\n        line-height: 32px;\n        width: 36px;\n        height: 36px;\n        border-radius: 100%;\n        display: inline-block;\n        cursor: pointer;\n        text-align: center;\n        vertical-align: middle;\n        pointer-events: all;\n\n        .el-icon-moon {\n          color: #f7f7f7;\n          line-height: 34px;\n        }\n        .el-icon-sunny {\n          color: #121212;\n          line-height: 34px;\n        }\n      }\n    }\n\n    .setting-wrapper:nth-last-child(1) {\n      padding-bottom: 20px;\n    }\n  }\n\n  .shelf-wrapper {\n    padding: 48px 48px;\n    height: 100%;\n    max-height: 100%;\n    width: 100%;\n    background-color: #fff;\n    display: flex;\n    flex-direction: column;\n    box-sizing: border-box;\n\n    .shelf-title {\n      font-size: 20px;\n      font-weight: 600;\n      font-family: -apple-system, \"Noto Sans\", \"Helvetica Neue\", Helvetica, \"Nimbus Sans L\", Arial, \"Liberation Sans\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans CJK SC\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Microsoft YaHei\", \"Wenquanyi Micro Hei\", \"WenQuanYi Zen Hei\", \"ST Heiti\", SimHei, \"WenQuanYi Zen Hei Sharp\", sans-serif;\n      margin-bottom: 5px;\n      min-width: 320px;\n      box-sizing: border-box;\n\n      .el-icon-menu {\n        cursor: pointer;\n      }\n\n      .title-btn {\n        font-size: 14px;\n        line-height: 28px;\n        float: right;\n        cursor: pointer;\n        user-select: none;\n        margin-left: 10px;\n\n        >>>.el-icon-loading {\n          font-size: 16px;\n        }\n      }\n    }\n\n    >>>.el-icon-loading {\n      font-size: 36px;\n      color: #B5B5B5;\n    }\n\n    >>>.el-loading-text {\n      font-weight: 500;\n      color: #B5B5B5;\n    }\n\n    .book-group-wrapper {\n      padding: 5px 0;\n      margin-bottom: 10px;\n\n      .book-group-tabs {\n        width: 100%;\n      }\n\n      .book-group-btn {\n        margin-right: 10px;\n        cursor: pointer;\n      }\n\n      .book-group-btn.selected {\n        color: #fff;\n        background: #409EFF;\n        border-color: #409EFF;\n      }\n    }\n\n    .books-wrapper {\n      flex: 1;\n      overflow-x: hidden;\n      overflow-y: scroll;\n\n      .wrapper {\n        display: grid ;\n        grid-template-columns: repeat(auto-fill, 380px);\n        justify-content: space-around;\n        grid-gap: 10px;\n\n        .book {\n          user-select: none;\n          display: flex;\n          cursor: pointer;\n          margin-bottom: 18px;\n          padding: 24px 24px;\n          width: 360px;\n          flex-direction: row;\n          justify-content: space-around;\n\n          .cover-img {\n            width: 84px;\n            height: 112px;\n\n            .cover {\n              width: 84px;\n              height: 112px;\n            }\n          }\n\n          .info {\n            position: relative;\n            display: flex;\n            flex-direction: column;\n            justify-content: space-between;\n            align-items: left;\n            height: 112px;\n            margin-left: 20px;\n            flex: 1;\n\n            .book-operation {\n              position: absolute;\n              right: 5px;\n              top: 0px;\n              font-size: 24px;\n              color: #969ba3;\n\n              i {\n                margin-left: 10px;\n              }\n            }\n\n            .name {\n              width: fit-content;\n              font-size: 16px;\n              font-weight: 700;\n              color: #33373D;\n              margin-right: 38px;\n              max-height: 45px;\n              word-wrap: break-word;\n              overflow: hidden;\n              text-overflow: ellipsis;\n              display: -webkit-box;\n              -webkit-box-orient: vertical;\n              -webkit-line-clamp: 2;\n            }\n\n            .name.edit {\n              margin-right: 62px;\n            }\n\n            .sub {\n              display: flex;\n              flex-direction: row;\n              font-size: 12px;\n              font-weight: 600;\n              color: #969ba3;\n\n              .dot {\n                margin: 0 7px;\n              }\n            }\n\n            .intro, .dur-chapter, .last-chapter {\n              color: #6b6b6b;\n              font-size: 13px;\n              margin-top: 3px;\n              font-weight: 500;\n              word-wrap: break-word;\n              overflow: hidden;\n              text-overflow: ellipsis;\n              display: -webkit-box;\n              -webkit-box-orient: vertical;\n              -webkit-line-clamp: 1;\n              text-align: left;\n            }\n          }\n        }\n      }\n\n      .wrapper:last-child {\n        margin-right: auto;\n      }\n    }\n\n    .books-wrapper::-webkit-scrollbar {\n      width: 0 !important;\n    }\n  }\n}\n\n.unread-num-badge {\n  >>>.el-badge__content {\n    border: none;\n  }\n}\n\n.night {\n  >>>.navigation-wrapper {\n    background-color: #121212;\n    border-right: 1px solid #555;\n  }\n  >>>.navigation-title {\n    color: #bbb;\n  }\n  >>>.shelf-title {\n    color: #bbb;\n  }\n  >>>.shelf-wrapper {\n    background-color: #222;\n  }\n  >>>.el-input__inner {\n    background-color: #444;\n    border: 1px solid #444 !important;\n    color: #aaa;\n  }\n  .book .info .name {\n    color: #bbb !important;\n  }\n  .book .info .book-operation {\n    color: #6b6b6b !important;\n  }\n  .book .info .sub {\n    color: #6b6b6b !important;\n  }\n  .book .info .intro, .book .info .dur-chapter, .book .info .last-chapter {\n    color: #969ba3 !important;\n  }\n\n  >>>.check-tip {\n    color: #bbb;\n  }\n}\n\n.source-container {\n  // max-height: 400px;\n  // overflow-y: auto;\n  padding: 0 10px;\n\n  &.table-container {\n    padding: 0;\n  }\n\n  .check-form {\n    display: flex;\n    flex-direction: row;\n    overflow-x: auto;\n    align-items: center;\n\n    .check-form-label {\n      min-width: 60px;\n    }\n\n    .el-input {\n      width: auto;\n      min-width: 100px;\n      margin-right: 10px;\n    }\n\n    .el-input-number {\n      min-width: 130px;\n      margin-right: 10px;\n    }\n\n    .book-cover {\n      width: 84px;\n      height: 112px;\n\n      .cover {\n        width: 84px;\n        height: 112px;\n      }\n    }\n\n    .book-info {\n      display: flex;\n      flex-direction: column;\n      margin-left: 30px;\n      justify-content: space-between;\n      min-height: 100px;\n\n      .toc-refresh-btn {\n        margin-left: 5px;\n      }\n\n      span {\n        display: inline-block;\n        min-width: 56px;\n        text-align-last: justify;\n      }\n      .el-input {\n        width: auto;\n        min-width: 100px;\n        margin-right: 10px;\n      }\n      .el-input-number {\n        min-width: 130px;\n        margin-right: 10px;\n      }\n    }\n  }\n\n  .chapter-title {\n    font-size: 15px;\n    padding: 5px 0;\n    font-weight: 600;\n    margin-top: 10px;\n  }\n\n  .chapter-list {\n    overflow-y: auto;\n    box-sizing: border-box;\n    padding: 0 5px;\n\n    p {\n      margin-top: 0.4em;\n      margin-bottom: 0.4em;\n    }\n  }\n\n  .source-group-wrapper {\n    display: flex;\n    flex-direction: row;\n    overflow-x: auto;\n    padding: 5px 0;\n\n    .source-group-btn {\n      margin-right: 10px;\n      cursor: pointer;\n    }\n\n    .source-group-btn.selected {\n      color: #fff;\n      background: #409EFF;\n      border-color: #409EFF;\n    }\n  }\n\n  .el-pagination {\n    margin-top: 8px;\n    float: right;\n    max-width: 100%;\n    overflow-x: auto;\n    box-sizing: border-box;\n  }\n\n  >>>.source-checkbox {\n    display: block;\n    padding: 8px 0;\n    width: 100%;\n  }\n\n  pre {\n    margin: 0;\n  }\n\n  .source-pagination::after {\n    display: table;\n    content: \"\";\n    clear: both;\n  }\n}\n\n.source-list-container {\n  max-height: calc(var(--vh, 1vh) * 70 - 54px - 60px - 66px);\n  overflow-y: auto;\n  overflow-x: auto;\n}\n\n.night {\n  .source-container {\n    .source-group-wrapper {\n      .source-group-btn.selected {\n        color: #fff;\n        background: #185798;\n        border-color: #185798;\n      }\n    }\n  }\n  .book-group-wrapper {\n    .book-group-btn.selected {\n      color: #fff;\n      background: #185798 !important;\n      border-color: #185798 !important;\n    }\n  }\n}\n\n.source-container::-webkit-scrollbar {\n  width: 0 !important;\n}\n.navigation-inner-wrapper::-webkit-scrollbar {\n  width: 0 !important;\n}\n>>> .el-table__body-wrapper::-webkit-scrollbar {\n  width: 0 !important;\n}\n>>> .el-dialog__wrapper::-webkit-scrollbar {\n  width: 0 !important;\n}\n@media screen and (max-width: 750px) {\n  .index-wrapper {\n    overflow-x: hidden;\n\n    >>>.navigation-wrapper {\n      .navigation-inner-wrapper {\n        padding: 20px 36px 66px 36px;\n      }\n    }\n    >>>.shelf-wrapper {\n      padding: 0;\n      padding-top: constant(safe-area-inset-top) !important;\n      padding-top: env(safe-area-inset-top) !important;\n\n      .shelf-title {\n        padding: 20px 24px 0 24px;\n      }\n\n      .book-group-wrapper {\n        margin-left: 24px;\n        margin-right: 24px;\n      }\n\n      .books-wrapper {\n        .wrapper {\n          display: flex;\n          flex-direction: column;\n\n          .book {\n            box-sizing: border-box;\n            width: 100%;\n            margin-bottom: 0;\n            padding: 10px 20px;\n          }\n        }\n      }\n    }\n  }\n  .source-list-container  {\n    max-height: calc(var(--vh, 1vh) * 100 - 54px - 40px - 66px);\n  }\n}\n@media screen and (max-width: 480px) {\n  .source-container.table-container {\n    margin: -15px -5px;\n  }\n}\n</style>\n<style>\n.navigation-hidden {\n  margin-left: -260px;\n}\n.navigation-in {\n  margin-left: 0px;\n  transition: margin-left 0.3s;\n}\n.navigation-out {\n  margin-left: -260px;\n  transition: margin-left 0.3s;\n}\n.popper-intro {\n  padding: 15px;\n}\n.book-kind span {\n  display: inline-block;\n  margin-left: 5px;\n  margin-right: 5px;\n}\n.night-theme .popper-intro {\n  background: #121212;\n  color: #bbb !important;\n  border: none;\n}\n.night-theme .popper-intro.el-popper[x-placement^=\"bottom\"] .popper__arrow,\n.night-theme\n  .popper-intro.el-popper[x-placement^=\"bottom\"]\n  .popper__arrow::after {\n  border-bottom-color: #121212 !important;\n}\n.night-theme .popper-intro.el-popper[x-placement^=\"top\"] .popper__arrow,\n.night-theme .popper-intro.el-popper[x-placement^=\"top\"] .popper__arrow::after {\n  border-top-color: #121212 !important;\n}\n.night-theme .el-popover__title {\n  color: #ddd !important;\n}\n.status-bar-light-bg {\n  background-image: linear-gradient(\n    to bottom,\n    rgba(0, 0, 0, 0.2) 0,\n    transparent 36px\n  ) !important;\n}\n.status-bar-light-bg-dialog .el-dialog.is-fullscreen {\n  background-image: linear-gradient(\n    to bottom,\n    rgba(0, 0, 0, 0.2) 0,\n    transparent 36px\n  ) !important;\n}\n@media (hover: hover) {\n  .book:hover {\n    background: rgba(0, 0, 0, 0.1);\n    transition-duration: 0.5s;\n  }\n  .el-icon-close:hover {\n    color: #409eff;\n  }\n  .el-icon-edit:hover {\n    color: #409eff;\n  }\n}\n\n.mini-interface .el-dialog__body {\n  padding: 15px 20px;\n}\n.book-group-tabs .el-tabs__header {\n  margin-bottom: 0px;\n}\n</style>\n"
  },
  {
    "path": "web/src/views/Reader.vue",
    "content": "<template>\n  <div\n    class=\"chapter-wrapper\"\n    :style=\"bodyTheme\"\n    :class=\"{\n      night: isNight,\n      day: !isNight,\n      'mini-interface': $store.state.miniInterface\n    }\"\n    ref=\"chapterWrapperRef\"\n  >\n    <div class=\"tool-bar\" :style=\"leftBarTheme\">\n      <div class=\"tools\">\n        <el-popover\n          placement=\"right\"\n          :width=\"popperWidth\"\n          trigger=\"click\"\n          :visible-arrow=\"false\"\n          v-model=\"popBookShelfVisible\"\n          popper-class=\"popper-component\"\n        >\n          <BookShelf\n            ref=\"popBookShelf\"\n            class=\"popup\"\n            :visible=\"popBookShelfVisible\"\n            @changeBook=\"changeBook\"\n            @toShelf=\"toShelf\"\n          />\n          <div class=\"tool-icon\" slot=\"reference\">\n            <div class=\"iconfont\">\n              &#58892;\n            </div>\n            <div class=\"icon-text\">书架</div>\n          </div>\n        </el-popover>\n        <el-popover\n          placement=\"right\"\n          :width=\"popperWidth\"\n          trigger=\"click\"\n          :visible-arrow=\"false\"\n          v-model=\"popBookSourceVisible\"\n          popper-class=\"popper-component\"\n        >\n          <BookSource\n            ref=\"popBookSource\"\n            class=\"popup\"\n            :visible=\"popBookSourceVisible\"\n            @changeBookSource=\"changeBookSource()\"\n            @close=\"popBookSourceVisible = false\"\n          />\n\n          <div class=\"tool-icon\" slot=\"reference\">\n            <div class=\"tool-el-icon\">\n              <i class=\"el-icon-menu\"></i>\n            </div>\n            <div class=\"icon-text\">书源</div>\n          </div>\n        </el-popover>\n        <el-popover\n          placement=\"right\"\n          :width=\"popperWidth\"\n          trigger=\"click\"\n          :visible-arrow=\"false\"\n          v-model=\"popCataVisible\"\n          popper-class=\"popper-component\"\n        >\n          <PopCata\n            @getContent=\"getContent\"\n            ref=\"popCata\"\n            class=\"popup\"\n            @refresh=\"refreshCatalog\"\n            :visible=\"popCataVisible\"\n            @close=\"popCataVisible = false\"\n          />\n\n          <div class=\"tool-icon\" slot=\"reference\">\n            <div class=\"iconfont\">\n              &#58905;\n            </div>\n            <div class=\"icon-text\">目录</div>\n          </div>\n        </el-popover>\n        <el-popover\n          placement=\"right\"\n          :width=\"popperWidth\"\n          trigger=\"click\"\n          :visible-arrow=\"false\"\n          v-model=\"readSettingsVisible\"\n          popper-class=\"popper-component\"\n        >\n          <ReadSettings\n            class=\"popup\"\n            :visible=\"readSettingsVisible\"\n            @close=\"readSettingsVisible = false\"\n            @showClickZone=\"showClickZone = true\"\n            @readMethodChange=\"beforeReadMethodChange\"\n          />\n\n          <div class=\"tool-icon\" slot=\"reference\">\n            <div class=\"iconfont\">\n              &#58971;\n            </div>\n            <div class=\"icon-text\">设置</div>\n          </div>\n        </el-popover>\n        <div\n          class=\"tool-icon\"\n          @click=\"toShelf\"\n          :style=\"$store.state.miniInterface ? { order: -1 } : {}\"\n        >\n          <div class=\"iconfont\">\n            &#58920;\n          </div>\n          <div class=\"icon-text\">首页</div>\n        </div>\n        <div\n          class=\"tool-icon\"\n          @click=\"toTop(0)\"\n          v-if=\"!$store.state.miniInterface\"\n        >\n          <div class=\"iconfont\">\n            &#58914;\n          </div>\n          <div class=\"icon-text\">顶部</div>\n        </div>\n        <div\n          class=\"tool-icon\"\n          @click=\"toBottom(0)\"\n          v-if=\"!$store.state.miniInterface\"\n        >\n          <div class=\"iconfont\">\n            &#58915;\n          </div>\n          <div class=\"icon-text\">底部</div>\n        </div>\n      </div>\n    </div>\n    <div class=\"read-bar\" :style=\"rightBarTheme\">\n      <div class=\"float-btn-zone\">\n        <div class=\"float-left-btn-zone\">\n          <div\n            class=\"float-btn\"\n            :style=\"popupAbsoluteBtnStyle\"\n            @click=\"showBookmarkDialog\"\n          >\n            <i class=\"el-icon-collection-tag\"></i>\n          </div>\n          <div\n            class=\"float-btn\"\n            :style=\"popupAbsoluteBtnStyle\"\n            @click=\"showSearchBookContentDialog\"\n          >\n            <i class=\"el-icon-search\"></i>\n          </div>\n          <div\n            class=\"float-btn\"\n            :style=\"popupAbsoluteBtnStyle\"\n            @click=\"showReadingBookInfo\"\n          >\n            <i class=\"el-icon-info\"></i>\n          </div>\n          <div\n            class=\"float-btn\"\n            :style=\"popupAbsoluteBtnStyle\"\n            @click=\"toTop(0)\"\n            v-if=\"$store.state.miniInterface\"\n          >\n            <i class=\"el-icon-top\"></i>\n          </div>\n          <div\n            class=\"float-btn\"\n            :style=\"popupAbsoluteBtnStyle\"\n            @click=\"toBottom(0)\"\n            v-if=\"$store.state.miniInterface\"\n          >\n            <i class=\"el-icon-bottom\"></i>\n          </div>\n        </div>\n        <div class=\"float-right-btn-zone\">\n          <div\n            class=\"float-btn\"\n            :style=\"popupAbsoluteBtnStyle\"\n            @click=\"refreshContent\"\n          >\n            <i class=\"el-icon-refresh-right\"></i>\n          </div>\n          <div\n            class=\"float-btn\"\n            :style=\"popupAbsoluteBtnStyle\"\n            @click=\"toggleAutoReading()\"\n            v-if=\"!isEpub && !isCarToon && !isAudio\"\n          >\n            <i class=\"el-icon-view\"></i>\n          </div>\n          <div\n            class=\"float-btn\"\n            :style=\"popupAbsoluteBtnStyle\"\n            @click=\"showReadBar = !showReadBar\"\n            v-if=\"speechAvalable && !isEpub && !isCarToon && !isAudio\"\n          >\n            <i class=\"el-icon-headset\"></i>\n          </div>\n          <div\n            class=\"float-btn\"\n            :style=\"popupAbsoluteBtnStyle\"\n            @click=\"toogleNight\"\n          >\n            <i class=\"el-icon-moon\" v-if=\"!isNight\"></i>\n            <i class=\"el-icon-sunny\" v-else></i>\n          </div>\n        </div>\n      </div>\n      <div class=\"progress\" v-if=\"$store.state.miniInterface && !isAudio\">\n        <div class=\"progress-bar\">\n          <el-slider\n            v-model=\"currentPage\"\n            :min=\"1\"\n            :max=\"totalPages\"\n            :show-tooltip=\"false\"\n            @change=\"showPage\"\n            @input=\"progressValue = $event\"\n          ></el-slider>\n        </div>\n        <span class=\"progress-tip\">{{ formatProgressTip() }}</span>\n      </div>\n      <div class=\"cache-content-zone\" v-if=\"showCacheContentZone\">\n        <div>\n          缓存章节\n        </div>\n        <div\n          class=\"cache-content-btn\"\n          v-show=\"!isCachingContent\"\n          @click=\"cacheChapterContent(50)\"\n        >\n          后面50章\n        </div>\n        <div\n          class=\"cache-content-btn\"\n          v-show=\"!isCachingContent\"\n          @click=\"cacheChapterContent(100)\"\n        >\n          后面100章\n        </div>\n        <div\n          class=\"cache-content-btn\"\n          v-show=\"!isCachingContent\"\n          @click=\"cacheChapterContent(true)\"\n        >\n          后面全部\n        </div>\n        <div class=\"caching-tip\" v-show=\"isCachingContent\">\n          {{ cachingContentTip }}\n        </div>\n        <div\n          class=\"caching-cancel-btn\"\n          v-show=\"isCachingContent\"\n          @click=\"cancelCaching\"\n        >\n          <i class=\"el-icon-close\"></i>\n        </div>\n      </div>\n      <div class=\"tools\">\n        <div class=\"tool-icon progress-text\" @click=\"showCacheContent\">\n          <span v-if=\"$store.state.miniInterface\">阅读进度: </span>\n          {{ readingProgress }}\n        </div>\n        <div\n          class=\"tool-icon\"\n          @click=\"toLastChapter()\"\n          :style=\"$store.state.miniInterface ? { order: -1 } : {}\"\n        >\n          <div class=\"iconfont\">\n            &#58920;\n          </div>\n          <span v-if=\"$store.state.miniInterface\">上一章</span>\n        </div>\n        <div class=\"tool-icon\" @click=\"toNextChapter()\">\n          <span v-if=\"$store.state.miniInterface\">下一章</span>\n          <div class=\"iconfont\">\n            &#58913;\n          </div>\n        </div>\n      </div>\n    </div>\n    <div class=\"read-bar\" :style=\"readBarTheme\">\n      <div class=\"reader-bar-inner\">\n        <div class=\"operate-bar\">\n          <div class=\"close-btn\" @click=\"exitRead\">\n            <i class=\"el-icon-close\"></i>\n          </div>\n          <div class=\"center\">\n            <span class=\"ctrl-btn\" @click=\"speechPrev\">上一段</span>\n            <span class=\"play-pause-btn\" @click=\"toggleSpeech\">\n              <i\n                class=\"el-icon-video-pause\"\n                :style=\"popupAbsoluteBtnStyle\"\n                v-if=\"speechSpeaking\"\n              ></i>\n              <i\n                class=\"el-icon-video-play\"\n                :style=\"popupAbsoluteBtnStyle\"\n                v-else\n              ></i>\n            </span>\n            <span class=\"ctrl-btn\" @click=\"speechNext\">下一段</span>\n          </div>\n          <div\n            class=\"collapse-btn\"\n            @click=\"showSpeechConfig = !showSpeechConfig\"\n          >\n            <i class=\"el-icon-bottom\" v-if=\"showSpeechConfig\"></i>\n            <i class=\"el-icon-top\" v-else></i>\n          </div>\n        </div>\n        <div class=\"setting-item\" v-if=\"showSpeechConfig\">\n          <div class=\"setting-title\">语音库</div>\n          <div class=\"setting-value\">\n            <div class=\"voice-list\">\n              <el-radio-group\n                v-model=\"voiceName\"\n                size=\"small\"\n                class=\"radio-group\"\n              >\n                <el-radio-button\n                  class=\"radio-button\"\n                  :label=\"voice.name\"\n                  :key=\"index\"\n                  v-for=\"(voice, index) in voiceList\"\n                ></el-radio-button>\n              </el-radio-group>\n            </div>\n          </div>\n        </div>\n        <div class=\"setting-item\" v-if=\"showSpeechConfig\">\n          <div class=\"setting-title\">语音设置</div>\n          <div class=\"setting-value\">\n            <div class=\"progress\">\n              <span class=\"progress-tip\">语速</span>\n              <div class=\"progress-bar\">\n                <el-slider\n                  v-model=\"speechRate\"\n                  :min=\"0.5\"\n                  :max=\"2\"\n                  :step=\"0.1\"\n                  :show-tooltip=\"false\"\n                  @change=\"changeSpeechRate\"\n                ></el-slider>\n              </div>\n              <span class=\"setting-btn\" @click=\"changeSpeechRate(1)\">重置</span>\n            </div>\n            <div class=\"progress\">\n              <span class=\"progress-tip\">语调</span>\n              <div class=\"progress-bar\">\n                <el-slider\n                  v-model=\"speechPitch\"\n                  :min=\"0\"\n                  :max=\"2\"\n                  :step=\"0.1\"\n                  :show-tooltip=\"false\"\n                  @change=\"changeSpeechPitch\"\n                ></el-slider>\n              </div>\n              <span class=\"setting-btn\" @click=\"changeSpeechPitch(1)\"\n                >重置</span\n              >\n            </div>\n            <div class=\"progress\">\n              <span class=\"progress-tip\">定时</span>\n              <div class=\"progress-bar\">\n                <el-slider\n                  v-model=\"speechMinutes\"\n                  :min=\"0\"\n                  :max=\"180\"\n                  :step=\"1\"\n                  :show-tooltip=\"false\"\n                  @change=\"changeSpeechMinutes\"\n                ></el-slider>\n              </div>\n              <span class=\"setting-btn\">{{ speechMinutes }}分钟</span>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"chapter\"\n      ref=\"content\"\n      :class=\"chapterClass\"\n      :style=\"chapterTheme\"\n    >\n      <div\n        class=\"click-zone\"\n        v-if=\"showClickZone\"\n        :style=\"!isSlideRead ? { position: 'fixed' } : {}\"\n      >\n        <div :style=\"showPrevPageStyle\"><span>点击前一页</span></div>\n        <div :style=\"showMenuZoneStyle\"><span>点击显示菜单</span></div>\n        <div :style=\"showNextPageStyle\"><span>点击后一页</span></div>\n        <div class=\"close-btn\" @click=\"showClickZone = false\">关闭</div>\n      </div>\n      <div class=\"top-bar\" ref=\"top\">\n        {{ $store.state.miniInterface ? title : \"\" }}\n      </div>\n      <div\n        class=\"content\"\n        @touchstart=\"handleTouchStart\"\n        @touchmove=\"handleTouchMove\"\n        @touchend=\"handleTouchEnd\"\n        @click=\"handlerClick\"\n      >\n        <div class=\"content-inner\" v-if=\"show\">\n          <Content\n            class=\"book-content\"\n            :title=\"title\"\n            :content=\"content\"\n            :showContent=\"show\"\n            :error=\"error\"\n            :style=\"contentStyle\"\n            :showChapterList=\"showChapterList\"\n            :isScrollRead=\"isScrollRead\"\n            ref=\"bookContentRef\"\n            @prevChapter=\"toLastChapter\"\n            @nextChapter=\"toNextChapter\"\n            @updateProgress=\"saveReadingPosition\"\n            @iframeLoad=\"$emit('iframeLoad')\"\n            @contentChange=\"computePages()\"\n            @epubClick=\"eventHandler\"\n            @epubLocationChange=\"epubLocationChangeHandler\"\n            @epubClickHash=\"epubClickHash\"\n            @epubKeydown=\"keydownHandler($event, true)\"\n          />\n        </div>\n      </div>\n      <div class=\"bottom-bar\" ref=\"bottom\">\n        <span v-if=\"isSlideRead\">{{\n          `第${currentPage}/${totalPages}页 ${readingProgress}`\n        }}</span>\n        <span v-if=\"isSlideRead\">{{ timeStr }}</span>\n        <span\n          class=\"bottom-btn\"\n          v-if=\"show && !isSlideRead && !error && !isScrollRead\"\n          @click=\"toNextChapter()\"\n          >加载下一章</span\n        >\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport PopCata from \"../components/PopCatalog.vue\";\nimport ReadSettings from \"../components/ReadSettings.vue\";\nimport BookSource from \"../components/BookSource.vue\";\nimport BookShelf from \"../components/BookShelf.vue\";\nimport Content from \"../components/Content.vue\";\nimport Axios from \"../plugins/axios\";\nimport jump from \"../plugins/jump\";\nimport Animate from \"../plugins/animate\";\nimport { setCache, getCache } from \"../plugins/cache\";\nimport { simplized, traditionalized } from \"../plugins/chinese\";\nimport {\n  LimitResquest,\n  networkFirstRequest,\n  editDistance\n} from \"../plugins/helper\";\nimport { defaultReplaceRule, defaultBookmark } from \"../plugins/config.js\";\nimport eventBus from \"../plugins/eventBus\";\n// eslint-disable-next-line no-useless-escape\nconst symboRegex = /[\\u2000-\\u206F\\u2E00-\\u2E7F\\\\'!\"#$%&\\(\\)*+,-\\./:;<=>?@\\[\\]^_`{\\|}~，。？《》；：、«]/g;\n\nexport default {\n  components: {\n    PopCata,\n    BookSource,\n    BookShelf,\n    Content,\n    ReadSettings\n  },\n  mounted() {\n    window.readerPage = this;\n    this.speechAvalable =\n      window.speechSynthesis && window.speechSynthesis.getVoices;\n    if (this.speechAvalable) {\n      this.fetchVoiceList();\n      if (window.speechSynthesis.onvoiceschanged !== undefined) {\n        window.speechSynthesis.onvoiceschanged = this.fetchVoiceList;\n      }\n    }\n    window.addEventListener(\"unload\", this.saveReadingPosition);\n    eventBus.$on(\"showSearchContent\", data => {\n      if (this._inactive) {\n        return;\n      }\n      if (this.chapterIndex === data.chapterIndex) {\n        this.showMatchKeyword(data);\n        return;\n      }\n      if (this.isScrollRead) {\n        this.scrollStartChapterIndex = data.chapterIndex;\n        this.computeShowChapterList().then(() => {\n          this.showMatchKeyword(data);\n        });\n        return;\n      }\n      this.$once(\"showContent\", () => {\n        this.showMatchKeyword(data);\n      });\n      this.getContent(data.chapterIndex);\n    });\n    eventBus.$on(\"showBookmark\", bookmark => {\n      if (this._inactive) {\n        return;\n      }\n      // console.log(this.chapterIndex, bookmark);\n      if (this.chapterIndex === bookmark.chapterIndex) {\n        this.showBookmark(bookmark);\n        return;\n      }\n      if (this.isScrollRead) {\n        this.scrollStartChapterIndex = bookmark.chapterIndex;\n        this.computeShowChapterList().then(() => {\n          this.showBookmark(bookmark);\n        });\n        return;\n      }\n      this.$once(\"showContent\", () => {\n        this.showBookmark(bookmark);\n      });\n      this.getContent(bookmark.chapterIndex);\n    });\n  },\n  activated() {\n    this.init();\n    window.addEventListener(\"keydown\", this.keydownHandler);\n    if (this.title) {\n      document.title =\n        this.$store.getters.readingBook.name + \" - \" + this.title;\n    } else {\n      document.title = this.$store.getters.readingBook.name;\n    }\n    this.formatTime();\n    this.timer = setInterval(() => {\n      this.formatTime();\n    }, 5000);\n    this.unwatchFn = this.$store.watch(\n      state => state.config,\n      () => {\n        this.$nextTick(() => {\n          this.computePages(() => {\n            if (this.currentPage > this.totalPages) {\n              this.showPage(this.totalPages, 0);\n            }\n          });\n        });\n      },\n      {\n        deep: true\n      }\n    );\n    window.addEventListener(\"scroll\", this.scrollHandler);\n    try {\n      this.releaseWakeLockFn = this.wakeLock();\n    } catch (e) {\n      //\n    }\n    this.$Lazyload.$on(\"loaded\", this.lazyloadHandler);\n  },\n  deactivated() {\n    this.saveBookProgress();\n    this.startSavePosition = false;\n    this.lastReadingBook = this.$store.getters.readingBook;\n    this.timer && clearInterval(this.timer);\n    window.removeEventListener(\"keydown\", this.keydownHandler);\n    window.removeEventListener(\"scroll\", this.scrollHandler);\n    this.unwatchFn && this.unwatchFn();\n    this.releaseWakeLockFn && this.releaseWakeLockFn();\n    this.$Lazyload.$off(\"loaded\", this.lazyloadHandler);\n  },\n  watch: {\n    chapterName(to) {\n      this.title = to;\n    },\n    content() {\n      this.contentStyle = {};\n      this.transformX = 0;\n      this.currentPage = 1;\n      this.$nextTick(() => {\n        this.computePages();\n        this.saveReadingPosition();\n      });\n      if (this.isEpub) {\n        this.$once(\"iframeLoad\", () => {\n          this.computePages();\n        });\n      }\n    },\n    readSettingsVisible(visible) {\n      if (!visible) {\n        //\n      }\n    },\n    title(title) {\n      if (title) {\n        document.title = this.$store.getters.readingBook.name + \" - \" + title;\n      } else {\n        document.title = this.$store.getters.readingBook.name;\n      }\n    },\n    isSlideRead(val) {\n      if (!val) {\n        this.contentStyle = {};\n        this.transformX = 0;\n      }\n      this.$nextTick(() => {\n        this.computePages(() => {\n          if (this.currentParagraph) {\n            this.showParagraph(this.currentParagraph, true);\n          } else {\n            this.showPage(this.currentPage, 0);\n          }\n        });\n      });\n    },\n    isScrollRead(val) {\n      if (val) {\n        this.scrollStartChapterIndex = this.chapterIndex;\n        this.computeShowChapterList();\n      }\n    },\n    windowSize() {\n      this.$nextTick(() => {\n        this.computePages(() => {\n          this.showPage(this.currentPage, 0);\n        });\n      });\n    },\n    loginAuth(val) {\n      if (val) {\n        this.init(true);\n      }\n    },\n    showReadBar(val) {\n      if (val) {\n        this.showToolBar = false;\n      }\n    },\n    readingBook(val, oldVal) {\n      if (val.bookUrl !== oldVal.bookUrl) {\n        this.startSavePosition = false;\n        this.autoShowPosition();\n      }\n    },\n    currentPage(val, oldVal) {\n      // 还剩两页的时候，预读下一章节\n      if (val !== oldVal && val >= this.totalPages - 2) {\n        if (\n          this.$store.getters.readingBook.index <\n          this.$store.getters.readingBook.catalog.length - 1\n        ) {\n          if (!this.isScrollRead) {\n            if (!this.preCaching) {\n              this.preCaching = true;\n              this.getBookContent(\n                this.$store.getters.readingBook.index + 1,\n                {\n                  timeout: 30000,\n                  silent: true\n                },\n                false,\n                true\n              ).then(() => {\n                this.preCaching = false;\n              });\n            }\n          }\n        }\n      }\n    },\n    filterRules() {\n      if (this.isScrollRead) {\n        //\n        this.computeShowChapterList();\n      } else {\n        this.content = this.filterContent(this.content);\n      }\n    },\n    chineseFont() {\n      this.title = this.filterContent(this.title);\n      this.content = this.filterContent(this.content);\n      this.computeShowChapterList();\n    }\n  },\n  data() {\n    return {\n      title: \"\",\n      content: \"\",\n      error: false,\n      popCataVisible: false,\n      readSettingsVisible: false,\n      popBookSourceVisible: false,\n      popBookShelfVisible: false,\n      showToolBar: true,\n      book: null,\n      show: false,\n      contentStyle: {},\n      currentPage: 1,\n      totalPages: 1,\n      transformX: 0,\n      transforming: false,\n      showLastPage: false,\n      showClickZone: false,\n      timeStr: \"\",\n      progressValue: 1,\n\n      speechAvalable: false,\n      showReadBar: false,\n      voiceList: [],\n      speechSpeaking: false,\n      showSpeechConfig: true,\n\n      currentParagraph: null,\n\n      startSavePosition: false,\n\n      showCacheContentZone: false,\n      isCachingContent: false,\n      cachingContentTip: \"\",\n\n      autoReading: false,\n      showChapterList: [],\n\n      scrollStartChapterIndex: 0,\n      showNextChapterSize: 1,\n      showPrevChapterSize: 0,\n\n      speechMinutes: 0,\n      speechEndTime: 0\n    };\n  },\n  computed: {\n    readingBook() {\n      return this.$store.getters.readingBook || {};\n    },\n    catalog() {\n      return (this.$store.getters.readingBook || {}).catalog || [];\n    },\n    chapterIndex() {\n      return ((this.$store.getters.readingBook || {}).index || 0) | 0;\n    },\n    windowSize() {\n      return this.$store.state.windowSize;\n    },\n    config() {\n      return this.$store.getters.config;\n    },\n    theme() {\n      return this.config.theme;\n    },\n    animateMSTime() {\n      return this.config.animateMSTime;\n    },\n    isNight() {\n      return this.$store.getters.isNight;\n    },\n    bodyTheme() {\n      return {\n        background: this.$store.getters.currentThemeConfig.body\n      };\n    },\n    isSlideRead() {\n      return this.autoReading ||\n        this.showReadBar ||\n        this.isEpub ||\n        this.isCarToon ||\n        this.isAudio\n        ? false\n        : this.$store.getters.isSlideRead;\n    },\n    isScrollRead() {\n      return (\n        !this.isEpub &&\n        !this.isAudio &&\n        !this.isSlideRead &&\n        (this.config.readMethod === \"上下滚动\" ||\n          this.config.readMethod === \"上下滚动2\")\n      );\n    },\n    chapterClass() {\n      return this.isSlideRead\n        ? \"slide-reader\"\n        : this.isEpub\n        ? \"epub\"\n        : this.isCarToon\n        ? \"cartoon\"\n        : this.isAudio\n        ? \"audio\"\n        : \"\";\n    },\n    chapterTheme() {\n      let readingStyle = this.showReadBar\n        ? { paddingBottom: (this.showSpeechConfig ? 280 : 80) + \"px\" }\n        : {};\n      if (typeof this.$store.getters.currentThemeConfig.content === \"string\") {\n        return {\n          ...readingStyle,\n          background: this.$store.getters.currentThemeConfig.content,\n          width: this.readWidth\n        };\n      } else {\n        return {\n          ...readingStyle,\n          ...this.$store.getters.currentThemeConfig.content,\n          width: this.readWidth\n        };\n      }\n    },\n    leftBarTheme() {\n      return {\n        background: this.$store.getters.currentThemeConfig.popup,\n        marginLeft: this.$store.state.miniInterface\n          ? 0\n          : -(this.readWidthConfig / 2 + 68) + \"px\",\n        display:\n          this.$store.state.miniInterface && !this.showToolBar\n            ? \"none\"\n            : \"block\"\n      };\n    },\n    rightBarTheme() {\n      return {\n        background: this.$store.getters.currentThemeConfig.popupPure,\n        marginRight: this.$store.state.miniInterface\n          ? 0\n          : -(this.readWidthConfig / 2 + 52) + \"px\",\n        display:\n          this.$store.state.miniInterface && !this.showToolBar\n            ? \"none\"\n            : \"block\"\n      };\n    },\n    readBarTheme() {\n      return {\n        background: this.$store.getters.currentThemeConfig.popupPure,\n        marginRight: this.$store.state.miniInterface\n          ? 0\n          : -(this.readWidthConfig / 2) + \"px\",\n        zIndex: 200,\n        display: this.speechAvalable && this.showReadBar ? \"block\" : \"none\",\n        width: this.$store.state.miniInterface ? \"100vw\" : \"500px\"\n      };\n    },\n    readWidth() {\n      if (!this.$store.state.miniInterface) {\n        return this.readWidthConfig - 130 + \"px\";\n      } else {\n        return this.windowSize.width + \"px\";\n      }\n    },\n    readWidthConfig() {\n      var width = this.$store.getters.config.readWidth;\n      while (width > this.$store.state.windowSize.width - 140) {\n        width -= 20;\n      }\n      return width;\n    },\n    popperWidth() {\n      if (!this.$store.state.miniInterface) {\n        return this.readWidthConfig - 33;\n      } else {\n        return this.windowSize.width - 33;\n      }\n    },\n    readingProgress() {\n      if (this.catalog && this.catalog.length) {\n        return (\n          parseInt(((this.chapterIndex + 1) * 100) / this.catalog.length) + \"%\"\n        );\n      } else {\n        return \"\";\n      }\n    },\n    showPrevPageStyle() {\n      if (this.isSlideRead) {\n        // 左半部\n        return {\n          left: 0,\n          top: 0,\n          bottom: 0,\n          right: this.windowSize.width / 2 + \"px\",\n          background: \"#43987324\",\n          paddingRight: this.windowSize.width * 0.2 + \"px\"\n        };\n      } else {\n        // 上半部\n        return {\n          left: 0,\n          top: 0,\n          right: 0,\n          bottom: this.windowSize.height / 2 + \"px\",\n          background: \"#43987324\"\n        };\n      }\n    },\n    showMenuZoneStyle() {\n      return {\n        top: this.windowSize.height * 0.3 + \"px\",\n        bottom: this.windowSize.height * 0.3 + \"px\",\n        left: this.windowSize.width * 0.3 + \"px\",\n        right: this.windowSize.width * 0.3 + \"px\",\n        background: \"#636060\",\n        zIndex: 10\n      };\n    },\n    showNextPageStyle() {\n      if (this.isSlideRead) {\n        // 右半部\n        return {\n          right: 0,\n          top: 0,\n          bottom: 0,\n          left: this.windowSize.width / 2 + \"px\",\n          background: \"#6b1a7324\",\n          paddingLeft: this.windowSize.width * 0.2 + \"px\"\n        };\n      } else {\n        // 下半部\n        return {\n          left: 0,\n          bottom: 0,\n          right: 0,\n          top: this.windowSize.height / 2 + \"px\",\n          background: \"#6b1a7324\"\n        };\n      }\n    },\n    loginAuth() {\n      return this.$store.state.loginAuth;\n    },\n    filterRules() {\n      return this.$store.state.filterRules;\n    },\n    themeBtnStyle() {\n      // if (this.$store.getters.isNight) {\n      //   return {\n      //     background: \"#f7f7f7\"\n      //   };\n      // } else {\n      //   return {\n      //     background: \"#222\"\n      //   };\n      // }\n      return {\n        background: this.$store.getters.currentThemeConfig.popupPure\n      };\n    },\n    popupAbsoluteBtnStyle() {\n      return {\n        background: this.$store.getters.currentThemeConfig.popupPure\n      };\n    },\n    voiceName: {\n      get() {\n        return this.$store.state.speechVoiceConfig.voiceName;\n      },\n      set(val) {\n        if (val !== this.$store.state.speechVoiceConfig.voiceName) {\n          if (this.speechSpeaking) {\n            this.restartSpeech();\n          }\n        }\n        this.$store.commit(\"setSpeechVoiceConfig\", {\n          ...this.$store.state.speechVoiceConfig,\n          voiceName: val\n        });\n      }\n    },\n    speechRate: {\n      get() {\n        return this.$store.state.speechVoiceConfig.speechRate;\n      },\n      set(val) {\n        if (val !== this.$store.state.speechVoiceConfig.speechRate) {\n          if (this.speechSpeaking) {\n            this.restartSpeech();\n          }\n        }\n        this.$store.commit(\"setSpeechVoiceConfig\", {\n          ...this.$store.state.speechVoiceConfig,\n          speechRate: val\n        });\n      }\n    },\n    speechPitch: {\n      get() {\n        return this.$store.state.speechVoiceConfig.speechPitch;\n      },\n      set(val) {\n        if (val !== this.$store.state.speechVoiceConfig.speechPitch) {\n          if (this.speechSpeaking) {\n            this.restartSpeech();\n          }\n        }\n        this.$store.commit(\"setSpeechVoiceConfig\", {\n          ...this.$store.state.speechVoiceConfig,\n          speechPitch: val\n        });\n      }\n    },\n    isCarToon() {\n      return (\n        !this.error &&\n        !this.isEpub &&\n        !this.isCbz &&\n        (this.content || \"\").indexOf(\"<img\") >= 0\n      );\n    },\n    isAudio() {\n      return !this.error && this.$store.getters.readingBook.type === 1;\n    },\n    isEpub() {\n      return (\n        !this.error &&\n        this.$store.getters.readingBook.bookUrl.toLowerCase().endsWith(\".epub\")\n      );\n    },\n    isCbz() {\n      return (\n        !this.error &&\n        this.$store.getters.readingBook.bookUrl.toLowerCase().endsWith(\".cbz\")\n      );\n    },\n    scrollOffset() {\n      // 两行 + 两个段间距\n      return (\n        this.$store.getters.config.fontSize *\n          this.$store.getters.config.lineHeight *\n          2 +\n        this.$store.getters.config.fontSize *\n          this.$store.getters.config.paragraphSpace *\n          2\n      );\n    },\n    formatedTitle() {\n      return this.formatChinese(this.title);\n    },\n    chineseFont() {\n      return this.config.chineseFont;\n    }\n  },\n  methods: {\n    init(refresh) {\n      if (this.$store.getters.readingBook) {\n        if (\n          refresh ||\n          !this.lastReadingBook ||\n          this.lastReadingBook.bookUrl !==\n            this.$store.getters.readingBook.bookUrl\n        ) {\n          this.title = \"\";\n          this.show = false;\n          this.loading = this.$loading({\n            target: this.$refs.content,\n            lock: true,\n            text: \"正在获取内容\",\n            spinner: \"el-icon-loading\",\n            background: \"rgba(0,0,0,0)\"\n          });\n          this.lastReadingBook = this.$store.getters.readingBook;\n          // 跳转记住的位置\n          this.autoShowPosition();\n          this.loadCatalog(false, true);\n        } else {\n          if (this.isScrollRead) {\n            this.scrollStartChapterIndex = this.chapterIndex;\n            this.showPrevChapterSize = 0;\n            this.computeShowChapterList().then(() => {\n              this.autoShowPosition(true);\n            });\n          } else if (this.isEpub) {\n            // 跳转记住的位置\n            this.autoShowPosition(true);\n          } else {\n            this.startSavePosition = true;\n          }\n          setTimeout(() => {\n            // console.log(\"setReadingBook\", this.lastReadingBook);\n            this.$store.commit(\"setReadingBook\", this.lastReadingBook);\n          }, 100);\n        }\n      } else {\n        this.$message.error(\"请在书架选择书籍\");\n      }\n    },\n    changeBook(book) {\n      this.$message.info(\"换书成功\");\n      this.popBookShelfVisible = false;\n      this.show = false;\n      this.$store.commit(\"setReadingBook\", book);\n      this.loadCatalog(true, true);\n    },\n    changeBookSource() {\n      this.popBookSourceVisible = false;\n      this.show = false;\n      this.tryRefresh = false;\n      // TODO 使用相似度比较，校正章节index\n      this.loadCatalog(true, true);\n    },\n    loadCatalog(refresh, init) {\n      if (!this.api) {\n        setTimeout(() => {\n          if (this.loadCatalog) {\n            this.loadCatalog(refresh);\n          }\n        }, 1000);\n        return;\n      }\n      this.getCatalog(refresh).then(\n        res => {\n          if (res.data.isSuccess) {\n            var book = Object.assign({}, this.$store.getters.readingBook);\n            book.catalog = res.data.data;\n            this.$store.commit(\"setReadingBook\", book);\n            this.$emit(\"loadCatalog\");\n            var index = book.index || 0;\n            this.getContent(index);\n          } else {\n            if (init) {\n              this.title = \"\";\n              this.content = \"获取章节目录失败！\\n\" + res.data.errorMsg;\n              this.error = true;\n              this.show = true;\n              this.$emit(\"showContent\");\n            }\n            this.loading.close();\n          }\n        },\n        error => {\n          this.loading.close();\n          this.$message.error(\n            \"获取书籍目录列表 \" + (error && error.toString())\n          );\n        }\n      );\n    },\n    getCatalog(refresh) {\n      const params = {\n        url: this.$store.getters.readingBook.bookUrl,\n        refresh: refresh ? 1 : 0\n      };\n      if (this.$route.query.search) {\n        // 来自搜索结果，请求需要带上 书源链接\n        params.bookSourceUrl = this.$store.getters.readingBook.origin;\n      }\n      return networkFirstRequest(\n        () => Axios.post(this.api + \"/getChapterList\", params),\n        this.$store.getters.readingBook.name +\n          \"_\" +\n          this.$store.getters.readingBook.author +\n          \"@\" +\n          this.$store.getters.readingBook.bookUrl +\n          \"@chapterList\"\n      );\n    },\n    refreshCatalog() {\n      return this.loadCatalog(true);\n    },\n    getBookContent(chapterIndex, options, refresh, cache) {\n      return this.$root.$children[0].getBookContent(\n        chapterIndex,\n        options,\n        refresh,\n        cache\n      );\n    },\n    refreshContent() {\n      this.getContent(this.$store.getters.readingBook.index, true);\n    },\n    getContent(index, refresh) {\n      //展示进度条\n      this.show = false;\n      if (!this.loading || !this.loading.visible) {\n        this.loading = this.$loading({\n          target: this.$refs.content,\n          lock: true,\n          text: refresh ? \"正在刷新内容\" : \"正在获取内容\",\n          spinner: \"el-icon-loading\",\n          background: \"rgba(0,0,0,0)\"\n        });\n      }\n      let bookUrl = this.$store.getters.readingBook.bookUrl;\n      try {\n        // 保存阅读进度\n        let book = { ...this.$store.getters.readingBook };\n        book.index = index;\n        this.$store.commit(\"setReadingBook\", book);\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n      }\n      //强制滚回顶层\n      this.toTop(0);\n      // 如果超出目录范围，尝试刷新目录\n      if (!this.$store.getters.readingBook.catalog[index]) {\n        if (this.tryRefresh) {\n          this.tryRefresh = false;\n          this.content = \"获取章节内容失败，请更新目录！\";\n          this.error = true;\n          this.show = true;\n          this.$emit(\"showContent\");\n          this.loading.close();\n        } else {\n          this.tryRefresh = true;\n          this.refreshCatalog();\n        }\n        return;\n      }\n      //let chapterUrl = this.$store.getters.readingBook.catalog[index].url;\n      let chapterName = this.$store.getters.readingBook.catalog[index].title;\n      let chapterIndex = this.$store.getters.readingBook.catalog[index].index;\n      this.title = chapterName;\n      const now = new Date().getTime();\n      this.getBookContent(chapterIndex, {}, refresh).then(\n        res => {\n          if (\n            bookUrl !== this.$store.getters.readingBook.bookUrl ||\n            index !== this.$store.getters.readingBook.index\n          ) {\n            // 已经换书或者换章节了\n            return;\n          }\n          if (now + 100 > new Date().getTime()) {\n            // 不超过 100ms 假定为获取缓存，此时发送进度保存请求\n            this.saveBookProgress();\n          }\n          if (res.data.isSuccess) {\n            let data = res.data.data;\n            this.content = this.filterContent(data);\n            this.addChapterContentToCache({\n              bookUrl,\n              index: index,\n              title: chapterName,\n              content: res.data.data,\n              error: false\n            });\n            this.loading.close();\n            this.error = false;\n            this.show = true;\n            this.$emit(\"showContent\");\n          } else {\n            this.content = \"获取章节内容失败！\\n\" + res.data.errorMsg;\n            this.addChapterContentToCache({\n              bookUrl,\n              index: index,\n              title: chapterName,\n              content: \"获取章节内容失败！\\n\" + res.data.errorMsg,\n              error: true\n            });\n            this.error = true;\n            this.show = true;\n            this.$emit(\"showContent\");\n            this.loading.close();\n          }\n          if (this.isScrollRead) {\n            this.computeShowChapterList();\n          }\n        },\n        error => {\n          if (\n            bookUrl !== this.$store.getters.readingBook.bookUrl ||\n            index !== this.$store.getters.readingBook.index\n          ) {\n            // 已经换书或者换章节了\n            return;\n          }\n          this.content = \"获取章节内容失败！\\n\" + (error && error.toString());\n          this.addChapterContentToCache({\n            bookUrl,\n            index: index,\n            title: chapterName,\n            content: \"获取章节内容失败！\\n\" + (error && error.toString()),\n            error: true\n          });\n          this.error = true;\n          this.show = true;\n          this.$emit(\"showContent\");\n          this.loading.close();\n          this.$message.error(\n            \"获取章节内容失败 \" + (error && error.toString())\n          );\n          if (this.isScrollRead) {\n            this.computeShowChapterList();\n          }\n          throw error;\n        }\n      );\n    },\n    filterContent(content) {\n      if (this.isEpub || this.isAudio) {\n        return content;\n      }\n      if (!content) {\n        return content;\n      }\n      try {\n        this.filterRules.forEach(rule => {\n          if (\n            typeof rule.isEnabled !== \"undefined\" &&\n            rule.isEnabled === false\n          ) {\n            return;\n          }\n          const scope = rule.scope.split(\";\");\n          if (\n            scope[0] === \"*\" ||\n            scope[0] === this.$store.getters.readingBook.name\n          ) {\n            if (\n              scope.length == 1 ||\n              (scope.length > 1 &&\n                scope[1] === this.$store.getters.readingBook.bookUrl)\n            ) {\n              if (rule.isRegex) {\n                content = content.replace(\n                  new RegExp(rule.pattern, \"ig\"),\n                  rule.replacement\n                );\n              } else {\n                content = content.replace(rule.pattern, rule.replacement);\n              }\n            }\n          }\n        });\n      } catch (error) {\n        //\n      }\n      content.replace(/\\\\n+/g, \"\\n\");\n      content = this.formatChinese(content);\n      return content;\n    },\n    loadShowChapter(index, refresh) {\n      if (\n        !refresh &&\n        this.chapterContentCache &&\n        this.chapterContentCache.chapters[index] &&\n        !this.chapterContentCache.chapters[index].error\n      ) {\n        if (\n          index >= this.chapterIndex - this.showPrevChapterSize &&\n          index <= this.chapterIndex + this.showNextChapterSize\n        ) {\n          this.computeShowChapterList();\n        }\n        return Promise.resolve();\n      }\n      let bookUrl = this.$store.getters.readingBook.bookUrl;\n      if (!this.$store.getters.readingBook.catalog) {\n        return new Promise(resolve => {\n          this.$once(\"loadCatalog\", () => {\n            this.loadShowChapter(index, refresh).then(resolve);\n          });\n        });\n      }\n      // 如果超出目录范围，尝试刷新目录\n      if (!this.$store.getters.readingBook.catalog[index]) {\n        return Promise.reject(\"章节不存在\");\n      }\n      let chapterName = this.$store.getters.readingBook.catalog[index].title;\n      let chapterIndex = this.$store.getters.readingBook.catalog[index].index;\n      return this.getBookContent(chapterIndex, {}, refresh, true).then(\n        res => {\n          if (res.data.isSuccess) {\n            this.addChapterContentToCache({\n              bookUrl,\n              index: index,\n              title: chapterName,\n              content: res.data.data,\n              error: false\n            });\n          } else {\n            this.addChapterContentToCache({\n              bookUrl,\n              index: index,\n              title: chapterName,\n              content: \"获取章节内容失败！\\n\" + res.data.errorMsg,\n              error: true\n            });\n          }\n        },\n        error => {\n          this.addChapterContentToCache({\n            bookUrl,\n            index: index,\n            title: chapterName,\n            content: \"获取章节内容失败！\\n\" + (error && error.toString()),\n            error: true\n          });\n          throw error;\n        }\n      );\n    },\n    addChapterContentToCache(chapter) {\n      if (\n        !this.chapterContentCache ||\n        this.chapterContentCache.bookUrl !== this.readingBook.bookUrl\n      ) {\n        this.chapterContentCache = {\n          bookUrl: this.readingBook.bookUrl,\n          chapters: {}\n        };\n      }\n      if (\n        typeof this.chapterContentCache.chapters[chapter.index] ===\n          \"undefined\" || // 没有缓存\n        !chapter.error || // 当前内容正确\n        this.chapterContentCache.chapters[chapter.index].error // 缓存内容错误\n      ) {\n        // 查询是否卷名\n        chapter.isVolume = !!(this.readingBook.catalog[chapter.index] || {})\n          .isVolume;\n        this.chapterContentCache.chapters[chapter.index] = chapter;\n      }\n    },\n    computeShowChapterList(reset) {\n      if (!this.chapterContentCache) {\n        return new Promise(resolve => {\n          setTimeout(() => {\n            this.computeShowChapterList(reset).then(resolve);\n          }, 10);\n        });\n      }\n      if (!this.isScrollRead) {\n        return Promise.resolve();\n      }\n      const list = [];\n      let startIndex = this.scrollStartChapterIndex || this.chapterIndex;\n      if (this.config.readMethod === \"上下滚动2\") {\n        startIndex = this.chapterIndex - this.showPrevChapterSize;\n      }\n      const waitPromise = [];\n      for (\n        let i = startIndex;\n        i <= this.chapterIndex + this.showNextChapterSize;\n        i++\n      ) {\n        if (!this.chapterContentCache.chapters[i]) {\n          waitPromise.push(this.loadShowChapter(i));\n          continue;\n        }\n        list.push({\n          ...this.chapterContentCache.chapters[i],\n          content: this.filterContent(\n            this.chapterContentCache.chapters[i].content\n          )\n        });\n      }\n      if (waitPromise.length) {\n        return Promise.all(waitPromise).then(() => {\n          this.computeShowChapterList(reset);\n        });\n      }\n      this.saveReadingPosition();\n      // 暂停记录位置\n      this.startSavePosition = false;\n      // 记录当前章节\n      this.showChapterList = list;\n      this.$nextTick(() => {\n        this.computePages(() => {\n          if (reset) {\n            // 切换上下章节，滚动到顶部\n            this.toTop(0);\n            this.startSavePosition = true;\n          } else if (this.config.readMethod === \"上下滚动2\") {\n            this.autoShowPosition(true);\n          } else {\n            this.startSavePosition = true;\n          }\n        });\n      });\n    },\n    saveBookProgress() {\n      return Axios.post(\n        this.api + \"/saveBookProgress\",\n        {\n          url: this.$store.getters.readingBook.bookUrl,\n          index: this.chapterIndex\n        },\n        {\n          silent: true\n        }\n      ).catch(() => {});\n    },\n    toTop(interval) {\n      if (this.$store.state.miniInterface) {\n        this.scrollContent(\n          -(document.documentElement.scrollTop || document.body.scrollTop),\n          interval\n        );\n      } else {\n        jump(this.$refs.top, { duration: interval });\n      }\n    },\n    toBottom(interval) {\n      jump(this.$refs.bottom, { duration: interval });\n    },\n    toNextChapter(onError) {\n      if (\n        !this.$store.getters.readingBook ||\n        !this.$store.getters.readingBook.bookUrl ||\n        !this.$store.getters.readingBook.catalog\n      ) {\n        onError && onError();\n        return;\n      }\n      let index = this.$store.getters.readingBook.index;\n      index++;\n      if (\n        typeof this.$store.getters.readingBook.catalog[index] !== \"undefined\"\n      ) {\n        if (this.isScrollRead) {\n          this.scrollStartChapterIndex = index;\n          this.computeShowChapterList(true);\n          return;\n        }\n        this.getContent(index);\n      } else {\n        onError && onError();\n        this.$message.error(\"本章是最后一章\");\n      }\n    },\n    toLastChapter(onError) {\n      if (\n        !this.$store.getters.readingBook ||\n        !this.$store.getters.readingBook.bookUrl ||\n        !this.$store.getters.readingBook.catalog\n      ) {\n        onError && onError();\n        return;\n      }\n      let index = this.$store.getters.readingBook.index;\n      index--;\n      if (\n        typeof this.$store.getters.readingBook.catalog[index] !== \"undefined\"\n      ) {\n        if (this.isScrollRead) {\n          this.scrollStartChapterIndex = index;\n          this.computeShowChapterList(true);\n          return;\n        }\n        this.getContent(index);\n      } else {\n        this.$message.error(\"本章是第一章\");\n        onError && onError();\n      }\n    },\n    toShelf() {\n      this.$router.push(\"/\");\n    },\n    computePages(cb) {\n      if (!this.$refs.bookContentRef || !this.$refs.bookContentRef.$el) {\n        setTimeout(() => {\n          this.computePages(cb);\n        }, 30);\n        return;\n      }\n      if (this.isSlideRead) {\n        this.totalPages = Math.ceil(\n          this.$refs.bookContentRef.$el.scrollWidth /\n            (this.windowSize.width - 16)\n        );\n      } else {\n        this.totalPages = Math.ceil(\n          this.$refs.bookContentRef.$el.scrollHeight /\n            (this.windowSize.height - this.scrollOffset)\n        );\n      }\n      if (this.showLastPage) {\n        this.showPage(this.totalPages, 0);\n        this.showLastPage = false;\n      }\n      cb && cb();\n    },\n    nextPage(moveX) {\n      if (!this.show) {\n        return;\n      }\n      if (this.transforming) {\n        return;\n      }\n      if (this.isSlideRead) {\n        if (this.currentPage < this.totalPages) {\n          if (typeof moveX === \"undefined\") {\n            this.transformX =\n              -(this.windowSize.width - 16) * (this.currentPage - 1);\n          }\n          this.currentPage += 1;\n          this.transforming = true;\n          this.transform(\n            typeof moveX === \"undefined\"\n              ? -(this.windowSize.width - 16)\n              : moveX,\n            this.animateMSTime\n          );\n        } else {\n          this.toNextChapter(() => {\n            if (typeof moveX !== \"undefined\") {\n              // 没有下一章，但是已经做了动画，恢复\n              this.showPage(this.currentPage, 0);\n            }\n          });\n        }\n      } else {\n        if (\n          (document.documentElement.scrollTop || document.body.scrollTop) +\n            this.windowSize.height <\n          document.documentElement.scrollHeight\n        ) {\n          this.currentPage += 1;\n          const moveY = this.windowSize.height - this.scrollOffset;\n          this.transforming = true;\n          this.scrollContent(moveY, this.animateMSTime);\n        } else {\n          this.currentPage = 1;\n          this.toNextChapter();\n        }\n      }\n    },\n    prevPage(moveX) {\n      if (!this.show) {\n        return;\n      }\n      if (this.transforming) {\n        return;\n      }\n      if (this.isSlideRead) {\n        if (this.currentPage > 1) {\n          if (typeof moveX === \"undefined\") {\n            this.transformX =\n              -(this.windowSize.width - 16) * (this.currentPage - 1);\n          }\n          this.currentPage -= 1;\n          this.transforming = true;\n          this.transform(\n            typeof moveX === \"undefined\" ? this.windowSize.width - 16 : moveX,\n            this.animateMSTime\n          );\n        } else {\n          this.showLastPage = true;\n          this.toLastChapter(() => {\n            if (typeof moveX !== \"undefined\") {\n              // 没有下一章，但是已经做了动画，恢复\n              this.showPage(this.currentPage, 0);\n            }\n          });\n        }\n      } else {\n        if (\n          (document.documentElement.scrollTop || document.body.scrollTop) > 0\n        ) {\n          this.currentPage -= 1;\n          const moveY = -this.windowSize.height + this.scrollOffset;\n          this.transforming = true;\n          this.scrollContent(moveY, this.animateMSTime);\n        } else {\n          this.currentPage = 1;\n          this.toLastChapter();\n        }\n      }\n    },\n    showPage(page, duration) {\n      if (!this.show) {\n        return;\n      }\n      this.currentPage = Math.min(page, this.totalPages);\n      if (this.isSlideRead) {\n        const moveX =\n          -(this.windowSize.width - 16) * (this.currentPage - 1) -\n          this.transformX;\n        this.transform(\n          moveX,\n          typeof duration === \"undefined\" ? this.animateMSTime : duration\n        );\n      } else {\n        const moveY =\n          (this.windowSize.height - 10) * (this.currentPage - 1) -\n          (document.documentElement.scrollTop || document.body.scrollTop);\n        this.scrollContent(\n          moveY,\n          typeof duration === \"undefined\" ? this.animateMSTime : duration\n        );\n      }\n    },\n    transform(moveX, duration) {\n      const onEnd = () => {\n        this.contentStyle = {\n          transform: `translateX(${this.transformX + moveX}px)`\n        };\n        this.transformX += moveX;\n        this.transforming = false;\n        // 保存进度\n        setTimeout(this.saveReadingPosition, duration);\n      };\n      if (!duration) {\n        onEnd();\n        return;\n      }\n      const timing = Animate.Utils.makeEaseInOut(\n        Animate.Timings.power.bind(null, 3)\n      );\n\n      new Animate({\n        duration: duration || 500,\n        timing: timing,\n        draw: progress => {\n          this.contentStyle = {\n            transform: `translateX(${this.transformX + moveX * progress}px)`\n          };\n        },\n        onEnd\n      });\n    },\n    scrollContent(moveY, duration, isAccurate) {\n      // console.log(\"scrollContent\", moveY);\n      const lastScrollTop = isAccurate\n        ? 0\n        : document.documentElement.scrollTop || document.body.scrollTop;\n      const onEnd = () => {\n        document.documentElement.scrollTop = lastScrollTop + moveY;\n        document.body.scrollTop = lastScrollTop + moveY;\n        this.transforming = false;\n        // 保存进度\n        setTimeout(this.saveReadingPosition, duration);\n      };\n      if (!duration) {\n        onEnd();\n        return;\n      }\n      const timing = Animate.Utils.makeEaseInOut(\n        Animate.Timings.power.bind(null, 3)\n      );\n\n      new Animate({\n        duration: duration || 500,\n        timing: timing,\n        draw: progress => {\n          document.documentElement.scrollTop = lastScrollTop + moveY * progress;\n          document.body.scrollTop = lastScrollTop + moveY * progress;\n        },\n        onEnd\n      });\n    },\n    handlerClick(e) {\n      if (this.isEpub) {\n        return;\n      }\n      if (!this.lastTouch && !this.ignoreNextClick) {\n        this.eventHandler(e);\n      }\n      this.ignoreNextClick = false;\n    },\n    handleTouchStart(e) {\n      this.lastSelection = this.checkSelection();\n      if (this.lastSelection) {\n        return;\n      }\n      if (this.isAudio) {\n        return;\n      }\n      if (this.isEpub) {\n        return;\n      }\n      // e.preventDefault();\n      // e.stopPropagation();\n      this.lastTouch = false;\n      this.lastMoveX = false;\n      if (e.touches && e.touches[0]) {\n        this.lastTouch = e.touches[0];\n      }\n    },\n    handleTouchMove(e) {\n      if (this.checkSelection()) {\n        return;\n      }\n      if (e.touches && e.touches[0] && this.lastTouch) {\n        this.lastMoveY = e.touches[0].clientY - this.lastTouch.clientY;\n        if (this.isSlideRead) {\n          e.preventDefault();\n          e.stopPropagation();\n          const moveX = e.touches[0].clientX - this.lastTouch.clientX;\n          this.contentStyle = {\n            transform: `translateX(${this.transformX + moveX}px)`\n          };\n          this.lastMoveX = moveX;\n        }\n      }\n    },\n    handleTouchEnd() {\n      if (this.checkSelection(true)) {\n        return;\n      }\n      if (this.lastSelection) {\n        setTimeout(() => {\n          this.showTextFilterPrompt(this.lastSelection);\n          this.lastSelection = false;\n        }, 200);\n        return;\n      }\n      if (this.lastMoveX) {\n        this.transformX += this.lastMoveX;\n        if (this.lastMoveX > 0) {\n          // 上一页\n          this.prevPage(this.windowSize.width - 16 - this.lastMoveX);\n        } else {\n          // 下一页\n          this.nextPage(-(this.windowSize.width - 16) - this.lastMoveX);\n        }\n      } else if (Math.abs(this.lastMoveY) <= 3 && this.lastTouch) {\n        this.eventHandler(this.lastTouch);\n      }\n      setTimeout(() => {\n        this.lastTouch = false;\n        this.lastMoveX = false;\n        this.lastMoveY = false;\n      }, 300);\n    },\n    epubClickHash(rect) {\n      if (typeof rect.top !== \"undefined\") {\n        this.scrollContent(\n          rect.top -\n            (this.$store.state.miniInterface\n              ? this.getFirstParagraphPos().bottom\n              : 0) -\n            (window.webAppDistance | 0) -\n            (this.$store.state.safeArea.top | 0),\n          0,\n          true\n        );\n      }\n    },\n    epubLocationChangeHandler(url) {\n      function getPathname(path) {\n        const a = document.createElement(\"a\");\n        a.href = path;\n        return decodeURIComponent(a.pathname);\n      }\n      url = getPathname(url);\n      // 判断是否跳转了其他章节\n      const currentChapter = this.catalog[this.chapterIndex];\n      if (currentChapter) {\n        const chapterPrefix = this.content.replace(currentChapter.url, \"\");\n        const iframeUrlPath = url.replace(chapterPrefix, \"\");\n        let newChapterIndex = -1;\n        for (let i = 0; i < this.catalog.length; i++) {\n          if (this.catalog[i].url === iframeUrlPath) {\n            newChapterIndex = i;\n            break;\n          }\n        }\n        if (newChapterIndex >= 0) {\n          let book = { ...this.$store.getters.readingBook };\n          book.index = newChapterIndex;\n          this.$store.commit(\"setReadingBook\", book);\n          this.title = this.$store.getters.readingBook.catalog[\n            newChapterIndex\n          ].title;\n        }\n      }\n    },\n    eventHandler(point) {\n      // console.log(point);\n      if (this.checkSelection(true)) {\n        // 选择文本\n        this.ignoreNextClick = true;\n        return;\n      }\n      if (\n        this.popBookSourceVisible ||\n        this.popBookShelfVisible ||\n        this.popCataVisible ||\n        this.readSettingsVisible\n      ) {\n        if (this.isEpub) {\n          this.popBookSourceVisible = false;\n          this.popBookShelfVisible = false;\n          this.popCataVisible = false;\n          this.readSettingsVisible = false;\n        }\n        return;\n      }\n      if (this.isAudio) {\n        // 音频\n        // 点击中部区域显示菜单\n        if (!this.showReadBar) {\n          this.showToolBar = !this.showToolBar;\n        }\n        return;\n      }\n      if (this.autoReading) {\n        this.showToolBar = !this.showToolBar;\n        return;\n      }\n      // 根据点击位置判断操作\n      const midX = this.windowSize.width / 2;\n      const midY = this.windowSize.height / 2;\n      if (this.isEpub) {\n        point.clientY =\n          point.clientY +\n          45 -\n          (document.documentElement.scrollTop || document.body.scrollTop);\n      }\n      if (\n        Math.abs(point.clientY - midY) <= this.windowSize.height * 0.2 &&\n        Math.abs(point.clientX - midX) <= this.windowSize.width * 0.2\n      ) {\n        // 点击中部区域显示菜单\n        if (!this.showReadBar) {\n          this.showToolBar = !this.showToolBar;\n        }\n      } else if (this.$store.getters.config.clickMethod === \"下一页\") {\n        // 全屏点击下一页\n        this.showToolBar = false;\n        this.nextPage();\n        return;\n      } else if (this.$store.getters.config.clickMethod === \"不翻页\") {\n        // 全屏点击不翻页\n        this.showToolBar = !this.showToolBar;\n        return;\n      } else if (this.isSlideRead) {\n        if (point.clientX > midX) {\n          // 点击右侧，下一页\n          this.showToolBar = false;\n          this.nextPage();\n        } else if (point.clientX < midX) {\n          // 点击左侧，上一页\n          this.showToolBar = false;\n          this.prevPage();\n        }\n      } else {\n        if (point.clientY > midY) {\n          // 点击下部，下一页\n          this.showToolBar = false;\n          this.nextPage();\n        } else if (point.clientY < midY) {\n          // 点击上部，上一页\n          this.showToolBar = false;\n          this.prevPage();\n        }\n      }\n    },\n    keydownHandler(event, force) {\n      // console.log(\"keyup\", event);\n      if (\n        this.popBookSourceVisible ||\n        this.popBookShelfVisible ||\n        this.popCataVisible ||\n        this.readSettingsVisible ||\n        this.showTextFilterPrompting\n      ) {\n        return;\n      }\n      if (!force && document.activeElement !== document.body) {\n        return;\n      }\n      if (this.isAudio) {\n        return;\n      }\n      const keyCodeMap = {\n        37: \"ArrowLeft\",\n        38: \"ArrowUp\",\n        39: \"ArrowRight\",\n        40: \"ArrowDown\",\n        27: \"Escape\"\n      };\n      const eventKey = event.key || keyCodeMap[event.keyCode];\n      switch (eventKey) {\n        case \"ArrowLeft\":\n          event.preventDefault && event.preventDefault();\n          event.stopPropagation && event.stopPropagation();\n          this.showToolBar = false;\n          if (this.isSlideRead) {\n            this.prevPage();\n          } else {\n            this.toLastChapter();\n          }\n          break;\n        case \"ArrowRight\":\n          event.preventDefault && event.preventDefault();\n          event.stopPropagation && event.stopPropagation();\n          this.showToolBar = false;\n          if (this.isSlideRead) {\n            this.nextPage();\n          } else {\n            this.toNextChapter();\n          }\n          break;\n        case \"ArrowUp\":\n          event.preventDefault && event.preventDefault();\n          event.stopPropagation && event.stopPropagation();\n          this.showToolBar = false;\n          this.prevPage();\n          break;\n        case \"ArrowDown\":\n          event.preventDefault && event.preventDefault();\n          event.stopPropagation && event.stopPropagation();\n          this.showToolBar = false;\n          this.nextPage();\n          break;\n        case \"Escape\":\n          this.toShelf();\n          break;\n      }\n    },\n    formatProgressTip(value) {\n      return `第 ${value || this.progressValue}/${this.totalPages} 页`;\n    },\n    formatTime() {\n      const now = new Date();\n      const pad = v => (v >= 10 ? \"\" + v : \"0\" + v);\n      this.timeStr = pad(now.getHours()) + \":\" + pad(now.getMinutes());\n    },\n    checkSelection(show) {\n      let text = \"\";\n      if (window.getSelection) {\n        text = window.getSelection().toString();\n      } else if (document.selection && document.selection.type != \"Control\") {\n        text = document.selection.createRange().text;\n      }\n      if (text && show) {\n        setTimeout(() => {\n          if (\n            this.$store.getters.config.selectionAction === \"过滤弹窗\" ||\n            this.$store.getters.config.selectionAction === \"操作弹窗\"\n          ) {\n            this.showTextOperate(text);\n          }\n        }, 200);\n      }\n      return text;\n    },\n    async showTextOperate(text) {\n      const res = await this.$confirm(`请选择操作?`, \"提示\", {\n        confirmButtonText: \"添加过滤规则\",\n        cancelButtonText: \"添加书签\",\n        type: \"warning\",\n        closeOnClickModal: false,\n        closeOnPressEscape: false,\n        distinguishCancelAndClose: true\n      }).catch(action => {\n        return action === \"close\" ? \"close\" : false;\n      });\n      if (res === \"close\") {\n        return;\n      }\n      if (res) {\n        return this.showTextFilterPrompt(text);\n      } else {\n        return this.showAddBookmark(text);\n      }\n    },\n    async showTextFilterPrompt(text) {\n      if (this.showTextFilterPrompting) {\n        return;\n      }\n      if (!text.replace(/^\\s+/, \"\").replace(/\\s+$/, \"\")) {\n        return;\n      }\n\n      const replaceRule = Object.assign({}, defaultReplaceRule, {\n        name: \"文本替换\",\n        pattern: text,\n        replacement: \"\",\n        isRegex: false,\n        isEnabled: true,\n        scope:\n          this.$store.getters.readingBook.name +\n          \";\" +\n          this.$store.getters.readingBook.bookUrl\n      });\n      this.showTextFilterPrompting = true;\n      eventBus.$emit(\"showReplaceRuleForm\", replaceRule, true, () => {\n        this.showTextFilterPrompting = false;\n      });\n      // const h = this.$createElement;\n      // const bgColor = this.isNight ? \"#121212\" : \"#eee\";\n      // const preEle = h(\n      //   \"pre\",\n      //   {\n      //     key: \"\" + new Date().getTime(),\n      //     attrs: {\n      //       contenteditable: \"true\"\n      //     },\n      //     style: `margin-top: 10px;background: ${bgColor};padding: 10px;border: 1px solid ${bgColor};border-radius: 5px;white-space: pre-wrap;word-wrap: break-word;word-break: break-all;`\n      //   },\n      //   text\n      // );\n      // const result = await this.$prompt(\n      //   h(\"div\", null, [\n      //     h(\"p\", null, \"是否要将下列文字替换为输入内容:\"),\n      //     preEle\n      //   ]),\n      //   \"操作确认\",\n      //   {\n      //     inputPlaceholder: \"留空为过滤\"\n      //   }\n      // ).catch(() => {});\n      // if (result && result.action === \"confirm\") {\n      //   text = ((preEle.elm || {}).innerText || \"\")\n      //     .replace(/^\\s+/, \"\")\n      //     .replace(/\\s+$/, \"\");\n      //   if (text) {\n      //     this.$store.commit(\"addFilterRule\", {\n      //       name: \"文本替换\",\n      //       pattern: text,\n      //       replacement: result.value || \"\",\n      //       isRegex: false,\n      //       isEnabled: true,\n      //       scope:\n      //         this.$store.getters.readingBook.name +\n      //         \";\" +\n      //         this.$store.getters.readingBook.bookUrl\n      //     });\n      //   } else {\n      //     this.$message.error(\"过滤内容为空!\");\n      //   }\n      // }\n      // this.showTextFilterPrompting = false;\n    },\n    async showAddBookmark(text) {\n      if (this.showAddBookmarking) {\n        return;\n      }\n      let pureText = text.replace(/^\\s+/, \"\").replace(/\\s+$/, \"\");\n      // console.log(pureText);\n      const paragraph = this.getContentMatchParagraph(pureText, 1, 0.7);\n      if (!paragraph) {\n        this.$message.error(\"选择1-2段整段文字才能定位段落\");\n        return;\n      }\n      const paragraphLength = 5;\n      const paragraphTextLength = 150;\n      const paragraphList = [paragraph];\n      let bookText = paragraph.innerText + \"\\n\";\n      if (\n        paragraphList.length < paragraphLength &&\n        bookText.length < paragraphTextLength\n      ) {\n        // 补全内容\n        let paragraphIndex = -1;\n        const list = this.$refs.bookContentRef.$el.querySelectorAll(\"h3,p\");\n        for (let i = 0; i < list.length; i++) {\n          if (paragraphIndex > 0 && i > paragraphIndex) {\n            paragraphList.push(list[i]);\n            bookText += list[i].innerText + \"\\n\";\n            if (\n              paragraphList.length >= paragraphLength ||\n              bookText.length >= paragraphTextLength\n            ) {\n              break;\n            }\n          } else if (paragraphList[paragraphList.length - 1] === list[i]) {\n            paragraphIndex = i;\n          }\n        }\n      }\n      // console.log(paragraphList, bookText);\n      bookText = bookText.replace(/\\\\n*$/, \"\");\n\n      const bookmark = Object.assign({}, defaultBookmark, {\n        bookName: this.$store.getters.readingBook.name,\n        bookAuthor: this.$store.getters.readingBook.author,\n        chapterIndex: this.chapterIndex,\n        chapterPos: this.currentPage,\n        chapterName: this.title,\n        bookText: bookText,\n        content: \"\"\n      });\n      this.showAddBookmarking = true;\n      eventBus.$emit(\"showBookmarkForm\", bookmark, true, () => {\n        this.showAddBookmarking = false;\n      });\n    },\n    toogleNight() {\n      if (this.isNight) {\n        this.$store.commit(\"setNightTheme\", false);\n      } else {\n        this.$store.commit(\"setNightTheme\", true);\n      }\n    },\n    fetchVoiceList() {\n      this.voiceList = window.speechSynthesis.getVoices().sort((a, b) => {\n        if (a.lang.startsWith(\"zh-\") && b.lang.startsWith(\"zh-\")) {\n          return a.lang > b.lang ? 1 : a.lang < b.lang ? -1 : 0;\n        } else if (a.lang.startsWith(\"zh-\")) {\n          return -1;\n        } else if (b.lang.startsWith(\"zh-\")) {\n          return 1;\n        }\n        return a.lang > b.lang ? 1 : a.lang < b.lang ? -1 : 0;\n      });\n    },\n    changeSpeechRate(rate) {\n      this.speechRate = rate;\n    },\n    changeSpeechPitch(pitch) {\n      this.speechPitch = pitch;\n    },\n    changeSpeechMinutes(minute) {\n      this.speechMinutes = minute;\n      if (minute) {\n        this.speechEndTime = new Date().getTime() + minute * 60 * 1000;\n      } else {\n        this.speechEndTime = 0;\n      }\n    },\n    startSpeech() {\n      if (this.error) {\n        return;\n      }\n      if (!this.voiceName) {\n        return;\n      }\n      const voice = this.voiceList.find(v => v.name === this.voiceName);\n      if (!voice) {\n        return;\n      }\n\n      if (window.speechSynthesis.speaking) {\n        return;\n      }\n\n      if (this.speechSpeaking) {\n        if (\n          this.speechEndTime > 0 &&\n          new Date().getTime() > this.speechEndTime\n        ) {\n          this.$message.info(\"定时关闭朗读\");\n          return;\n        }\n      }\n\n      const paragraph = this.getCurrentParagraph();\n      if (!paragraph.innerText) {\n        this.speechNext();\n        return;\n      }\n      this.utterance = new SpeechSynthesisUtterance(paragraph.innerText);\n\n      this.utterance.onstart = () => {\n        this.speechSpeaking = true;\n        this.skipAutoNext = false;\n      };\n      this.utterance.onend = () => {\n        // 下一段\n        if (!this.skipAutoNext) {\n          this.speechNext();\n        } else {\n          this.skipAutoNext = false;\n          this.speechSpeaking = false;\n        }\n      };\n      this.utterance.onerror = event => {\n        if (event.error || event.name) {\n          this.$message.error(\n            `朗读错误:  ${event.type || \"\"}  ${event.error ||\n              event.name ||\n              event.toString()}`\n          );\n        }\n        this.speechSpeaking = window.speechSynthesis.speaking || false;\n      };\n      this.utterance.voice = voice;\n      this.utterance.pitch = this.speechPitch;\n      this.utterance.rate = this.speechRate;\n\n      this.showParagraph(paragraph, true);\n      paragraph.className = \"reading\";\n      this.speechSpeaking = true;\n      window.speechSynthesis.speak(this.utterance);\n    },\n    stopSpeech() {\n      try {\n        this.skipAutoNext = true;\n        window.speechSynthesis.cancel();\n        const current = this.getCurrentParagraph();\n        if (current) {\n          current.className = \"\";\n        }\n      } catch (error) {\n        //\n      }\n    },\n    restartSpeech() {\n      this.stopSpeech();\n      setTimeout(() => {\n        this.startSpeech();\n      }, 100);\n    },\n    toggleSpeech() {\n      this.speechSpeaking ? this.stopSpeech() : this.startSpeech();\n    },\n    speechPrev() {\n      if (window.speechSynthesis.speaking) {\n        this.stopSpeech();\n      }\n      const current = this.getCurrentParagraph();\n      const prev = this.getPrevParagraph();\n      if (prev) {\n        this.showParagraph(prev, true);\n        current.className = \"\";\n        prev.className = \"reading\";\n        this.startSpeech();\n      } else {\n        // 上一章\n        this.$once(\"showContent\", () => {\n          setTimeout(() => {\n            this.startSpeech();\n          }, 100);\n        });\n        this.toLastChapter();\n      }\n    },\n    speechNext() {\n      if (window.speechSynthesis.speaking) {\n        this.stopSpeech();\n      }\n      const current = this.getCurrentParagraph();\n      const next = this.getNextParagraph();\n      if (next) {\n        this.showParagraph(next, true);\n        current.className = \"\";\n        next.className = \"reading\";\n        this.startSpeech();\n      } else {\n        // 下一章\n        this.$once(\"showContent\", () => {\n          setTimeout(() => {\n            this.startSpeech();\n          }, 100);\n        });\n        this.toNextChapter();\n      }\n    },\n    getCurrentParagraph() {\n      const readingEle = this.$refs.bookContentRef.$el.querySelectorAll(\n        \".reading\"\n      );\n      let currentParagraph = null;\n      if (!readingEle.length) {\n        // 没有正在读的段落，遍历找到当前页面的第一段\n        const list = this.$refs.bookContentRef.$el.querySelectorAll(\"h3,p\");\n        for (let i = 0; i < list.length; i++) {\n          const elePos = list[i].getBoundingClientRect();\n          if (this.isSlideRead) {\n            // 段尾出现在视野里\n            if (elePos.right > 0) {\n              currentParagraph = list[i];\n              break;\n            }\n          } else {\n            // 段尾出现在视野里\n            if (\n              elePos.bottom >\n              30 +\n                20 +\n                (window.webAppDistance | 0) +\n                (this.$store.state.safeArea.top | 0)\n            ) {\n              currentParagraph = list[i];\n              break;\n            }\n          }\n        }\n      } else {\n        currentParagraph = readingEle[0];\n      }\n      return currentParagraph;\n    },\n    getPrevParagraph() {\n      const current = this.getCurrentParagraph();\n      const list = this.$refs.bookContentRef.$el.querySelectorAll(\"h3,p\");\n      for (let i = 0; i < list.length; i++) {\n        if (i > 0 && current === list[i]) {\n          return list[i - 1];\n        }\n      }\n      return null;\n    },\n    getNextParagraph() {\n      const current = this.getCurrentParagraph();\n      const list = this.$refs.bookContentRef.$el.querySelectorAll(\"h3,p\");\n      for (let i = 0; i < list.length; i++) {\n        if (current === list[i]) {\n          return list[i + 1];\n        }\n      }\n      return null;\n    },\n    exitRead() {\n      this.stopSpeech();\n      const current = this.getCurrentParagraph();\n      this.showReadBar = false;\n      this.showParagraph(current);\n    },\n    showParagraph(paragraph, scroll) {\n      if (!paragraph) {\n        return;\n      }\n      if (this.isSlideRead) {\n        // 跳转位置\n        this.$nextTick(() => {\n          const pos = paragraph.getBoundingClientRect();\n          if (pos.left > this.windowSize.width - 16) {\n            this.showPage(\n              Math.round(pos.left / (this.windowSize.width - 16)) + 1,\n              0\n            );\n          }\n        });\n      } else if (scroll) {\n        // 跳转位置\n        this.$nextTick(() => {\n          const pos = paragraph.getBoundingClientRect();\n          this.scrollContent(\n            pos.top -\n              (this.$store.state.miniInterface\n                ? this.getFirstParagraphPos().bottom\n                : 0) -\n              (window.webAppDistance | 0) -\n              (this.$store.state.safeArea.top | 0),\n            0\n          );\n        });\n      }\n    },\n    getFirstParagraphPos() {\n      return this.$refs.top.getBoundingClientRect();\n    },\n    scrollHandler() {\n      const scrollTop =\n        document.documentElement.scrollTop || document.body.scrollTop;\n      if (!this.isSlideRead) {\n        this.currentPage = Math.round(\n          (scrollTop + this.windowSize.height) /\n            (this.windowSize.height - this.scrollOffset)\n        );\n      }\n      if (this.isScrollRead) {\n        const lastScrollTop = this.lastScrollTop || 0;\n        if (lastScrollTop > 0 && scrollTop == 0) {\n          // 往上滚动到顶\n          // if (!this.preCaching) {\n          //   this.preCaching = true;\n          //   const prevIndex = this.showChapterList[0].index - 1;\n          //   if (prevIndex > 0) {\n          //     this.showPrevChapterSize = this.chapterIndex - prevIndex;\n          //     this.loadShowChapter(prevIndex).then(() => {\n          //       setTimeout(() => {\n          //         this.preCaching = false;\n          //       }, 3000);\n          //     });\n          //   }\n          // }\n        } else if (\n          scrollTop >\n          document.documentElement.scrollHeight - 2 * this.windowSize.height // 倒数第三页\n        ) {\n          // 往下滚动到 倒数第三页\n          if (!this.preCaching && this.startSavePosition) {\n            this.preCaching = true;\n            let nextIndex = this.chapterIndex + 1;\n            if (this.showChapterList.length) {\n              nextIndex =\n                this.showChapterList[this.showChapterList.length - 1].index + 1;\n            }\n            this.showNextChapterSize = nextIndex - this.chapterIndex;\n            // console.log(\"到底部了，加载下一章\");\n            this.loadShowChapter(nextIndex)\n              .then(() => {\n                this.computeShowChapterList();\n                this.preCaching = false;\n              })\n              .catch(() => {\n                this.preCaching = false;\n              });\n          }\n        }\n      }\n      this.lastScrollTop = scrollTop;\n      this.scrollTimer && clearTimeout(this.scrollTimer);\n      this.scrollTimer = setTimeout(this.saveReadingPosition, 100);\n    },\n    beforeReadMethodChange() {\n      this.currentParagraph = this.getCurrentParagraph();\n    },\n    // 只会在进入的时候调用\n    showPosition(pos, callback) {\n      if (this.isAudio) {\n        // seek\n        if (!this.$refs.bookContentRef) {\n          setTimeout(() => {\n            this.showPosition(pos, callback);\n          }, 10);\n          return;\n        }\n        this.$refs.bookContentRef.ensureSeekTime(pos);\n      } else if (this.isEpub || this.isCarToon) {\n        // 跳转\n        this.scrollContent(pos, 0, true);\n        if (this.isEpub) {\n          this.$once(\"iframeLoad\", () => {\n            this.scrollContent(pos, 0, true);\n            callback && callback();\n          });\n        }\n      } else {\n        if (!this.$refs.bookContentRef) {\n          setTimeout(() => {\n            this.showPosition(pos, callback);\n          }, 10);\n          return;\n        }\n        const list = this.$refs.bookContentRef.$el.querySelectorAll(\n          \".reading-chapter h3,p\"\n        );\n        for (let i = 0; i < list.length; i++) {\n          if (\n            list[i].dataset &&\n            typeof list[i].dataset.pos !== \"undefined\" &&\n            +list[i].dataset.pos >= pos\n          ) {\n            this.showParagraph(list[i], true);\n            break;\n          }\n        }\n        callback && callback();\n      }\n    },\n    saveReadingPosition() {\n      try {\n        if (this.error || !this.startSavePosition) {\n          return;\n        }\n        let position = 0;\n        if (this.isAudio) {\n          position = this.$refs.bookContentRef\n            ? this.$refs.bookContentRef.currentTime\n            : 0;\n        } else if (this.isEpub || this.isCarToon) {\n          position =\n            document.documentElement.scrollTop || document.body.scrollTop;\n        } else {\n          // 更新当前章节 和 当前段落\n          if (this.preCaching) {\n            return;\n          }\n          this.currentParagraph = this.getCurrentParagraph();\n          if (this.currentParagraph) {\n            // 找到最近的 .chapter-content\n            let currentChapter = this.currentParagraph;\n            while (currentChapter.className.indexOf(\"chapter-content\") < 0) {\n              currentChapter = currentChapter.parentNode;\n              if (currentChapter === this.$refs.bookContentRef.$el) {\n                break;\n              }\n            }\n            if (currentChapter) {\n              if (\n                currentChapter.dataset &&\n                typeof currentChapter.dataset.index !== \"undefined\"\n              ) {\n                const chapterIndex = +currentChapter.dataset.index;\n                if (chapterIndex != this.$store.getters.readingBook.index) {\n                  let book = { ...this.$store.getters.readingBook };\n                  book.index = chapterIndex;\n                  this.$store.commit(\"setReadingBook\", book);\n                  // 保存阅读进度\n                  this.saveBookProgress();\n                  this.title = this.$store.getters.readingBook.catalog[\n                    chapterIndex\n                  ].title;\n                }\n              }\n              position = currentChapter.innerText.indexOf(\n                this.currentParagraph.innerText\n              );\n            }\n          }\n        }\n        setCache(\n          \"bookChapterProgress@\" +\n            this.$store.getters.readingBook.name +\n            \"_\" +\n            this.$store.getters.readingBook.author,\n          position\n        );\n      } catch (error) {\n        //\n      }\n    },\n    autoShowPosition(immediate) {\n      const handler = () => {\n        setTimeout(() => {\n          this.startSavePosition = true;\n        }, 2000);\n        if (this.error) {\n          return;\n        }\n        const lastPosition = getCache(\n          \"bookChapterProgress@\" +\n            this.$store.getters.readingBook.name +\n            \"_\" +\n            this.$store.getters.readingBook.author\n        );\n        if (lastPosition && +lastPosition) {\n          this.$nextTick(() => {\n            this.showPosition(+lastPosition, () => {\n              this.startSavePosition = true;\n            });\n          });\n        }\n      };\n      if (immediate) {\n        handler();\n      } else {\n        this.$once(\"showContent\", handler);\n      }\n    },\n    wakeLock() {\n      if (\"WakeLock\" in window && \"request\" in window.WakeLock) {\n        let wakeLock = null;\n        const requestWakeLock = () => {\n          const controller = new AbortController();\n          const signal = controller.signal;\n          window.WakeLock.request(\"screen\", { signal }).catch(e => {\n            if (e.name === \"AbortError\") {\n              // console.log(\"Wake Lock was aborted\");\n            } else {\n              // console.error(`${e.name}, ${e.message}`);\n            }\n          });\n          // console.log(\"Wake Lock is active\");\n          return controller;\n        };\n\n        wakeLock = requestWakeLock();\n\n        const handleVisibilityChange = () => {\n          if (wakeLock !== null && document.visibilityState === \"visible\") {\n            wakeLock = requestWakeLock();\n          }\n        };\n\n        document.addEventListener(\"visibilitychange\", handleVisibilityChange);\n        document.addEventListener(\"fullscreenchange\", handleVisibilityChange);\n        return () => {\n          if (wakeLock != null) {\n            wakeLock.abort();\n            wakeLock = null;\n          }\n          document.removeEventListener(\n            \"visibilitychange\",\n            handleVisibilityChange\n          );\n          document.removeEventListener(\n            \"fullscreenchange\",\n            handleVisibilityChange\n          );\n        };\n      } else if (\"wakeLock\" in navigator && \"request\" in navigator.wakeLock) {\n        let wakeLock = null;\n        const requestWakeLock = async () => {\n          try {\n            wakeLock = await navigator.wakeLock.request(\"screen\");\n            wakeLock.addEventListener(\"release\", () => {\n              // console.log(\"Wake Lock was released\");\n            });\n            // console.log(\"Wake Lock is active\");\n          } catch (e) {\n            // console.error(`${e.name}, ${e.message}`);\n          }\n        };\n        requestWakeLock();\n        const handleVisibilityChange = () => {\n          if (wakeLock !== null && document.visibilityState === \"visible\") {\n            requestWakeLock();\n          }\n        };\n        document.addEventListener(\"visibilitychange\", handleVisibilityChange);\n        document.addEventListener(\"fullscreenchange\", handleVisibilityChange);\n        return () => {\n          if (wakeLock != null) {\n            wakeLock.release();\n            wakeLock = null;\n          }\n          document.removeEventListener(\n            \"visibilitychange\",\n            handleVisibilityChange\n          );\n          document.removeEventListener(\n            \"fullscreenchange\",\n            handleVisibilityChange\n          );\n        };\n      }\n    },\n    lazyloadHandler() {\n      if (!this.isAudio) {\n        this.computePages();\n      }\n    },\n    showCacheContent() {\n      this.showCacheContentZone = !this.showCacheContentZone;\n    },\n    cacheChapterContent(cacheCount) {\n      //\n      let cacheChapterList = [];\n      if (cacheCount === true) {\n        //\n        cacheChapterList = cacheChapterList.concat(\n          this.catalog.slice(this.chapterIndex + 1, this.catalog.length)\n        );\n      } else {\n        //\n        cacheChapterList = cacheChapterList.concat(\n          this.catalog.slice(\n            this.chapterIndex + 1,\n            Math.min(this.catalog.length, this.chapterIndex + 1 + cacheCount)\n          )\n        );\n      }\n      if (!cacheChapterList.length) {\n        this.$message.error(\"不需要缓存\");\n        return;\n      }\n      this.isCachingContent = true;\n      this.cachingContentTip = \"正在缓存章节  0/\" + cacheChapterList.length;\n      this.cachingHandler = LimitResquest(2, handler => {\n        this.cachingContentTip =\n          \"正在缓存章节  \" +\n          handler.requestCount +\n          \"/\" +\n          cacheChapterList.length;\n        if (handler.isEnd()) {\n          this.$message.success(\"缓存完成\");\n          this.isCachingContent = false;\n          this.cachingContentTip = \"\";\n        }\n      });\n      cacheChapterList.forEach(v => {\n        this.cachingHandler(() => {\n          return this.getBookContent(\n            v.index,\n            {\n              timeout: 30000,\n              silent: true\n            },\n            false,\n            true\n          );\n        });\n      });\n    },\n    cancelCaching() {\n      if (this.cachingHandler && this.cachingHandler.cancel) {\n        this.cachingHandler.cancel();\n        this.isCachingContent = false;\n        this.cachingContentTip = \"\";\n      }\n    },\n    startAutoReading() {\n      this.showToolBar = false;\n      this.autoReading = true;\n      this.autoRead();\n    },\n    autoRead() {\n      if (!this.autoReading) {\n        return;\n      }\n      if (this.showToolBar) {\n        this.autoReadingTimer = setTimeout(() => {\n          this.autoRead();\n        }, 300);\n        return;\n      }\n      if (this.config.autoReadingMethod === \"像素滚动\") {\n        this.autoReadByPixel();\n        return;\n      }\n      const current = this.getCurrentParagraph();\n      const next = this.getNextParagraph();\n      if (next) {\n        current.className = \"reading\";\n        next.className = \"\";\n        // 计算当前段落\n        let delayTime = this.config.autoReadingLineTime;\n        try {\n          const currentPos = current.getBoundingClientRect();\n          delayTime =\n            delayTime *\n            Math.ceil(\n              currentPos.height / this.config.fontSize / this.config.lineHeight\n            );\n        } catch (error) {\n          //\n        }\n        // console.log(delayTime, next);\n        this.autoReadingTimer = setTimeout(() => {\n          current.className = \"\";\n          next.className = \"reading\";\n          this.showParagraph(next, true);\n\n          setTimeout(() => {\n            this.autoRead();\n          }, 32);\n        }, delayTime);\n      } else {\n        // 下一章\n        this.$once(\"showContent\", () => {\n          setTimeout(() => {\n            this.autoRead();\n          }, 100);\n        });\n        this.toNextChapter(() => {\n          this.autoReading = false;\n        });\n      }\n    },\n    autoReadByPixel() {\n      if (!this.autoReading) {\n        return;\n      }\n      if (this.showToolBar) {\n        this.autoReadingTimer = setTimeout(() => {\n          this.autoRead();\n        }, 300);\n        return;\n      }\n      if (this.config.autoReadingMethod !== \"像素滚动\") {\n        this.autoRead();\n        return;\n      }\n      const scrollTop =\n        document.documentElement.scrollTop || document.body.scrollTop;\n      if (\n        scrollTop + this.windowSize.height <\n        document.documentElement.scrollHeight\n      ) {\n        // console.log(delayTime, next);\n        this.autoReadingTimer = setTimeout(() => {\n          // 滚动\n          this.scrollContent(this.config.autoReadingPixel, 0);\n          this.autoReadByPixel();\n        }, this.config.autoReadingLineTime);\n      } else {\n        // 下一章\n        this.$once(\"showContent\", () => {\n          setTimeout(() => {\n            this.autoReadByPixel();\n          }, 100);\n        });\n        this.toNextChapter(() => {\n          this.autoReading = false;\n        });\n      }\n    },\n    stopAutoReading() {\n      if (this.autoReadingTimer) {\n        clearInterval(this.autoReadingTimer);\n      }\n      this.autoReading = false;\n      const current = this.getCurrentParagraph();\n      current.className = \"\";\n    },\n    toggleAutoReading() {\n      if (this.autoReading) {\n        this.stopAutoReading();\n      } else {\n        this.startAutoReading();\n      }\n    },\n    showReadingBookInfo() {\n      let book = { ...this.$store.getters.readingBook };\n      const shelfBook = this.$store.getters.shelfBooks.find(\n        v => v.bookUrl === book.bookUrl\n      );\n      book = Object.assign(book, shelfBook || {});\n      eventBus.$emit(\"showBookInfoDialog\", book);\n    },\n    formatChinese(text) {\n      if (this.isEpub || this.isAudio || this.isCbz || this.isCarToon) {\n        return text;\n      }\n      if (this.config.chineseFont === \"简体\") {\n        return simplized(text);\n      } else {\n        return traditionalized(text);\n      }\n    },\n    showSearchBookContentDialog() {\n      let book = { ...this.$store.getters.readingBook };\n      const shelfBook = this.$store.getters.shelfBooks.find(\n        v => v.bookUrl === book.bookUrl\n      );\n      book = Object.assign(book, shelfBook || {});\n      eventBus.$emit(\"showSearchBookContentDialog\", book);\n    },\n    showMatchKeyword(data) {\n      if (this._inactive) {\n        return;\n      }\n      if (!this.$refs.bookContentRef) {\n        setTimeout(() => {\n          this.showMatchKeyword(data);\n        }, 10);\n        return;\n      }\n      try {\n        const list = this.$refs.bookContentRef.$el.querySelectorAll(\n          \".reading-chapter h3,p\"\n        );\n        let matchCount = 0;\n        for (let i = 0; i < list.length; i++) {\n          const pContent = list[i].innerText;\n          let startIndex = -1;\n          let isFound = false;\n          // eslint-disable-next-line no-constant-condition\n          while (true) {\n            startIndex = pContent.indexOf(data.query, startIndex + 1);\n            if (startIndex >= 0) {\n              matchCount++;\n              if (matchCount === data.resultCountWithinChapter + 1) {\n                isFound = true;\n                this.showParagraph(list[i], true);\n                break;\n              }\n            } else {\n              break;\n            }\n          }\n          if (isFound) {\n            break;\n          }\n        }\n      } catch (error) {\n        // console.error(error);\n      }\n    },\n    getParagraphListInView() {\n      // 获取视口内的所有段落\n      const list = this.$refs.bookContentRef.$el.querySelectorAll(\"h3,p\");\n      const paragraphList = [];\n      for (let i = 0; i < list.length; i++) {\n        const elePos = list[i].getBoundingClientRect();\n        if (this.isSlideRead) {\n          // 段尾出现在视野里\n          if (elePos.right > 0 && elePos.left > 0) {\n            paragraphList.push(list[i]);\n          }\n        } else {\n          // 段尾出现在视野里\n          if (\n            elePos.bottom >\n              30 +\n                20 +\n                (window.webAppDistance | 0) +\n                (this.$store.state.safeArea.top | 0) &&\n            elePos.bottom < this.windowSize.height\n          ) {\n            paragraphList.push(list[i]);\n          }\n        }\n      }\n      return paragraphList;\n    },\n    showBookmarkDialog() {\n      let book = { ...this.$store.getters.readingBook };\n      const shelfBook = this.$store.getters.shelfBooks.find(\n        v => v.bookUrl === book.bookUrl\n      );\n      book = Object.assign(book, shelfBook || {});\n      eventBus.$emit(\"showBookmarkDialog\", book);\n    },\n    getContentMatchParagraph(text, distance, minDistance) {\n      distance = distance || 0.7;\n      // 正则过滤标点符号后，近似匹配每一段内容\n      let paragraphList = text\n        .replace(/\\\\n+/g, \"\\n\")\n        .split(/\\n+/)\n        .map(v => v.replace(symboRegex, \"\"))\n        .filter(v => v);\n      try {\n        const list = this.$refs.bookContentRef.$el.querySelectorAll(\n          \".reading-chapter h3,p\"\n        );\n        let paragraph = null;\n        for (let i = 0; i < list.length; i++) {\n          let isMatch = true;\n          let pos = 0;\n          let startPos = i;\n          for (let j = 0; j < paragraphList.length; j++) {\n            // 过滤所有字符\n            let content = null;\n            while (i + pos < list.length) {\n              content = list[i + pos].innerText.replace(symboRegex, \"\");\n              if (!content.length) {\n                pos++;\n                startPos++;\n              } else {\n                break;\n              }\n            }\n            if (!content) {\n              // 说明没找到有内容的段落，终止匹配\n              isMatch = false;\n              break;\n            }\n            const paragraphDistance = editDistance(content, paragraphList[j]);\n            if (paragraphDistance < distance) {\n              isMatch = false;\n              break;\n            } else {\n              pos++;\n            }\n          }\n          if (isMatch) {\n            paragraph = list[startPos];\n            break;\n          }\n        }\n        if (paragraph) {\n          return paragraph;\n        }\n        if (distance - 0.1 >= minDistance) {\n          return this.getContentMatchParagraph(\n            text,\n            distance - 0.1,\n            minDistance\n          );\n        }\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n      }\n      return null;\n    },\n    showContentMatchParagraph(content) {\n      if (this._inactive) {\n        return;\n      }\n      const paragraph = this.getContentMatchParagraph(content, 1, 0.6);\n      if (paragraph) {\n        this.showParagraph(paragraph, true);\n      } else {\n        this.$message.error(\"无法定位内容所在段落\");\n      }\n    },\n    showBookmark(bookmark) {\n      if (this._inactive) {\n        return;\n      }\n      if (!this.$refs.bookContentRef) {\n        setTimeout(() => {\n          this.showBookmark(bookmark);\n        }, 10);\n        return;\n      }\n      this.showContentMatchParagraph(bookmark.bookText);\n    }\n  }\n};\n</script>\n\n<style lang=\"stylus\" scoped>\n>>>.popper-component {\n  margin-left: 10px;\n}\n\n.chapter-wrapper {\n  padding: 0;\n  flex-direction: column;\n  align-items: center;\n\n  >>>.no-point {\n    pointer-events: none;\n  }\n\n  .tool-bar {\n    position: fixed;\n    top: 0;\n    padding-top: 0;\n    padding-top: constant(safe-area-inset-top) !important;\n    padding-top: env(safe-area-inset-top) !important;\n    left: 50%;\n    z-index: 2001;\n\n    .tools {\n      display: flex;\n      flex-direction: column;\n\n      .tool-icon {\n        font-size: 18px;\n        width: 58px;\n        height: 48px;\n        text-align: center;\n        padding-top: 12px;\n        cursor: pointer;\n        outline: none;\n\n        .iconfont {\n          font-family: iconfont;\n          width: 16px;\n          font-size: 16px;\n          margin: 0 auto;\n          height: 22px;\n          line-height: 22px;\n          vertical-align: middle;\n        }\n\n        .tool-el-icon {\n          font-size: 18px;\n          line-height: 22px;\n          height: 22px;\n\n          i {\n            line-height: 22px;\n          }\n        }\n\n        .icon-text {\n          font-size: 12px;\n        }\n      }\n    }\n  }\n\n  .read-bar {\n    position: fixed;\n    bottom: 0;\n    right: 50%;\n    z-index: 100;\n\n    .progress {\n      padding: 10px 36px;\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n\n      .progress-bar {\n        flex: 1;\n        padding: 0 10px;\n      }\n\n      .progress-tip {\n        font-size: 14px;\n        margin-left: 5px;\n      }\n    }\n\n    .cache-content-zone {\n      padding: 10px 36px;\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n      font-size: 14px;\n      position: absolute;\n      right: 55px;\n      width: 300px;\n      background: inherit;\n\n      .cache-content-btn {\n        cursor: pointer;\n      }\n    }\n\n    .float-left-btn-zone {\n      position: absolute;\n      bottom: 155px;\n      left: 4px;\n      right: auto;\n      display: flex;\n      flex-direction: column;\n\n      .float-btn {\n        line-height: 32px;\n        width: 36px;\n        height: 36px;\n        border-radius: 100%;\n        display: block;\n        cursor: pointer;\n        text-align: center;\n        vertical-align: middle;\n        pointer-events: all;\n        margin-top: 20px;\n\n        .el-icon-top, .el-icon-bottom, .el-icon-info, .el-icon-search, .el-icon-collection-tag {\n          line-height: 36px;\n        }\n      }\n    }\n\n    .float-right-btn-zone {\n      position: absolute;\n      bottom: 155px;\n      left: 4px;\n      right: auto;\n      display: flex;\n      flex-direction: column;\n\n      .float-btn {\n        line-height: 32px;\n        width: 36px;\n        height: 36px;\n        border-radius: 100%;\n        display: block;\n        cursor: pointer;\n        text-align: center;\n        vertical-align: middle;\n        pointer-events: all;\n        margin-top: 20px;\n\n        .el-icon-refresh-right, .el-icon-headset, .el-icon-view {\n          line-height: 36px;\n        }\n        .el-icon-moon {\n          color: #121212;\n          line-height: 34px;\n        }\n        .el-icon-sunny {\n          color: #666;\n          line-height: 34px;\n        }\n      }\n    }\n\n    .tools {\n      display: flex;\n      flex-direction: column;\n\n      .tool-icon {\n        font-size: 18px;\n        width: 42px;\n        height: 31px;\n        padding-top: 12px;\n        text-align: center;\n        align-items: center;\n        cursor: pointer;\n        outline: none;\n        margin-top: -1px;\n\n        &.progress-text {\n          font-size: 16px;\n        }\n\n        .iconfont {\n          font-family: iconfont;\n          width: 16px;\n          height: 16px;\n          font-size: 16px;\n          margin: 0 auto 6px;\n        }\n      }\n    }\n\n    .reader-bar-inner {\n      display: flex;\n      flex-direction: column;\n      padding-bottom: 10px;\n      padding-bottom: calc(10px + constant(safe-area-inset-top));\n      padding-bottom: calc(10px + env(safe-area-inset-top));\n      padding-left: 5px;\n      padding-right: 5px;\n\n      .operate-bar {\n        display: flex;\n        flex-direction: row;\n        justify-content: space-between;\n        padding: 10px 10px 0 10px;\n        align-items: center;\n\n        .close-btn, .collapse-btn {\n          font-size: 22px;\n          height: 35px;\n          cursor: pointer;\n        }\n\n        .center {\n          span {\n            display: inline-block;\n            cursor: pointer;\n          }\n          .play-pause-btn {\n            font-size: 50px;\n            margin-top: -40px;\n            i {\n              border-radius: 100%;\n            }\n          }\n          .ctrl-btn {\n            margin: 0px 15px;\n          }\n        }\n      }\n\n      .setting-item {\n        display: flex;\n        flex-direction: column;\n        padding: 5px 10px;\n\n        .setting-title {\n          font-size: 14px;\n        }\n\n        .setting-btn {\n          font-size: 14px;\n          cursor: pointer;\n          display: inline-block;\n          margin-left: 5px;\n        }\n\n        .voice-list {\n          display: flex;\n          flex-direction: row;\n          overflow-x: auto;\n          padding: 5px 10px;\n\n          .radio-group {\n            white-space: nowrap;\n\n            .radio-button {\n              margin-right: 10px;\n\n              .el-radio-button__inner {\n                border-radius: 4px 4px 4px 4px;\n              }\n            }\n          }\n        }\n\n        .progress {\n          padding: 5px 10px;\n\n          .progress-tip {\n            margin-left: 0;\n            margin-right: 5px;\n          }\n        }\n      }\n    }\n  }\n\n  .chapter-bar {\n    .el-breadcrumb {\n      .item {\n        font-size: 14px;\n        color: #606266;\n      }\n    }\n  }\n\n  .chapter {\n    font-family: 'Microsoft YaHei', PingFangSC-Regular, HelveticaNeue-Light, 'Helvetica Neue Light', sans-serif;\n    text-align: left;\n    padding: 0 65px;\n    min-height: 100vh;\n    min-height: calc(var(--vh, 1vh) * 100);\n    width: 670px;\n    margin: 0 auto;\n    background-size: cover;\n    position: relative;\n\n    >>>.el-icon-loading {\n      font-size: 36px;\n      color: #B5B5B5;\n    }\n\n    >>>.el-loading-text {\n      font-weight: 500;\n      color: #B5B5B5;\n    }\n\n    .click-zone {\n      position: absolute;\n      z-index: 120;\n      top: 0;\n      bottom: 0;\n      left: 0;\n      right: 0;\n      background: #333;\n      opacity: 0.8;\n      color: #fff;\n      font-size: 14px;\n      pointer-events: none;\n\n      div {\n        position: absolute;\n        text-align: center;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n      }\n\n      .close-btn {\n        left: 0;\n        right: 0;\n        bottom: 20px;\n        height: 45px;\n        line-height: 45px;\n        z-index: 10;\n        padding: 0;\n        cursor: pointer;\n        pointer-events: all;\n      }\n    }\n\n    .content {\n      font-size: 18px;\n      line-height: 1.8;\n      overflow: hidden;\n      font-family: 'Microsoft YaHei', PingFangSC-Regular, HelveticaNeue-Light, 'Helvetica Neue Light', sans-serif;\n\n      .content-inner {\n        min-height: calc(var(--vh, 1vh) * 80);\n        padding-bottom: 25px;\n        box-sizing: border-box;\n      }\n    }\n\n    .bottom-bar, .top-bar {\n      box-sizing: border-box;\n    }\n    .top-bar {\n      height: 44px;\n      padding: 10px;\n    }\n    .bottom-bar {\n      width: 100%;\n      text-align: center;\n      padding-bottom: 30px;\n      .bottom-btn {\n        font-size: 14px;\n        cursor: pointer;\n        display: inline-block;\n        margin: 0 auto;\n        padding: 10px 40px;\n        width: 80%;\n        box-sizing: border-box;\n      }\n    }\n  }\n\n  .chapter.audio {\n    .top-bar, .bottom-bar {\n      display: none;\n    }\n    .content-inner {\n      height: calc(var(--vh, 1vh) * 100);\n      margin-top: 0 !important;\n      padding-top: 0 !important;\n      padding-bottom: 0 !important;\n      display: flex;\n      align-items: center;\n    }\n  }\n}\n\n.day {\n  >>>.popup {\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);\n  }\n\n  >>>.tool-icon {\n    border: 1px solid rgba(0, 0, 0, 0.1);\n    margin-top: -1px;\n    color: #000;\n\n    .icon-text {\n      color: rgba(0, 0, 0, 0.4);\n    }\n  }\n\n  >>>.progress-tip {\n    color: rgba(0, 0, 0, 0.4);\n  }\n\n  >>>.cache-content-zone {\n    color: rgba(0, 0, 0, 0.4);\n  }\n\n  >>>.float-left-btn-zone {\n    color: #121212;\n  }\n\n  >>>.float-right-btn-zone {\n    color: #121212;\n  }\n\n  >>>.reader-bar-inner {\n    color: #121212;\n\n    .setting-title {\n      color: rgba(0, 0, 0, 0.8);\n    }\n\n    .setting-value {\n      color: rgba(0, 0, 0, 0.4);\n    }\n  }\n\n  >>>.chapter {\n    border: 1px solid #d8d8d8;\n    color: #262626;\n  }\n\n  .bottom-bar, .top-bar {\n    color: rgba(0, 0, 0, 0.4);\n  }\n\n  >>>.el-slider__runway {\n    background-color: #fff;\n  }\n\n  >>>.play-pause-btn {\n    color: #409EFF;\n  }\n}\n\n.night {\n  >>>.popup {\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.48), 0 0 6px rgba(0, 0, 0, 0.16);\n  }\n\n  >>>.tool-icon {\n    border: 1px solid #444;\n    margin-top: -1px;\n    color: #666;\n\n    .icon-text {\n      color: #666;\n    }\n  }\n\n  >>>.progress-tip {\n    color: #666;\n  }\n\n  >>>.cache-content-zone {\n    color: #666;\n  }\n\n  >>>.float-left-btn-zone {\n    color: #666;\n  }\n\n  >>>.float-right-btn-zone {\n    color: #666;\n  }\n\n  >>>.reader-bar-inner {\n    color: #666;\n  }\n\n  >>>.chapter {\n    border: 1px solid #444;\n    color: #666;\n  }\n\n  >>>.popper__arrow {\n    background: #666;\n  }\n\n  .bottom-bar, .top-bar {\n    color: #666;\n  }\n\n  >>>.el-slider__runway {\n    background-color: #282828;\n  }\n  >>>.el-slider__bar {\n    background-color: #185798;\n  }\n  >>>.el-slider__button {\n    border: 2px solid #185798;\n    background-color: #282828;\n  }\n  >>>.play-pause-btn {\n    color: #185798;\n  }\n}\n\n.chapter-wrapper {\n  .read-bar {\n    .float-btn-zone {\n      position: absolute;\n      bottom: 135px;\n      left: 4px;\n\n      .float-left-btn-zone {\n        position: relative;\n        left: auto;\n        bottom: auto;\n      }\n\n      .float-right-btn-zone {\n        position: relative;\n        left: auto;\n        bottom: auto;\n        margin-bottom: 20px;\n      }\n    }\n\n  }\n}\n\n.chapter-wrapper.mini-interface {\n  padding: 0;\n  position: relative;\n  height: 100%;\n\n  .tool-bar {\n    left: 0;\n    width: 100vw;\n    margin-left: 0 !important;\n\n    .tools {\n      flex-direction: row;\n      justify-content: space-around;\n      .tool-icon {\n        border: none;\n      }\n    }\n  }\n\n  .read-bar {\n    right: 0;\n    width: 100vw;\n    margin-right: 0 !important;\n\n    .cache-content-zone {\n      position: relative;\n      width: auto;\n      right: 0;\n      background: inherit;\n    }\n\n    .float-btn-zone {\n      position: static;\n      bottom: 0;\n      left: 0;\n    }\n\n    .float-left-btn-zone {\n      position: absolute;\n      right: auto;\n      left: 20px;\n      bottom: 135px;\n    }\n\n    .float-right-btn-zone {\n      position: absolute;\n      left: auto;\n      right: 20px;\n      bottom: 135px;\n    }\n\n    .tools {\n      flex-direction: row;\n      justify-content: space-around;\n      padding: 0 15px;\n      height: 45px;\n      .tool-icon {\n        border: none;\n        width: auto;\n        padding: 0;\n        height: 45px;\n        line-height: 45px;\n        .iconfont {\n          display: inline-block;\n        }\n        span {\n          vertical-align: middle;\n        }\n      }\n    }\n  }\n\n  .chapter {\n    width: 100vw !important;\n    padding: 0 16px;\n    box-sizing: border-box;\n    border: none;\n    text-align: justify;\n    position: relative;\n\n    .top-bar {\n      position: fixed;\n      top: 0;\n      left: 0;\n      width: 100vw;\n      z-index: 50;\n      background: inherit;\n      height: 30px;\n      height: calc(30px + constant(safe-area-inset-top));\n      height: calc(30px + env(safe-area-inset-top));\n      padding: 6px 16px;\n      padding-top: calc(6px + constant(safe-area-inset-top));\n      padding-top: calc(6px + env(safe-area-inset-top));\n      font-size: 12px;\n    }\n\n    .content-inner {\n      margin-top: 30px;\n      margin-top: calc(30px + constant(safe-area-inset-top));\n      margin-top: calc(30px + env(safe-area-inset-top));\n      padding-top: 15px;\n      padding-bottom: 15px;\n    }\n  }\n\n  .chapter.cartoon {\n    padding: 0;\n\n    .content-inner {\n      padding-top: 1px;\n    }\n  }\n\n  .chapter.slide-reader {\n    padding: 0;\n    height: 100%;\n\n    .bottom-bar {\n      height: 24px;\n      position: absolute;\n      bottom: 0;\n      padding: 0 16px;\n      padding-bottom: 6px;\n      display: flex;\n      justify-content: space-between;\n      font-size: 12px;\n    }\n\n    .top-bar {\n      position: relative;\n    }\n\n    .content {\n      position: absolute;\n      overflow: visible;\n      top: 30px;\n      top: calc(30px + constant(safe-area-inset-top));\n      top: calc(30px + env(safe-area-inset-top));\n      bottom: 24px;\n    }\n\n    .content-inner {\n      margin: 0 16px;\n      overflow: hidden;\n      text-align: justify;\n      padding: 0;\n      height: 100%;\n    }\n\n    .book-content {\n      height: 100%;\n      -webkit-columns: calc(100vw - 32px) 1;\n      -webkit-column-gap: 32px;\n      columns: calc(100vw - 16px) 1;\n      column-gap: 16px;\n    }\n  }\n}\n.chapter-wrapper.mini-interface::-webkit-scrollbar {\n  width: 0 !important;\n}\n</style>\n<style lang=\"stylus\">\n.voice-list {\n  .el-radio-button__inner {\n    border-radius: 4px !important;\n    border-left: 1px solid #DCDFE6;\n    box-shadow: none;\n  }\n}\n.night-theme {\n  .voice-list {\n    .el-radio-button {\n      box-shadow: none !important;\n    }\n    .el-radio-button__inner {\n      background-color: #bbb;\n      border-color: #bbb;\n    }\n    .el-radio-button__inner:hover {\n      color: #185798;\n    }\n    .el-radio-button__orig-radio:checked+.el-radio-button__inner {\n      background-color: #185798;\n      border-color: #185798;\n      color: #fff;\n      box-shadow: none;\n    }\n  }\n}\n.kindle-page {\n  .day {\n    .tool-icon {\n      border: 1px solid #fefefefe;\n\n      .icon-text {\n        color: #444;\n      }\n    }\n\n    .progress-tip {\n      color: #444;\n    }\n\n    .cache-content-zone {\n      color: #444;\n    }\n\n    .reader-bar-inner {\n\n      .setting-title {\n        color: rgba(0, 0, 0, 0.8);\n      }\n\n      .setting-value {\n        color: #444;\n      }\n    }\n\n    .bottom-bar, .top-bar {\n      color: #444;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "web/vue.config.js",
    "content": "// vue.config.js\nvar packageInfo = require(\"./package.json\");\n\nfunction buildVersion() {\n  const now = new Date();\n  const pad = v => (v >= 10 ? \"\" + v : \"0\" + v);\n  return (\n    pad(now.getMonth() + 1) +\n    pad(now.getDate()) +\n    pad(now.getHours()) +\n    pad(now.getMinutes())\n  );\n}\nprocess.env.VUE_APP_BUILD_VERSION =\n  process.env.VUE_APP_BUILD_VERSION ||\n  \"v\" + packageInfo.version + \"-\" + buildVersion();\n\nfunction customWorkboxPlugin(generateCacheKey, checkResponse) {\n  return {\n    generateCacheKey,\n    checkResponse,\n    // Return `response`, a different `Response` object, or `null`.\n    cacheWillUpdate: async function cacheWillUpdate({\n      request,\n      response,\n      event,\n      state\n    }) {\n      // console.log({ request, response, event, state });\n      const resCopy = response.clone();\n      if (this.checkResponse) {\n        return await this.checkResponse({\n          request,\n          response: resCopy,\n          event,\n          state\n        });\n      }\n      const body = await resCopy.json().catch(() => false);\n      if (body && body.isSuccess) {\n        // 请求成功\n        return response;\n      } else {\n        // 请求失败\n        return null;\n      }\n    },\n    cacheKeyWillBeUsed: async function cacheKeyWillBeUsed({\n      request,\n      mode,\n      params,\n      event,\n      state\n    }) {\n      // `request` is the `Request` object that would otherwise be used as the cache key.\n      // `mode` is either 'read' or 'write'.\n      // Return either a string, or a `Request` whose `url` property will be used as the cache key.\n      // Returning the original `request` will make this a no-op.\n      // 只使用 url 参数 作为缓存key\n      // console.log({\n      //   request,\n      //   mode,\n      //   params,\n      //   event,\n      //   state\n      // });\n      if (this.generateCacheKey) {\n        const cacheKey = this.generateCacheKey({\n          request,\n          mode,\n          params,\n          event,\n          state\n        });\n        return cacheKey || request;\n      } else {\n        return request;\n      }\n    }\n  };\n}\n\nmodule.exports = {\n  publicPath: \"./\",\n  productionSourceMap: false,\n  devServer: {\n    port: 8081\n  },\n  // 编译依赖为 es5\n  transpileDependencies: [\"element-ui\", \"codejar\", \"vue-lazyload\"],\n  pwa: {\n    name: \"阅读\",\n    themeColor: \"#ffffff\",\n    msTileColor: \"#000000\",\n\n    appleMobileWebAppCapable: \"yes\",\n    appleMobileWebAppStatusBarStyle: \"black-translucent\",\n\n    manifestOptions: {\n      // display: \"standalone\"\n      display: \"fullscreen\"\n    },\n\n    // configure the workbox plugin\n    // workboxPluginMode: \"InjectManifest\",\n    workboxOptions: {\n      // swSrc is required in InjectManifest mode.\n      // swSrc: \"src/service-worker.js\"\n      // ignoreURLParametersMatching: [new RegExp(\"accessToken\")],\n      exclude: [\"index.html\"],\n      importScripts: [\"sw.js\"],\n      cleanupOutdatedCaches: true,\n      // skipWaiting: true,\n      runtimeCaching: [\n        {\n          // 首页\n          urlPattern: new RegExp(\"^https?://[^/]*/?$\"),\n          handler: \"networkFirst\",\n          options: {\n            cacheName: \"home\",\n            cacheableResponse: {\n              statuses: [200]\n            }\n          }\n        },\n        // {\n        //   // 获取书架\n        //   urlPattern: new RegExp(\"^https?://[^/]*/reader3/getBookshelf\"),\n        //   handler: \"networkFirst\",\n        //   options: {\n        //     cacheName: \"bookshelf\",\n        //     cacheableResponse: {\n        //       statuses: [200]\n        //     },\n        //     plugins: [\n        //       customWorkboxPlugin(({ request, mode }) => {\n        //         const searchParams = new URL(request.url).searchParams;\n        //         if (mode === \"read\" && searchParams.get(\"refresh\")) {\n        //           // 刷新时不读取缓存\n        //           return false;\n        //         }\n        //         const accessToken = searchParams.get(\"accessToken\");\n        //         if (!accessToken) {\n        //           return request;\n        //         }\n        //         return \"getBookshelf@\" + accessToken.split(\":\")[0];\n        //       })\n        //     ]\n        //   }\n        // },\n        // 书源手动缓存在 localStorage\n        // {\n        //   // 获取书源\n        //   urlPattern: new RegExp(\"^https?://[^/]*/reader3/getBookSources\"),\n        //   handler: \"networkFirst\",\n        //   options: {\n        //     cacheName: \"bookSources\",\n        //     cacheableResponse: {\n        //       statuses: [200]\n        //     }\n        //   }\n        // },\n        // {\n        //   // 获取书籍章节列表\n        //   urlPattern: new RegExp(\"^https?://[^/]*/reader3/getChapterList\"),\n        //   handler: \"networkFirst\",\n        //   options: {\n        //     cacheName: \"bookChapterList\",\n        //     networkTimeoutSeconds: 5,\n        //     cacheableResponse: {\n        //       statuses: [200]\n        //     },\n        //     plugins: [\n        //       customWorkboxPlugin(({ request, mode }) => {\n        //         const searchParams = new URL(request.url).searchParams;\n        //         if (mode === \"read\" && searchParams.get(\"refresh\")) {\n        //           // 刷新时不读取缓存\n        //           return false;\n        //         }\n        //         return searchParams.get(\"url\") + \"@chapterList\";\n        //       })\n        //     ]\n        //   }\n        // },\n        // {\n        //   // 获取书籍内容\n        //   urlPattern: new RegExp(\"^https?://[^/]*/reader3/getBookContent\"),\n        //   handler: \"cacheFirst\",\n        //   options: {\n        //     cacheName: \"bookContent\",\n        //     cacheableResponse: {\n        //       statuses: [200]\n        //     },\n        //     expiration: {\n        //       maxAgeSeconds: 86400 * 30,\n        //       maxEntries: 10000\n        //     },\n        //     plugins: [\n        //       customWorkboxPlugin(({ request, mode }) => {\n        //         const searchParams = new URL(request.url).searchParams;\n        //         if (mode === \"read\" && searchParams.get(\"refresh\")) {\n        //           // 刷新时不读取缓存\n        //           return false;\n        //         }\n        //         return (\n        //           searchParams.get(\"url\") +\n        //           \"@chapterContent-\" +\n        //           searchParams.get(\"index\")\n        //         );\n        //       })\n        //     ]\n        //   }\n        // },\n        {\n          // 获取书籍封面\n          urlPattern: new RegExp(\"^https?://[^/]*/reader3/cover\"),\n          handler: \"cacheFirst\",\n          options: {\n            cacheName: \"bookCover\",\n            cacheableResponse: {\n              statuses: [200]\n            },\n            expiration: {\n              maxAgeSeconds: 86400 * 30,\n              maxEntries: 1000\n            },\n            plugins: [\n              customWorkboxPlugin(\n                ({ request }) => {\n                  const searchParams = new URL(request.url).searchParams;\n                  return searchParams.get(\"path\");\n                },\n                ({ response }) => {\n                  if (response.status === 200) {\n                    return response;\n                  }\n                  return null;\n                }\n              )\n            ]\n          }\n        }\n      ]\n    }\n  }\n};\n"
  }
]