Greasy Fork

Greasy Fork is available in English.

AO3: Fic's Style, Blacklist, Bookmarks

Change font, size, width, background... of a work + blacklist: hide works that contain certains tags, have too many fandoms/relations/chapters/words and other options + fullscreen reading mode + bookmarks: save the position you stopped reading a fic + number of words for each chapter and estimated reading time

当前为 2021-03-04 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AO3: Fic's Style, Blacklist, Bookmarks
// @namespace    https://github.com/Schegge
// @version      3.5.3
// @description  Change font, size, width, background... of a work + blacklist: hide works that contain certains tags, have too many fandoms/relations/chapters/words and other options + fullscreen reading mode + bookmarks: save the position you stopped reading a fic + number of words for each chapter and estimated reading time
// @author       Schegge
// @include      http*://archiveofourown.org/*
// @grant        none
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAYAAABXuSs3AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjc0REE4QjE3Qjk1ODExRTY4MEZCQTc3RERGMTNGQ0Y0IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjc0REE4QjE4Qjk1ODExRTY4MEZCQTc3RERGMTNGQ0Y0Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzREQThCMTVCOTU4MTFFNjgwRkJBNzdEREYxM0ZDRjQiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzREQThCMTZCOTU4MTFFNjgwRkJBNzdEREYxM0ZDRjQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz78uZUsAAACeUlEQVR42uxZvW7bMBCWDaFLuih9A3bJrqJLVhVdigwFZGQPIHdpO9aPYC+dGz1CNXXpEiFzFq0NMtjIE0QvYNQ9Bh+BA6Mf0rKkBuABHyxQJPHxdPfdSZ7sdjvvOdrUe6bmiDvijrgj7og/mn/oDX/4T7YMCAkhxLW0nJARNnzip+32v/H4N8KaEIHsipASBMYv2WHG9TizS3haEp1r96S3C8yRT+IdobTZfNLUq1Q89iYLQEJ6Nib8RCi8NjicfBKLsUJFhsUViCwxlrasSdlaMZaqJPi9YCSKljX8fjwG8YQl2Vs2nhusVXPCMYjHB9gjGJq4gNyV3oA2PVCYyFhd1BzK5OAm+dAL8RTarHs9NAiPUYirpFSkM+3+R8Pc2FSs7ZV4BG+XNbp93uD1gOn9fMgYF/BYpukyb5z+oihF2tozwh/CS8KtasBsKnUX4jFI5jXVUO3/G+V/jUPcE37hkCeEL6zqBkMQT2pKuj72gnBMmKEneYXQ+sAOnsLrcd/EY4RKFfFSG1dzC5C80/twliPGHvc7EC8aEm9TMX+F6zcNWp6bEtinrVUvATbW1N5GiG+pLKlpa+t3SMo2CQuZ3An2FsTbYHV/ZePtfT2+RgyvDKrig5a08wY9l8k+I49nfSSnSjSTzfUkTWqSr2ROWPalKklFkTHptZV9RYgENcks+lAVFacLizXqE4Qi9Bma7mmhJmrUqLPHA1S/fYyH1bGm2zxRPRun+AbKECFEBItDVVCKmnZUYG3I3kW5fUfneEM4xfyZTYfYRnxZ0SDxF+Mc30Sqkrgp0Y4I7wnXUJqN7WOcuP+AHHFH3BF3xB1xR9zC/gkwALX7mp/233xAAAAAAElFTkSuQmCC
// ==/UserScript==

(function() {
   const NAMESPACE = 'ficstyle';

   /** FEATURES **/
   const Feature = {
      style: true,
      book: true,
      black: true,
      wpm: 250
   }
   // Object.assign() changes only the same keys
   Object.assign(Feature, getStorage(`${NAMESPACE}_feature`, '{}'));


   // check which page
   const Check = {
      // script version
      version: function() {
         if (getStorage(`${NAMESPACE}_version`, '1') !== 353) {
            setStorage(`${NAMESPACE}_version`, 353);
            return true;
         }
         return false;
      },
      // on search pages but not on personal user profile
      black: function() {
         let user = document.querySelector('#greeting .user a[href *= "/users/"]') || false;
         user = user && window.location.pathname.includes(user.href.split('/users/')[1]);
         return document.querySelector('li.blurb.group:not(.collection):not(.tagset)') && !user;
      },
      // include /works/(numbers) and /works/(numbers)/chapters/(numbers) and exclude /works/(whatever)navigate
      work: function() {
         return /\/works\/\d+(\/chapters\/\d+)?(?!.*navigate)/.test(window.location.pathname);
      },
      // Full Screen
      fullScreen: false
   };


   // new version notification
   if (Check.version()) {
      document.body.insertAdjacentHTML('beforeend', `<div style="position: fixed; bottom: 50px; right: 50px; width: 40%; z-index: 999; font-size: .9em; background: #fff; padding: 1em; border: 1px solid #900;"><b>AO3: Fic's Style, Blacklist, Bookmarks</b> UPDATES (v3.5.3)<br><br>You can now blacklist <b>authors</b> by putting <i>@author_name</i> in the text area of the blacklist (wildcards are allowed, so <i>@*something*</i> is ok and it'll only target the authors' names).<br><br><a target="_blanket" href="http://greasyfork.icu/en/scripts/10944-ao3-fic-s-style-blacklist-bookmarks">More info.</a><br><br><span id="${NAMESPACE}-close" style="cursor: pointer; color: #900;">close</span>`);
      document.getElementById(`${NAMESPACE}-close`).addEventListener('click', function() { this.parentElement.style.display = 'none'; });
   }


   // Features' menu
   addCSS(`${NAMESPACE}-menus`,
      `li[id |= "${NAMESPACE}"] > a { cursor: pointer; }
      li[id |= "${NAMESPACE}"] .dropdown-menu li > a { padding-bottom: 0.75em!important; }
      li[id |= "${NAMESPACE}"] .dropdown-menu li > a.${NAMESPACE}-menu-save { color: #900!important; font-weight: bold; text-align: center; }
      li[id |= "${NAMESPACE}"] .dropdown-menu li span { font-size: .85em; }
      li[id |= "${NAMESPACE}"] .dropdown-menu input[type="number"], li[id |= "${NAMESPACE}"] .dropdown-menu input[type="text"] { width: 4em; padding: 0 0 0 .2em; margin: .2em 0; background: #fff; border: 0; box-shadow: 0 0 0 1px #888; border-radius: 0; box-sizing: border-box; }
      li[id |= "${NAMESPACE}"] .dropdown-menu textarea { font-size: .9em; line-height: 1.2em; min-height: 10em; margin: .5em!important; padding: .3em; box-shadow: 0 0 0 1px #888; width: calc(100% - 1em); border: 0; box-sizing: border-box; resize: vertical; }
      li[id |= "${NAMESPACE}"] .opts { font-variant: small-caps; display: flex; flex-wrap: wrap; }
      li[id |= "${NAMESPACE}"] .opts span  { width: 50%; }
      li[id |= "${NAMESPACE}"] .opts span:last-of-type:nth-of-type(odd) { width: 100%; padding-bottom: 1em; }
      li[id |= "${NAMESPACE}"] .info { display: flex; flex-wrap: wrap; align-content: center; font-size: .8em; }
      li[id |= "${NAMESPACE}"] .info span { width: 33%; }
      li[id |= "${NAMESPACE}"] .opts span, li[id |= "${NAMESPACE}"] .info span { flex: auto; }

      #${NAMESPACE}-book ul li { display: flex!important; justify-content: space-between; }
      #${NAMESPACE}-book ul li a:first-child { flex-grow: 1; font-size: .9em; }

      a.${NAMESPACE}-book-delete { color: #900!important; }
      .${NAMESPACE}-book-left { font-family: Consolas, monospace; left: 0; position: fixed; bottom: .8em; margin: 0; padding: 0 0 0 .5em; color: #000; text-shadow: 0 0 1px rgba(0, 0, 0, .4); border-radius: .3em; z-index: 999; }
      .${NAMESPACE}-book-left button { padding: 0 .2em; margin: 0 .2em 0 0; }
      .${NAMESPACE}-book-top { margin: 1em 0 0; font-size: 80%; text-align: right; padding: 0; }
      .${NAMESPACE}-no-book { display: none; }

      #${NAMESPACE}-black .dropdown-menu > li { text-align: center!important; }
      #${NAMESPACE}-black input[type="text"] { width: auto; }

      #${NAMESPACE}-style { position: fixed; bottom: .8em; margin: 0; padding: 0; color: #000; text-shadow: 0 0 1px rgba(0, 0, 0, .4); border-radius: .3em; z-index: 999; }
      #${NAMESPACE}-style { right: 0; background-color: transparent; text-align: right; margin-right: .5em; }
      #${NAMESPACE}-style:not(.${NAMESPACE}-options-hide) { width: 25em; }
      #${NAMESPACE}-style > button { padding: 0 .3em; }
      #${NAMESPACE}-style.${NAMESPACE}-options-hide > div { display: none; }
      #${NAMESPACE}-style > div { background-color: #ddd;  padding: .5em; box-shadow: 1px 1px 3px -1px #444; margin: 0; border-radius: .2em; }
      #${NAMESPACE}-style label { display: block; border-bottom: 1px solid #888; padding: .2em 0; margin: 0; }
      #${NAMESPACE}-style input, #${NAMESPACE}-style select { width: 50%; padding: 0; margin: 0 0 0 1em; font-size: 1em; }
      #${NAMESPACE}-style button { margin: .3em .2em; }

      .${NAMESPACE}-words { font-size: .7em; color: inherit; font-family: consolas, monospace; text-transform: uppercase; text-align: center; margin: 3em 0 .5em; }`
   );

   let featureMenu = document.createElement('li');
   featureMenu.id = `${NAMESPACE}-feature`;
   featureMenu.className = 'dropdown';
   featureMenu.innerHTML = `<a style="font-weight: bold;">Features</a>
      <ul class="menu dropdown-menu" role="menu">
         <li role="menu-item"><a><input id="${NAMESPACE}-feature-style" type="checkbox" ${Feature.style ? 'checked' : ''}> Styling</a></li>
         <li role="menu-item"><a><input id="${NAMESPACE}-feature-book" type="checkbox" ${Feature.book ? 'checked' : ''}> Bookmarks / Full Screen</a></li>
         <li role="menu-item"><a><input id="${NAMESPACE}-feature-black" type="checkbox" ${Feature.black ? 'checked' : ''}> Blacklist</a></li>
         <li role="menu-item"><a><input id="${NAMESPACE}-feature-wpm" type="number" min="0" max="1000" step="10" value="${Feature.wpm}"> Words per minute</a></li>
         <li role="menu-item"><a class="${NAMESPACE}-menu-save" id="${NAMESPACE}-feature-save">SAVE</a></li>
      </ul>`;
   document.querySelector('#header > ul').appendChild(featureMenu);

   document.getElementById(`${NAMESPACE}-feature-save`).addEventListener('click', function() {
      Feature.style = document.getElementById(`${NAMESPACE}-feature-style`).checked;
      Feature.book = document.getElementById(`${NAMESPACE}-feature-book`).checked;
      Feature.black = document.getElementById(`${NAMESPACE}-feature-black`).checked;
      let wpm = document.getElementById(`${NAMESPACE}-feature-wpm`).value;
      Feature.wpm = wpm ? Math.min(Math.max(parseInt(wpm, 10), 0), 1000) : 0;
      setStorage(`${NAMESPACE}_feature`, Feature);
      this.textContent = 'SAVING...';
      window.location.replace(window.location.href);
   });


   // add estimated reading time for every fic found
   if (Feature.wpm) {
      document.querySelectorAll('dl.stats dd.words').forEach(w => {
         let numWords = w.textContent.replace(/,/g, '');
         w.insertAdjacentHTML('afterend', `<dt>Time:</dt><dd>${countTime(numWords)}</dd>`);
      });
   }


   /** BOOKMARKS **/
   let Bookmarks = {};
   
   if (Feature.book) {
      Bookmarks = {
         list: [],
         get: function() {
            this.list = getStorage(`${NAMESPACE}_bookmarks`, '[]');
         },
         set: function() {
            setStorage(`${NAMESPACE}_bookmarks`, this.list);
         },
         fromBook: window.location.search === '?bookmark',
         getUrl: window.location.pathname.split('/works/')[1],
         getTitle: function() {
            let title = document.querySelector('#workskin .preface.group h2.title.heading').textContent.trim().substring(0, 28);
            // get the number of the chapter if chapter by chapter
            if (this.getUrl.includes('/chapters/')) title += ` (${document.querySelector('#chapters > .chapter > .chapter.preface.group > h3 > a').textContent.replace('Chapter ', 'ch')})`;
            return title;
         },
         getPosition: function() {
            let position = getScroll();
            // calculate % if chapter by chapter view or work completed (number/number is the same)
            if (window.location.pathname.includes('/chapters/') || /(\d+)\/\1/.test(document.querySelector('dl.stats dd.chapters').textContent)) {
               position = (position / getDocHeight()).toFixed(4) + '%';
            }
            return position;
         },
         checkIfExist: function(what, link) {
            let url = link || this.getUrl;
            for (let i = 0, len = this.list.length; i < len; i++) {
               // check if the same fic already exists
               if (this.list[i][0].split('/chapters/')[0] === url.split('/chapters/')[0]) {
                  // i need the index to delete the old bookmark (for change or cancel)
                  if (what === 'cancel') {
                     return i;
                  // check if the same chapter
                  } else if (this.list[i][0] === url) {
                     // retrieve the bookmark position
                     if (what === 'book') {
                        let book = this.list[i][2];
                        // if the bookmark is in %
                        if (book.toString().includes('%')) {
                           book = parseFloat(book.replace('%', ''));
                           book *= getDocHeight();
                        }
                        return book;
                     }
                     // just check if a bookmark exist
                     return true;
                  }
               }
            }
            return false;
         },
         cancel: function(url) {
            let found = this.checkIfExist('cancel', url);
            if (found !== false) this.list.splice(found, 1);
         },
         getNew: function() {
            this.cancel();
            this.list.push([this.getUrl, this.getTitle(), this.getPosition()]);
            this.set();
         },
         html: function() {
            let bookMenu = document.createElement('li');
            bookMenu.id = `${NAMESPACE}-book`;
            bookMenu.className = 'dropdown';
            bookMenu.innerHTML = '<a>Bookmarks</a>';
            let bookMenuDrop = document.createElement('ul');
            bookMenuDrop.className = 'menu dropdown-menu';
            bookMenu.appendChild(bookMenuDrop);
            document.querySelector('#header > ul').appendChild(bookMenu);

            if (this.list.length) {
               let self = this;
               let clickDelete = function() {
                  self.cancel(this.getAttribute('data-url'));
                  self.set();
                  this.style.display = 'none';
                  this.previousSibling.style.opacity = '.4';
               };

               this.list.forEach(item => {
                  let bookMenuLi = document.createElement('li');
                  bookMenuLi.innerHTML = `<a href="https://archiveofourown.org/works/${item[0]}?bookmark">${item[1]}</a>`;
                  let bookMenuDelete = document.createElement('a');
                  bookMenuDelete.className = `${NAMESPACE}-book-delete`;
                  bookMenuDelete.title = 'delete bookmark';
                  bookMenuDelete.setAttribute('data-url', item[0]);
                  bookMenuDelete.textContent = 'x';
                  bookMenuDelete.addEventListener('click', clickDelete);
                  bookMenuLi.appendChild(bookMenuDelete);
                  bookMenuDrop.appendChild(bookMenuLi);
               });
            } else {
               bookMenuDrop.innerHTML = '<li><a>No bookmark yet.</a></li>';
            }
         }
      };
      Bookmarks.get();
      Bookmarks.html();
   }


   /** FIC'S STYLE + FULLSCREEN + BOOKMARKING **/
   if (Check.work()) {

      if (Feature.style) {
         addCSS(`${NAMESPACE}-generalstyle`,
            `#main div.wrapper { margin-bottom: 1em; }
            #workskin { margin: 0; text-align: justify; max-width: none!important; }
            #workskin .notes, #workskin .summary, blockquote { font-size: inherit; font-family: inherit; }
            .preface a, #chapters a, .preface a:link, #chapters a:link, .preface a:visited, #chapters a:visited, .preface a:visited:hover, #chapters a:visited:hover { color: inherit !important; }
            .actions { font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'GNU Unifont', Verdana, Helvetica, sans-serif; font-size: 14px; }
            .chapter .preface { border-top: 0; margin-bottom: 0; padding: 0 2em; }
            .chapter .preface[role="complementary"] { border-width: 0; margin: 0; }
            .preface.group, div.preface { color: inherit; background-color: inherit; margin-left: 0; margin-right: 0; padding: 0 2em; }
            #workskin #chapters .preface .userstuff p, #workskin .preface .userstuff p  { margin: .1em auto; line-height: 1.1em; }
            div.preface .byline a, #workskin #chapters a, #chapters a:link, #chapters a:visited { color: inherit; }
            div.preface .notes, div.preface .summary, div.preface .series, div.preface .children { min-height: 0; }
            div.preface .jump { margin-top: 1em; font-size: .9em; }
            .preface blockquote { box-shadow: 0 0 0 2px rgba(0, 0, 0, .1), 0 0 0 2px rgba(255, 255, 255, .2); padding: .6em; margin: 0; }
            .preface h3.title { background: repeating-linear-gradient(45deg, rgba(0, 0, 0, .05), rgba(0, 0, 0, .1) 2px, rgba(255, 255, 255, .2) 2px, rgba(255, 255, 255, .2) 4px); padding: .6em; margin: 0; }
            .preface h3.heading { font-size: inherit; border-width: 0; }
            h3.title a { border: 0; font-style: italic; }
            div.preface .associations, .preface .notes h3+p { margin-bottom: 0; font-style: italic; font-size: .8em; }
            #workskin #chapters, #workskin #chapters .userstuff { width: 100%!important; box-sizing: border-box; }
            #workskin #chapters .userstuff p { font-family: inherit; text-align: justify; }
            #workskin #chapters .userstuff { font-family: inherit; text-align: justify; }
            #workskin #chapters .userstuff br { display: block; margin-top: .6em; content: " "; }
            .userstuff hr { width: 100%; height: 2px; border: 0; margin: 1.5em 0; background-image: linear-gradient(90deg, transparent, rgba(0, 0, 0, .2), transparent), linear-gradient(90deg, transparent, rgba(255, 255, 255, .3), transparent); }
            #workskin #chapters .userstuff blockquote { padding-top: 1px; padding-bottom: 1px; margin: 0 .5em; font-size: inherit; }
            .userstuff img { max-width: 100%; height: auto; display: block; margin: auto; }`
         );

         // CSS changes depending on the user
         const Styling = {
            opts: {
               fontName: 'inherit',
               colors: 'light',
               fontSize: '100',
               margins: '7',
               lineSpacing: '5'
            },
            choices: [
               // 0:css/id, 1:name, 2+:options
               ['fontName', 'Font', 'inherit', 'Arial Black', 'Consolas', 'Courier', 'Garamond', 'Georgia', 'Helvetica', 'Segoe UI', 'Times New Roman', 'Verdana'],
               ['colors', 'Background', 'light', 'grey', 'sepia', 'dark', 'darkblue', 'black'],
               ['fontSize', 'Text Size', 100, 50, 300],
               ['margins', 'Page Margins', 7, 5, 40],
               ['lineSpacing', 'Line Spacing', 5, 3, 10]
            ],
            colors: {
               // background, font color
               light: ['#ffffff', '#000000'],
               grey: ['#eeeeee', '#111111'],
               sepia: ['#fbf0d9', '#54331b'],
               dark: ['#333333', '#e1e1e1'],
               darkblue: ['#282a36', '#f8f8e6'],
               black: ['#000000', '#ffffff']
            },
            get: function() {
               Object.assign(this.opts, getStorage(`${NAMESPACE}_styling`, '{}'));
            },
            set: function() {
               setStorage(`${NAMESPACE}_styling`, this.opts);
               addCSS(`${NAMESPACE}-userstyle`,
                  `#workskin { font-family: ${this.opts.fontName}; font-size: ${this.opts.fontSize}%; padding: 0 ${this.opts.margins}%; color: ${this.colors[this.opts.colors][1]}; background-color: ${this.colors[this.opts.colors][0]}; }
                  #workskin #chapters .userstuff p { line-height: ${this.opts.lineSpacing * 0.3}em; margin: ${this.opts.lineSpacing * 0.5 - 1.4}em auto; } #workskin #chapters .userstuff { line-height: ${this.opts.lineSpacing * 0.3}em; }`
               );
            },
            setDefs: function() {
               this.opts = {
                  fontName: this.choices[0][2],
                  colors: this.choices[1][2],
                  fontSize: this.choices[2][2],
                  margins: this.choices[3][2],
                  lineSpacing: this.choices[4][2]
               };
            },
            html: function() {
               this.set();

               // the options displayed on the page
               let styleMenu = document.createElement('div');
               styleMenu.id = `${NAMESPACE}-style`;
               styleMenu.className = `${NAMESPACE}-options-hide`;

               let styleMenuEls = document.createElement('div');
               styleMenu.appendChild(styleMenuEls);

               this.choices.forEach(item => {
                  let el = document.createElement('label');
                  let h = item[1];
                  if (typeof item[2] === 'string') {
                     h += `<select id="${item[0]}">`;
                     for (let i = 2, len = item.length; i < len; i++) {
                        h += `<option value="${item[i]}" ${item[i] === this.opts[item[0]] ? 'selected' : ''}>${item[i]}</option>`;
                     }
                     h += '</select>';
                  } else {
                     h += `<input type="range" min="${item[3]}" max="${item[4]}" id="${item[0]}" value="${this.opts[item[0]]}">`;
                  }
                  el.innerHTML = h;
                  styleMenuEls.appendChild(el);
               });

               let save = document.createElement('button');
               save.innerHTML = 'save';
               save.addEventListener('click', () => {
                  let pos = getScroll() / getDocHeight();
                  for (let i = 0, len = this.choices.length; i < len; i++) {
                     this.opts[this.choices[i][0]] = styleMenuEls.querySelector(`#${this.choices[i][0]}`).value;
                  }
                  this.set();
                  setScroll(pos * getDocHeight());
               });
               styleMenuEls.appendChild(save);

               let reset = document.createElement('button');
               reset.innerHTML = 'reset';
               reset.addEventListener('click', () => {
                  let pos = getScroll() / getDocHeight();
                  styleMenu.parentElement.removeChild(styleMenu);
                  this.setDefs();
                  this.html();
                  setScroll(pos * getDocHeight());
               });
               styleMenuEls.appendChild(reset);

               let styleMenuButton = document.createElement('button');
               styleMenuButton.innerHTML = '&#9776;';
               styleMenuButton.addEventListener('click', function() {
                  if (this.parentElement.className === `${NAMESPACE}-options-hide`) {
                     this.parentElement.className = '';
                     this.innerHTML = 'close';
                  } else {
                     this.parentElement.className = `${NAMESPACE}-options-hide`;
                     this.innerHTML = '&#9776;';
                  }

               });
               styleMenu.appendChild(styleMenuButton);

               document.body.appendChild(styleMenu);
            }
         };
         Styling.get();
         Styling.html();

      } // END Feature.style

      // remove all the non-breaking white spaces
      document.getElementById('chapters').innerHTML = document.getElementById('chapters').innerHTML.replace(/&nbsp;/g, ' ');

      // # words and time for every chapter, if the fic has chapters
      if (Feature.wpm) {
         document.querySelectorAll('#chapters > .chapter > div.userstuff.module').forEach(el => {
            let numWords = el.textContent.match(/\S+\b/g).length - 2; // -2 because of hidden <h3>Chapter Text</h3>
            el.parentNode.querySelector('.chapter.preface.group[role="complementary"]').insertAdjacentHTML('beforebegin', `<div class="${NAMESPACE}-words">this chapter has ${numWords} words (time: ${countTime(numWords)})</div>`);
         });
      }

      // FULL SCREEN
      if (Feature.book) {
         let workskin = document.getElementById('workskin');

         let ficTop = document.createElement('div');
         ficTop.className = `actions ${NAMESPACE}-book-top`;
         let toFullScreen = document.createElement('div');
         toFullScreen.innerHTML = '<a>Full Screen</a>';
         ficTop.appendChild(toFullScreen);
         workskin.insertAdjacentElement('afterbegin', ficTop);

         // changes to create full screen
         let fullScreen = () => {
            if (Check.fullScreen) {
               window.location.replace(window.location.pathname);
               return;
            }

            setScroll();
            Check.fullScreen = true;

            addCSS(`${NAMESPACE}-fullscreen`,
               `#outer { display: none; }
               #workskin .preface { margin: 0; padding-bottom: 0; }
               div.preface .notes, div.preface .summary, div.preface .series, div.preface .children { min-height: 0; }
               div.preface .module { padding-bottom: 0; text-align: center; }
               .preface .module h3.heading { display: inline; cursor: pointer; text-align: center; opacity: .5; font-style: italic; font-size: 100%; }
               .preface .module > :not(h3) { display: none; }
               .preface h3 + p { border: 3px solid rgba(0, 0, 0, .1); border-left: 0; border-right: 0; padding: .6em; margin: 0; }
               .preface .module > h3:hover ~ .userstuff, .preface .module > .userstuff:hover, .preface .module > h3:hover ~ ul, .preface .module > ul:hover, .preface .module > h3:hover + p, .preface .module > h3 + p:hover { display: block!important; position: absolute; width: 100%; max-height: 6em; font-size: .8em; transform: translateY(-100%); color: rgb(42, 42, 42); background-color: #fff; padding: 10px; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .4); margin: 0; overflow: auto; z-index: 999; cursor: pointer; }
               .actions:not(.${NAMESPACE}-book-top) li > a:not([href *= "chapters"]):not([href = "#workskin"]) { display: none; }
               .actions:not(.${NAMESPACE}-book-top) { margin-top: 2em; }`
            );

            document.body.appendChild(workskin);
            toFullScreen.innerHTML = '<a>Exit</a>';

            let goToBook = document.createElement('div');
            goToBook.innerHTML = '<a>Go to Bookmark</a>';
            goToBook.addEventListener('click', () => setScroll(Bookmarks.checkIfExist('book')));

            let ficLeft = document.createElement('div');
            ficLeft.className = `${NAMESPACE}-book-left`;

            let deleteBook = document.createElement('button');
            deleteBook.title = 'delete bookmark';
            deleteBook.textContent = 'x';
            deleteBook.addEventListener('click', () => {
               Bookmarks.cancel();
               Bookmarks.set();
               goToBook.className = `${NAMESPACE}-no-book`;
               deleteBook.className = `${NAMESPACE}-no-book`;
            });

            let newBook = document.createElement('button');
            newBook.title = 'new bookmark';
            newBook.textContent = '+';
            newBook.addEventListener('click', function() {
               Bookmarks.getNew();
               goToBook.className = '';
               deleteBook.className = '';
               this.textContent = 'saved';
               setTimeout(() => this.textContent = '+', 1000);
            });

            if (!Bookmarks.checkIfExist()) {
               goToBook.className = `${NAMESPACE}-no-book`;
               deleteBook.className = `${NAMESPACE}-no-book`;
            }

            ficTop.insertBefore(goToBook, toFullScreen);
            ficLeft.appendChild(newBook);
            ficLeft.appendChild(deleteBook);
            document.body.appendChild(ficLeft);

            (document.querySelector('#feedback .actions a[href="#main"]')).href = '#workskin';
            workskin.appendChild(document.querySelector('#feedback .actions'));
         };
         if (Bookmarks.fromBook) fullScreen();
         toFullScreen.addEventListener('click', fullScreen);

      } // END Feature.book

   } // END Check.work()


   /** BLACKLIST **/
   if (Feature.black && Check.black()) {
      addCSS(`${NAMESPACE}-blacklisting`,
         `[data-visibility="remove"], [data-visibility="hide"] > :not(.header), [data-visibility="hide"] > .header > :not(h4) { display: none!important; }
         [data-visibility="hide"] { opacity: .6; }
         [data-visibility="hide"] > .header, [data-visibility="hide"] > .header > h4 { margin: 0!important; min-height: auto; font-size: .9em; font-style: italic; }
         [data-visibility="hide"]::before { content: "\\2573  " attr(data-reasons); font-size: .8em; }`
      );

      const Blacklist = {
         list: [],
         opts: {
            show: true,
            pause: false,
            maxFandoms: 0,
            maxRelations: 0,
            minIncomplete: 0,
            maxChapters: 0,
            minWords: 0,
            maxWords: 0,
            langs: ''
         },
         where: 'li.blurb.group',
         what: '.tags .tag, .required-tags span:not(.warnings) span.text, .header .fandoms .tag, .work a[rel = "author"]',
         get: function() {
            this.list = getStorage(`${NAMESPACE}_blacklist`, '[]');
            Object.assign(this.opts, getStorage(`${NAMESPACE}_blacklist_opts`, '{}'));
         },
         set: function(v) {
            this.list = v.list.trim() ? JSON.parse(`["${v.list.trim().replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n').split(',').join('","')}"]`) : [];
            setStorage(`${NAMESPACE}_blacklist`, this.list);
            this.opts.langs = v.langs.trim().replace(/[\\"]/g, '');
            this.opts.show = v.show;
            this.opts.pause = v.pause;
            this.opts.maxFandoms = v.maxFandoms ? Math.max(parseInt(v.maxFandoms, 10), 0) : 0;
            this.opts.maxRelations = v.maxRelations ? Math.max(parseInt(v.maxRelations, 10), 0) : 0;
            this.opts.minIncomplete = v.minIncomplete ? Math.max(parseInt(v.minIncomplete, 10), 0) : 0;
            this.opts.maxChapters = v.maxChapters ? Math.max(parseInt(v.maxChapters, 10), 0) : 0;
            this.opts.minWords = v.minWords ? Math.max(parseInt(v.minWords, 10), 0) : 0;
            this.opts.maxWords = v.maxWords ? Math.max(parseInt(v.maxWords, 10), 0) : 0;
            if (this.opts.maxWords > 0 && this.opts.minWords >= this.opts.maxWords) this.opts.maxWords = 0;
            setStorage(`${NAMESPACE}_blacklist_opts`, this.opts);
         },
         findFandoms: function(w) {
            return this.opts.maxFandoms && w.querySelectorAll('.header .fandoms .tag').length > this.opts.maxFandoms;
         },
         findRelations: function(w) {
            return this.opts.maxRelations && w.querySelectorAll('.tags .relationships .tag').length > this.opts.maxRelations;
         },
         findIncomplete: function(w) {
            if (!this.opts.minIncomplete || !w.querySelector('dd.chapters') || /(\d+)\/\1/.test(w.querySelector('dd.chapters').textContent)) return false;
            let today = new Date();
            let last = new Date(w.querySelector('.datetime').textContent);
            // millisecs in a month = 30.4days*24hrs*60mins*60secs*1000
            return Math.abs(last.getTime() - today.getTime()) / (30.4 * 24 * 60 * 60 * 1000) > this.opts.minIncomplete;
         },
         findChapters: function(w) {
            return this.opts.maxChapters && w.querySelector('dd.chapters') && parseInt(w.querySelector('dd.chapters').textContent.split('/')[0], 10) > this.opts.maxChapters;
         },
         findWords: function(w) {
            if ((this.opts.minWords || this.opts.maxWords) && w.querySelector('dd.words')) {
               let numWords = parseInt(w.querySelector('dd.words').textContent.replace(/,/g, ''), 10) / 1000;
               if ((this.opts.minWords && numWords <= this.opts.minWords) || (this.opts.maxWords && numWords >= this.opts.maxWords)) return true;
            }
            return false;
         },
         findLangs: function(w) {
            return this.opts.langs && w.querySelector('dd.language') && !this.opts.langs.toLowerCase().includes(w.querySelector('dd.language').textContent.toLowerCase().trim());
         },
         findTags: function(w) {
            return Array.prototype.map.call(w.querySelectorAll(this.what), tag => [tag.textContent.trim(), tag.parentElement.className, tag.rel || false]);
         },
         ifMatch: function(tag) {
            return this.list.some(b => {
               b = b.trim().replace(/[.+?^${}()|[\]\\]/g, '\\$&');

               // author
               if (tag[2] === 'author') {
                  if (b.startsWith('@')) b = b.replace('@', '');
                  else return false;
               }
               // wildcard
               b = b.replace(/\*/g, '.*');
               // match 2 words in any order
               b = b.replace(/(.+)&&(.+)/, '(?=.*$1)(?=.*$2).*');
               // only otp
               if (tag[1] === 'relationships') b = b.replace(/(.+)&!(.+)/, '(?=.*\\/)((?=.*$1)(?!.*$2)|(?=.*$2)(?!.*$1)).*');

               let reg = new RegExp(`^${b}$`, 'i');
               return reg.test(tag[0]) === true;
            });
         },
         findMatch: function() {
            if (this.opts.pause) return;
            document.querySelectorAll(this.where).forEach(w => {
               if (this.opts.show) {
                  let reasons = this.findTags(w).filter(this.ifMatch, this).map(tag => tag[0]);
                  if (this.findRelations(w)) reasons.unshift('Relations');
                  if (this.findChapters(w)) reasons.unshift('Chapters');
                  if (this.findWords(w)) reasons.unshift('Words');
                  if (this.findFandoms(w)) reasons.unshift('Fandoms');
                  if (this.findIncomplete(w)) reasons.unshift('Incomplete');
                  if (this.findLangs(w)) reasons.unshift('Language');
                  if (!reasons.length) return;
                  w.setAttribute('data-visibility', 'hide');
                  w.setAttribute('data-reasons', reasons.join(' > '));
               } else {
                  if (!this.findLangs(w) && !this.findIncomplete(w) && !this.findFandoms(w) && !this.findWords(w) && !this.findChapters(w) && !this.findRelations(w) && !this.findTags(w).some(this.ifMatch, this)) return;
                  w.setAttribute('data-visibility', 'remove');
               }
            });
         },
         clear: function() {
            document.querySelectorAll(`${this.where}[data-visibility]`).forEach(el => {
               el.removeAttribute('data-visibility');
               el.removeAttribute('data-reasons');
            });
         },
         save: function() {
            this.set({
               list: document.getElementById(`${NAMESPACE}-black-tags`).value,
               langs: document.getElementById(`${NAMESPACE}-black-langs`).value,
               show: document.getElementById(`${NAMESPACE}-black-show`).checked,
               pause: document.getElementById(`${NAMESPACE}-black-pause`).checked,
               maxFandoms: document.getElementById(`${NAMESPACE}-black-maxFandoms`).value,
               maxRelations: document.getElementById(`${NAMESPACE}-black-maxRelations`).value,
               minIncomplete: document.getElementById(`${NAMESPACE}-black-minIncomplete`).value,
               maxChapters: document.getElementById(`${NAMESPACE}-black-maxChapters`).value,
               minWords: document.getElementById(`${NAMESPACE}-black-minWords`).value,
               maxWords: document.getElementById(`${NAMESPACE}-black-maxWords`).value
            });
            this.clear();
            this.findMatch();
         },
         html: function() {
            let blackMenu = document.createElement('li');
            blackMenu.id = `${NAMESPACE}-black`;
            blackMenu.className = 'dropdown';
            blackMenu.innerHTML = `<a>Blacklist</a>
               <ul class="menu dropdown-menu" role="menu">
                  <li role="menu-item"><a class="${NAMESPACE}-menu-save" id="${NAMESPACE}-black-save">SAVE</a></li>
                  <li role="menu-item" style="padding: .5em 0;"><div class="opts"><span>SHOW REASONS <input id="${NAMESPACE}-black-show" type="checkbox" ${this.opts.show ? 'checked' : ''}></span> <span>PAUSE <input id="${NAMESPACE}-black-pause" type="checkbox" ${this.opts.pause ? 'checked' : ''}></span></div></li>
                  <li role="menu-item" style="padding: .5em 0;">
                     <div class="info"><span>LEGEND</span><span title="separate tags with commas">separator: , (comma)</span><span title="@: hide author's works">author: @</span><span title="*: match zero or more of any character (letter, white space, symbol...) [it can be used multiple times in the same tag]">wildcard: *</span><span title="&&: match two pair of words in any order [it can be used only once in the same tag]">matched pair: &&</span><span title="&!: hide relationships that include only one person of your favourite ship [it can be used only once in the same tag]">only otp: &!</span></div>
                     <textarea id="${NAMESPACE}-black-tags" spellcheck="false">${this.list.join(',')}</textarea>
                     <div class="opts"><span>max fandoms <input id="${NAMESPACE}-black-maxFandoms" type="number" min="0" step="1" value="${this.opts.maxFandoms}"></span> <span>max relations <input id="${NAMESPACE}-black-maxRelations" type="number" min="0" step="1" value="${this.opts.maxRelations}"></span> <span>max chapters <input id="${NAMESPACE}-black-maxChapters" type="number" min="0" step="1" value="${this.opts.maxChapters}"></span> <span title="for incompleted works">last updated <input id="${NAMESPACE}-black-minIncomplete" type="number" min="0" step="1" title="in months" value="${this.opts.minIncomplete}"></span> <span>min words <input id="${NAMESPACE}-black-minWords" type="number" min="0" step="1" title="in thousands" value="${this.opts.minWords}"></span> <span>max words <input id="${NAMESPACE}-black-maxWords" type="number" min="0" step="1" title="in thousands" value="${this.opts.maxWords}"></span> <span title="show only specified">languages <input type="text" id="${NAMESPACE}-black-langs" spellcheck="false" placeholder="leave empty for any" title="separate languages by a space" value="${this.opts.langs}"></span></div>
                  </li>
               </ul>`;
            document.querySelector('#header > ul').appendChild(blackMenu);

            document.getElementById(`${NAMESPACE}-black-save`).addEventListener('click', function() {
               Blacklist.save();
               this.textContent = 'SAVED';
               setTimeout(() => this.textContent = 'SAVE', 1000);
            });
         }
      };
      Blacklist.get();
      Blacklist.findMatch();
      Blacklist.html();

   } // END Feature.black AND Check.black()


   /** GLOBAL FUNCTIONS **/
   function getStorage(key, def) {
      if (typeof def !== 'string') def = JSON.stringify(def);
      return JSON.parse(localStorage.getItem(key) || def);
   }
   function setStorage(key, value) {
      localStorage.setItem(key, typeof value !== 'string' ? JSON.stringify(value) : value);
   }

   function addCSS(id, css) {
      if (!document.querySelector(`style#${id}`)) {
         let style = document.createElement('style');
         style.id = id;
         style.textContent = css;
         document.getElementsByTagName('head')[0].appendChild(style);
      } else {
         document.querySelector(`style#${id}`).textContent = css;
      }
   }

   function countTime(num) {
      // estimate reading time
      if (!num) return '?';
      let time = (parseInt(num, 10) / Feature.wpm / 60).toFixed(2).toString().split('.');
      return (time[0] !== '0' ? time[0] + 'hr ' : '') + (time[1] !== '00' ? Math.round(parseInt(time[1], 10) / 100 * 60) + 'min' : '') || '<1min';
   }

   function getScroll() {
      return Math.max(document.documentElement.scrollTop, window.scrollY, 0);
   }
   function setScroll(s) {
      window.scroll(0, s ? s : 0);
   }
   function getDocHeight() {
      return Math.max(document.documentElement.scrollHeight, document.documentElement.offsetHeight, document.body.scrollHeight, document.body.offsetHeight);
   }

})();