Full Code of liriliri/eruda for AI

master 0c55928fec80 cached
99 files
352.1 KB
94.7k tokens
356 symbols
1 requests
Download .txt
Showing preview only (376K chars total). Download the full file or copy to clipboard to get everything.
Repository: liriliri/eruda
Branch: master
Commit: 0c55928fec80
Files: 99
Total size: 352.1 KB

Directory structure:
gitextract_e87c_nux/

├── .eustia.js
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── main.yml
│       └── publish.yml
├── .gitignore
├── .gitmodules
├── .prettierignore
├── .prettierrc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build/
│   ├── build.js
│   ├── loaders/
│   │   └── handlebars-minifier-loader.js
│   ├── webpack.analyser.js
│   ├── webpack.base.js
│   ├── webpack.dev.js
│   ├── webpack.polyfill.js
│   └── webpack.prod.js
├── eruda.d.ts
├── eslint.config.mjs
├── karma.conf.js
├── package.json
├── src/
│   ├── Console/
│   │   ├── Console.js
│   │   └── Console.scss
│   ├── DevTools/
│   │   ├── DevTools.js
│   │   ├── DevTools.scss
│   │   └── Tool.js
│   ├── Elements/
│   │   ├── CssStore.js
│   │   ├── Detail.js
│   │   ├── Elements.js
│   │   ├── Elements.scss
│   │   └── util.js
│   ├── EntryBtn/
│   │   ├── EntryBtn.js
│   │   └── EntryBtn.scss
│   ├── Info/
│   │   ├── Info.js
│   │   ├── Info.scss
│   │   └── defInfo.js
│   ├── Network/
│   │   ├── Detail.js
│   │   ├── Network.js
│   │   ├── Network.scss
│   │   └── util.js
│   ├── Resources/
│   │   ├── Cookie.js
│   │   ├── Resources.js
│   │   ├── Resources.scss
│   │   ├── Storage.js
│   │   └── util.js
│   ├── Settings/
│   │   ├── Settings.js
│   │   └── Settings.scss
│   ├── Snippets/
│   │   ├── Snippets.js
│   │   ├── Snippets.scss
│   │   ├── defSnippets.js
│   │   └── searchText.scss
│   ├── Sources/
│   │   ├── Sources.js
│   │   └── Sources.scss
│   ├── eruda.js
│   ├── index.js
│   ├── lib/
│   │   ├── chobitsu.js
│   │   ├── emitter.js
│   │   ├── empty.js
│   │   ├── evalCss.js
│   │   ├── logger.js
│   │   ├── micromark.js
│   │   ├── themes.js
│   │   └── util.js
│   ├── polyfill.js
│   └── style/
│       ├── icon.css
│       ├── icon.json
│       ├── luna.scss
│       ├── mixin.scss
│       ├── reset.scss
│       ├── style.scss
│       └── variable.scss
└── test/
    ├── boot.js
    ├── console.html
    ├── console.js
    ├── data.json
    ├── elements.html
    ├── elements.js
    ├── eruda.html
    ├── eruda.js
    ├── index.html
    ├── info.html
    ├── info.js
    ├── init.js
    ├── inline.html
    ├── manual.html
    ├── network.html
    ├── network.js
    ├── resources.html
    ├── resources.js
    ├── settings.html
    ├── settings.js
    ├── snippets.html
    ├── snippets.js
    ├── sources.html
    ├── sources.js
    ├── style.css
    └── util.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .eustia.js
================================================
module.exports = {
  test: {
    library: ['node_modules/eustia-module'],
    files: ['test/*.js', 'test/*.html'],
    exclude: ['js'],
    namespace: 'util',
    output: 'test/util.js',
  },
}


================================================
FILE: .gitattributes
================================================
* text=auto eol=lf

================================================
FILE: .github/FUNDING.yml
================================================
open_collective: eruda
ko_fi: surunzi
custom: [surunzi.com/wechatpay.html]

================================================
FILE: .github/workflows/main.yml
================================================
name: CI

on:
  workflow_dispatch:
  push:
    branches:
      - 'master'
    paths:
      - 'src/**/*'
      - 'test/**/*'

jobs:
  ci:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: '18.x' 
    - run: |
        npm install -g @liriliri/lsla
        npm i
        npm run ci
    - uses: codecov/codecov-action@v4
      with:
        token: ${{ secrets.CODECOV_TOKEN }} 

================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish to NPM

on:
  workflow_dispatch:
  release:
    types: [created]

jobs:
  publish:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18.x'
          registry-url: 'https://registry.npmjs.org'
      - run: |
          npm i -g @liriliri/lsla
          npm i
          npm run build
      - working-directory: dist
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

================================================
FILE: .gitignore
================================================
.idea/
dist/
node_modules/
test/lib/
coverage/
test/playground.html
npm-debug.log
package-lock.json

================================================
FILE: .gitmodules
================================================
[submodule "src/style/icon"]
	path = src/style/icon
	url = https://github.com/liriliri/icon-share.git


================================================
FILE: .prettierignore
================================================
test/util.js

================================================
FILE: .prettierrc.json
================================================
{
  "singleQuote": true,
  "tabWidth": 2,
  "semi": false
}


================================================
FILE: CHANGELOG.md
================================================
## 3.4.3 (15 Jun 2025)

* fix: redundant code imported

## 3.4.2 (15 Jun 2025)

* fix: elements horizontal scrollbar [#504](https://github.com/liriliri/eruda/issues/504)

## 3.4.1 (10 Nov 2024)

* fix: no copy and delete for shadow root
* fix: fetch remains pending when error occurs
* fix: theme not updated if system theme changed

## 3.4.0 (27 Sep 2024)

* feat: support shadow dom [#158](https://github.com/liriliri/eruda/issues/158)
* fix: quirks mode table rendering [#459](https://github.com/liriliri/eruda/issues/459)

## 3.3.0 (9 Sep 2024)

* feat: add vue devtools plugin

## 3.2.3 (10 AUG 2024)

* fix: WebSocket message base64 encoded [#447](https://github.com/liriliri/eruda/issues/447)

## 3.2.2 (8 AUG 2024)

* chore: update plugin versions

## 3.2.1 (20 JUL 2024)

* fix: touches plugin [#344](https://github.com/liriliri/eruda/issues/344)

## 3.2.0 (16 JUL 2024)

* feat: support inline mode
* feat: allow spaces in plugin name
* fix: some typescript d.ts mistakes
* chore: remove elements set api
* chore: update monitor plugin version

## 3.1.0 (9 JUL 2024)

* feat: add AMOLED theme [#414](https://github.com/liriliri/eruda/pull/414)
* feat: support system preference theme config
* feat: add isDarkTheme, getTheme util
* fix: backers.svg lazy loading [#407](https://github.com/liriliri/eruda/issues/407)

## 3.0.1 (18 JUL 2023)

* fix: can not print string with %o [#336](https://github.com/liriliri/eruda/issues/336)
* fix: mouse event on touch device [#302](https://github.com/liriliri/eruda/issues/302)
* fix: unable to remove snippets [#349](https://github.com/liriliri/eruda/issues/349)

## 3.0.0 (2 Apr 2023)

* feat: replace fps and memory with monitor plugin
* fix: resource stylesheet show failed
* chore: remove licia utils
* chore: separate polyfill

## 2.11.3 (3 Mar 2023)

* fix: scale [#307](https://github.com/liriliri/eruda/issues/307)

## 2.11.2 (28 Jan 2023)

* fix: check safe area error

## 2.11.1 (28 Jan 2023)

* fix: bottom safe area
* fix(console): filter function support
* fix: click event stop propagation [#155](https://github.com/liriliri/eruda/issues/155)
* fix: worker null error [#152](https://github.com/liriliri/eruda/issues/152)

## 2.11.0 (19 Jan 2023)

* feat(network): filter
* feat(info): add backers
* feat(settings): use luna setting
* feat(resources): use luna data grid
* feat(resources): copy storage, cookie
* fix(sources): code not selectable
* fix(console): filter api

## 2.10.0 (24 Dec 2022)

* feat(sources): use luna text viewer
* feat(elements): split mode
* feat(network): split mode
* fix(resources): delete cookie

## 2.9.1 (20 Dec 2022)

* fix(elements): select element using touch events

## 2.9.0 (20 Dec 2022)

* feat(elements): integrate dom viewer
* feat(elements): element crumbs
* feat(elements): copy node and delete node
* feat(network): copy response
* feat(network): toggle recording
* chore: remove dom plugin snippet

## 2.8.3 (13 Dec 2022)

* fix(network): remove data grid ios outline
* chore: update luna console and luna object viewer 

## 2.8.2 (12 Dec 2022)

* fix: some variables not reset when destroy

## 2.8.1 (12 Dec 2022)

* fix: remove luna syntax highlighter

## 2.8.0 (11 Dec 2022)

* feat(info): copy
* feat(sources): use luna syntax highlighter
* feat(network): use luna data grid
* feat(network): copy as curl [#220](https://github.com/liriliri/eruda/issues/220)
* fix(network): recognize JSON [#201](https://github.com/liriliri/eruda/issues/201)
* fix: init with shadow dom style error [#195](https://github.com/liriliri/eruda/issues/195)

## 2.7.4 (10 Dec 2022)

* fix: firefox document.body is null error [#293](https://github.com/liriliri/eruda/issues/293)

## 2.7.3 (8 Dec 2022)

* fix: remove tabs horizontal scrollbar [#236](https://github.com/liriliri/eruda/issues/236)

## 2.7.2 (7 Dec 2022)

* fix: luna modal style

## 2.7.1 (7 Dec 2022)

* fix: remove debug log

## 2.7.0 (7 Dec 2022)

* feat: drag to resize
* feat: update icons
* feat: use luna modal to replace browser prompt

## 2.6.2 (3 Dec 2022)

* feat: support android 5.0
* feat(sources): remove code beautify
* fix: code plugin theme

## 2.6.1 (26 Nov 2022)

* fix: dark mode scrollbar style
* fix: unable to load timing plugin

## 2.6.0 (25 Nov 2022)

* feat(console): select and copy
* chore: update luna console
* chore: update chobitsu

## 2.5.0 (9 Jul 2022)

* feat: add ts declaration [#187](https://github.com/liriliri/eruda/pull/187)
* refactor: use luna console
* refactor: use chobitsu for highlighting element

## 2.4.1 (28 Sep 2020)

* fix: remove arrow function [#160](https://github.com/liriliri/eruda/issues/160)

## 2.4.0 (14 Sep 2020)

* feat: default settings [#141](https://github.com/liriliri/eruda/issues/141)
* fix(elements): highlight
* fix(console): blinks frequently as it scroll to the border
* refactor: use chobitsu

## 2.3.3 (3 May 2020)

* fix: unsafe-eval CSP violation [#140](https://github.com/liriliri/eruda/issues/140)

## v2.3.2 (29 Apr 2020)

* fix(console): scroll performance

## v2.3.1 (28 Apr 2020)

* fix(elements): content highlight

## v2.3.0 (28 Apr 2020)

* feat: refresh notification
* fix(console): safari bounce effect
* fix(elements): highlight

## v2.2.2 (17 Apr 2020)

* fix(console): extra info from
* chore: update icons

## v2.2.1 (20 Mar 2020)

* fix: redundant evaluated style
* chore: use [luna-object-viewer](https://github.com/liriliri/luna) for viewing object

## v2.2.0 (9 Feb 2020)

* feat: use dark theme for dark mode
* feat(elements): computed style filter
* feat(resources): storage and cookie filter
* fix(snippet): error loading plugin for local page
* fix(console): unable to clear filter

## v2.1.0 (2 Feb 2020)

* feat: change navigation bar height
* feat: change default transparency to 1
* feat: change loaded plugin position
* feat(console): remove debug filter
* feat(console): improve input style
* feat(console): show filter text
* feat(network): add requests api [#132](https://github.com/liriliri/eruda/issues/132)

## v2.0.2 (9 Jan 2020)

* chore: reduce file size (452kb -> 418kb)

## v2.0.1 (6 Jan 2020)

* chore: update plugins

## v2.0.0 (3 Jan 2020)

* feat: theme support
* feat(console): $x utility
* feat(console): remove useWorker
* feat(sources): indent size configuration
* fix(console): url recognition
* fix(console): log style
* fix(sources): scrolling
* perf(console): large object expansion
* chore: reduce file size (472kb -> 452kb)

## v1.10.3 (8 Nov 2019)

* fix(info): escape location [#127](https://github.com/liriliri/eruda/issues/127)
* chore: update refresh icon
* chore: update timing plugin version

## v1.10.2 (5 Nov 2019)

* fix: must add .default if using require 

## v1.10.1 (4 Nov 2019)

* fix(console): error display when js execution disabled

## v1.10.0 (4 Nov 2019)

* chore: updated to babel7, must add .default if using require 
* feat(console): multiple console instance
* perf(console): rendering for a large number of logs

## v1.9.2 (1 Nov 2019)

* perf(console): rendering

## v1.9.1 (27 Oct 2019)

* perf(console): asynchronous log render
* perf(console): reduce memory usage, 50% drop

## v1.9.0 (20 Oct 2019)

* feat: add snippet for loading touches plugin
* feat: add fit screen snippet
* fix(console): filter shouldn't affect group

## v1.8.1 (14 Oct 2019)

* fix(network): style [#121](https://github.com/liriliri/eruda/issues/121)

## v1.8.0 (13 Oct 2019)

* feat(network): display optimization
* feat: move http view from sources to network
* fix(console): group object expansion

## v1.7.2 (11 Oct 2019)

* fix(console): blank bottom if js input is disabled
* chore: update eruda-dom version

## v1.7.1 (10 Oct 2019)

* fix: resize

## v1.7.0 (8 Oct 2019)

* feat: resize [#89](https://github.com/liriliri/eruda/issues/89)
* feat(console): replace help button with filter
* feat(console): disable js execution
* feat(console): [utilities api](https://developers.google.cn/web/tools/chrome-devtools/console/utilities)
* fix(console): disable log collapsing for group
* fix(elements): select not working for desktop

## v1.6.3 (1 Oct 2019)

* fix(console): log border style

## v1.6.2 (29 Sep 2019)

* fix: container style affected [#119](https://github.com/liriliri/eruda/issues/119)
* fix(console): log style, line-height should be normal

## v1.6.1 (27 Sep 2019)

* feat(network): catch fetch request headers
* feat(console): timeLog, countReset
* fix(console): clear not working
* fix(console): table

## v1.6.0 (26 Sep 2019)

* feat: console group
* fix: console style, width and height is forbidden
* fix: regexp json view
* chore: update fps and memory plugin version

## v1.5.8 (2 Aug 2019)

* fix: safeStorage undefined [#108](https://github.com/liriliri/eruda/issues/108)

## v1.5.7 (15 Jul 2019)

* Fix iOS max log number
* Disable calling init if already initialized
* Disable worker by default
* Support xhr blob response type [#104](https://github.com/liriliri/eruda/issues/100)

## v1.5.6 (17 Jun 2019)

* Disable log collapse for objects

## v1.5.5 (25 May 2019)

* Fix resources error when cookie has % [#100](https://github.com/liriliri/eruda/issues/100)
* Update dom plugin version

## v1.5.4 (23 Sep 2018)

* Fix network url start with //
* Smaller padding for logs

## v1.5.3 (2 Sep 2018)

* Add load dom plugin snippet
* Disable highlight for invisible elements
* Fix unexpected token \t in JSON
* Add load orientation plugin snippet

## v1.5.2 (23 Aug 2018)

* Fix console show in sources panel
* Fix log merge
* Support getting entryBtn instance
* Update timing plugin version
* Add remove setting api
* Fix safari merge log exception

## v1.5.1 (18 Aug 2018)

* Fix uglifyjs unicode escape [#69](https://github.com/liriliri/eruda/issues/69)
* Update icons, use [iconfont](http://www.iconfont.cn) instead of [icomoon](https://icomoon.io/)
* Show custom request headers [#78](https://github.com/liriliri/eruda/pull/78)
* Add get api to info panel [#83](https://github.com/liriliri/eruda/issues/83)
* Fix responseType json error [#82](https://github.com/liriliri/eruda/issues/82)
* Support console lazy evaluation

## v1.5.0 (19 Jun 2018)

* Use shadow dom to encapsulate css
* Enable sources copy [#71](https://github.com/liriliri/eruda/issues/71)
* Improve **borderAll** style
* Add **position** api [#74](https://github.com/liriliri/eruda/issues/74)
* Fix nav bottom bar wrong position when removed

## v1.4.4 (27 May 2018)

* Improve console line break display
* Add **rmCookie** util
* Add **Load Geolocation Plugin** snippet
* Fix Elements cssRules [#63](https://github.com/liriliri/eruda/issues/63)
* Support console events [#66](https://github.com/liriliri/eruda/issues/66)
* Fix Uc browser console worker [#62](https://github.com/liriliri/eruda/issues/62)

## v1.4.3 (7 Feb 2018)

* Dynamic info content support [#51](https://github.com/liriliri/eruda/issues/51)
* Fix console input covered by error log
* Add elements box model chart
* Fix source code white-space style [#53](https://github.com/liriliri/eruda/issues/53)
* Resources support iframe
* Add **Load Benchmark Plugin** snippet

## v1.4.2 (28 Jan 2018)

* Extract viewportScale util into [eris](https://github.com/liriliri/eris)
* Improve image list view using flex
* Add DevTools display event hooks [#50](https://github.com/liriliri/eruda/issues/50)

## v1.4.1 (13 Jan 2018)

* Update timing plugin version
* Fix viewportScale
* Optimize console performance for big data
* Expose snippets run api
* Delete desktop scrollbar style
* Add code plugin to snippets

## v1.4.0 (7 Jan 2018)

* Remove network timing into external plugin
* Add system info
* Add memory plugin snippet
* Monitor fetch requests [#24](https://github.com/liriliri/eruda/issues/24)
* Reduce json viewer click area
* Use resource timing for image capture

## v1.3.2 (14 Dec 2017)

* Fix restore settings snippet
* Extract *features* into an external plugin

## v1.3.1 (19 Nov 2017)

* Observe elements in resources panel
* Fix performance timing not supported [#40](https://github.com/liriliri/eruda/issues/40)

## v1.3.0 (5 Nov 2017)

* Remove log margin
* Fix css custom properties [#33](https://github.com/liriliri/eruda/issues/33)
* Add version info
* Change icomoon generated font name
* Improve snippets style
* Add *Load Fps Plugin* and *Restore Settings* snippets
* Support navbar color customization
* Support range in settings panel
* Support auto scale [#32](https://github.com/liriliri/eruda/issues/32)
* Improve *Border All* snippet
* Use high resolution time for console time

## v1.2.6 (31 Aug 2017)

* Fix catch global errors

## v1.2.5 (20 Aug 2017)

* Fix cookie URI malformed
* Fix single string argument unescaped
* Update util library and dependencies
* Fix catch event listeners [#31](https://github.com/liriliri/eruda/issues/31)
* Console log scroll automatically only at bottom
* Fix unformatted html tag

## v1.2.4 (1 Jul 2017)

* Fix uncaught promise error [#29](https://github.com/liriliri/eruda/issues/23)
* Fix bad classes [#28](https://github.com/liriliri/eruda/issues/23)

## v1.2.3 (15 May 2017)

* Disable modernizr classes
* Update eustia util
* Fix console resize [#23](https://github.com/liriliri/eruda/issues/23)
* Improve object log
* Use outline for borderAll snippet

## v1.2.2 (11 Mar 2017)

* Fix log url recognition
* Fix error log stack url and style
* Fix table log ouput
* Fix storage initialization [#20](https://github.com/liriliri/eruda/issues/20)
* Update eustia lib
* Elements auto refresh
* Add pc scrollbar style

================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016-present liriliri

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<div align="center">
  <a href="https://eruda.liriliri.io/" target="_blank">
    <img src="https://eruda.liriliri.io/icon.png" width="400">
  </a>
</div>

<h1 align="center">Eruda</h1>

<div align="center">

Console for Mobile Browsers.

[![NPM version][npm-image]][npm-url]
[![Build status][ci-image]][ci-url]
[![Test coverage][codecov-image]][codecov-url]
[![Downloads][jsdelivr-image]][jsdelivr-url]
[![License][license-image]][npm-url]

</div>

[npm-image]: https://img.shields.io/npm/v/eruda?style=flat-square
[npm-url]: https://npmjs.org/package/eruda
[jsdelivr-image]: https://img.shields.io/jsdelivr/npm/hm/eruda?style=flat-square
[jsdelivr-url]: https://www.jsdelivr.com/package/npm/eruda
[ci-image]: https://img.shields.io/github/actions/workflow/status/liriliri/eruda/main.yml?branch=master&style=flat-square
[ci-url]: https://github.com/liriliri/eruda/actions/workflows/main.yml 
[codecov-image]: https://img.shields.io/codecov/c/github/liriliri/eruda?style=flat-square
[codecov-url]: https://codecov.io/github/liriliri/eruda?branch=master
[license-image]: https://img.shields.io/npm/l/eruda?style=flat-square
[donate-image]: https://img.shields.io/badge/$-donate-0070ba.svg?style=flat-square

<img src="https://eruda.liriliri.io/screenshot.jpg" style="width:100%">

## Demo

![Demo](https://eruda.liriliri.io/qrcode.png)

Browse it on your phone: [eruda.liriliri.io](https://eruda.liriliri.io/)

## Install

You can get it on npm.

```bash
npm install eruda --save-dev
```

Add this script to your page.

```html
<script src="node_modules/eruda/eruda.js"></script>
<script>eruda.init();</script>
```

It's also available on [jsDelivr](http://www.jsdelivr.com/projects/eruda) and [cdnjs](https://cdnjs.com/libraries/eruda).

```html
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
```

For more detailed usage instructions, please read the documentation at [eruda.liriliri.io](https://eruda.liriliri.io/docs/)!

## Related Projects

* [eruda-android](https://github.com/liriliri/eruda-android): Simple webview with eruda loaded automatically.
* [chii](https://github.com/liriliri/chii): Remote debugging tool.
* [chobitsu](https://github.com/liriliri/chobitsu): Chrome devtools protocol JavaScript implementation.
* [licia](https://github.com/liriliri/licia): Utility library used by eruda.
* [luna](https://github.com/liriliri/luna): UI components used by eruda.
* [vivy](https://github.com/liriliri/vivy-docs): Icon image generation.

## Third Party

* [eruda-pixel](https://github.com/Faithree/eruda-pixel): UI pixel restoration tool.
* [eruda-webpack-plugin](https://github.com/huruji/eruda-webpack-plugin): Eruda webpack plugin.
* [eruda-vue-devtools](https://github.com/Zippowxk/vue-devtools-plugin): Eruda Vue-devtools plugin.

## Backers

<a rel="noreferrer noopener" href="https://opencollective.com/eruda" target="_blank"><img src="https://opencollective.com/eruda/backers.svg?width=890"></a>

## Contribution

Read [Contributing Guide](https://eruda.liriliri.io/docs/contributing.html) for development setup instructions.


================================================
FILE: build/build.js
================================================
const path = require('path')
const fs = require('licia/fs')

const pkg = require('../package.json')

delete pkg.scripts
delete pkg.devDependencies

fs.writeFile(
  path.resolve(__dirname, '../dist/package.json'),
  JSON.stringify(pkg, null, 2),
  'utf8'
)


================================================
FILE: build/loaders/handlebars-minifier-loader.js
================================================
module.exports = function (src) {
    return src.replace(/"loc":\{"start":\{"line":\d+,"column":\d+},"end":\{"line":\d+,"column":\d+\}\}/g, '')
}

================================================
FILE: build/webpack.analyser.js
================================================
const BundleAnalyzerPlugin =
  require('webpack-bundle-analyzer').BundleAnalyzerPlugin

exports = require('./webpack.prod')

exports.plugins.push(new BundleAnalyzerPlugin())

module.exports = exports


================================================
FILE: build/webpack.base.js
================================================
const autoprefixer = require('autoprefixer')
const prefixer = require('postcss-prefixer')
const clean = require('postcss-clean')
const webpack = require('webpack')
const pkg = require('../package.json')
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')

process.traceDeprecation = true

const banner = pkg.name + ' v' + pkg.version + ' ' + pkg.homepage

const postcssLoader = {
  loader: 'postcss-loader',
  options: {
    plugins: [
      prefixer({
        prefix: '_',
        ignore: [/luna-*/],
      }),
      autoprefixer,
      clean(),
    ],
  },
}

const rawLoader = {
  loader: 'raw-loader',
  options: {
    esModule: false,
  },
}

module.exports = {
  entry: './src/index',
  resolve: {
    symlinks: false,
    alias: {
      axios: path.resolve(__dirname, '../src/lib/empty.js'),
      micromark: path.resolve(__dirname, '../src/lib/micromark.js'),
    },
  },
  devServer: {
    static: {
      directory: path.join(__dirname, '../test'),
    },
    port: 8080,
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    publicPath: '/assets/',
    library: 'eruda',
    libraryTarget: 'umd',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        include: [
          path.resolve(__dirname, '../src'),
          path.resolve(__dirname, '../node_modules/luna-console'),
          path.resolve(__dirname, '../node_modules/luna-modal'),
          path.resolve(__dirname, '../node_modules/luna-tab'),
          path.resolve(__dirname, '../node_modules/luna-data-grid'),
          path.resolve(__dirname, '../node_modules/luna-object-viewer'),
          path.resolve(__dirname, '../node_modules/luna-dom-viewer'),
          path.resolve(__dirname, '../node_modules/luna-text-viewer'),
          path.resolve(__dirname, '../node_modules/luna-setting'),
          path.resolve(__dirname, '../node_modules/luna-box-model'),
          path.resolve(__dirname, '../node_modules/luna-notification'),
        ],
        use: [
          {
            loader: 'babel-loader',
            options: {
              sourceType: 'unambiguous',
              presets: ['@babel/preset-env'],
              plugins: [
                '@babel/plugin-transform-runtime',
                '@babel/plugin-proposal-class-properties',
              ],
            },
          },
        ],
      },
      {
        test: /\.scss$/,
        use: [
          'css-loader',
          postcssLoader,
          { loader: 'sass-loader', options: { api: 'modern' } },
        ],
      },
      {
        test: /\.css$/,
        exclude: /luna-dom-highlighter/,
        use: ['css-loader', postcssLoader],
      },
      {
        test: /luna-dom-highlighter\.css$/,
        use: [rawLoader],
      },
    ],
  },
  plugins: [
    new webpack.BannerPlugin(banner),
    new webpack.DefinePlugin({
      VERSION: '"' + pkg.version + '"',
    }),
    new ESLintPlugin(),
  ],
}


================================================
FILE: build/webpack.dev.js
================================================
const webpack = require('webpack')

exports = require('./webpack.base')

exports.mode = 'development'
exports.output.filename = 'eruda.js'
exports.devtool = 'source-map'
exports.plugins = exports.plugins.concat([
  new webpack.DefinePlugin({
    ENV: '"development"',
  }),
])

module.exports = exports


================================================
FILE: build/webpack.polyfill.js
================================================
const path = require('path')

module.exports = {
  mode: 'production',
  entry: './src/polyfill',
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'eruda-polyfill.js',
  },
}


================================================
FILE: build/webpack.prod.js
================================================
const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin')

exports = require('./webpack.base')

exports.mode = 'production'
exports.output.filename = 'eruda.js'
exports.devtool = 'source-map'
exports.plugins = exports.plugins.concat([
  new webpack.DefinePlugin({
    ENV: '"production"',
  }),
])
exports.optimization = {
  minimize: true,
  minimizer: [
    new TerserPlugin({
      extractComments: false,
    }),
  ],
}

module.exports = exports


================================================
FILE: eruda.d.ts
================================================
/**
 * Type definitions for Eruda
 * @see https://github.com/liriliri/eruda
 */
declare module 'eruda' {
  export interface InitDefaults {
    /**
     * Transparency, 0 to 1
     */
    transparency?: number
    /**
     * Display size, 0 to 100
     */
    displaySize?: number
    /**
     * Theme, defaults to Light or Dark in dark mode
     */
    theme?: string
  }

  export interface InitOptions {
    /**
     * Container element. If not set, it will append an element directly under html root element
     */
    container?: HTMLElement
    /**
     * Choose which default tools you want, by default all will be added
     */
    tool?: string[]
    /**
     * Auto scale eruda for different viewport settings
     */
    autoScale?: boolean
    /**
     * Use shadow dom for css encapsulation
     */
    useShadowDom?: boolean
    /**
     * Enable inline mode
     */
    inline?: boolean
    /**
     * Default settings
     */
    defaults?: InitDefaults
  }

  export interface Position {
    x: number
    y: number
  }

  type AnyFn = (...args: any[]) => any

  export interface Emitter {
    on(event: string, listener: AnyFn): Emitter
    off(event: string, listener: AnyFn): Emitter
    once(event: string, listener: AnyFn): Emitter
    emit(event: string, ...args: any[]): Emitter
    removeAllListeners(event?: string): Emitter
  }

  /**
   * Eruda Plugin
   * @see https://eruda.liriliri.io/docs/plugin.html
   */
  export interface Tool {
    /**
     * Every plugin must have a unique name, which will be shown as the tab name on the top.
     */
    name: string
    /**
     * Called when plugin is added, and a document element used to display content is passed in.
     * The element is wrapped as a jQuery like object, provided by the licia utility library.
     */
    init(el: unknown): void
    /**
     * Called when switch to the panel. Usually all you need to do is to show the container element.
     */
    show(): Tool | undefined
    /**
     * Called when switch to other panel. You should at least hide the container element here.
     */
    hide(): Tool | undefined
    /**
     * Called when plugin is removed using `eruda.remove('plugin name')`.
     */
    destroy(): void
  }

  export interface ToolConstructor {
    new (): Tool
    readonly prototype: Tool

    extend(tool: Tool): ToolConstructor
  }

  export interface ConsoleConfig {
    /**
     * Asynchronous rendering
     */
    asyncRender?: boolean
    /**
     * Enable JavaScript execution
     */
    jsExecution?: boolean
    /**
     * Catch global errors
     */
    catchGlobalErr?: boolean
    /**
     * Override console
     */
    overrideConsole?: boolean
    /**
     * Display extra information
     */
    displayExtraInfo?: boolean
    /**
     * Display unenumerable properties
     */
    displayUnenumerable?: boolean
    /**
     * Access getter value
     */
    displayGetterVal?: boolean
    /**
     * Stringify object when clicked
     */
    lazyEvaluation?: boolean
    /**
     * Auto display if error occurs
     */
    displayIfErr?: boolean
    /**
     * Max log number
     */
    maxLogNum?: string
  }

  export interface Log {
    type: string
  }

  export interface ErudaConsole extends Tool, Console {
    config: {
      set<K extends keyof ConsoleConfig>(name: K, value: ConsoleConfig[K]): void
    }
    /**
     * Custom filter
     */
    filter(pattern: string | RegExp | ((log: Log) => boolean)): void
    /**
     * Html string
     */
    html(htmlStr: string): void
  }

  export interface ErudaConsoleConstructor {
    new (): ErudaConsole
    readonly prototype: ErudaConsole
  }

  export interface ElementsConfig {
    /**
     * Catch Event Listeners
     */
    overrideEventTarget?: boolean
    /**
     * Auto Refresh
     */
    observeElement?: boolean
  }

  export interface Elements extends Tool {
    config: {
      set<K extends keyof ElementsConfig>(
        name: K,
        value: ElementsConfig[K]
      ): void
    }
    /**
     * Element to display
     */
    select(el: HTMLElement): void
  }

  export interface ElementsConstructor {
    new (): Elements
    readonly prototype: Elements
  }

  export interface Network extends Tool {
    /**
     * Clear requests
     */
    clear(): void
    /**
     * Get request data
     */
    requests(): object[]
  }

  export interface NetworkConstructor {
    new (): Network
    readonly prototype: Network
  }

  export interface ResourcesConfig {
    /**
     * Hide Eruda Setting
     */
    hideErudaSetting?: boolean
    /**
     * Auto Refresh Elements
     */
    observeElement?: boolean
  }

  export interface Resources extends Tool {
    config: {
      set<K extends keyof ResourcesConfig>(
        name: K,
        value: ResourcesConfig[K]
      ): void
    }
  }

  export interface ResourcesConstructor {
    new (): Resources
    readonly prototype: Resources
  }

  export interface SourcesConfig {
    /**
     * Show Line Numbers
     */
    showLineNum?: boolean
    /**
     * Beautify Code
     */
    formatCode?: boolean
    /**
     * Indent Size
     */
    indentSize?: string
  }

  export interface Sources extends Tool {
    config: {
      set<K extends keyof SourcesConfig>(name: K, value: SourcesConfig[K]): void
    }
  }

  export interface SourcesConstructor {
    new (): Sources
    readonly prototype: Sources
  }

  export interface InfoItem {
    name: string
    val: string
  }

  export interface Info extends Tool {
    /**
     * Clear infos
     */
    clear(): void
    /**
     * Add info
     */
    add(name: string, content: string | (() => void)): void
    /**
     * Get info or infos
     */
    get(): InfoItem[]
    get(name: string): string
    /**
     * Remove specified info
     */
    remove(name: string): void
  }

  export interface InfoConstructor {
    new (): Info
    readonly prototype: Info
  }

  export interface Snippets extends Tool {
    /**
     * Clear snippets
     */
    clear(): void
    /**
     * Add snippet
     * @param name Snippet name
     * @param fn Function to be triggered
     * @param desc Snippet description
     */
    add(name: string, fn: Function, desc: string): void
    /**
     * Remove specified snippet
     * @param name Snippet name
     */
    remove(name: string): void
    /**
     * Run specified snippet
     * @param name Snippet name
     */
    run(name: string): void
  }

  export interface SnippetsConstructor {
    new (): Snippets
    readonly prototype: Snippets
  }

  export interface SettingsRangeOptions {
    min?: number
    max?: number
    step?: number
  }

  export interface Settings extends Tool {
    /**
     * Clear settings
     */
    clear(): void
    /**
     * Remove setting
     * @param cfg Config object
     * @param name Option name
     */
    remove(cfj: object, name: string): void
    /**
     * Add text
     */
    text(str: string): void
    /**
     * Add switch to toggle a boolean value
     * @param cfg Config object created by util.createCfg
     * @param name Option name
     * @param desc Option description
     */
    switch(cfg: object, name: string, desc: string): void
    /**
     * Add select to select a number of string values
     * @param cfg Config object
     * @param name Option name
     * @param desc Option description
     * @param values Array of strings to select
     */
    select(cfg: object, name: string, desc: string, values: string[]): void
    /**
     * Add range to input a number
     * @param cfg Config object
     * @param name Option name
     * @param desc Option description
     * @param options Min, max, step
     */
    range(
      cfg: object,
      name: string,
      desc: string,
      options?: SettingsRangeOptions
    ): void
    /**
     * Add a separator
     */
    separator(): void
  }

  export interface SettingsConstructor {
    new (): Settings
    readonly prototype: Settings
  }

  export interface EntryBtn extends Emitter {
    show(): void
    hide(): void
    getPos(): Position
    setPos(pos: Position): void
    destroy(): void
  }

  export interface EntryBtnConstructor {
    new (): EntryBtn
    readonly prototype: EntryBtn
  }

  export interface DevTools extends Emitter {
    show(): DevTools
    hide(): DevTools
    toggle(): void
    add(tool: Tool | object): DevTools
    remove(name: string): DevTools
    removeAll(): DevTools
    get<T extends ToolConstructor>(name: string): InstanceType<T> | undefined
    showTool(name: string): DevTools
    initCfg(settings: Settings): void
    notify(content: string, options: object): void
    destroy(): void
  }

  export interface DevToolsConstructor {
    new (): DevTools
    readonly prototype: DevTools
  }

  /**
   * Eruda Util
   * @see https://eruda.liriliri.io/docs/plugin.html#utility
   */
  export interface Util {
    evalCss(css: string): HTMLStyleElement
    isErudaEl(val: any): boolean
    isDarkTheme(theme?: string): boolean
    getTheme(): string
  }

  interface IToolNameMap {
    console: InstanceType<ErudaConsoleConstructor>
    elements: InstanceType<ElementsConstructor>
    info: InstanceType<InfoConstructor>
    network: InstanceType<NetworkConstructor>
    resources: InstanceType<ResourcesConstructor>
    settings: InstanceType<SettingsConstructor>
    snippets: InstanceType<SnippetsConstructor>
    sources: InstanceType<SourcesConstructor>
    entryBtn: InstanceType<EntryBtnConstructor>
  }

  /**
   * Eruda APIs
   * @see https://eruda.liriliri.io/docs/api.html
   */
  export interface ErudaApis {
    /**
     * Initialize eruda.
     */
    init(options?: InitOptions): void
    /**
     * Destory eruda.
     * Note: You can call `init` method again after destruction.
     */
    destroy(): void
    /**
     * Set or get scale.
     */
    scale(): number
    scale(s: number): Eruda
    /**
     * Set or get entry button position.
     * It will not take effect if given pos is out of range.
     */
    position(): Position
    position(p: Position): Eruda
    /**
     * Get tool, eg. console, elements panels.
     */
    get<K extends keyof IToolNameMap>(name: K): IToolNameMap[K]
    get<T extends ToolConstructor>(name: string): InstanceType<T> | undefined
    get(): InstanceType<DevToolsConstructor>
    /**
     * Add tool.
     */
    add<T extends ToolConstructor>(
      tool: InstanceType<T> | ((eruda: Eruda) => InstanceType<T>)
    ): Eruda | undefined
    /**
     * Remove tool.
     */
    remove(name: string): Eruda | undefined
    /**
     * Show eruda panel.
     */
    show(name?: string): Eruda | undefined
    /**
     * Hide eruda panel.
     */
    hide(): Eruda | undefined
  }

  export interface Eruda extends ErudaApis {
    /**
     * Display console logs. Implementation detail follows the console api spec.
     */
    Console: ErudaConsoleConstructor
    /**
     * Check dom element status.
     */
    Elements: ElementsConstructor
    /**
     * Display special information, could be used for displaying user info to track user logs.
     * By default, page url and browser user agent is shown.
     */
    Info: InfoConstructor
    /**
     * Display requests.
     */
    Network: NetworkConstructor
    /**
     * LocalStorage, sessionStorage, cookies, scripts, styleSheets and images.
     */
    Resources: ResourcesConstructor
    /**
     * Customization for all tools.
     */
    Settings: SettingsConstructor
    /**
     * Allow you to register small functions that can be triggered multiple times.
     */
    Snippets: SnippetsConstructor
    /**
     * View object, html, js, and css.
     */
    Sources: SourcesConstructor
    /**
     * Eruda Tool
     */
    Tool: ToolConstructor
    /**
     * Eruda Util
     */
    util: Util
    /**
     * Eruda version
     */
    readonly version: string
  }

  const eruda: Eruda

  export default eruda
}


================================================
FILE: eslint.config.mjs
================================================
import babelEslintParser from '@babel/eslint-parser'
import eslintJs from '@eslint/js'
import globals from 'globals'

export default [
  eslintJs.configs.recommended,
  {
    languageOptions: {
      parser: babelEslintParser,
      parserOptions: {
        requireConfigFile: false,
        babelOptions: {
          babelrc: false,
          configFile: false,
        },
      },
      globals: {
        ...globals.builtin,
        ...globals.browser,
        ...globals.commonjs,
        VERSION: true,
        ENV: true,
      },
    },
    rules: {
      quotes: ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
      'prefer-const': 2,
    },
  },
  {files: ['build/**/*.js'], languageOptions:{globals: {...globals.node}}},
  {
    ignores: ['test','dist','coverage'],
  }
]


================================================
FILE: karma.conf.js
================================================
const webpackCfg = require('./build/webpack.dev')
webpackCfg.devtool = 'inline-source-map'
webpackCfg.module.rules.push({
  test: /\.js$/,
  exclude: /node_modules|lib\/util\.js/,
  loader: '@jsdevtools/coverage-istanbul-loader',
  enforce: 'post',
  options: {
    esModules: true,
  },
})

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jquery-1.8.3'],
    files: [
      'src/index.js',
      'test/init.js',
      'node_modules/jasmine-core/lib/jasmine-core/jasmine.js',
      'node_modules/karma-jasmine/lib/boot.js',
      'node_modules/karma-jasmine/lib/adapter.js',
      'node_modules/jasmine-jquery/lib/jasmine-jquery.js',
      'test/util.js',
      'test/console.js',
      'test/elements.js',
      'test/info.js',
      'test/network.js',
      'test/resources.js',
      'test/snippets.js',
      'test/sources.js',
      'test/settings.js',
      'test/eruda.js',
    ],
    plugins: [
      'karma-jasmine',
      'karma-jquery',
      'karma-chrome-launcher',
      'karma-webpack',
      'karma-sourcemap-loader',
      'karma-coverage-istanbul-reporter',
    ],
    webpackServer: {
      noInfo: true,
    },
    preprocessors: {
      'src/index.js': ['webpack', 'sourcemap'],
    },
    webpack: webpackCfg,
    coverageIstanbulReporter: {
      reports: ['html', 'lcovonly', 'text', 'text-summary'],
    },
    reporters: ['progress', 'coverage-istanbul'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    browsers: ['ChromeHeadless'],
    singleRun: true,
    concurrency: Infinity,
  })
}


================================================
FILE: package.json
================================================
{
  "name": "eruda",
  "version": "3.4.3",
  "description": "Console for Mobile Browsers",
  "main": "eruda.js",
  "browserslist": [
    "since 2015",
    "not dead"
  ],
  "scripts": {
    "ci": "npm run lint && npm run test && npm run build && npm run es5",
    "build": "lsla shx rm -rf dist && webpack --config build/webpack.prod.js && webpack --config build/webpack.polyfill.js && node build/build && lsla shx cp README.md eruda.d.ts dist",
    "build:analyser": "webpack --config build/webpack.analyser.js",
    "dev": "webpack-dev-server --config build/webpack.dev.js --host 0.0.0.0",
    "test": "karma start",
    "format": "lsla prettier \"*.{js,ts}\" \"src/**/*.{js,scss,css,json}\" \"build/*.js\" \"test/*.{js,html}\" --write",
    "lint": "eslint .",
    "lint:fix": "npm run lint -- --fix",
    "es5": "es-check es5 dist/eruda.js dist/eruda-polyfill.js",
    "setup": "lsla shx mkdir -p test/lib && lsla shx cp node_modules/jasmine-core/lib/jasmine-core/{jasmine.css,jasmine.js,jasmine-html.js,boot.js} test/lib && lsla shx cp node_modules/jasmine-jquery/lib/jasmine-jquery.js test/lib && lsla shx cp node_modules/jquery/dist/jquery.js test/lib",
    "genIcon": "lsla genIcon --input src/style/ --output src/style/icon.css --name eruda-icon --source src/style/icon/ && lsla prettier src/**/*.css --write"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/liriliri/eruda.git"
  },
  "keywords": [
    "console",
    "mobile",
    "debug"
  ],
  "author": "redhoodsu",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/liriliri/eruda/issues"
  },
  "homepage": "https://eruda.liriliri.io/",
  "devDependencies": {
    "@babel/core": "^7.18.6",
    "@babel/eslint-parser": "^7.26.10",
    "@babel/plugin-proposal-class-properties": "^7.18.6",
    "@babel/plugin-transform-runtime": "^7.18.6",
    "@babel/preset-env": "^7.18.6",
    "@babel/runtime": "^7.18.6",
    "@eslint/js": "^9.22.0",
    "@jsdevtools/coverage-istanbul-loader": "^3.0.5",
    "autoprefixer": "^9.7.4",
    "babel-loader": "^8.2.5",
    "chobitsu": "^1.8.4",
    "core-js": "^3.37.1",
    "css-loader": "^3.4.2",
    "es-check": "^6.2.1",
    "eslint": "^9.22.0",
    "eslint-webpack-plugin": "^5.0.0",
    "globals": "^16.0.0",
    "jasmine-core": "^2.99.1",
    "jasmine-jquery": "^2.1.1",
    "jquery": "^3.4.1",
    "karma": "^6.4.0",
    "karma-chrome-launcher": "^3.1.0",
    "karma-coverage-istanbul-reporter": "^2.1.1",
    "karma-jasmine": "^1.1.2",
    "karma-jquery": "^0.2.4",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-webpack": "^5.0.0",
    "licia": "^1.44.0",
    "luna-box-model": "^1.0.1",
    "luna-console": "^1.3.6",
    "luna-data-grid": "^1.6.4",
    "luna-dom-viewer": "^1.8.3",
    "luna-modal": "^1.3.1",
    "luna-notification": "^0.3.3",
    "luna-object-viewer": "^0.3.2",
    "luna-setting": "^2.0.2",
    "luna-tab": "^0.4.3",
    "luna-text-viewer": "^0.2.1",
    "postcss-clean": "^1.2.2",
    "postcss-loader": "^3.0.0",
    "postcss-prefixer": "^2.1.3",
    "raw-loader": "^4.0.2",
    "sass": "^1.77.6",
    "sass-loader": "^14.2.1",
    "webpack": "^5.92.1",
    "webpack-bundle-analyzer": "^4.7.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.0.4"
  }
}


================================================
FILE: src/Console/Console.js
================================================
import Tool from '../DevTools/Tool'
import noop from 'licia/noop'
import $ from 'licia/$'
import toStr from 'licia/toStr'
import isFn from 'licia/isFn'
import Emitter from 'licia/Emitter'
import isStr from 'licia/isStr'
import isRegExp from 'licia/isRegExp'
import uncaught from 'licia/uncaught'
import trim from 'licia/trim'
import upperFirst from 'licia/upperFirst'
import isHidden from 'licia/isHidden'
import isNull from 'licia/isNull'
import isArr from 'licia/isArr'
import extend from 'licia/extend'
import evalCss from '../lib/evalCss'
import Settings from '../Settings/Settings'
import LunaConsole from 'luna-console'
import LunaModal from 'luna-modal'
import { classPrefix as c } from '../lib/util'

uncaught.start()

export default class Console extends Tool {
  constructor({ name = 'console' } = {}) {
    super()

    Emitter.mixin(this)

    this.name = name
    this._selectedLog = null
  }
  init($el, container) {
    super.init($el)
    this._container = container

    this._appendTpl()

    this._initCfg()

    this._initLogger()
    this._exposeLogger()
    this._bindEvent()
  }
  show() {
    super.show()
    this._handleShow()
  }
  overrideConsole() {
    const origConsole = (this._origConsole = {})
    const winConsole = window.console

    CONSOLE_METHOD.forEach((name) => {
      let origin = (origConsole[name] = noop)
      if (winConsole[name]) {
        origin = origConsole[name] = winConsole[name].bind(winConsole)
      }

      winConsole[name] = (...args) => {
        this[name](...args)
        origin(...args)
      }
    })

    return this
  }
  setGlobal(name, val) {
    this._logger.setGlobal(name, val)
  }
  restoreConsole() {
    if (!this._origConsole) return this

    CONSOLE_METHOD.forEach(
      (name) => (window.console[name] = this._origConsole[name])
    )
    delete this._origConsole

    return this
  }
  catchGlobalErr() {
    uncaught.addListener(this._handleErr)

    return this
  }
  ignoreGlobalErr() {
    uncaught.rmListener(this._handleErr)

    return this
  }
  filter(filter) {
    const $filterText = this._$filterText
    const logger = this._logger

    if (isStr(filter)) {
      $filterText.text(filter)
      logger.setOption('filter', trim(filter))
    } else if (isRegExp(filter)) {
      $filterText.text(toStr(filter))
      logger.setOption('filter', filter)
    } else if (isFn(filter)) {
      $filterText.text('ƒ')
      logger.setOption('filter', filter)
    }
  }
  destroy() {
    this._logger.destroy()
    super.destroy()

    this._container.off('show', this._handleShow)

    if (this._style) {
      evalCss.remove(this._style)
    }
    this.ignoreGlobalErr()
    this.restoreConsole()
    this._rmCfg()
  }
  _handleShow = () => {
    if (isHidden(this._$el.get(0))) return
    this._logger.renderViewport()
  }
  _handleErr = (err) => {
    this._logger.error(err)
  }
  _enableJsExecution(enabled) {
    const $el = this._$el
    const $jsInput = $el.find(c('.js-input'))

    if (enabled) {
      $jsInput.show()
      $el.rmClass(c('js-input-hidden'))
    } else {
      $jsInput.hide()
      $el.addClass(c('js-input-hidden'))
    }
  }
  _appendTpl() {
    const $el = this._$el

    this._style = evalCss(require('./Console.scss'))
    $el.append(
      c(`
      <div class="control">
        <span class="icon-clear clear-console"></span>
        <span class="level active" data-level="all">All</span>
        <span class="level" data-level="info">Info</span>
        <span class="level" data-level="warning">Warning</span>
        <span class="level" data-level="error">Error</span>
        <span class="filter-text"></span>
        <span class="icon-filter filter"></span>
        <span class="icon-copy icon-disabled copy"></span>
      </div>
      <div class="logs-container"></div>
      <div class="js-input">
        <div class="buttons">
          <div class="button cancel">Cancel</div>
          <div class="button execute">Execute</div>
        </div>
        <span class="icon-right"></span>
        <textarea></textarea>
      </div>
    `)
    )

    const _$inputContainer = $el.find(c('.js-input'))
    const _$input = _$inputContainer.find('textarea')
    const _$inputBtns = _$inputContainer.find(c('.buttons'))

    extend(this, {
      _$control: $el.find(c('.control')),
      _$logs: $el.find(c('.logs-container')),
      _$inputContainer,
      _$input,
      _$inputBtns,
      _$filterText: $el.find(c('.filter-text')),
    })
  }
  _initLogger() {
    const cfg = this.config
    let maxLogNum = cfg.get('maxLogNum')
    maxLogNum = maxLogNum === 'infinite' ? 0 : +maxLogNum

    const $level = this._$control.find(c('.level'))
    const logger = new LunaConsole(this._$logs.get(0), {
      asyncRender: cfg.get('asyncRender'),
      maxNum: maxLogNum,
      showHeader: cfg.get('displayExtraInfo'),
      unenumerable: cfg.get('displayUnenumerable'),
      accessGetter: cfg.get('displayGetterVal'),
      lazyEvaluation: cfg.get('lazyEvaluation'),
    })

    logger.on('optionChange', (name, val) => {
      switch (name) {
        case 'level':
          $level.each(function () {
            const $this = $(this)
            const level = $this.data('level')
            const isMatch = level === val || (level === 'all' && isArr(val))

            $this[isMatch ? 'addClass' : 'rmClass'](c('active'))
          })
          break
      }
    })

    if (cfg.get('overrideConsole')) this.overrideConsole()

    this._logger = logger
  }
  _exposeLogger() {
    const logger = this._logger
    const methods = ['html'].concat(CONSOLE_METHOD)

    methods.forEach(
      (name) =>
        (this[name] = (...args) => {
          logger[name](...args)
          this.emit(name, ...args)

          return this
        })
    )
  }
  _bindEvent() {
    const container = this._container
    const $input = this._$input
    const $inputBtns = this._$inputBtns
    const $control = this._$control

    const logger = this._logger
    const config = this.config

    $control
      .on('click', c('.clear-console'), () => logger.clear(true))
      .on('click', c('.level'), function () {
        let level = $(this).data('level')
        if (level === 'all') {
          level = ['verbose', 'info', 'warning', 'error']
        }
        logger.setOption('level', level)
      })
      .on('click', c('.filter'), () => {
        LunaModal.prompt('Filter').then((filter) => {
          if (isNull(filter)) return
          this.filter(filter)
        })
      })
      .on('click', c('.copy'), () => {
        this._selectedLog.copy()
        container.notify('Copied', { icon: 'success' })
      })

    $inputBtns
      .on('click', c('.cancel'), () => this._hideInput())
      .on('click', c('.execute'), () => {
        const jsInput = $input.val().trim()
        if (jsInput === '') return

        logger.evaluate(jsInput)
        $input.val('').get(0).blur()
        this._hideInput()
      })

    $input.on('focusin', () => this._showInput())

    logger.on('insert', (log) => {
      const autoShow = log.type === 'error' && config.get('displayIfErr')

      if (autoShow) container.showTool('console').show()
    })

    logger.on('select', (log) => {
      this._selectedLog = log
      $control.find(c('.icon-copy')).rmClass(c('icon-disabled'))
    })

    logger.on('deselect', () => {
      this._selectedLog = null
      $control.find(c('.icon-copy')).addClass(c('icon-disabled'))
    })

    container.on('show', this._handleShow)
  }
  _hideInput() {
    this._$inputContainer.rmClass(c('active'))
    this._$inputBtns.css('display', 'none')
  }
  _showInput() {
    this._$inputContainer.addClass(c('active'))
    this._$inputBtns.css('display', 'flex')
  }
  _rmCfg() {
    const cfg = this.config

    const settings = this._container.get('settings')
    if (!settings) return

    settings
      .remove(cfg, 'asyncRender')
      .remove(cfg, 'jsExecution')
      .remove(cfg, 'catchGlobalErr')
      .remove(cfg, 'overrideConsole')
      .remove(cfg, 'displayExtraInfo')
      .remove(cfg, 'displayUnenumerable')
      .remove(cfg, 'displayGetterVal')
      .remove(cfg, 'lazyEvaluation')
      .remove(cfg, 'displayIfErr')
      .remove(cfg, 'maxLogNum')
      .remove(upperFirst(this.name))
  }
  _initCfg() {
    const container = this._container

    const cfg = (this.config = Settings.createCfg(this.name, {
      asyncRender: true,
      catchGlobalErr: true,
      jsExecution: true,
      overrideConsole: true,
      displayExtraInfo: false,
      displayUnenumerable: true,
      displayGetterVal: true,
      lazyEvaluation: true,
      displayIfErr: false,
      maxLogNum: 'infinite',
    }))

    this._enableJsExecution(cfg.get('jsExecution'))
    if (cfg.get('catchGlobalErr')) this.catchGlobalErr()

    cfg.on('change', (key, val) => {
      const logger = this._logger
      switch (key) {
        case 'asyncRender':
          return logger.setOption('asyncRender', val)
        case 'jsExecution':
          return this._enableJsExecution(val)
        case 'catchGlobalErr':
          return val ? this.catchGlobalErr() : this.ignoreGlobalErr()
        case 'overrideConsole':
          return val ? this.overrideConsole() : this.restoreConsole()
        case 'maxLogNum':
          return logger.setOption('maxNum', val === 'infinite' ? 0 : +val)
        case 'displayExtraInfo':
          return logger.setOption('showHeader', val)
        case 'displayUnenumerable':
          return logger.setOption('unenumerable', val)
        case 'displayGetterVal':
          return logger.setOption('accessGetter', val)
        case 'lazyEvaluation':
          return logger.setOption('lazyEvaluation', val)
      }
    })

    const settings = container.get('settings')
    if (!settings) return

    settings
      .text(upperFirst(this.name))
      .switch(cfg, 'asyncRender', 'Asynchronous Rendering')
      .switch(cfg, 'jsExecution', 'Enable JavaScript Execution')
      .switch(cfg, 'catchGlobalErr', 'Catch Global Errors')
      .switch(cfg, 'overrideConsole', 'Override Console')
      .switch(cfg, 'displayIfErr', 'Auto Display If Error Occurs')
      .switch(cfg, 'displayExtraInfo', 'Display Extra Information')
      .switch(cfg, 'displayUnenumerable', 'Display Unenumerable Properties')
      .switch(cfg, 'displayGetterVal', 'Access Getter Value')
      .switch(cfg, 'lazyEvaluation', 'Lazy Evaluation')
      .select(cfg, 'maxLogNum', 'Max Log Number', [
        'infinite',
        '250',
        '125',
        '100',
        '50',
        '10',
      ])
      .separator()
  }
}

const CONSOLE_METHOD = [
  'log',
  'error',
  'info',
  'warn',
  'dir',
  'time',
  'timeLog',
  'timeEnd',
  'clear',
  'table',
  'assert',
  'count',
  'countReset',
  'debug',
  'group',
  'groupCollapsed',
  'groupEnd',
]


================================================
FILE: src/Console/Console.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;

#console {
  padding-top: 40px;
  padding-bottom: 24px;
  width: 100%;
  height: 100%;
  &.js-input-hidden {
    padding-bottom: 0;
  }
  .control {
    padding: 10px 10px 10px 35px;
    @include control();
    .icon-clear {
      padding-right: 0px;
      left: 0;
    }
    .icon-copy {
      right: 0;
    }
    .icon-filter {
      right: 23px;
    }
    .level {
      cursor: pointer;
      font-size: $font-size-s;
      height: 20px;
      display: inline-block;
      margin: 0 2px;
      padding: 0 4px;
      line-height: 20px;
      transition: background-color $anim-duration, color $anim-duration;
      &.active {
        background: var(--highlight);
        color: var(--select-foreground);
      }
    }
    .filter-text {
      white-space: nowrap;
      position: absolute;
      line-height: 20px;
      max-width: 80px;
      overflow: hidden;
      right: 55px;
      font-size: $font-size;
      text-overflow: ellipsis;
    }
  }
  .js-input {
    pointer-events: none;
    position: absolute;
    z-index: 100;
    left: 0;
    bottom: 0;
    width: 100%;
    border-top: 1px solid var(--border);
    height: 24px;
    .icon-right {
      line-height: 23px;
      color: var(--accent);
      position: absolute;
      left: 10px;
      top: 0;
      z-index: 10;
    }
    &.active {
      height: 100%;
      padding-top: 40px;
      padding-bottom: 40px;
      border-top: none;
      .icon-right {
        display: none;
      }
      textarea {
        overflow: auto;
        padding-left: 10px;
      }
    }
    .buttons {
      display: none;
      position: absolute;
      left: 0;
      bottom: 0;
      width: 100%;
      height: 40px;
      color: var(--primary);
      background: var(--darker-background);
      font-size: $font-size-s;
      border-top: 1px solid var(--border);
      .button {
        pointer-events: all;
        cursor: pointer;
        flex: 1;
        text-align: center;
        border-right: 1px solid var(--border);
        height: 40px;
        line-height: 40px;
        transition: background-color $anim-duration, color $anim-duration;
        &:last-child {
          border-right: none;
        }
        &:active {
          color: var(--select-foreground);
          background: var(--highlight);
        }
      }
    }
    textarea {
      overflow: hidden;
      pointer-events: all;
      padding: 3px 10px;
      padding-left: 25px;
      outline: none;
      border: none;
      font-size: $font-size;
      width: 100%;
      height: 100%;
      user-select: text;
      resize: none;
      color: var(--primary);
      background: var(--background);
    }
  }
}

.safe-area #console {
  @include safe-area(padding-bottom, 24px);
  &.js-input-hidden {
    padding-bottom: 0;
  }
  .js-input {
    @include safe-area(height, 24px);
    &.active {
      height: 100%;
      @include safe-area(padding-bottom, 40px);
    }
    .buttons {
      @include safe-area(height, 40px);
      .button {
        @include safe-area(height, 40px);
      }
    }
  }
}


================================================
FILE: src/DevTools/DevTools.js
================================================
import logger from '../lib/logger'
import Tool from './Tool'
import Settings from '../Settings/Settings'
import Emitter from 'licia/Emitter'
import defaults from 'licia/defaults'
import keys from 'licia/keys'
import last from 'licia/last'
import each from 'licia/each'
import isNum from 'licia/isNum'
import nextTick from 'licia/nextTick'
import $ from 'licia/$'
import toNum from 'licia/toNum'
import extend from 'licia/extend'
import isStr from 'licia/isStr'
import theme from 'licia/theme'
import upperFirst from 'licia/upperFirst'
import startWith from 'licia/startWith'
import ready from 'licia/ready'
import pointerEvent from 'licia/pointerEvent'
import evalCss from '../lib/evalCss'
import emitter from '../lib/emitter'
import { isDarkTheme } from '../lib/themes'
import LunaNotification from 'luna-notification'
import LunaModal from 'luna-modal'
import LunaTab from 'luna-tab'
import {
  classPrefix as c,
  eventClient,
  hasSafeArea,
  safeStorage,
} from '../lib/util'

export default class DevTools extends Emitter {
  constructor($container, { defaults = {}, inline = false } = {}) {
    super()

    this._defCfg = extend(
      {
        transparency: 1,
        displaySize: 80,
        theme: 'System preference',
      },
      defaults
    )

    this._style = evalCss(require('./DevTools.scss'))

    this.$container = $container
    this._isShow = false
    this._opacity = 1
    this._tools = {}
    this._isResizing = false
    this._resizeTimer = null
    this._resizeStartY = 0
    this._resizeStartSize = 0
    this._inline = inline

    this._initTpl()
    this._initTab()
    this._initNotification()
    this._initModal()

    ready(() => this._checkSafeArea())
    this._bindEvent()
  }
  show() {
    this._isShow = true

    this._$el.show()
    this._tab.updateSlider()

    // Need a delay after show to enable transition effect.
    setTimeout(() => {
      this._$el.css('opacity', this._opacity)
    }, 50)

    this.emit('show')

    return this
  }
  hide() {
    if (this._inline) {
      return
    }

    this._isShow = false
    this.emit('hide')

    this._$el.css({ opacity: 0 })
    setTimeout(() => this._$el.hide(), 300)

    return this
  }
  toggle() {
    return this._isShow ? this.hide() : this.show()
  }
  add(tool) {
    const tab = this._tab

    if (!(tool instanceof Tool)) {
      const { init, show, hide, destroy } = new Tool()
      defaults(tool, { init, show, hide, destroy })
    }

    const name = tool.name
    if (!name) {
      return logger.error('You must specify a name for a tool')
    }

    if (this._tools[name]) {
      return logger.warn(`Tool ${name} already exists`)
    }

    const id = name.replace(/\s+/g, '-')
    this._$tools.prepend(`<div id="${c(id)}" class="${c(id + ' tool')}"></div>`)
    tool.init(this._$tools.find(`.${c(id)}.${c('tool')}`), this)
    tool.active = false
    this._tools[name] = tool

    if (name === 'settings') {
      tab.append({
        id: name,
        title: name,
      })
    } else {
      tab.insert(tab.length - 1, {
        id: name,
        title: name,
      })
    }

    return this
  }
  remove(name) {
    const tools = this._tools

    if (!tools[name]) return logger.warn(`Tool ${name} doesn't exist`)

    this._tab.remove(name)

    const tool = tools[name]
    delete tools[name]
    if (tool.active) {
      const toolKeys = keys(tools)
      if (toolKeys.length > 0) this.showTool(tools[last(toolKeys)].name)
    }
    tool.destroy()

    return this
  }
  removeAll() {
    each(this._tools, (tool) => this.remove(tool.name))

    return this
  }
  get(name) {
    const tool = this._tools[name]

    if (tool) return tool
  }
  showTool(name) {
    if (this._curTool === name) {
      return this
    }
    this._curTool = name

    const tools = this._tools

    const tool = tools[name]
    if (!tool) return

    let lastTool = {}

    each(tools, (tool) => {
      if (tool.active) {
        lastTool = tool
        tool.active = false
        tool.hide()
      }
    })

    tool.active = true
    tool.show()

    this._tab.select(name)

    this.emit('showTool', name, lastTool)

    return this
  }
  initCfg(settings) {
    const cfg = (this.config = Settings.createCfg('dev-tools', this._defCfg))

    this._setTransparency(cfg.get('transparency'))
    this._setDisplaySize(cfg.get('displaySize'))
    this._setTheme(cfg.get('theme'))

    cfg.on('change', (key, val) => {
      switch (key) {
        case 'transparency':
          return this._setTransparency(val)
        case 'displaySize':
          return this._setDisplaySize(val)
        case 'theme':
          return this._setTheme(val)
      }
    })

    settings
      .separator()
      .select(cfg, 'theme', 'Theme', [
        'System preference',
        ...keys(evalCss.getThemes()),
      ])

    if (!this._inline) {
      settings
        .range(cfg, 'transparency', 'Transparency', {
          min: 0.2,
          max: 1,
          step: 0.01,
        })
        .range(cfg, 'displaySize', 'Display Size', {
          min: 40,
          max: 100,
          step: 1,
        })
    }

    settings
      .button('Restore defaults and reload', function () {
        const store = safeStorage('local')

        const data = JSON.parse(JSON.stringify(store))
        each(data, (val, key) => {
          if (!isStr(val)) {
            return
          }

          if (startWith(key, 'eruda')) {
            store.removeItem(key)
          }
        })

        window.location.reload()
      })
      .separator()
  }
  notify(content, options) {
    this._notification.notify(content, options)
  }
  destroy() {
    evalCss.remove(this._style)
    this.removeAll()
    this._tab.destroy()
    this._$el.remove()
    window.removeEventListener('resize', this._checkSafeArea)
    emitter.off(emitter.SCALE, this._updateTabHeight)
  }
  _checkSafeArea = () => {
    const { $container } = this

    if (hasSafeArea()) {
      $container.addClass(c('safe-area'))
    } else {
      $container.rmClass(c('safe-area'))
    }
  }
  _setTheme(t) {
    const { $container } = this

    if (t === 'System preference') {
      t = upperFirst(theme.get())
    }

    if (isDarkTheme(t)) {
      $container.addClass(c('dark'))
    } else {
      $container.rmClass(c('dark'))
    }
    evalCss.setTheme(t)
  }
  _setTransparency(opacity) {
    if (!isNum(opacity)) return

    this._opacity = opacity
    if (this._isShow) this._$el.css({ opacity })
  }
  _setDisplaySize(height) {
    if (this._inline) {
      height = 100
    }

    if (!isNum(height)) return

    this._$el.css({ height: height + '%' })
  }
  _initTpl() {
    const $container = this.$container

    $container.append(
      c(`
      <div class="dev-tools">
        <div class="resizer"></div>
        <div class="tab"></div>
        <div class="tools"></div>
        <div class="notification"></div>
        <div class="modal"></div>
      </div>
      `)
    )

    this._$el = $container.find(c('.dev-tools'))
    this._$tools = this._$el.find(c('.tools'))
  }
  _initTab() {
    this._tab = new LunaTab(this._$el.find(c('.tab')).get(0), {
      height: 40,
    })
    this._tab.on('select', (id) => this.showTool(id))
  }
  _updateTabHeight = (scale) => {
    this._tab.setOption('height', 40 * scale)
    nextTick(() => {
      this._tab.updateSlider()
    })
  }
  _initNotification() {
    this._notification = new LunaNotification(
      this._$el.find(c('.notification')).get(0),
      {
        position: {
          x: 'center',
          y: 'top',
        },
      }
    )
  }
  _initModal() {
    LunaModal.setContainer(this._$el.find(c('.modal')).get(0))
  }
  _bindEvent() {
    const $resizer = this._$el.find(c('.resizer'))
    const $navBar = this._$el.find(c('.nav-bar'))
    const $document = $(document)

    if (this._inline) {
      $resizer.hide()
    }

    const startListener = (e) => {
      e.preventDefault()
      e.stopPropagation()

      e = e.origEvent
      this._isResizing = true
      this._resizeStartSize = this.config.get('displaySize')
      this._resizeStartY = eventClient('y', e)

      $resizer.css('height', '100%')

      $document.on(pointerEvent('move'), moveListener)
      $document.on(pointerEvent('up'), endListener)
    }
    const moveListener = (e) => {
      if (!this._isResizing) {
        return
      }
      e.preventDefault()
      e.stopPropagation()

      e = e.origEvent
      const deltaY =
        ((this._resizeStartY - eventClient('y', e)) / window.innerHeight) * 100
      let displaySize = this._resizeStartSize + deltaY
      if (displaySize < 40) {
        displaySize = 40
      } else if (displaySize > 100) {
        displaySize = 100
      }
      this.config.set('displaySize', toNum(displaySize.toFixed(2)))
    }
    const endListener = () => {
      clearTimeout(this._resizeTimer)
      this._isResizing = false

      $resizer.css('height', 10)

      $document.off(pointerEvent('move'), moveListener)
      $document.off(pointerEvent('up'), endListener)
    }
    $resizer.css('height', 10)
    $resizer.on(pointerEvent('down'), startListener)

    $navBar.on('contextmenu', (e) => e.preventDefault())
    this.$container.on('click', (e) => e.stopPropagation())
    window.addEventListener('resize', this._checkSafeArea)

    emitter.on(emitter.SCALE, this._updateTabHeight)

    theme.on('change', () => {
      const t = this.config.get('theme')
      if (t === 'System preference') {
        this._setTheme(t)
      }
    })
  }
}


================================================
FILE: src/DevTools/DevTools.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;

.dev-tools {
  position: absolute;
  width: 100%;
  height: 100%;
  left: 0;
  bottom: 0;
  background: var(--background);
  z-index: 500;
  display: none;
  padding-top: 40px !important;
  opacity: 0;
  transition: opacity $anim-duration;
  border-top: 1px solid var(--border);
  .resizer {
    position: absolute;
    width: 100%;
    touch-action: none;
    left: 0;
    top: -8px;
    cursor: row-resize;
    z-index: 120;
  }
  .tools {
    @include overflow-auto();
    height: 100%;
    width: 100%;
    position: relative;
    .tool {
      @include absolute();
      overflow: hidden;
      display: none;
    }
  }
}


================================================
FILE: src/DevTools/Tool.js
================================================
import Class from 'licia/Class'

export default Class({
  init($el) {
    this._$el = $el
  },
  show() {
    this._$el.show()

    return this
  },
  hide() {
    this._$el.hide()

    return this
  },
  destroy() {
    this._$el.remove()
  },
})


================================================
FILE: src/Elements/CssStore.js
================================================
import each from 'licia/each'
import sortKeys from 'licia/sortKeys'

function formatStyle(style) {
  const ret = {}

  for (let i = 0, len = style.length; i < len; i++) {
    const name = style[i]

    if (style[name] === 'initial') continue

    ret[name] = style[name]
  }

  return sortStyleKeys(ret)
}

const elProto = Element.prototype

let matchesSel = function () {
  return false
}

if (elProto.webkitMatchesSelector) {
  matchesSel = (el, selText) => el.webkitMatchesSelector(selText)
} else if (elProto.mozMatchesSelector) {
  matchesSel = (el, selText) => el.mozMatchesSelector(selText)
}

export default class CssStore {
  constructor(el) {
    this._el = el
  }
  getComputedStyle() {
    const computedStyle = window.getComputedStyle(this._el)

    return formatStyle(computedStyle)
  }
  getMatchedCSSRules() {
    const ret = []

    each(document.styleSheets, (styleSheet) => {
      try {
        // Started with version 64, Chrome does not allow cross origin script to access this property.
        if (!styleSheet.cssRules) return
      } catch {
        return
      }

      each(styleSheet.cssRules, (cssRule) => {
        let matchesEl = false

        // Mobile safari will throw DOM Exception 12 error, need to try catch it.
        try {
          matchesEl = this._elMatchesSel(cssRule.selectorText)
        } catch {
          // No op
        }

        if (!matchesEl) return

        ret.push({
          selectorText: cssRule.selectorText,
          style: formatStyle(cssRule.style),
        })
      })
    })

    return ret
  }
  _elMatchesSel(selText) {
    return matchesSel(this._el, selText)
  }
}

function sortStyleKeys(style) {
  return sortKeys(style, {
    comparator: (a, b) => {
      const lenA = a.length
      const lenB = b.length
      const len = lenA > lenB ? lenB : lenA

      for (let i = 0; i < len; i++) {
        const codeA = a.charCodeAt(i)
        const codeB = b.charCodeAt(i)
        const cmpResult = cmpCode(codeA, codeB)

        if (cmpResult !== 0) return cmpResult
      }

      if (lenA > lenB) return 1
      if (lenA < lenB) return -1

      return 0
    },
  })
}

function cmpCode(a, b) {
  a = transCode(a)
  b = transCode(b)

  if (a > b) return 1
  if (a < b) return -1
  return 0
}

function transCode(code) {
  // - should be placed after lowercase chars.
  if (code === 45) return 123
  return code
}


================================================
FILE: src/Elements/Detail.js
================================================
import isEmpty from 'licia/isEmpty'
import lowerCase from 'licia/lowerCase'
import pick from 'licia/pick'
import toStr from 'licia/toStr'
import map from 'licia/map'
import isEl from 'licia/isEl'
import escape from 'licia/escape'
import startWith from 'licia/startWith'
import contain from 'licia/contain'
import unique from 'licia/unique'
import each from 'licia/each'
import keys from 'licia/keys'
import isNull from 'licia/isNull'
import trim from 'licia/trim'
import isFn from 'licia/isFn'
import isBool from 'licia/isBool'
import safeGet from 'licia/safeGet'
import $ from 'licia/$'
import h from 'licia/h'
import extend from 'licia/extend'
import MutationObserver from 'licia/MutationObserver'
import CssStore from './CssStore'
import Settings from '../Settings/Settings'
import LunaModal from 'luna-modal'
import LunaBoxModel from 'luna-box-model'
import chobitsu from '../lib/chobitsu'
import { formatNodeName } from './util'
import { isErudaEl, classPrefix as c } from '../lib/util'

export default class Detail {
  constructor($container, devtools) {
    this._$container = $container
    this._devtools = devtools
    this._curEl = document.documentElement
    this._initObserver()
    this._initCfg()
    this._initTpl()
    this._bindEvent()
  }
  show(el) {
    this._curEl = el
    this._rmDefComputedStyle = true
    this._computedStyleSearchKeyword = ''
    this._enableObserver()
    this._render()
    this._highlight()
  }
  hide = () => {
    this._$container.hide()
    this._disableObserver()
    chobitsu.domain('Overlay').hideHighlight()
  }
  destroy() {
    this._disableObserver()
    this.restoreEventTarget()
    this._rmCfg()
  }
  overrideEventTarget() {
    const winEventProto = getWinEventProto()

    const origAddEvent = (this._origAddEvent = winEventProto.addEventListener)
    const origRmEvent = (this._origRmEvent = winEventProto.removeEventListener)

    winEventProto.addEventListener = function (type, listener, useCapture) {
      addEvent(this, type, listener, useCapture)
      origAddEvent.apply(this, arguments)
    }

    winEventProto.removeEventListener = function (type, listener, useCapture) {
      rmEvent(this, type, listener, useCapture)
      origRmEvent.apply(this, arguments)
    }
  }
  restoreEventTarget() {
    const winEventProto = getWinEventProto()

    if (this._origAddEvent) winEventProto.addEventListener = this._origAddEvent
    if (this._origRmEvent) winEventProto.removeEventListener = this._origRmEvent
  }
  _highlight = (type) => {
    const el = this._curEl

    const highlightConfig = {
      showInfo: false,
    }
    if (!type || type === 'all') {
      extend(highlightConfig, {
        showInfo: true,
        contentColor: 'rgba(111, 168, 220, .66)',
        paddingColor: 'rgba(147, 196, 125, .55)',
        borderColor: 'rgba(255, 229, 153, .66)',
        marginColor: 'rgba(246, 178, 107, .66)',
      })
    } else if (type === 'margin') {
      highlightConfig.marginColor = 'rgba(246, 178, 107, .66)'
    } else if (type === 'border') {
      highlightConfig.borderColor = 'rgba(255, 229, 153, .66)'
    } else if (type === 'padding') {
      highlightConfig.paddingColor = 'rgba(147, 196, 125, .55)'
    } else if (type === 'content') {
      highlightConfig.contentColor = 'rgba(111, 168, 220, .66)'
    }

    const { nodeId } = chobitsu.domain('DOM').getNodeId({ node: el })
    chobitsu.domain('Overlay').highlightNode({
      nodeId,
      highlightConfig,
    })
  }
  _initTpl() {
    const $container = this._$container

    const html = `<div class="${c('control')}">
      <span class="${c('icon-left back')}"></span>
      <span class="${c('element-name')}"></span>
      <span class="${c('icon-refresh refresh')}"></span>
    </div>
    <div class="${c('element')}">
      <div class="${c('attributes section')}"></div>
      <div class="${c('styles section')}"></div>
      <div class="${c('computed-style section')}"></div>
      <div class="${c('listeners section')}"></div>
    </div>`

    $container.html(html)

    this._$elementName = $container.find(c('.element-name'))
    this._$attributes = $container.find(c('.attributes'))
    this._$styles = $container.find(c('.styles'))
    this._$listeners = $container.find(c('.listeners'))
    this._$computedStyle = $container.find(c('.computed-style'))

    const boxModelContainer = h('div')
    this._$boxModel = $(boxModelContainer)
    this._boxModel = new LunaBoxModel(boxModelContainer)
  }
  _toggleAllComputedStyle() {
    this._rmDefComputedStyle = !this._rmDefComputedStyle

    this._render()
  }
  _render() {
    const data = this._getData(this._curEl)
    const $attributes = this._$attributes
    const $elementName = this._$elementName
    const $styles = this._$styles
    const $computedStyle = this._$computedStyle
    const $listeners = this._$listeners

    $elementName.html(data.name)

    let attributes = '<tr><td>Empty</td></tr>'
    if (!isEmpty(data.attributes)) {
      attributes = map(data.attributes, ({ name, value }) => {
        return `<tr>
          <td class="${c('attribute-name-color')}">${escape(name)}</td>
          <td class="${c('string-color')}">${value}</td>
        </tr>`
      }).join('')
    }
    attributes = `<h2>Attributes</h2>
    <div class="${c('table-wrapper')}">
      <table>
        <tbody>
          ${attributes} 
        </tbody>
      </table>
    </div>`
    $attributes.html(attributes)

    let styles = ''
    if (!isEmpty(data.styles)) {
      const style = map(data.styles, ({ selectorText, style }) => {
        style = map(style, (val, key) => {
          return `<div class="${c('rule')}"><span>${escape(
            key
          )}</span>: ${val};</div>`
        }).join('')
        return `<div class="${c('style-rules')}">
          <div>${escape(selectorText)} {</div>
            ${style}
          <div>}</div>
        </div>`
      }).join('')
      styles = `<h2>Styles</h2>
      <div class="${c('style-wrapper')}">
        ${style}
      </div>`
      $styles.html(styles).show()
    } else {
      $styles.hide()
    }

    let computedStyle = ''
    if (data.computedStyle) {
      let toggleButton = c(`<div class="btn toggle-all-computed-style">
        <span class="icon-expand"></span>
      </div>`)
      if (data.rmDefComputedStyle) {
        toggleButton = c(`<div class="btn toggle-all-computed-style">
          <span class="icon-compress"></span>
        </div>`)
      }

      computedStyle = `<h2>
        Computed Style
        ${toggleButton}
        <div class="${c('btn computed-style-search')}">
          <span class="${c('icon-filter')}"></span>
        </div>
        ${
          data.computedStyleSearchKeyword
            ? `<div class="${c('btn filter-text')}">${escape(
                data.computedStyleSearchKeyword
              )}</div>`
            : ''
        }
      </h2>
      <div class="${c('box-model')}"></div>
      <div class="${c('table-wrapper')}">
        <table>
          <tbody>
          ${map(data.computedStyle, (val, key) => {
            return `<tr>
              <td class="${c('key')}">${escape(key)}</td>
              <td>${val}</td>
            </tr>`
          }).join('')}
          </tbody>
        </table>
      </div>`

      $computedStyle.html(computedStyle).show()
      this._boxModel.setOption('element', this._curEl)
      $computedStyle.find(c('.box-model')).append(this._$boxModel.get(0))
    } else {
      $computedStyle.text('').hide()
    }

    let listeners = ''
    if (data.listeners) {
      listeners = map(data.listeners, (listeners, key) => {
        listeners = map(listeners, ({ useCapture, listenerStr }) => {
          return `<li ${useCapture ? `class="${c('capture')}"` : ''}>${escape(
            listenerStr
          )}</li>`
        }).join('')
        return `<div class="${c('listener')}">
          <div class="${c('listener-type')}">${escape(key)}</div>
          <ul class="${c('listener-content')}">
            ${listeners}
          </ul>
        </div>`
      }).join('')
      listeners = `<h2>Event Listeners</h2>
      <div class="${c('listener-wrapper')}">
        ${listeners} 
      </div>`
      $listeners.html(listeners).show()
    } else {
      $listeners.hide()
    }

    this._$container.show()
  }
  _getData(el) {
    const ret = {}

    const cssStore = new CssStore(el)

    const { className, id, attributes, tagName } = el

    ret.computedStyleSearchKeyword = this._computedStyleSearchKeyword
    ret.attributes = formatAttr(attributes)
    ret.name = formatNodeName({ tagName, id, className, attributes })

    const events = el.erudaEvents
    if (events && keys(events).length !== 0) ret.listeners = events

    if (needNoStyle(tagName)) {
      return ret
    }

    let computedStyle = cssStore.getComputedStyle()

    const styles = cssStore.getMatchedCSSRules()
    styles.unshift(getInlineStyle(el.style))
    styles.forEach((style) => processStyleRules(style.style))
    ret.styles = styles

    if (this._rmDefComputedStyle) {
      computedStyle = rmDefComputedStyle(computedStyle, styles)
    }
    ret.rmDefComputedStyle = this._rmDefComputedStyle
    const computedStyleSearchKeyword = lowerCase(ret.computedStyleSearchKeyword)
    if (computedStyleSearchKeyword) {
      computedStyle = pick(computedStyle, (val, property) => {
        return (
          contain(property, computedStyleSearchKeyword) ||
          contain(val, computedStyleSearchKeyword)
        )
      })
    }
    processStyleRules(computedStyle)
    ret.computedStyle = computedStyle

    return ret
  }
  _bindEvent() {
    const devtools = this._devtools

    this._$container
      .on('click', c('.toggle-all-computed-style'), () =>
        this._toggleAllComputedStyle()
      )
      .on('click', c('.computed-style-search'), () => {
        LunaModal.prompt('Filter').then((filter) => {
          if (isNull(filter)) return
          filter = trim(filter)
          this._computedStyleSearchKeyword = filter
          this._render()
        })
      })
      .on('click', '.eruda-listener-content', function () {
        const text = $(this).text()
        const sources = devtools.get('sources')

        if (sources) {
          sources.set('js', text)
          devtools.showTool('sources')
        }
      })
      .on('click', c('.element-name'), () => {
        const sources = devtools.get('sources')

        if (sources) {
          sources.set('object', this._curEl)
          devtools.showTool('sources')
        }
      })
      .on('click', c('.back'), this.hide)
      .on('click', c('.refresh'), () => {
        this._render()
        devtools.notify('Refreshed', { icon: 'success' })
      })

    this._boxModel.on('highlight', this._highlight)
  }
  _initObserver() {
    this._observer = new MutationObserver((mutations) => {
      each(mutations, (mutation) => this._handleMutation(mutation))
    })
  }
  _enableObserver() {
    this._observer.observe(document.documentElement, {
      attributes: true,
      childList: true,
      subtree: true,
    })
  }
  _disableObserver() {
    this._observer.disconnect()
  }
  _handleMutation(mutation) {
    if (isErudaEl(mutation.target)) return

    if (mutation.type === 'attributes') {
      if (mutation.target !== this._curEl) return
      this._render()
    }
  }
  _rmCfg() {
    const cfg = this.config

    const settings = this._devtools.get('settings')

    if (!settings) return

    settings
      .remove(cfg, 'overrideEventTarget')
      .remove(cfg, 'observeElement')
      .remove('Elements')
  }
  _initCfg() {
    const cfg = (this.config = Settings.createCfg('elements', {
      overrideEventTarget: true,
    }))

    if (cfg.get('overrideEventTarget')) this.overrideEventTarget()

    cfg.on('change', (key, val) => {
      switch (key) {
        case 'overrideEventTarget':
          return val ? this.overrideEventTarget() : this.restoreEventTarget()
      }
    })

    const settings = this._devtools.get('settings')
    if (!settings) return

    settings
      .text('Elements')
      .switch(cfg, 'overrideEventTarget', 'Catch Event Listeners')

    settings.separator()
  }
}

function processStyleRules(style) {
  each(style, (val, key) => (style[key] = processStyleRule(val)))
}

const formatAttr = (attributes) =>
  map(attributes, (attr) => {
    let { value } = attr
    const { name } = attr
    value = escape(value)

    const isLink =
      (name === 'src' || name === 'href') && !startWith(value, 'data')
    if (isLink) value = wrapLink(value)
    if (name === 'style') value = processStyleRule(value)

    return { name, value }
  })

const regColor = /rgba?\((.*?)\)/g
const regCssUrl = /url\("?(.*?)"?\)/g

function processStyleRule(val) {
  // For css custom properties, val is unable to retrieved.
  val = toStr(val)

  return val
    .replace(
      regColor,
      '<span class="eruda-style-color" style="background-color: $&"></span>$&'
    )
    .replace(regCssUrl, (match, url) => `url("${wrapLink(url)}")`)
}

function getInlineStyle(style) {
  const ret = {
    selectorText: 'element.style',
    style: {},
  }

  for (let i = 0, len = style.length; i < len; i++) {
    const s = style[i]

    ret.style[s] = style[s]
  }

  return ret
}

function rmDefComputedStyle(computedStyle, styles) {
  const ret = {}

  let keepStyles = ['display', 'width', 'height']
  each(styles, (style) => {
    keepStyles = keepStyles.concat(keys(style.style))
  })
  keepStyles = unique(keepStyles)

  each(computedStyle, (val, key) => {
    if (!contain(keepStyles, key)) return

    ret[key] = val
  })

  return ret
}

const NO_STYLE_TAG = ['script', 'style', 'meta', 'title', 'link', 'head']

const needNoStyle = (tagName) => {
  NO_STYLE_TAG.indexOf(tagName.toLowerCase()) > -1
}

const wrapLink = (link) => `<a href="${link}" target="_blank">${link}</a>`

function addEvent(el, type, listener, useCapture = false) {
  if (!isEl(el) || !isFn(listener) || !isBool(useCapture)) return

  const events = (el.erudaEvents = el.erudaEvents || {})

  events[type] = events[type] || []
  events[type].push({
    listener: listener,
    listenerStr: listener.toString(),
    useCapture: useCapture,
  })
}

function rmEvent(el, type, listener, useCapture = false) {
  if (!isEl(el) || !isFn(listener) || !isBool(useCapture)) return

  const events = el.erudaEvents

  if (!(events && events[type])) return

  const listeners = events[type]

  for (let i = 0, len = listeners.length; i < len; i++) {
    if (listeners[i].listener === listener) {
      listeners.splice(i, 1)
      break
    }
  }

  if (listeners.length === 0) delete events[type]
  if (keys(events).length === 0) delete el.erudaEvents
}

const getWinEventProto = () => {
  return safeGet(window, 'EventTarget.prototype') || window.Node.prototype
}


================================================
FILE: src/Elements/Elements.js
================================================
import Tool from '../DevTools/Tool'
import $ from 'licia/$'
import isEl from 'licia/isEl'
import nextTick from 'licia/nextTick'
import Emitter from 'licia/Emitter'
import map from 'licia/map'
import MediaQuery from 'licia/MediaQuery'
import isEmpty from 'licia/isEmpty'
import toNum from 'licia/toNum'
import copy from 'licia/copy'
import isMobile from 'licia/isMobile'
import isShadowRoot from 'licia/isShadowRoot'
import LunaDomViewer from 'luna-dom-viewer'
import { isErudaEl, classPrefix as c, isChobitsuEl } from '../lib/util'
import evalCss from '../lib/evalCss'
import Detail from './Detail'
import chobitsu from '../lib/chobitsu'
import emitter from '../lib/emitter'
import { formatNodeName } from './util'

export default class Elements extends Tool {
  constructor() {
    super()

    this._style = evalCss(require('./Elements.scss'))

    this.name = 'elements'
    this._selectElement = false
    this._observeElement = true
    this._history = []

    Emitter.mixin(this)
  }
  init($el, container) {
    super.init($el)

    this._container = container

    this._initTpl()
    this._htmlEl = document.documentElement
    this._detail = new Detail(this._$detail, container)
    this.config = this._detail.config
    this._splitMediaQuery = new MediaQuery('screen and (min-width: 680px)')
    this._splitMode = this._splitMediaQuery.isMatch()
    this._domViewer = new LunaDomViewer(this._$domViewer.get(0), {
      node: this._htmlEl,
      ignore: (node) => isErudaEl(node) || isChobitsuEl(node),
    })
    this._domViewer.expand()
    this._bindEvent()
    chobitsu.domain('Overlay').enable()

    nextTick(() => this._updateHistory())
  }
  show() {
    super.show()
    this._isShow = true

    if (!this._curNode) {
      this.select(document.body)
    } else if (this._splitMode) {
      this._showDetail()
    }
  }
  hide() {
    super.hide()
    this._isShow = false

    chobitsu.domain('Overlay').hideHighlight()
  }
  select(node) {
    this._domViewer.select(node)
    this._setNode(node)
    this.emit('change', node)
    return this
  }
  destroy() {
    super.destroy()

    emitter.off(emitter.SCALE, this._updateScale)
    evalCss.remove(this._style)
    this._detail.destroy()
    chobitsu
      .domain('Overlay')
      .off('inspectNodeRequested', this._inspectNodeRequested)
    chobitsu.domain('Overlay').disable()
    this._splitMediaQuery.removeAllListeners()
  }
  _updateButtons() {
    const $control = this._$control
    const $showDetail = $control.find(c('.show-detail'))
    const $copyNode = $control.find(c('.copy-node'))
    const $deleteNode = $control.find(c('.delete-node'))
    const iconDisabled = c('icon-disabled')

    $showDetail.addClass(iconDisabled)
    $copyNode.addClass(iconDisabled)
    $deleteNode.addClass(iconDisabled)

    const node = this._curNode

    if (!node || isShadowRoot(node)) {
      return
    }

    if (node !== document.documentElement && node !== document.body) {
      $deleteNode.rmClass(iconDisabled)
    }
    $copyNode.rmClass(iconDisabled)

    if (node.nodeType === Node.ELEMENT_NODE) {
      $showDetail.rmClass(iconDisabled)
    }
  }
  _showDetail = () => {
    if (!this._isShow || !this._curNode) {
      return
    }
    if (this._curNode.nodeType === Node.ELEMENT_NODE) {
      this._detail.show(this._curNode)
    } else {
      this._detail.show(this._curNode.parentNode || this._curNode.host)
    }
  }
  _initTpl() {
    const $el = this._$el

    $el.html(
      c(`<div class="elements">
        <div class="control">
          <span class="icon icon-select select"></span>
          <span class="icon icon-eye show-detail"></span>
          <span class="icon icon-copy copy-node"></span>
          <span class="icon icon-delete delete-node"></span>
        </div>
        <div class="dom-viewer-container">
          <div class="dom-viewer"></div>
        </div>
        <div class="crumbs"></div>
      </div>
      <div class="detail"></div>`)
    )

    this._$detail = $el.find(c('.detail'))
    this._$domViewer = $el.find(c('.dom-viewer'))
    this._$control = $el.find(c('.control'))
    this._$crumbs = $el.find(c('.crumbs'))
  }
  _renderCrumbs() {
    const crumbs = getCrumbs(this._curNode)
    let html = ''
    if (!isEmpty(crumbs)) {
      html = map(crumbs, ({ text, idx }) => {
        return `<li class="${c('crumb')}" data-idx="${idx}">${text}</div></li>`
      }).join('')
    }
    this._$crumbs.html(html)
  }
  _back = () => {
    if (this._curNode === this._htmlEl) return

    const parentQueue = this._curParentQueue
    let parent = parentQueue.shift()

    while (!isElExist(parent)) {
      parent = parentQueue.shift()
    }

    this.set(parent)
  }
  _bindEvent() {
    const self = this

    this._$el.on('click', c('.crumb'), function () {
      let idx = toNum($(this).data('idx'))
      let node = self._curNode

      while (idx-- && node.parentElement) {
        node = node.parentElement
      }

      if (isElExist(node)) {
        self.select(node)
      }
    })

    this._$control
      .on('click', c('.select'), this._toggleSelect)
      .on('click', c('.show-detail'), this._showDetail)
      .on('click', c('.copy-node'), this._copyNode)
      .on('click', c('.delete-node'), this._deleteNode)

    this._domViewer.on('select', this._setNode).on('deselect', this._back)

    chobitsu
      .domain('Overlay')
      .on('inspectNodeRequested', this._inspectNodeRequested)

    this._splitMediaQuery.on('match', () => {
      this._splitMode = true
      this._showDetail()
    })
    this._splitMediaQuery.on('unmatch', () => {
      this._splitMode = false
      this._detail.hide()
    })

    emitter.on(emitter.SCALE, this._updateScale)
  }
  _updateScale = (scale) => {
    this._splitMediaQuery.setQuery(`screen and (min-width: ${680 * scale}px)`)
  }
  _deleteNode = () => {
    const node = this._curNode

    if (node.parentNode) {
      node.parentNode.removeChild(node)
    }
  }
  _copyNode = () => {
    const node = this._curNode

    if (node.nodeType === Node.ELEMENT_NODE) {
      copy(node.outerHTML)
    } else {
      copy(node.nodeValue)
    }

    this._container.notify('Copied', { icon: 'success' })
  }
  _toggleSelect = () => {
    this._$el.find(c('.select')).toggleClass(c('active'))
    this._selectElement = !this._selectElement

    if (this._selectElement) {
      chobitsu.domain('Overlay').setInspectMode({
        mode: 'searchForNode',
        highlightConfig: {
          showInfo: !isMobile(),
          showRulers: false,
          showAccessibilityInfo: !isMobile(),
          showExtensionLines: false,
          contrastAlgorithm: 'aa',
          contentColor: 'rgba(111, 168, 220, .66)',
          paddingColor: 'rgba(147, 196, 125, .55)',
          borderColor: 'rgba(255, 229, 153, .66)',
          marginColor: 'rgba(246, 178, 107, .66)',
        },
      })
      this._container.hide()
    } else {
      chobitsu.domain('Overlay').setInspectMode({
        mode: 'none',
      })
      chobitsu.domain('Overlay').hideHighlight()
    }
  }
  _inspectNodeRequested = ({ backendNodeId }) => {
    this._container.show()
    this._toggleSelect()
    try {
      const { node } = chobitsu.domain('DOM').getNode({ nodeId: backendNodeId })
      this.select(node)
    } catch {
      // No op
    }
  }
  _setNode = (node) => {
    if (node === this._curNode) return

    this._curNode = node
    this._renderCrumbs()

    const parentQueue = []

    let parent = node.parentNode
    while (parent) {
      parentQueue.push(parent)
      parent = parent.parentNode
    }
    this._curParentQueue = parentQueue

    if (this._splitMode) {
      this._showDetail()
    }
    this._updateButtons()
    this._updateHistory()
  }
  _updateHistory() {
    const console = this._container.get('console')
    if (!console) return

    const history = this._history
    history.unshift(this._curNode)
    if (history.length > 5) history.pop()
    for (let i = 0; i < 5; i++) {
      console.setGlobal(`$${i}`, history[i])
    }
  }
}

const isElExist = (val) => isEl(val) && val.parentNode

function getCrumbs(el) {
  const ret = []
  let i = 0

  while (el) {
    ret.push({
      text: formatNodeName(el, { noAttr: true }),
      idx: i++,
    })

    if (isShadowRoot(el)) {
      el = el.host
    }
    if (!el.parentElement && isShadowRoot(el.parentNode)) {
      el = el.parentNode
    } else {
      el = el.parentElement
    }
  }

  return ret.reverse()
}


================================================
FILE: src/Elements/Elements.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;

#elements {
  .elements {
    @include absolute();
    padding-top: 40px;
    padding-bottom: 24px;
    font-size: 14px;
  }
  .control {
    padding: 10px 0;
    @include control();
    .icon-eye {
      right: 0;
    }
    .icon-copy {
      right: 23px;
    }
    .icon-delete {
      right: 46px;
    }
  }
  .dom-viewer-container {
    @include overflow-auto();
    height: 100%;
    padding: 5px 0;
  }
  .crumbs {
    @include absolute(100%, 24px);
    top: initial;
    line-height: 24px;
    bottom: 0;
    border-top: 1px solid var(--border);
    background: var(--darker-background);
    color: var(--primary);
    font-size: $font-size-s;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    li {
      cursor: pointer;
      padding: 0 7px;
      display: inline-block;
      &:hover,
      &:last-child {
        background: var(--highlight);
      }
    }
  }
  .detail {
    @include absolute();
    z-index: 10;
    padding-top: 40px;
    display: none;
    background: var(--background);
    .control {
      padding: 10px 35px;
      .element-name {
        font-size: $font-size-s;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        width: 100%;
        display: inline-block;
      }
      .icon-left {
        left: 0;
      }
      .icon-refresh {
        right: 0;
      }
    }
    .element {
      @include overflow-auto(y);
      height: 100%;
    }
  }
  .section {
    border-bottom: 1px solid var(--border);
    color: var(--foreground);
    margin: 10px 0;
    h2 {
      color: var(--primary);
      background: var(--darker-background);
      border-top: 1px solid var(--border);
      padding: $padding;
      line-height: 18px;
      font-size: $font-size;
      transition: background-color $anim-duration;
      @include right-btn();
      &.active-effect {
        cursor: pointer;
      }
      &.active-effect:active {
        background: var(--highlight);
        color: var(--select-foreground);
      }
    }
  }
  .attributes {
    font-size: $font-size-s;
    a {
      color: var(--link-color);
    }
    .table-wrapper {
      @include overflow-auto(x);
    }
    table {
      td {
        padding: 5px 10px;
      }
    }
  }
  .text-content {
    background: #fff;
    .content {
      @include overflow-auto(x);
      padding: $padding;
    }
  }
  .style-color {
    position: relative;
    top: 1px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    margin-right: 2px;
    border: 1px solid var(--border);
    display: inline-block;
  }
  .box-model {
    @include overflow-auto(x);
    padding: $padding;
    text-align: center;
    border-bottom: 1px solid var(--color);
  }
  .computed-style {
    font-size: $font-size-s;
    a {
      color: var(--link-color);
    }
    .table-wrapper {
      @include overflow-auto(y);
      max-height: 200px;
      border-top: 1px solid var(--border);
    }
    table {
      td {
        padding: 5px 10px;
        &.key {
          white-space: nowrap;
          color: var(--var-color);
        }
      }
    }
  }
  .styles {
    font-size: $font-size-s;
    .style-wrapper {
      padding: $padding;
      .style-rules {
        border: 1px solid var(--border);
        padding: $padding;
        margin-bottom: 10px;
        .rule {
          padding-left: 2em;
          word-break: break-all;
          a {
            color: var(--link-color);
          }
          span {
            color: var(--var-color);
          }
        }
        &:last-child {
          margin-bottom: 0;
        }
      }
    }
  }
  .listeners {
    font-size: $font-size-s;
    .listener-wrapper {
      padding: $padding;
      .listener {
        margin-bottom: 10px;
        overflow: hidden;
        border: 1px solid var(--border);
        .listener-type {
          padding: $padding;
          background: var(--darker-background);
          color: var(--primary);
        }
        .listener-content {
          li {
            @include overflow-auto(x);
            padding: $padding;
            border-top: none;
          }
        }
      }
    }
  }
}

.safe-area #elements {
  .elements {
    @include safe-area(padding-bottom, 24px);
  }
  .crumbs {
    @include safe-area(height, 24px);
  }
  .element {
    @include safe-area(padding-bottom, 0px);
  }
}

@media screen and (min-width: 680px) {
  #elements {
    .elements {
      width: 50%;
      .control {
        .icon-eye {
          display: none;
        }
        .icon-copy {
          right: 0;
        }
        .icon-delete {
          right: 23px;
        }
      }
    }
    .detail {
      width: 50%;
      left: initial;
      right: 0;
      border-left: 1px solid var(--border);
      .control {
        padding-left: 10px;
        .icon-left {
          display: none;
        }
      }
    }
  }
}


================================================
FILE: src/Elements/util.js
================================================
import each from 'licia/each'
import isStr from 'licia/isStr'
import isShadowRoot from 'licia/isShadowRoot'
import { classPrefix as c } from '../lib/util'

export function formatNodeName(node, { noAttr = false } = {}) {
  if (node.nodeType === Node.TEXT_NODE) {
    return `<span class="${c('tag-name-color')}">(text)</span>`
  } else if (node.nodeType === Node.COMMENT_NODE) {
    return `<span class="${c('tag-name-color')}"><!--></span>`
  } else if (isShadowRoot(node)) {
    return `<span class="${c('tag-name-color')}">#shadow-root</span>`
  }

  const { id, className, attributes } = node

  let ret = `<span class="eruda-tag-name-color">${node.tagName.toLowerCase()}</span>`

  if (id !== '') ret += `<span class="eruda-function-color">#${id}</span>`

  if (isStr(className)) {
    let classes = ''
    each(className.split(/\s+/g), (val) => {
      if (val.trim() === '') return
      classes += `.${val}`
    })
    ret += `<span class="eruda-attribute-name-color">${classes}</span>`
  }

  if (!noAttr) {
    each(attributes, (attr) => {
      const name = attr.name
      if (name === 'id' || name === 'class' || name === 'style') return
      ret += ` <span class="eruda-attribute-name-color">${name}</span><span class="eruda-operator-color">="</span><span class="eruda-string-color">${attr.value}</span><span class="eruda-operator-color">"</span>`
    })
  }

  return ret
}


================================================
FILE: src/EntryBtn/EntryBtn.js
================================================
import emitter from '../lib/emitter'
import Settings from '../Settings/Settings'
import Emitter from 'licia/Emitter'
import $ from 'licia/$'
import nextTick from 'licia/nextTick'
import orientation from 'licia/orientation'
import pointerEvent from 'licia/pointerEvent'
import { pxToNum, classPrefix as c, eventClient } from '../lib/util'
import evalCss from '../lib/evalCss'

const $document = $(document)

export default class EntryBtn extends Emitter {
  constructor($container) {
    super()

    this._style = evalCss(require('./EntryBtn.scss'))

    this._$container = $container
    this._initTpl()
    this._bindEvent()
    this._registerListener()
  }
  hide() {
    this._$el.hide()
  }
  show() {
    this._$el.show()
  }
  setPos(pos) {
    if (this._isOutOfRange(pos)) {
      pos = this._getDefPos()
    }

    this._$el.css({
      left: pos.x,
      top: pos.y,
    })

    this.config.set('pos', pos)
  }
  getPos() {
    return this.config.get('pos')
  }
  destroy() {
    evalCss.remove(this._style)
    this._unregisterListener()
    this._$el.remove()
  }
  _isOutOfRange(pos) {
    pos = pos || this.config.get('pos')
    const defPos = this._getDefPos()

    return (
      pos.x > defPos.x + 10 || pos.x < 0 || pos.y < 0 || pos.y > defPos.y + 10
    )
  }
  _registerListener() {
    this._scaleListener = () =>
      nextTick(() => {
        if (this._isOutOfRange()) this._resetPos()
      })
    emitter.on(emitter.SCALE, this._scaleListener)
  }
  _unregisterListener() {
    emitter.off(emitter.SCALE, this._scaleListener)
  }
  _initTpl() {
    const $container = this._$container

    $container.append(
      c('<div class="entry-btn"><span class="icon-tool"></span></div>')
    )
    this._$el = $container.find('.eruda-entry-btn')
  }
  _resetPos(orientationChanged) {
    const cfg = this.config
    let pos = cfg.get('pos')
    const defPos = this._getDefPos()

    if (!cfg.get('rememberPos') || orientationChanged) {
      pos = defPos
    }

    this.setPos(pos)
  }
  _onDragStart = (e) => {
    const $el = this._$el
    $el.addClass(c('active'))

    this._isClick = true
    e = e.origEvent
    this._startX = eventClient('x', e)
    this._oldX = pxToNum($el.css('left'))
    this._oldY = pxToNum($el.css('top'))
    this._startY = eventClient('y', e)
    $document.on(pointerEvent('move'), this._onDragMove)
    $document.on(pointerEvent('up'), this._onDragEnd)
  }
  _onDragMove = (e) => {
    const btnSize = this._$el.get(0).offsetWidth
    const maxWidth = this._$container.get(0).offsetWidth
    const maxHeight = this._$container.get(0).offsetHeight

    e = e.origEvent
    const deltaX = eventClient('x', e) - this._startX
    const deltaY = eventClient('y', e) - this._startY
    if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
      this._isClick = false
    }
    let newX = this._oldX + deltaX
    let newY = this._oldY + deltaY
    if (newX < 0) {
      newX = 0
    } else if (newX > maxWidth - btnSize) {
      newX = maxWidth - btnSize
    }
    if (newY < 0) {
      newY = 0
    } else if (newY > maxHeight - btnSize) {
      newY = maxHeight - btnSize
    }
    this._$el.css({
      left: newX,
      top: newY,
    })
  }
  _onDragEnd = (e) => {
    const $el = this._$el

    if (this._isClick) {
      this.emit('click')
    }

    this._onDragMove(e)
    $document.off(pointerEvent('move'), this._onDragMove)
    $document.off(pointerEvent('up'), this._onDragEnd)

    const cfg = this.config

    if (cfg.get('rememberPos')) {
      cfg.set('pos', {
        x: pxToNum($el.css('left')),
        y: pxToNum($el.css('top')),
      })
    }

    $el.rmClass('eruda-active')
  }
  _bindEvent() {
    const $el = this._$el

    $el.on(pointerEvent('down'), this._onDragStart)

    orientation.on('change', () => this._resetPos(true))
    window.addEventListener('resize', () => this._resetPos())
  }
  initCfg(settings) {
    const cfg = (this.config = Settings.createCfg('entry-button', {
      rememberPos: true,
      pos: this._getDefPos(),
    }))

    settings.switch(cfg, 'rememberPos', 'Remember Entry Button Position')

    this._resetPos()
  }
  _getDefPos() {
    const minWidth = this._$el.get(0).offsetWidth + 10

    return {
      x: window.innerWidth - minWidth,
      y: window.innerHeight - minWidth,
    }
  }
}


================================================
FILE: src/EntryBtn/EntryBtn.scss
================================================
.container {
  .entry-btn {
    touch-action: none;
    width: 40px;
    height: 40px;
    display: flex;
    background: #000;
    opacity: 0.3;
    border-radius: 10px;
    position: relative;
    z-index: 1000;
    transition: opacity 0.3s;
    color: #fff;
    font-size: 25px;
    align-items: center;
    justify-content: center;
    &.active,
    &:active {
      opacity: 0.8;
    }
  }
}


================================================
FILE: src/Info/Info.js
================================================
import Tool from '../DevTools/Tool'
import defInfo from './defInfo'
import each from 'licia/each'
import isFn from 'licia/isFn'
import isUndef from 'licia/isUndef'
import cloneDeep from 'licia/cloneDeep'
import evalCss from '../lib/evalCss'
import map from 'licia/map'
import escape from 'licia/escape'
import copy from 'licia/copy'
import $ from 'licia/$'
import { classPrefix as c } from '../lib/util'

export default class Info extends Tool {
  constructor() {
    super()

    this._style = evalCss(require('./Info.scss'))

    this.name = 'info'
    this._infos = []
  }
  init($el, container) {
    super.init($el)
    this._container = container

    this._addDefInfo()
    this._bindEvent()
  }
  destroy() {
    super.destroy()

    evalCss.remove(this._style)
  }
  add(name, val) {
    const infos = this._infos
    let isUpdate = false

    each(infos, (info) => {
      if (name !== info.name) return

      info.val = val
      isUpdate = true
    })

    if (!isUpdate) infos.push({ name, val })

    this._render()

    return this
  }
  get(name) {
    const infos = this._infos

    if (isUndef(name)) {
      return cloneDeep(infos)
    }

    let result

    each(infos, (info) => {
      if (name === info.name) result = info.val
    })

    return result
  }
  remove(name) {
    const infos = this._infos

    for (let i = infos.length - 1; i >= 0; i--) {
      if (infos[i].name === name) infos.splice(i, 1)
    }

    this._render()

    return this
  }
  clear() {
    this._infos = []

    this._render()

    return this
  }
  _addDefInfo() {
    each(defInfo, (info) => this.add(info.name, info.val))
  }
  _render() {
    const infos = []

    each(this._infos, ({ name, val }) => {
      if (isFn(val)) val = val()

      infos.push({ name, val })
    })

    const html = `<ul>${map(
      infos,
      (info) =>
        `<li><h2 class="${c('title')}">${escape(info.name)}<span class="${c(
          'icon-copy copy'
        )}"></span></h2><div class="${c('content')}">${info.val}</div></li>`
    ).join('')}</ul>`

    this._renderHtml(html)
  }
  _bindEvent() {
    const container = this._container

    this._$el.on('click', c('.copy'), function () {
      const $li = $(this).parent().parent()
      const name = $li.find(c('.title')).text()
      const content = $li.find(c('.content')).text()
      copy(`${name}: ${content}`)
      container.notify('Copied', { icon: 'success' })
    })
  }
  _renderHtml(html) {
    if (html === this._lastHtml) return
    this._lastHtml = html
    this._$el.html(html)
  }
}


================================================
FILE: src/Info/Info.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;

#info {
  @include overflow-auto(y);
  li {
    margin: 10px;
    border: 1px solid var(--border);
    .title,
    .content {
      padding: $padding;
    }
    .title {
      position: relative;
      padding-bottom: 0;
      color: var(--accent);
      .icon-copy {
        position: absolute;
        right: 10px;
        top: 14px;
        color: var(--primary);
        cursor: pointer;
        transition: color $anim-duration;
        &:active {
          color: var(--accent);
        }
      }
    }
    .content {
      margin: 0;
      user-select: text;
      color: var(--foreground);
      font-size: $font-size-s;
      word-break: break-all;
      table {
        width: 100%;
        border-collapse: collapse;
        th,
        td {
          border: 1px solid var(--border);
          padding: 10px;
        }
      }
      * {
        user-select: text;
      }
      a {
        color: var(--link-color);
      }
    }
    .device-key,
    .system-key {
      width: 100px;
    }
  }
}

.safe-area #info {
  @include safe-area(padding-bottom, 10px);
}


================================================
FILE: src/Info/defInfo.js
================================================
import detectBrowser from 'licia/detectBrowser'
import detectOs from 'licia/detectOs'
import escape from 'licia/escape'
import map from 'licia/map'

const browser = detectBrowser()

export default [
  {
    name: 'Location',
    val() {
      return escape(location.href)
    },
  },
  {
    name: 'User Agent',
    val: navigator.userAgent,
  },
  {
    name: 'Device',
    val: [
      '<table><tbody>',
      `<tr><td class="eruda-device-key">screen</td><td>${screen.width} * ${screen.height}</td></tr>`,
      `<tr><td>viewport</td><td>${window.innerWidth} * ${window.innerHeight}</td></tr>`,
      `<tr><td>pixel ratio</td><td>${window.devicePixelRatio}</td></tr>`,
      '</tbody></table>',
    ].join(''),
  },
  {
    name: 'System',
    val: [
      '<table><tbody>',
      `<tr><td class="eruda-system-key">os</td><td>${detectOs()}</td></tr>`,
      `<tr><td>browser</td><td>${
        browser.name + ' ' + browser.version
      }</td></tr>`,
      '</tbody></table>',
    ].join(''),
  },
  {
    name: 'Sponsor this Project',
    val() {
      return (
        '<table><tbody>' +
        map(
          [
            {
              name: 'Open Collective',
              link: 'https://opencollective.com/eruda',
            },
            {
              name: 'Ko-fi',
              link: 'https://ko-fi.com/surunzi',
            },
            {
              name: 'Wechat Pay',
              link: 'https://surunzi.com/wechatpay.html',
            },
          ],
          (item) => {
            return `<tr><td>${
              item.name
            }</td><td><a rel="noreferrer noopener" href="${
              item.link
            }" target="_blank">${item.link.replace(
              'https://',
              ''
            )}</a></td></tr>`
          }
        ).join(' ') +
        '</tbody></table>'
      )
    },
  },
  {
    name: 'About',
    val:
      '<a href="https://eruda.liriliri.io" target="_blank">Eruda v' +
      VERSION +
      '</a>',
  },
]


================================================
FILE: src/Network/Detail.js
================================================
import trim from 'licia/trim'
import isEmpty from 'licia/isEmpty'
import map from 'licia/map'
import each from 'licia/each'
import escape from 'licia/escape'
import copy from 'licia/copy'
import isJson from 'licia/isJson'
import Emitter from 'licia/Emitter'
import truncate from 'licia/truncate'
import { classPrefix as c } from '../lib/util'

export default class Detail extends Emitter {
  constructor($container, devtools) {
    super()
    this._$container = $container
    this._devtools = devtools

    this._detailData = {}
    this._bindEvent()
  }
  show(data) {
    if (data.resTxt && trim(data.resTxt) === '') {
      delete data.resTxt
    }
    if (isEmpty(data.resHeaders)) {
      delete data.resHeaders
    }
    if (isEmpty(data.reqHeaders)) {
      delete data.reqHeaders
    }

    let postData = ''
    if (data.data) {
      postData = `<pre class="${c('data')}">${escape(data.data)}</pre>`
    }

    let reqHeaders = '<tr><td>Empty</td></tr>'
    if (data.reqHeaders) {
      reqHeaders = map(data.reqHeaders, (val, key) => {
        return `<tr>
          <td class="${c('key')}">${escape(key)}</td>
          <td>${escape(val)}</td>
        </tr>`
      }).join('')
    }

    let resHeaders = '<tr><td>Empty</td></tr>'
    if (data.resHeaders) {
      resHeaders = map(data.resHeaders, (val, key) => {
        return `<tr>
          <td class="${c('key')}">${escape(key)}</td>
          <td>${escape(val)}</td>
        </tr>`
      }).join('')
    }

    let resTxt = ''
    if (data.resTxt) {
      let text = data.resTxt
      if (text.length > MAX_RES_LEN) {
        text = truncate(text, MAX_RES_LEN)
      }
      resTxt = `<pre class="${c('response')}">${escape(text)}</pre>`
    }

    const html = `<div class="${c('control')}">
      <span class="${c('icon-left back')}"></span>
      <span class="${c('icon-delete back')}"></span>
      <span class="${c('url')}">${escape(data.url)}</span>
      <span class="${c('icon-copy copy-res')}"></span>
    </div>
    <div class="${c('http')}">
      ${postData}
      <div class="${c('section')}">
        <h2>Response Headers</h2>
        <table class="${c('headers')}">
          <tbody>
            ${resHeaders}
          </tbody>
        </table>
      </div>
      <div class="${c('section')}">
        <h2>Request Headers</h2>
        <table class="${c('headers')}">
          <tbody>
            ${reqHeaders}
          </tbody>
        </table>
      </div>
      ${resTxt}
    </div>`

    this._$container.html(html).show()
    this._detailData = data
  }
  hide() {
    this._$container.hide()
    this.emit('hide')
  }
  _copyRes = () => {
    const detailData = this._detailData

    let data = `${detailData.method} ${detailData.url} ${detailData.status}\n`
    if (!isEmpty(detailData.data)) {
      data += '\nRequest Data\n\n'
      data += `${detailData.data}\n`
    }
    if (!isEmpty(detailData.reqHeaders)) {
      data += '\nRequest Headers\n\n'
      each(detailData.reqHeaders, (val, key) => (data += `${key}: ${val}\n`))
    }
    if (!isEmpty(detailData.resHeaders)) {
      data += '\nResponse Headers\n\n'
      each(detailData.resHeaders, (val, key) => (data += `${key}: ${val}\n`))
    }
    if (detailData.resTxt) {
      data += `\n${detailData.resTxt}\n`
    }

    copy(data)
    this._devtools.notify('Copied', { icon: 'success' })
  }
  _bindEvent() {
    const devtools = this._devtools

    this._$container
      .on('click', c('.back'), () => this.hide())
      .on('click', c('.copy-res'), this._copyRes)
      .on('click', c('.http .response'), () => {
        const data = this._detailData
        const resTxt = data.resTxt

        if (isJson(resTxt)) {
          return showSources('object', resTxt)
        }

        switch (data.subType) {
          case 'css':
            return showSources('css', resTxt)
          case 'html':
            return showSources('html', resTxt)
          case 'javascript':
            return showSources('js', resTxt)
          case 'json':
            return showSources('object', resTxt)
        }
        switch (data.type) {
          case 'image':
            return showSources('img', data.url)
        }
      })

    const showSources = (type, data) => {
      const sources = devtools.get('sources')
      if (!sources) {
        return
      }

      sources.set(type, data)

      devtools.showTool('sources')
    }
  }
}

const MAX_RES_LEN = 100000


================================================
FILE: src/Network/Network.js
================================================
import Tool from '../DevTools/Tool'
import $ from 'licia/$'
import ms from 'licia/ms'
import each from 'licia/each'
import map from 'licia/map'
import Detail from './Detail'
import throttle from 'licia/throttle'
import { getFileName, classPrefix as c } from '../lib/util'
import evalCss from '../lib/evalCss'
import chobitsu from '../lib/chobitsu'
import emitter from '../lib/emitter'
import LunaDataGrid from 'luna-data-grid'
import ResizeSensor from 'licia/ResizeSensor'
import MediaQuery from 'licia/MediaQuery'
import { getType } from './util'
import copy from 'licia/copy'
import extend from 'licia/extend'
import trim from 'licia/trim'
import isNull from 'licia/isNull'
import LunaModal from 'luna-modal'
import { curlStr } from './util'

export default class Network extends Tool {
  constructor() {
    super()

    this._style = evalCss(require('./Network.scss'))

    this.name = 'network'
    this._requests = {}
    this._selectedRequest = null
    this._isRecording = true
  }
  init($el, container) {
    super.init($el)

    this._container = container
    this._initTpl()
    this._detail = new Detail(this._$detail, container)
    this._splitMediaQuery = new MediaQuery('screen and (min-width: 680px)')
    this._splitMode = this._splitMediaQuery.isMatch()
    this._requestDataGrid = new LunaDataGrid(this._$requests.get(0), {
      columns: [
        {
          id: 'name',
          title: 'Name',
          sortable: true,
          weight: 30,
        },
        {
          id: 'method',
          title: 'Method',
          sortable: true,
          weight: 14,
        },
        {
          id: 'status',
          title: 'Status',
          sortable: true,
          weight: 14,
        },
        {
          id: 'type',
          title: 'Type',
          sortable: true,
          weight: 14,
        },
        {
          id: 'size',
          title: 'Size',
          sortable: true,
          weight: 14,
        },
        {
          id: 'time',
          title: 'Time',
          sortable: true,
          weight: 14,
        },
      ],
    })
    this._resizeSensor = new ResizeSensor($el.get(0))
    this._bindEvent()
  }
  show() {
    super.show()
    this._updateDataGridHeight()
  }
  clear() {
    this._requests = {}
    this._requestDataGrid.clear()
  }
  requests() {
    const ret = []
    each(this._requests, (request) => {
      ret.push(request)
    })
    return ret
  }
  _updateDataGridHeight() {
    this._requestDataGrid.fit()
  }
  _reqWillBeSent = (params) => {
    if (!this._isRecording) {
      return
    }

    const request = {
      name: getFileName(params.request.url),
      url: params.request.url,
      status: 'pending',
      type: 'unknown',
      subType: 'unknown',
      size: 0,
      data: params.request.postData,
      method: params.request.method,
      startTime: params.timestamp * 1000,
      time: 0,
      resTxt: '',
      done: false,
      reqHeaders: params.request.headers || {},
      resHeaders: {},
    }
    let node
    request.render = () => {
      const data = {
        name: request.name,
        method: request.method,
        status: request.status,
        type: request.subType,
        size: request.size,
        time: request.displayTime,
      }
      if (node) {
        node.data = data
        node.render()
      } else {
        node = this._requestDataGrid.append(data, { selectable: true })
        $(node.container).data('id', params.requestId)
      }
      if (request.hasErr) {
        $(node.container).addClass(c('request-error'))
      }
    }
    request.render()
    this._requests[params.requestId] = request
  }
  _resReceivedExtraInfo = (params) => {
    const request = this._requests[params.requestId]
    if (!this._isRecording || !request) {
      return
    }

    request.resHeaders = params.headers

    this._updateType(request)
    request.render()
  }
  _updateType(request) {
    const contentType = request.resHeaders['content-type'] || ''
    const { type, subType } = getType(contentType)
    request.type = type
    request.subType = subType
  }
  _resReceived = (params) => {
    const request = this._requests[params.requestId]
    if (!this._isRecording || !request) {
      return
    }

    const { response } = params
    const { status, headers } = response
    request.status = status
    if (status < 200 || status >= 300) {
      request.hasErr = true
    }
    if (headers) {
      request.resHeaders = headers
      this._updateType(request)
    }

    request.render()
  }
  _loadingFinished = (params) => {
    const request = this._requests[params.requestId]
    if (!this._isRecording || !request) {
      return
    }

    const time = params.timestamp * 1000
    request.time = time - request.startTime
    request.displayTime = ms(request.time)

    request.size = params.encodedDataLength
    request.done = true
    request.resTxt = chobitsu.domain('Network').getResponseBody({
      requestId: params.requestId,
    }).body

    request.render()
  }
  _loadingFailed = (params) => {
    const request = this._requests[params.requestId]
    if (!this._isRecording || !request) {
      return
    }

    const time = params.timestamp * 1000
    request.time = time - request.startTime
    request.displayTime = ms(request.time)

    request.hasErr = true
    request.status = 0
    request.done = true

    request.render()
  }
  _copyCurl = () => {
    const request = this._selectedRequest

    copy(
      curlStr({
        requestMethod: request.method,
        url() {
          return request.url
        },
        requestFormData() {
          return request.data
        },
        requestHeaders() {
          const reqHeaders = request.reqHeaders || {}
          extend(reqHeaders, {
            'User-Agent': navigator.userAgent,
            Referer: location.href,
          })

          return map(reqHeaders, (value, name) => {
            return {
              name,
              value,
            }
          })
        },
      })
    )

    this._container.notify('Copied', { icon: 'success' })
  }
  _updateButtons() {
    const $control = this._$control
    const $showDetail = $control.find(c('.show-detail'))
    const $copyCurl = $control.find(c('.copy-curl'))
    const iconDisabled = c('icon-disabled')

    $showDetail.addClass(iconDisabled)
    $copyCurl.addClass(iconDisabled)

    if (this._selectedRequest) {
      $showDetail.rmClass(iconDisabled)
      $copyCurl.rmClass(iconDisabled)
    }
  }
  _toggleRecording = () => {
    this._$control.find(c('.record')).toggleClass(c('recording'))
    this._isRecording = !this._isRecording
  }
  _showDetail = () => {
    if (this._selectedRequest) {
      if (this._splitMode) {
        this._$network.css('width', '50%')
      }
      this._detail.show(this._selectedRequest)
    }
  }
  _bindEvent() {
    const $control = this._$control
    const $filterText = this._$filterText
    const requestDataGrid = this._requestDataGrid

    const self = this

    $control
      .on('click', c('.clear-request'), () => this.clear())
      .on('click', c('.show-detail'), this._showDetail)
      .on('click', c('.copy-curl'), this._copyCurl)
      .on('click', c('.record'), this._toggleRecording)
      .on('click', c('.filter'), () => {
        LunaModal.prompt('Filter').then((filter) => {
          if (isNull(filter)) return

          $filterText.text(filter)
          requestDataGrid.setOption('filter', trim(filter))
        })
      })

    requestDataGrid.on('select', (node) => {
      const id = $(node.container).data('id')
      const request = self._requests[id]
      this._selectedRequest = request
      this._updateButtons()
      if (this._splitMode) {
        this._showDetail()
      }
    })

    requestDataGrid.on('deselect', () => {
      this._selectedRequest = null
      this._updateButtons()
      this._detail.hide()
    })

    this._resizeSensor.addListener(
      throttle(() => this._updateDataGridHeight(), 15)
    )

    this._splitMediaQuery.on('match', () => {
      this._detail.hide()
      this._splitMode = true
    })
    this._splitMediaQuery.on('unmatch', () => {
      this._detail.hide()
      this._splitMode = false
    })
    this._detail.on('hide', () => {
      if (this._splitMode) {
        this._$network.css('width', '100%')
      }
    })

    chobitsu.domain('Network').enable()

    const network = chobitsu.domain('Network')
    network.on('requestWillBeSent', this._reqWillBeSent)
    network.on('responseReceivedExtraInfo', this._resReceivedExtraInfo)
    network.on('responseReceived', this._resReceived)
    network.on('loadingFinished', this._loadingFinished)
    network.on('loadingFailed', this._loadingFailed)

    emitter.on(emitter.SCALE, this._updateScale)
  }
  _updateScale = (scale) => {
    this._splitMediaQuery.setQuery(`screen and (min-width: ${680 * scale}px)`)
  }
  destroy() {
    super.destroy()

    this._resizeSensor.destroy()
    evalCss.remove(this._style)
    this._splitMediaQuery.removeAllListeners()

    const network = chobitsu.domain('Network')
    network.off('requestWillBeSent', this._reqWillBeSent)
    network.off('responseReceivedExtraInfo', this._resReceivedExtraInfo)
    network.off('responseReceived', this._resReceived)
    network.off('loadingFinished', this._loadingFinished)

    emitter.off(emitter.SCALE, this._updateScale)
  }
  _initTpl() {
    const $el = this._$el
    $el.html(
      c(`<div class="network">
        <div class="control">
          <span class="icon-record record recording"></span>
          <span class="icon-clear clear-request"></span>
          <span class="icon-eye icon-disabled show-detail"></span>
          <span class="icon-copy icon-disabled copy-curl"></span>
          <span class="filter-text"></span>
          <span class="icon-filter filter"></span>
        </div>
        <div class="requests"></div>
      </div>
      <div class="detail"></div>`)
    )
    this._$network = $el.find(c('.network'))
    this._$detail = $el.find(c('.detail'))
    this._$requests = $el.find(c('.requests'))
    this._$control = $el.find(c('.control'))
    this._$filterText = $el.find(c('.filter-text'))
  }
}


================================================
FILE: src/Network/Network.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;

#network {
  .network {
    @include absolute();
    padding-top: 39px;
  }
  .control {
    padding: 10px;
    border-bottom: none;
    @include control();
    .title {
      font-size: $font-size;
    }
    .icon-clear {
      left: 23px;
    }
    .icon-eye {
      right: 0;
    }
    .icon-copy {
      right: 23px;
    }
    .icon-filter {
      right: 46px;
    }
    .filter-text {
      white-space: nowrap;
      position: absolute;
      line-height: 20px;
      max-width: 80px;
      overflow: hidden;
      right: 88px;
      font-size: $font-size;
      text-overflow: ellipsis;
    }
    .icon-record {
      left: 0;
      &.recording {
        color: var(--console-error-foreground);
        text-shadow: 0 0 4px var(--console-error-foreground);
      }
    }
  }
  .request-error {
    color: var(--console-error-foreground);
  }
  .luna-data-grid:focus {
    .luna-data-grid-data-container {
      .request-error.luna-data-grid-selected {
        background: var(--console-error-background);
      }
    }
  }
  .luna-data-grid {
    border-left: none;
    border-right: none;
  }
  .detail {
    @include absolute();
    z-index: 10;
    display: none;
    padding-top: 40px;
    background: var(--background);
    .control {
      padding: 10px 35px;
      border-bottom: 1px solid var(--border);
      .url {
        font-size: $font-size-s;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        width: 100%;
        display: inline-block;
      }
      .icon-left {
        left: 0;
      }
      .icon-delete {
        left: 0;
        display: none;
      }
      .icon-copy {
        right: 0;
      }
    }
    .http {
      @include overflow-auto(y);
      height: 100%;
      .section {
        border-top: 1px solid var(--border);
        border-bottom: 1px solid var(--border);
        margin-top: 10px;
        margin-bottom: 10px;
        h2 {
          background: var(--darker-background);
          color: var(--primary);
          padding: $padding;
          line-height: 18px;
          font-size: $font-size;
        }
        table {
          color: var(--foreground);
          * {
            user-select: text;
          }
          td {
            font-size: $font-size-s;
            padding: 5px 10px;
            word-break: break-all;
          }
          .key {
            white-space: nowrap;
            font-weight: bold;
            color: var(--accent);
          }
        }
      }
      .response,
      .data {
        user-select: text;
        @include overflow-auto(x);
        padding: $padding;
        font-size: $font-size-s;
        margin: 10px 0;
        white-space: pre-wrap;
        border-top: 1px solid var(--border);
        color: var(--foreground);
        border-bottom: 1px solid var(--border);
      }
    }
  }
}

.safe-area #network {
  .http {
    @include safe-area(padding-bottom, 0px);
  }
}

@media screen and (min-width: 680px) {
  #network {
    .network {
      .control {
        .icon-eye {
          display: none;
        }
        .icon-copy {
          right: 0;
        }
        .icon-filter {
          right: 23px;
        }
        .filter-text {
          right: 55px;
        }
      }
    }
    .detail {
      width: 50%;
      left: initial;
      right: 0;
      border-left: 1px solid var(--border);
      .control {
        .icon-left {
          display: none;
        }
        .icon-delete {
          display: block;
        }
      }
    }
  }
}


================================================
FILE: src/Network/util.js
================================================
import last from 'licia/last'
import detectOs from 'licia/detectOs'
import arrToMap from 'licia/arrToMap'

export function getType(contentType) {
  if (!contentType) return 'unknown'

  const type = contentType.split(';')[0].split('/')

  return {
    type: type[0],
    subType: last(type),
  }
}

export function curlStr(request) {
  let platform = detectOs()
  if (platform === 'windows') {
    platform = 'win'
  }
  let command = []
  const ignoredHeaders = arrToMap([
    'accept-encoding',
    'host',
    'method',
    'path',
    'scheme',
    'version',
  ])

  function escapeStringWin(str) {
    const encapsChars = /[\r\n]/.test(str) ? '^"' : '"'
    return (
      encapsChars +
      str
        .replace(/\\/g, '\\\\')
        .replace(/"/g, '\\"')
        .replace(/[^a-zA-Z0-9\s_\-:=+~'/.',?;()*`&]/g, '^$&')
        .replace(/%(?=[a-zA-Z0-9_])/g, '%^')
        .replace(/\r?\n/g, '^\n\n') +
      encapsChars
    )
  }

  function escapeStringPosix(str) {
    function escapeCharacter(x) {
      const code = x.charCodeAt(0)
      let hexString = code.toString(16)
      while (hexString.length < 4) {
        hexString = '0' + hexString
      }

      return '\\u' + hexString
    }

    // eslint-disable-next-line no-control-regex
    if (/[\0-\x1F\x7F-\x9F!]|'/.test(str)) {
      return (
        "$'" +
        str
          .replace(/\\/g, '\\\\')
          .replace(/'/g, "\\'")
          .replace(/\n/g, '\\n')
          .replace(/\r/g, '\\r')
          // eslint-disable-next-line no-control-regex
          .replace(/[\0-\x1F\x7F-\x9F!]/g, escapeCharacter) +
        "'"
      )
    }
    return "'" + str + "'"
  }

  const escapeString = platform === 'win' ? escapeStringWin : escapeStringPosix

  command.push(escapeString(request.url()).replace(/[[{}\]]/g, '\\$&'))

  let inferredMethod = 'GET'
  const data = []
  const formData = request.requestFormData()
  if (formData) {
    data.push('--data-raw ' + escapeString(formData))
    ignoredHeaders['content-length'] = true
    inferredMethod = 'POST'
  }

  if (request.requestMethod !== inferredMethod) {
    command.push('-X ' + escapeString(request.requestMethod))
  }

  const requestHeaders = request.requestHeaders()
  for (let i = 0; i < requestHeaders.length; i++) {
    const header = requestHeaders[i]
    const name = header.name.replace(/^:/, '')
    if (ignoredHeaders[name.toLowerCase()]) {
      continue
    }
    command.push('-H ' + escapeString(name + ': ' + header.value))
  }
  command = command.concat(data)
  command.push('--compressed')

  return (
    'curl ' +
    command.join(
      command.length >= 3 ? (platform === 'win' ? ' ^\n  ' : ' \\\n  ') : ' '
    )
  )
}


================================================
FILE: src/Resources/Cookie.js
================================================
import map from 'licia/map'
import trim from 'licia/trim'
import isNull from 'licia/isNull'
import each from 'licia/each'
import copy from 'licia/copy'
import LunaModal from 'luna-modal'
import LunaDataGrid from 'luna-data-grid'
import { setState, getState } from './util'
import chobitsu from '../lib/chobitsu'
import { classPrefix as c } from '../lib/util'

export default class Cookie {
  constructor($container, devtools) {
    this._$container = $container
    this._devtools = devtools
    this._selectedItem = null

    this._initTpl()
    this._dataGrid = new LunaDataGrid(this._$dataGrid.get(0), {
      columns: [
        {
          id: 'key',
          title: 'Key',
          weight: 30,
        },
        {
          id: 'value',
          title: 'Value',
          weight: 90,
        },
      ],
      minHeight: 60,
      maxHeight: 223,
    })

    this._bindEvent()
  }
  refresh() {
    const $container = this._$container
    const dataGrid = this._dataGrid

    const { cookies } = chobitsu.domain('Network').getCookies()
    const cookieData = map(cookies, ({ name, value }) => ({
      key: name,
      val: value,
    }))

    dataGrid.clear()
    each(cookieData, ({ key, val }) => {
      dataGrid.append(
        {
          key,
          value: val,
        },
        {
          selectable: true,
        }
      )
    })

    const cookieState = getState('cookie', cookieData.length)
    setState($container, cookieState)
  }
  _initTpl() {
    const $container = this._$container

    $container.html(
      c(`<h2 class="title">
      Cookie
      <div class="btn refresh-cookie">
        <span class="icon-refresh"></span>
      </div>
      <div class="btn show-detail btn-disabled">
        <span class="icon icon-eye"></span>
      </div>
      <div class="btn copy-cookie btn-disabled">
        <span class="icon icon-copy"></span>
      </div>
      <div class="btn delete-cookie btn-disabled">
        <span class="icon icon-delete"></span>
      </div>
      <div class="btn clear-cookie">
        <span class="icon-clear"></span>
      </div>
      <div class="btn filter" data-type="cookie">
        <span class="icon-filter"></span>
      </div>
      <div class="btn filter-text"></div>
    </h2>
    <div class="data-grid"></div>`)
    )

    this._$dataGrid = $container.find(c('.data-grid'))
    this._$filterText = $container.find(c('.filter-text'))
  }
  _updateButtons() {
    const $container = this._$container
    const $showDetail = $container.find(c('.show-detail'))
    const $deleteCookie = $container.find(c('.delete-cookie'))
    const $copyCookie = $container.find(c('.copy-cookie'))
    const btnDisabled = c('btn-disabled')

    $showDetail.addClass(btnDisabled)
    $deleteCookie.addClass(btnDisabled)
    $copyCookie.addClass(btnDisabled)

    if (this._selectedItem) {
      $showDetail.rmClass(btnDisabled)
      $deleteCookie.rmClass(btnDisabled)
      $copyCookie.rmClass(btnDisabled)
    }
  }
  _getVal(key) {
    const { cookies } = chobitsu.domain('Network').getCookies()

    for (let i = 0, len = cookies.length; i < len; i++) {
      if (cookies[i].name === key) {
        return cookies[i].value
      }
    }

    return ''
  }
  _bindEvent() {
    const devtools = this._devtools

    this._$container
      .on('click', c('.refresh-cookie'), () => {
        devtools.notify('Refreshed', { icon: 'success' })
        this.refresh()
      })
      .on('click', c('.clear-cookie'), () => {
        chobitsu.domain('Storage').clearDataForOrigin({
          storageTypes: 'cookies',
        })
        this.refresh()
      })
      .on('click', c('.delete-cookie'), () => {
        const key = this._selectedItem

        chobitsu.domain('Network').deleteCookies({ name: key })
        this.refresh()
      })
      .on('click', c('.show-detail'), () => {
        const key = this._selectedItem
        const val = this._getVal(key)

        try {
          showSources('object', JSON.parse(val))
        } catch {
          showSources('raw', val)
        }
      })
      .on('click', c('.copy-cookie'), () => {
        const key = this._selectedItem
        copy(this._getVal(key))
        devtools.notify('Copied', { icon: 'success' })
      })
      .on('click', c('.filter'), () => {
        LunaModal.prompt('Filter').then((filter) => {
          if (isNull(filter)) return
          filter = trim(filter)
          this._filter = filter
          this._$filterText.text(filter)
          this._dataGrid.setOption('filter', filter)
        })
      })

    function showSources(type, data) {
      const sources = devtools.get('sources')
      if (!sources) return

      sources.set(type, data)

      devtools.showTool('sources')

      return true
    }

    this._dataGrid
      .on('select', (node) => {
        this._selectedItem = node.data.key
        this._updateButtons()
      })
      .on('deselect', () => {
        this._selectedItem = null
        this._updateButtons()
      })
  }
}


================================================
FILE: src/Resources/Resources.js
================================================
import Tool from '../DevTools/Tool'
import Settings from '../Settings/Settings'
import $ from 'licia/$'
import escape from 'licia/escape'
import isEmpty from 'licia/isEmpty'
import contain from 'licia/contain'
import unique from 'licia/unique'
import each from 'licia/each'
import sameOrigin from 'licia/sameOrigin'
import ajax from 'licia/ajax'
import MutationObserver from 'licia/MutationObserver'
import toArr from 'licia/toArr'
import concat from 'licia/concat'
import map from 'licia/map'
import { isErudaEl, classPrefix as c } from '../lib/util'
import evalCss from '../lib/evalCss'
import Storage from './Storage'
import Cookie from './Cookie'
import { setState, getState } from './util'

export default class Resources extends Tool {
  constructor() {
    super()

    this._style = evalCss(require('./Resources.scss'))

    this.name = 'resources'
    this._hideErudaSetting = false
    this._observeElement = true
  }
  init($el, container) {
    super.init($el)

    this._container = container

    this._initTpl()
    this._localStorage = new Storage(
      this._$localStorage,
      container,
      this,
      'local'
    )
    this._sessionStorage = new Storage(
      this._$sessionStorage,
      container,
      this,
      'session'
    )
    this._cookie = new Cookie(this._$cookie, container)

    this._bindEvent()
    this._initObserver()
    this._initCfg()
  }
  refresh() {
    return this.refreshLocalStorage()
      .refreshSessionStorage()
      .refreshCookie()
      .refreshScript()
      .refreshStylesheet()
      .refreshIframe()
      .refreshImage()
  }
  destroy() {
    super.destroy()

    this._localStorage.destroy()
    this._sessionStorage.destroy()
    this._disableObserver()
    evalCss.remove(this._style)
    this._rmCfg()
  }
  refreshScript() {
    let scriptData = []

    $('script').each(function () {
      const src = this.src

      if (src !== '') scriptData.push(src)
    })

    scriptData = unique(scriptData)

    const scriptState = getState('script', scriptData.length)
    let scriptDataHtml = '<li>Empty</li>'
    if (!isEmpty(scriptData)) {
      scriptDataHtml = map(scriptData, (script) => {
        script = escape(script)
        return `<li><a href="${script}" target="_blank" class="${c(
          'js-link'
        )}">${script}</a></li>`
      }).join('')
    }

    const scriptHtml = `<h2 class="${c('title')}">
      Script
      <div class="${c('btn refresh-script')}">
        <span class="${c('icon-refresh')}"></span>
      </div>
    </h2>
    <ul class="${c('link-list')}">
      ${scriptDataHtml}
    </ul>`

    const $script = this._$script
    setState($script, scriptState)
    $script.html(scriptHtml)

    return this
  }
  refreshStylesheet() {
    let stylesheetData = []

    $('link').each(function () {
      if (this.rel !== 'stylesheet') return

      stylesheetData.push(this.href)
    })

    stylesheetData = unique(stylesheetData)

    const stylesheetState = getState('stylesheet', stylesheetData.length)
    let stylesheetDataHtml = '<li>Empty</li>'
    if (!isEmpty(stylesheetData)) {
      stylesheetDataHtml = map(stylesheetData, (stylesheet) => {
        stylesheet = escape(stylesheet)
        return ` <li><a href="${stylesheet}" target="_blank" class="${c(
          'css-link'
        )}">${stylesheet}</a></li>`
      }).join('')
    }

    const stylesheetHtml = `<h2 class="${c('title')}">
      Stylesheet
      <div class="${c('btn refresh-stylesheet')}">
        <span class="${c('icon-refresh')}"></span>
      </div>
    </h2>
    <ul class="${c('link-list')}">
      ${stylesheetDataHtml}
    </ul>`

    const $stylesheet = this._$stylesheet
    setState($stylesheet, stylesheetState)
    $stylesheet.html(stylesheetHtml)

    return this
  }
  refreshIframe() {
    let iframeData = []

    $('iframe').each(function () {
      const $this = $(this)
      const src = $this.attr('src')

      if (src) iframeData.push(src)
    })

    iframeData = unique(iframeData)

    let iframeDataHtml = '<li>Empty</li>'
    if (!isEmpty(iframeData)) {
      iframeDataHtml = map(iframeData, (iframe) => {
        iframe = escape(iframe)
        return `<li><a href="${iframe}" target="_blank" class="${c(
          'iframe-link'
        )}">${iframe}</a></li>`
      }).join('')
    }
    const iframeHtml = `<h2 class="${c('title')}">
      Iframe
      <div class="${c('btn refresh-iframe')}">
        <span class="${c('icon-refresh')}"></span>
      </div>
    </h2>
    <ul class="${c('link-list')}">
      ${iframeDataHtml}
    </ul>`

    this._$iframe.html(iframeHtml)

    return this
  }
  refreshLocalStorage() {
    this._localStorage.refresh()

    return this
  }
  refreshSessionStorage() {
    this._sessionStorage.refresh()

    return this
  }
  refreshCookie() {
    this._cookie.refresh()

    return this
  }
  refreshImage() {
    let imageData = []

    const performance = (this._performance =
      window.webkitPerformance || window.performance)
    if (performance && performance.getEntries) {
      const entries = this._performance.getEntries()
      entries.forEach((entry) => {
        if (entry.initiatorType === 'img' || isImg(entry.name)) {
          if (contain(entry.name, 'exclude=true')) {
            return
          }
          imageData.push(entry.name)
        }
      })
    } else {
      $('img').each(function () {
        const $this = $(this)
        const src = $this.attr('src')

        if ($this.data('exclude') === 'true') {
          return
        }

        imageData.push(src)
      })
    }

    imageData = unique(imageData)
    imageData.sort()

    const imageState = getState('image', imageData.length)
    let imageDataHtml = '<li>Empty</li>'
    if (!isEmpty(imageData)) {
      // prettier-ignore
      imageDataHtml = map(imageData, (image) => {
        return `<li class="${c('image')}">
          <img src="${escape(image)}" data-exclude="true" class="${c('img-link')}"/>
        </li>`
      }).join('')
    }

    const imageHtml = `<h2 class="${c('title')}">
      Image
      <div class="${c('btn refresh-image')}">
        <span class="${c('icon-refresh')}"></span>
      </div>
    </h2>
    <ul class="${c('image-list')}">
      ${imageDataHtml}
    </ul>`

    const $image = this._$image
    setState($image, imageState)
    $image.html(imageHtml)

    return this
  }
  show() {
    super.show()
    if (this._observeElement) this._enableObserver()

    return this.refresh()
  }
  hide() {
    this._disableObserver()

    return super.hide()
  }
  _initTpl() {
    const $el = this._$el
    $el.html(
      c(`<div class="section local-storage"></div>
      <div class="section session-storage"></div>
      <div class="section cookie"></div>
      <div class="section script"></div>
      <div class="section stylesheet"></div>
      <div class="section iframe"></div>
      <div class="section image"></div>`)
    )
    this._$localStorage = $el.find(c('.local-storage'))
    this._$sessionStorage = $el.find(c('.session-storage'))
    this._$cookie = $el.find(c('.cookie'))
    this._$script = $el.find(c('.script'))
    this._$stylesheet = $el.find(c('.stylesheet'))
    this._$iframe = $el.find(c('.iframe'))
    this._$image = $el.find(c('.image'))
  }
  _bindEvent() {
    const $el = this._$el
    const container = this._container

    $el
      .on('click', '.eruda-refresh-script', () => {
        container.notify('Refreshed', { icon: 'success' })
        this.refreshScript()
      })
      .on('click', '.eruda-refresh-stylesheet', () => {
        container.notify('Refreshed', { icon: 'success' })
        this.refreshStylesheet()
      })
      .on('click', '.eruda-refresh-iframe', () => {
        container.notify('Refreshed', { icon: 'success' })
        this.refreshIframe()
      })
      .on('click', '.eruda-refresh-image', () => {
        container.notify('Refreshed', { icon: 'success' })
        this.refreshImage()
      })
      .on('click', '.eruda-img-link', function () {
        const src = $(this).attr('src')

        showSources('img', src)
      })
      .on('click', '.eruda-css-link', linkFactory('css'))
      .on('click', '.eruda-js-link', linkFactory('js'))
      .on('click', '.eruda-iframe-link', linkFactory('iframe'))

    function showSources(type, data) {
      const sources = container.get('sources')
      if (!sources) return

      sources.set(type, data)

      container.showTool('sources')

      return true
    }

    function linkFactory(type) {
      return function (e) {
        if (!container.get('sources')) return
        e.preventDefault()

        const url = $(this).attr('href')

        if (type === 'iframe' || !sameOrigin(location.href, url)) {
          showSources('iframe', url)
        } else {
          ajax({
            url,
            success: (data) => {
              showSources(type, data)
            },
            dataType: 'raw',
          })
        }
      }
    }
  }
  _rmCfg() {
    const cfg = this.config

    const settings = this._container.get('settings')

    if (!settings) return

    settings
      .remove(cfg, 'hideErudaSetting')
      .remove(cfg, 'observeElement')
      .remove('Resources')
  }
  _initCfg() {
    const cfg = (this.config = Settings.createCfg('resources', {
      hideErudaSetting: true,
      observeElement: true,
    }))

    if (cfg.get('hideErudaSetting')) this._hideErudaSetting = true
    if (!cfg.get('observeElement')) this._observeElement = false

    cfg.on('change', (key, val) => {
      switch (key) {
        case 'hideErudaSetting':
          this._hideErudaSetting = val
          return
        case 'observeElement':
          this._observeElement = val
          return val ? this._enableObserver() : this._disableObserver()
      }
    })

    const settings = this._container.get('settings')
    settings
      .text('Resources')
      .switch(cfg, 'hideErudaSetting', 'Hide Eruda Setting')
      .switch(cfg, 'observeElement', 'Auto Refresh Elements')
      .separator()
  }
  _initObserver() {
    this._observer = new MutationObserver((mutations) => {
      each(mutations, (mutation) => {
        this._handleMutation(mutation)
      })
    })
  }
  _handleMutation(mutation) {
    if (isErudaEl(mutation.target)) return

    const checkEl = (el) => {
      const tagName = getLowerCaseTagName(el)
      switch (tagName) {
        case 'script':
          this.refreshScript()
          break
        case 'img':
          this.refreshImage()
          break
        case 'link':
          this.refreshStylesheet()
          break
      }
    }

    if (mutation.type === 'attributes') {
      checkEl(mutation.target)
    } else if (mutation.type === 'childList') {
      checkEl(mutation.target)
      let nodes = toArr(mutation.addedNodes)
      nodes = concat(nodes, toArr(mutation.removedNodes))

      for (const node of nodes) {
        checkEl(node)
      }
    }
  }
  _enableObserver() {
    this._observer.observe(document.documentElement, {
      attributes: true,
      childList: true,
      subtree: true,
    })
  }
  _disableObserver() {
    this._observer.disconnect()
  }
}

function getLowerCaseTagName(el) {
  if (!el.tagName) return ''
  return el.tagName.toLowerCase()
}

const regImg = /\.(jpeg|jpg|gif|png)$/

const isImg = (url) => regImg.test(url)


================================================
FILE: src/Resources/Resources.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;

#resources {
  @include overflow-auto(y);
  padding: 10px;
  font-size: 14px;
  .section {
    margin-bottom: 10px;
    overflow: hidden;
    border: 1px solid var(--border);
    &.warn {
      border: 1px solid var(--console-warn-border);
      .title {
        background: var(--console-warn-background);
        color: var(--console-warn-foreground);
      }
    }
    &.danger {
      border: 1px solid var(--console-error-border);
      .title {
        background: var(--console-error-background);
        color: var(--console-error-foreground);
      }
    }
    &.local-storage,
    &.session-storage,
    &.cookie {
      border: none;
      .title {
        border: 1px solid var(--border);
        border-bottom: none;
      }
    }
  }
  .title {
    padding: $padding;
    line-height: 18px;
    color: var(--primary);
    background: var(--darker-background);
    @include right-btn();
  }
  .link-list {
    font-size: $font-size-s;
    color: var(--foreground);
    li {
      padding: 10px;
      word-break: break-all;
      a {
        color: var(--link-color) !important;
      }
    }
  }
  .image-list {
    color: var(--foreground);
    font-size: $font-size-s;
    display: flex;
    flex-wrap: wrap;
    padding-left: $padding;
    padding-top: $padding;
    &::after {
      content: '';
      flex-grow: 1000;
    }
    li {
      flex-grow: 1;
      cursor: pointer;
      overflow-y: hidden;
      margin-right: $padding;
      margin-bottom: $padding;
      border: 1px solid var(--border);
      &.image {
        height: 100px;
        font-size: 0;
      }
      img {
        height: 100px;
        min-width: 100%;
        object-fit: cover;
      }
    }
  }
}

.safe-area #resources {
  @include safe-area(padding-bottom, 10px);
}


================================================
FILE: src/Resources/Storage.js
================================================
import each from 'licia/each'
import isStr from 'licia/isStr'
import startWith from 'licia/startWith'
import truncate from 'licia/truncate'
import LunaModal from 'luna-modal'
import LunaDataGrid from 'luna-data-grid'
import isNull from 'licia/isNull'
import trim from 'licia/trim'
import copy from 'licia/copy'
import emitter from '../lib/emitter'
import { safeStorage, classPrefix as c } from '../lib/util'

export default class Storage {
  constructor($container, devtools, resources, type) {
    this._type = type
    this._$container = $container
    this._devtools = devtools
    this._resources = resources
    this._selectedItem = null
    this._storeData = []

    this._initTpl()
    this._dataGrid = new LunaDataGrid(this._$dataGrid.get(0), {
      columns: [
        {
          id: 'key',
          title: 'Key',
          weight: 30,
        },
        {
          id: 'value',
          title: 'Value',
          weight: 90,
        },
      ],
      minHeight: 60,
      maxHeight: 223,
    })

    this._bindEvent()
  }
  destroy() {
    emitter.off(emitter.SCALE, this._updateGridHeight)
  }
  refresh() {
    const dataGrid = this._dataGrid

    this._refreshStorage()
    dataGrid.clear()

    each(this._storeData, ({ key, val }) => {
      dataGrid.append(
        {
          key,
          value: val,
        },
        {
          selectable: true,
        }
      )
    })
  }
  _refreshStorage() {
    const resources = this._resources

    let store = safeStorage(this._type, false)

    if (!store) return

    const storeData = []

    // Mobile safari is not able to loop through localStorage directly.
    store = JSON.parse(JSON.stringify(store))

    each(store, (val, key) => {
      // According to issue 20, not all values are guaranteed to be string.
      if (!isStr(val)) return

      if (resources.config.get('hideErudaSetting')) {
        if (startWith(key, 'eruda') || key === 'active-eruda') return
      }

      storeData.push({
        key: key,
        val: truncate(val, 200),
      })
    })

    this._storeData = storeData
  }
  _updateButtons() {
    const $container = this._$container
    const $showDetail = $container.find(c('.show-detail'))
    const $deleteStorage = $container.find(c('.delete-storage'))
    const $copyStorage = $container.find(c('.copy-storage'))
    const btnDisabled = c('btn-disabled')

    $showDetail.addClass(btnDisabled)
    $deleteStorage.addClass(btnDisabled)
    $copyStorage.addClass(btnDisabled)

    if (this._selectedItem) {
      $showDetail.rmClass(btnDisabled)
      $deleteStorage.rmClass(btnDisabled)
      $copyStorage.rmClass(btnDisabled)
    }
  }
  _initTpl() {
    const $container = this._$container
    const type = this._type

    $container.html(
      c(`<h2 class="title">
      ${type === 'local' ? 'Local' : 'Session'} Storage
      <div class="btn refresh-storage">
        <span class="icon icon-refresh"></span>
      </div>
      <div class="btn show-detail btn-disabled">
        <span class="icon icon-eye"></span>
      </div>
      <div class="btn copy-storage btn-disabled">
        <span class="icon icon-copy"></span>
      </div>
      <div class="btn delete-storage btn-disabled">
        <span class="icon icon-delete"></span>
      </div>
      <div class="btn clear-storage">
        <span class="icon icon-clear"></span>
      </div>
      <div class="btn filter">
        <span class="icon icon-filter"></span>
      </div>
      <div class="btn filter-text"></div>
    </h2>
    <div class="data-grid"></div>`)
    )

    this._$dataGrid = $container.find(c('.data-grid'))
    this._$filterText = $container.find(c('.filter-text'))
  }
  _getVal(key) {
    return this._type === 'local'
      ? localStorage.getItem(key)
      : sessionStorage.getItem(key)
  }
  _updateGridHeight = (scale) => {
    this._dataGrid.setOption({
      minHeight: 60 * scale,
      maxHeight: 223 * scale,
    })
  }
  _bindEvent() {
    const type = this._type
    const devtools = this._devtools

    this._$container
      .on('click', c('.refresh-storage'), () => {
        devtools.notify('Refreshed', { icon: 'success' })
        this.refresh()
      })
      .on('click', c('.clear-storage'), () => {
        each(this._storeData, (val) => {
          if (type === 'local') {
            localStorage.removeItem(val.key)
          } else {
            sessionStorage.removeItem(val.key)
          }
        })
        this.refresh()
      })
      .on('click', c('.show-detail'), () => {
        const key = this._selectedItem
        const val = this._getVal(key)

        try {
          showSources('object', JSON.parse(val))
        } catch {
          showSources('raw', val)
        }
      })
      .on('click', c('.copy-storage'), () => {
        const key = this._selectedItem
        copy(this._getVal(key))
        devtools.notify('Copied', { icon: 'success' })
      })
      .on('click', c('.filter'), () => {
        LunaModal.prompt('Filter').then((filter) => {
          if (isNull(filter)) return
          filter = trim(filter)
          this._$filterText.text(filter)
          this._dataGrid.setOption('filter', filter)
        })
      })
      .on('click', c('.delete-storage'), () => {
        const key = this._selectedItem

        if (type === 'local') {
          localStorage.removeItem(key)
        } else {
          sessionStorage.removeItem(key)
        }

        this.refresh()
      })

    function showSources(type, data) {
      const sources = devtools.get('sources')
      if (!sources) return

      sources.set(type, data)

      devtools.showTool('sources')

      return true
    }

    this._dataGrid
      .on('select', (node) => {
        this._selectedItem = node.data.key
        this._updateButtons()
      })
      .on('deselect', () => {
        this._selectedItem = null
        this._updateButtons()
      })

    emitter.on(emitter.SCALE, this._updateGridHeight)
  }
}


================================================
FILE: src/Resources/util.js
================================================
import { classPrefix as c } from '../lib/util'

export function setState($el, state) {
  $el
    .rmClass(c('ok'))
    .rmClass(c('danger'))
    .rmClass(c('warn'))
    .addClass(c(state))
}

export function getState(type, len) {
  if (len === 0) return ''

  let warn = 0
  let danger = 0

  switch (type) {
    case 'cookie':
      warn = 30
      danger = 60
      break
    case 'script':
      warn = 5
      danger = 10
      break
    case 'stylesheet':
      warn = 4
      danger = 8
      break
    case 'image':
      warn = 50
      danger = 100
      break
  }

  if (len >= danger) return 'danger'
  if (len >= warn) return 'warn'

  return 'ok'
}


================================================
FILE: src/Settings/Settings.js
================================================
import Tool from '../DevTools/Tool'
import $ from 'licia/$'
import LocalStore from 'licia/LocalStore'
import uniqId from 'licia/uniqId'
import each from 'licia/each'
import filter from 'licia/filter'
import isStr from 'licia/isStr'
import contain from 'licia/contain'
import clone from 'licia/clone'
import evalCss from '../lib/evalCss'
import LunaSetting from 'luna-setting'

export default class Settings extends Tool {
  constructor() {
    super()

    this._style = evalCss(require('./Settings.scss'))

    this.name = 'settings'
    this._settings = []
  }
  init($el) {
    super.init($el)

    this._setting = new LunaSetting($el.get(0))

    this._bindEvent()
  }
  remove(config, key) {
    if (isStr(config)) {
      const self = this
      this._$el.find('.luna-setting-item-title').each(function () {
        const $this = $(this)
        if ($this.text() === config) {
          self._setting.remove(this.settingItem)
        }
      })
    } else {
      this._settings = filter(this._settings, (setting) => {
        if (setting.config === config && setting.key === key) {
          this._setting.remove(setting.item)
          return false
        }

        return true
      })
    }

    this._cleanSeparator()

    return this
  }
  destroy() {
    this._setting.destroy()
    super.destroy()

    evalCss.remove(this._style)
  }
  clear() {
    this._settings = []
    this._setting.clear()
  }
  switch(config, key, desc) {
    const id = this._genId()

    const item = this._setting.appendCheckbox(id, !!config.get(key), desc)
    this._settings.push({ config, key, id, item })

    return this
  }
  select(config, key, desc, selections) {
    const id = this._genId()

    const selectOptions = {}
    each(selections, (selection) => (selectOptions[selection] = selection))
    const item = this._setting.appendSelect(
      id,
      config.get(key),
      '',
      desc,
      selectOptions
    )
    this._settings.push({ config, key, id, item })

    return this
  }
  range(config, key, desc, { min = 0, max = 1, step = 0.1 }) {
    const id = this._genId()

    const item = this._setting.appendNumber(id, config.get(key), desc, {
      max,
      min,
      step,
      range: true,
    })
    this._settings.push({ config, key, min, max, step, id, item })

    return this
  }
  button(text, handler) {
    this._setting.appendButton(text, handler)

    return this
  }
  separator() {
    this._setting.appendSeparator()

    return this
  }
  text(text) {
    this._setting.appendTitle(text)

    return this
  }
  // Merge adjacent separators
  _cleanSeparator() {
    const children = clone(this._$el.get(0).children)

    function isSeparator(node) {
      return contain(node.getAttribute('class'), 'luna-setting-item-separator')
    }

    for (let i = 0, len = children.length; i < len - 1; i++) {
      if (isSeparator(children[i]) && isSeparator(children[i + 1])) {
        $(children[i]).remove()
      }
    }
  }
  _genId() {
    return uniqId('eruda-settings')
  }
  _getSetting(id) {
    let ret

    each(this._settings, (setting) => {
      if (setting.id === id) ret = setting
    })

    return ret
  }
  _bindEvent() {
    this._setting.on('change', (id, val) => {
      const setting = this._getSetting(id)
      setting.config.set(setting.key, val)
    })
  }
  static createCfg(name, data) {
    return new LocalStore('eruda-' + name, data)
  }
}


================================================
FILE: src/Settings/Settings.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;

#settings {
  @include overflow-auto(y);
}

.safe-area #settings {
  @include safe-area(padding-bottom, 0px);
}


================================================
FILE: src/Snippets/Snippets.js
================================================
import Tool from '../DevTools/Tool'
import defSnippets from './defSnippets'
import $ from 'licia/$'
import each from 'licia/each'
import escape from 'licia/escape'
import map from 'licia/map'
import remove from 'licia/remove'
import evalCss from '../lib/evalCss'
import { classPrefix as c } from '../lib/util'

export default class Snippets extends Tool {
  constructor() {
    super()

    this._style = evalCss(require('./Snippets.scss'))

    this.name = 'snippets'

    this._snippets = []
  }
  init($el) {
    super.init($el)

    this._bindEvent()
    this._addDefSnippets()
  }
  destroy() {
    super.destroy()

    evalCss.remove(this._style)
  }
  add(name, fn, desc) {
    this._snippets.push({ name, fn, desc })

    this._render()

    return this
  }
  remove(name) {
    remove(this._snippets, (snippet) => snippet.name === name)

    this._render()

    return this
  }
  run(name) {
    const snippets = this._snippets

    for (let i = 0, len = snippets.length; i < len; i++) {
      if (snippets[i].name === name) this._run(i)
    }

    return this
  }
  clear() {
    this._snippets = []
    this._render()

    return this
  }
  _bindEvent() {
    const self = this

    this._$el.on('click', '.eruda-run', function () {
      const idx = $(this).data('idx')

      self._run(idx)
    })
  }
  _run(idx) {
    this._snippets[idx].fn.call(null)
  }
  _addDefSnippets() {
    each(defSnippets, (snippet) => {
      this.add(snippet.name, snippet.fn, snippet.desc)
    })
  }
  _render() {
    const html = map(this._snippets, (snippet, idx) => {
      return `<div class="${c('section run')}" data-idx="${idx}">
        <h2 class="${c('name')}">${escape(snippet.name)}
          <div class="${c('btn')}">
            <span class="${c('icon-play')}"></span>
          </div>
        </h2>
        <div class="${c('description')}">
          ${escape(snippet.desc)}
        </div>
      </div>`
    }).join('')

    this._renderHtml(html)
  }
  _renderHtml(html) {
    if (html === this._lastHtml) return
    this._lastHtml = html
    this._$el.html(html)
  }
}


================================================
FILE: src/Snippets/Snippets.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;

#snippets {
  @include overflow-auto(y);
  padding: $padding;
  .section {
    margin-bottom: 10px;
    border: 1px solid var(--border);
    overflow: hidden;
    cursor: pointer;
    &:active {
      .name {
        background: var(--highlight);
        color: var(--select-foreground);
      }
    }
    .name {
      padding: $padding;
      line-height: 18px;
      color: var(--primary);
      background: var(--darker-background);
      transition: background-color $anim-duration;
      .btn {
        margin-left: 10px;
        float: right;
        text-align: center;
        width: 18px;
        height: 18px;
        font-size: $font-size-s;
      }
    }
    .description {
      font-size: $font-size-s;
      color: var(--foreground);
      padding: $padding;
      transition: background-color $anim-duration;
    }
  }
}

.safe-area #snippets {
  @include safe-area(padding-bottom, 10px);
}


================================================
FILE: src/Snippets/defSnippets.js
================================================
import logger from '../lib/logger'
import emitter from '../lib/emitter'
import Url from 'licia/Url'
import now from 'licia/now'
import startWith from 'licia/startWith'
import $ from 'licia/$'
import upperFirst from 'licia/upperFirst'
import loadJs from 'licia/loadJs'
import trim from 'licia/trim'
import LunaModal from 'luna-modal'
import { isErudaEl } from '../lib/util'
import evalCss from '../lib/evalCss'

let style = null

export default [
  {
    name: 'Border All',
    fn() {
      if (style) {
        evalCss.remove(style)
        style = null
        return
      }

      style = evalCss(
        '* { outline: 2px dashed #707d8b; outline-offset: -3px; }',
        document.head
      )
    },
    desc: 'Add color borders to all elements',
  },
  {
    name: 'Refresh Page',
    fn() {
      const url = new Url()
      url.setQuery('timestamp', now())

      window.location.replace(url.toString())
    },
    desc: 'Add timestamp to url and refresh',
  },
  {
    name: 'Search Text',
    fn() {
      LunaModal.prompt('Enter the text').then((keyword) => {
        if (!keyword || trim(keyword) === '') {
          return
        }

        search(keyword)
      })
    },
    desc: 'Highlight given text on page',
  },
  {
    name: 'Edit Page',
    fn() {
      const body = document.body

      body.contentEditable = body.contentEditable !== 'true'
    },
    desc: 'Toggle body contentEditable',
  },
  {
    name: 'Fit Screen',
    // https://achrafkassioui.com/birdview/
    fn() {
      const body = document.body
      const html = document.documentElement
      const $body = $(body)
      if ($body.data('scaled')) {
        window.scrollTo(0, +$body.data('scaled'))
        $body.rmAttr('data-scaled')
        $body.css('transform', 'none')
      } else {
        const documentHeight = Math.max(
          body.scrollHeight,
          body.offsetHeight,
          html.clientHeight,
          html.scrollHeight,
          html.offsetHeight
        )
        const viewportHeight = Math.max(
          document.documentElement.clientHeight,
          window.innerHeight || 0
        )
        const scaleVal = viewportHeight / documentHeight
        $body.css('transform', `scale(${scaleVal})`)
        $body.data('scaled', window.scrollY)
        window.scrollTo(0, documentHeight / 2 - viewportHeight / 2)
      }
    },
    desc: 'Scale down the whole page to fit screen',
  },
  {
    name: 'Load Vue Plugin',
    fn() {
      loadPlugin('vue')
    },
    desc: 'Vue devtools',
  },
  {
    name: 'Load Monitor Plugin',
    fn() {
      loadPlugin('monitor')
    },
    desc: 'Display page fps, memory and dom nodes',
  },
  {
    name: 'Load Features Plugin',
    fn() {
      loadPlugin('features')
    },
    desc: 'Browser feature detections',
  },
  {
    name: 'Load Timing Plugin',
    fn() {
      loadPlugin('timing')
    },
    desc: 'Show performance and resource timing',
  },
  {
    name: 'Load Code Plugin',
    fn() {
      loadPlugin('code')
    },
    desc: 'Edit and run JavaScript',
  },
  {
    name: 'Load Benchmark Plugin',
    fn() {
      loadPlugin('benchmark')
    },
    desc: 'Run JavaScript benchmarks',
  },
  {
    name: 'Load Geolocation Plugin',
    fn() {
      loadPlugin('geolocation')
    },
    desc: 'Test geolocation',
  },
  {
    name: 'Load Orientation Plugin',
    fn() {
      loadPlugin('orientation')
    },
    desc: 'Test orientation api',
  },
  {
    name: 'Load Touches Plugin',
    fn() {
      loadPlugin('touches')
    },
    desc: 'Visualize screen touches',
  },
]

evalCss(require('./searchText.scss'), document.head)

function search(text) {
  const root = document.body
  const regText = new RegExp(text, 'ig')

  traverse(root, (node) => {
    const $node = $(node)

    if (!$node.hasClass('eruda-search-highlight-block')) return

    return document.createTextNode($node.text())
  })

  traverse(root, (node) => {
    if (node.nodeType !== 3) return

    let val = node.nodeValue
    val = val.replace(
      regText,
      (match) => `<span class="eruda-keyword">${match}</span>`
    )
    if (val === node.nodeValue) return

    const $ret = $(document.createElement('div'))

    $ret.html(val)
    $ret.addClass('eruda-search-highlight-block')

    return $ret.get(0)
  })
}

function traverse(root, processor) {
  const childNodes = root.childNodes

  if (isErudaEl(root)) return

  for (let i = 0, len = childNodes.length; i < len; i++) {
    const newNode = traverse(childNodes[i], processor)
    if (newNode) root.replaceChild(newNode, childNodes[i])
  }

  return processor(root)
}

function loadPlugin(name) {
  const globalName = 'eruda' + upperFirst(name)
  if (window[globalName]) return

  let protocol = location.protocol
  if (!startWith(protocol, 'http')) protocol = 'http:'

  loadJs(
    `${protocol}//cdn.jsdelivr.net/npm/eruda-${name}@${pluginVersion[name]}`,
    (isLoaded) => {
      if (!isLoaded || !window[globalName])
        return logger.error('Fail to load plugin ' + name)

      emitter.emit(emitter.ADD, window[globalName])
      emitter.emit(emitter.SHOW, name)
    }
  )
}

const pluginVersion = {
  monitor: '1.1.1',
  features: '2.1.0',
  timing: '2.0.1',
  code: '2.2.0',
  benchmark: '2.0.1',
  geolocation: '2.1.0',
  orientation: '2.1.1',
  touches: '2.1.0',
  vue: '1.1.1',
}


================================================
FILE: src/Snippets/searchText.scss
================================================
@use '../style/variable' as *;

.search-highlight-block {
  display: inline;
  .keyword {
    background: var(--console-warn-background);
    color: var(--console-warn-foreground);
  }
}


================================================
FILE: src/Sources/Sources.js
================================================
import Tool from '../DevTools/Tool'
import LunaObjectViewer from 'luna-object-viewer'
import Settings from '../Settings/Settings'
import ajax from 'licia/ajax'
import each from 'licia/each'
import isStr from 'licia/isStr'
import escape from 'licia/escape'
import truncate from 'licia/truncate'
import replaceAll from 'licia/replaceAll'
import highlight from 'licia/highlight'
import LunaTextViewer from 'luna-text-viewer'
import evalCss from '../lib/evalCss'
import { classPrefix as c } from '../lib/util'

export default class Sources extends Tool {
  constructor() {
    super()

    this._style = evalCss(require('./Sources.scss'))

    this.name = 'sources'
    this._showLineNum = true
  }
  init($el, container) {
    super.init($el)

    this._container = container
    this._bindEvent()
    this._initCfg()
  }
  destroy() {
    super.destroy()

    evalCss.remove(this._style)
    this._rmCfg()
  }
  set(type, val) {
    if (type === 'img') {
      this._isFetchingData = true

      const img = new Image()

      const self = this

      img.onload = function () {
        self._isFetchingData = false
        self._data = {
          type: 'img',
          val: {
            width: this.width,
            height: this.height,
            src: val,
          },
        }

        self._render()
      }
      img.onerror = function () {
        self._isFetchingData = false
      }

      img.src = val

      return
    }

    this._data = { type, val }

    this._render()

    return this
  }
  show() {
    super.show()

    if (!this._data && !this._isFetchingData) {
      this._renderDef()
    }

    return this
  }
  _renderDef() {
    if (this._html) {
      this._data = {
        type: 'html',
        val: this._html,
      }

      return this._render()
    }

    if (this._isGettingHtml) return
    this._isGettingHtml = true

    ajax({
      url: location.href,
      success: (data) => (this._html = data),
      error: () => (this._html = 'Sorry, unable to fetch source code:('),
      complete: () => {
        this._isGettingHtml = false
        this._renderDef()
      },
      dataType: 'raw',
    })
  }
  _bindEvent() {
    this._container.on('showTool', (name, lastTool) => {
      if (name !== this.name && lastTool.name === this.name) {
        delete this._data
      }
    })
  }
  _rmCfg() {
    const cfg = this.config

    const settings = this._container.get('settings')

    if (!settings) return

    settings.remove(cfg, 'showLineNum').remove('Sources')
  }
  _initCfg() {
    const cfg = (this.config = Settings.createCfg('sources', {
      showLineNum: true,
    }))

    if (!cfg.get('showLineNum')) this._showLineNum = false

    cfg.on('change', (key, val) => {
      switch (key) {
        case 'showLineNum':
          this._showLineNum = val
          return
      }
    })

    const settings = this._container.get('settings')
    settings
      .text('Sources')
      .switch(cfg, 'showLineNum', 'Show Line Numbers')
      .separator()
  }
  _render() {
    this._isInit = true

    const data = this._data

    switch (data.type) {
      case 'html':
      case 'js':
      case 'css':
        return this._renderCode()
      case 'img':
        return this._renderImg()
      case 'object':
        return this._renderObj()
      case 'raw':
        return this._renderRaw()
      case 'iframe':
        return this._renderIframe()
    }
  }
  _renderImg() {
    const { width, height, src } = this._data.val

    this._renderHtml(`<div class="${c('image')}">
      <div class="${c('breadcrumb')}">${escape(src)}</div>
      <div class="${c('img-container')}" data-exclude="true">
        <img src="${escape(src)}">
      </div>
      <div class="${c('img-info')}">${escape(width)} × ${escape(height)}</div>
    </div>`)
  }
  _renderCode() {
    const data = this._data

    this._renderHtml(
      `<div class="${c('code')}" data-type="${data.type}"></div>`,
      false
    )

    let code = data.val
    const len = data.val.length

    if (len > MAX_RAW_LEN) {
      code = truncate(code, MAX_RAW_LEN)
    }

    // If source code too big, don't process it.
    if (len < MAX_BEAUTIFY_LEN) {
      code = highlight(code, data.type, {
        comment: '',
        string: '',
        number: '',
        keyword: '',
        operator: '',
      })
      each(['comment', 'string', 'number', 'keyword', 'operator'], (type) => {
        code = replaceAll(code, `class="${type}"`, `class="${c(type)}"`)
      })
    } else {
      code = escape(code)
    }

    const container = this._$el.find(c('.code')).get(0)
    new LunaTextViewer(container, {
      text: code,
      escape: false,
      wrapLongLines: true,
      showLineNumbers: data.val.length < MAX_LINE_NUM_LEN && this._showLineNum,
    })
  }
  _renderObj() {
    // Using cache will keep binding events to the same elements.
    this._renderHtml(`<ul class="${c('json')}"></ul>`, false)

    let val = this._data.val

    try {
      if (isStr(val)) {
        val = JSON.parse(val)
      }
    } catch {
      // No op
    }

    const objViewer = new LunaObjectViewer(
      this._$el.find('.eruda-json').get(0),
      {
        unenumerable: true,
        accessGetter: true,
        prototype: false,
      }
    )
    objViewer.set(val)
  }
  _renderRaw() {
    const data = this._data

    this._renderHtml(`<div class="${c('raw-wrapper')}">
      <div class="${c('raw')}"></div>
    </div>`)

    let val = data.val
    const container = this._$el.find(c('.raw')).get(0)
    if (val.length > MAX_RAW_LEN) {
      val = truncate(val, MAX_RAW_LEN)
    }

    new LunaTextViewer(container, {
      text: val,
      wrapLongLines: true,
      showLineNumbers: val.length < MAX_LINE_NUM_LEN && this._showLineNum,
    })
  }
  _renderIframe() {
    this._renderHtml(`<iframe src="${escape(this._data.val)}"></iframe>`)
  }
  _renderHtml(html, cache = true) {
    if (cache && html === this._lastHtml) return
    this._lastHtml = html
    this._$el.html(html)
    // Need setTimeout to make it work
    setTimeout(() => (this._$el.get(0).scrollTop = 0), 0)
  }
}

const MAX_BEAUTIFY_LEN = 30000
const MAX_LINE_NUM_LEN = 80000
const MAX_RAW_LEN = 100000


================================================
FILE: src/Sources/Sources.scss
================================================
@use '../style/variable' as *;
@use '../style/mixin' as *;

#sources {
  font-size: 0;
  @include overflow-auto(y);
  color: var(--foreground);
  .code-wrapper,
  .raw-wrapper {
    @include overflow-auto(x);
    width: 100%;
    min-height: 100%;
  }
  .raw,
  .code {
    height: 100%;
    .keyword {
      color: var(--keyword-color);
    }
    .comment {
      color: var(--comment-color);
    }
    .number {
      color: var(--number-color);
    }
    .string {
      color: var(--string-color);
    }
    .operator {
      color: var(--operator-color);
    }
    &[data-type='html'] {
      .keyword {
        color: var(--tag-name-color);
      }
    }
  }
  .image {
    font-size: $font-size-s;
    .breadcrumb {
      @include breadcrumb();
    }
    .img-container {
      text-align: center;
      img {
        max-width: 100%;
      }
    }
    .img-info {
      text-align: center;
      margin: 20px 0;
      color: var(--foreground);
    }
  }
  .json {
    padding: 0 $padding;
    * {
      user-select: text;
    }
  }
  iframe {
    width: 100%;
    height: 100%;
  }
}


================================================
FILE: src/eruda.js
================================================
import EntryBtn from './EntryBtn/EntryBtn'
import DevTools from './DevTools/DevTools'
import Tool from './DevTools/Tool'
import Console from './Console/Console'
import Network from './Network/Network'
import Elements from './Elements/Elements'
import Snippets from './Snippets/Snippets'
import Resources from './Resources/Resources'
import Info from './Info/Info'
import Sources from './Sources/Sources'
import Settings from './Settings/Settings'
import emitter from './lib/emitter'
import logger from './lib/logger'
import * as util from './lib/util'
import { isDarkTheme } from './lib/themes'
import themes from './lib/themes'
import isFn from 'licia/isFn'
import isNum from 'licia/isNum'
import isObj from 'licia/isObj'
import each from 'licia/each'
import isMobile from 'licia/isMobile'
import viewportScale from 'licia/viewportScale'
import detectBrowser from 'licia/detectBrowser'
import $ from 'licia/$'
import toArr from 'licia/toArr'
import upperFirst from 'licia/upperFirst'
import nextTick from 'licia/nextTick'
import isEqual from 'licia/isEqual'
import extend from 'licia/extend'
import evalCss from './lib/evalCss'
import chobitsu from './lib/chobitsu'

export default {
  init({
    container,
    tool,
    autoScale = true,
    useShadowDom = true,
    inline = false,
    defaults = {},
  } = {}) {
    if (this._isInit) {
      return
    }

    this._isInit = true
    this._scale = 1

    this._initContainer(container, useShadowDom)
    this._initStyle()
    this._initDevTools(defaults, inline)
    this._initEntryBtn()
    this._initSettings()
    this._initTools(tool)
    this._registerListener()

    if (autoScale) {
      this._autoScale()
    }
    if (inline) {
      this._entryBtn.hide()
      this._$el.addClass('eruda-inline')
      this.show()
    }
  },
  _isInit: false,
  version: VERSION,
  util: {
    isErudaEl: util.isErudaEl,
    evalCss,
    isDarkTheme(theme) {
      if (!theme) {
        theme = this.getTheme()
      }
      return isDarkTheme(theme)
    },
    getTheme: () => {
      const curTheme = evalCss.getCurTheme()

      let result = 'Light'
      each(themes, (theme, name) => {
        if (isEqual(theme, curTheme)) {
          result = name
        }
      })

      return result
    },
  },
  chobitsu,
  Tool,
  Console,
  Elements,
  Network,
  Sources,
  Resources,
  Info,
  Snippets,
  Settings,
  get(name) {
    if (!this._checkInit()) return

    if (name === 'entryBtn') return this._entryBtn

    const devTools = this._devTools

    return name ? devTools.get(name) : devTools
  },
  add(tool) {
    if (!this._checkInit()) return

    if (isFn(tool)) tool = tool(this)

    this._devTools.add(tool)

    return this
  },
  remove(name) {
    this._devTools.remove(name)

    return this
  },
  show(name) {
    if (!this._checkInit()) return

    const devTools = this._devTools

    name ? devTools.showTool(name) : devTools.show()

    return this
  },
  hide() {
    if (!this._checkInit()) return

    this._devTools.hide()

    return this
  },
  destroy() {
    this._devTools.destroy()
    delete this._devTools
    this._entryBtn.destroy()
    delete this._entryBtn
    this._unregisterListener()
    $(this._container).remove()
    evalCss.clear()
    this._isInit = false
    this._container = null
    this._shadowRoot = null
  },
  scale(s) {
    if (isNum(s)) {
      this._scale = s
      emitter.emit(emitter.SCALE, s)
      return this
    }

    return this._scale
  },
  position(p) {
    const entryBtn = this._entryBtn

    if (isObj(p)) {
      entryBtn.setPos(p)
      return this
    }

    return entryBtn.getPos()
  },
  _autoScale() {
    if (!isMobile()) return

    this.scale(1 / viewportScale())
  },
  _registerListener() {
    this._addListener = (...args) => this.add(...args)
    this._showListener = (...args) => this.show(...args)

    emitter.on(emitter.ADD, this._addListener)
    emitter.on(emitter.SHOW, this._showListener)
    emitter.on(emitter.SCALE, evalCss.setScale)
  },
  _unregisterListener() {
    emitter.off(emitter.ADD, this._addListener)
    emitter.off(emitter.SHOW, this._showListener)
    emitter.off(emitter.SCALE, evalCss.setScale)
  },
  _checkInit() {
    if (!this._isInit) logger.error('Please call "eruda.init()" first')
    return this._isInit
  },
  _initContainer(container, useShadowDom) {
    if (!container) {
      container = document.createElement('div')
      document.documentElement.appendChild(container)
    }

    container.id = 'eruda'
    container.style.all = 'initial'
    this._container = container

    let shadowRoot
    let el
    if (useShadowDom) {
      if (container.attachShadow) {
        shadowRoot = container.attachShadow({ mode: 'open' })
      } else if (container.createShadowRoot) {
        shadowRoot = container.createShadowRoot()
      }
      if (shadowRoot) {
        // font-face doesn't work inside shadow dom.
        evalCss.container = document.head
        evalCss(
          require('./style/icon.css') +
            require('luna-console/luna-console.css') +
            require('luna-object-viewer/luna-object-viewer.css') +
            require('luna-dom-viewer/luna-dom-viewer.css') +
            require('luna-text-viewer/luna-text-viewer.css') +
            require('luna-notification/luna-notification.css')
        )

        el = document.createElement('div')
        shadowRoot.appendChild(el)
        this._shadowRoot = shadowRoot
      }
    }

    if (!this._shadowRoot) {
      el = document.createElement('div')
      container.appendChild(el)
    }

    extend(el, {
      className: 'eruda-container __chobitsu-hide__',
      contentEditable: false,
    })

    // http://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari
    if (detectBrowser().name === 'ios') el.setAttribute('ontouchstart', '')

    this._$el = $(el)
  },
  _initDevTools(defaults, inline) {
    this._devTools = new DevTools(this._$el, {
      defaults,
      inline,
    })
  },
  _initStyle() {
    const className = 'eruda-style-container'
    const $el = this._$el

    if (this._shadowRoot) {
      evalCss.container = this._shadowRoot
      evalCss(':host { all: initial }')
    } else {
      $el.append(`<div class="${className}"></div>`)
      evalCss.container = $el.find(`.${className}`).get(0)
    }

    evalCss(
      require('./style/reset.scss') +
        require('luna-object-viewer/luna-object-viewer.css') +
        require('luna-console/luna-console.css') +
        require('luna-notification/luna-notification.css') +
        require('luna-data-grid/luna-data-grid.css') +
        require('luna-dom-viewer/luna-dom-viewer.css') +
        require('luna-modal/luna-modal.css') +
        require('luna-tab/luna-tab.css') +
        require('luna-text-viewer/luna-text-viewer.css') +
        require('luna-setting/luna-setting.css') +
        require('luna-box-model/luna-box-model.css') +
        require('./style/style.scss') +
        require('./style/icon.css')
    )
  },
  _initEntryBtn() {
    this._entryBtn = new EntryBtn(this._$el)
    this._entryBtn.on('click', () => this._devTools.toggle())
  },
  _initSettings() {
    const devTools = this._devTools
    const settings = new Settings()

    devTools.add(settings)

    this._entryBtn.initCfg(settings)
    devTools.initCfg(settings)
  },
  _initTools(
    tool = [
      'console',
      'elements',
      'network',
      'resources',
      'sources',
      'info',
      'snippets',
    ]
  ) {
    tool = toArr(tool)

    const devTools = this._devTools

    tool.forEach((name) => {
      const Tool = this[upperFirst(name)]
      try {
        if (Tool) devTools.add(new Tool())
      } catch (e) {
        // Use nextTick to make sure it is possible to be caught by console panel.
        nextTick(() => {
          logger.error(
            `Something wrong when initializing tool ${name}:`,
            e.message
          )
        })
      }
    })

    devTools.showTool(tool[0] || 'settings')
  },
}


================================================
FILE: src/index.js
================================================
const eruda = require('./eruda').default
module.exports = eruda
module.exports.default = eruda

//# sourceMappingURL=index.js.map


================================================
FILE: src/lib/chobitsu.js
================================================
import Chobitsu from 'chobitsu/Chobitsu'
import * as Network from 'chobitsu/domains/Network'
import * as Overlay from 'chobitsu/domains/Overlay'
import * as DOM from 'chobitsu/domains/DOM'
import * as Storage from 'chobitsu/domains/Storage'

const chobitsu = new Chobitsu()
chobitsu.register('Network', Network)
chobitsu.register('Overlay', Overlay)
chobitsu.register('DOM', {
  ...DOM,
  getNodeId: DOM.getDOMNodeId,
  getNode: DOM.getDOMNode,
})
chobitsu.register('Storage', Storage)

export default chobitsu


================================================
FILE: src/lib/emitter.js
================================================
import Emitter from 'licia/Emitter'

const emitter = new Emitter()
emitter.ADD = 'ADD'
emitter.SHOW = 'SHOW'
emitter.SCALE = 'SCALE'

export default emitter


================================================
FILE: src/lib/empty.js
================================================
export default {}


================================================
FILE: src/lib/evalCss.js
================================================
import toStr from 'licia/toStr'
import each from 'licia/each'
import filter from 'licia/filter'
import isStr from 'licia/isStr'
import keys from 'licia/keys'
import kebabCase from 'licia/kebabCase'
import defaults from 'licia/defaults'
import themes from './themes'

let styleList = []
let scale = 1

let curTheme = themes.Light

const exports = function (css, container) {
  css = toStr(css)

  for (let i = 0, len = styleList.length; i < len; i++) {
    if (styleList[i].css === css) return
  }

  container = container || exports.container || document.head
  const el = document.createElement('style')

  el.type = 'text/css'
  container.appendChild(el)

  const style = { css, el, container }
  resetStyle(style)
  styleList.push(style)

  return style
}

exports.setScale = function (s) {
  scale = s
  resetStyles()
}

exports.setTheme = function (theme) {
  if (isStr(theme)) {
    curTheme = themes[theme] || themes.Light
  } else {
    curTheme = defaults(theme, themes.Light)
  }

  resetStyles()
}

exports.getCurTheme = () => curTheme

exports.getThemes = () => themes

exports.clear = function () {
  each(styleList, ({ container, el }) => container.removeChild(el))
  styleList = []
}

exports.remove = function (style) {
  styleList = filter(styleList, (s) => s !== style)

  style.container.removeChild(style.el)
}

function resetStyles() {
  each(styleList, (style) => resetStyle(style))
}

function resetStyle({ css, el }) {
  css = css.replace(/(\d+)px/g, ($0, $1) => +$1 * scale + 'px')
  css = css.replace(/_/g, 'eruda-')
  const _keys = keys(themes.Light)
  each(_keys, (key) => {
    css = css.replace(
      new RegExp(`var\\(--${kebabCase(key)}\\)`, 'g'),
      curTheme[key]
    )
  })
  el.innerText = css
}

export default exports


================================================
FILE: src/lib/logger.js
================================================
import Logger from 'licia/Logger'

let logger

export default logger = new Logger(
  '[Eruda]',
  ENV === 'production' ? 'warn' : 'debug'
)

logger.formatter = function (type, argList) {
  argList.unshift(this.name)

  return argList
}


================================================
FILE: src/lib/micromark.js
================================================
export function micromark(str) {
  return str
}


================================================
FILE: src/lib/themes.js
================================================
import extend from 'licia/extend'
import isArr from 'licia/isArr'
import contain from 'licia/contain'

const keyMap = [
  'background',
  'foreground',
  'selectForeground',
  'accent',
  'highlight',
  'border',
  'primary',
  'contrast',
  'varColor',
  'stringColor',
  'keywordColor',
  'numberColor',
  'operatorColor',
  'linkColor',
  'textColor',
  'tagNameColor',
  'functionColor',
  'attributeNameColor',
  'commentColor',
]

const keyMapLen = keyMap.length

function arrToMap(arr) {
  const ret = {}

  for (let i = 0; i < keyMapLen; i++) {
    ret[keyMap[i]] = arr[i]
  }

  return ret
}

function createDarkTheme(theme) {
  if (isArr(theme)) theme = arrToMap(theme)
  if (!theme.darkerBackground) theme.darkerBackground = theme.contrast
  return extend(
    {
      consoleWarnBackground: '#332a00',
      consoleWarnForeground: '#ffcb6b',
      consoleWarnBorder: '#650',
      consoleErrorBackground: '#290000',
      consoleErrorForeground: '#ff8080',
      consoleErrorBorder: '#5c0000',
      light: '#ccc',
      dark: '#aaa',
    },
    theme
  )
}

function createLightTheme(theme) {
  if (isArr(theme)) theme = arrToMap(theme)
  if (!theme.darkerBackground) theme.darkerBackground = theme.contrast
  return extend(
    {
      consoleWarnBackground: '#fffbe5',
      consoleWarnForeground: '#5c5c00',
      consoleWarnBorder: '#fff5c2',
      consoleErrorBackground: '#fff0f0',
      consoleErrorForeground: '#f00',
      consoleErrorBorder: '#ffd6d6',
      light: '#fff',
      dark: '#eee',
    },
    theme
  )
}

const darkThemes = [
  'Dark',
  'Material Oceanic',
  'Material Darker',
  'Material Palenight',
  'Material Deep Ocean',
  'Monokai Pro',
  'Dracula',
  'Arc Dark',
  'Atom One Dark',
  'Solarized Dark',
  'Night Owl',
  'AMOLED',
]

export function isDarkTheme(theme) {
  return contain(darkThemes, theme)
}

// prettier-ignore
export default {
  Light: createLightTheme({
    darkerBackground: '#f3f3f3',
    background: '#fff',
    foreground: '#333',
    selectForeground: '#333',
    accent: '#1a73e8',
    highlight: '#eaeaea',
    border: '#ccc',
    primary: '#333',
    contrast: '#f2f7fd',
    varColor: '#c80000',
    stringColor: '#1a1aa6',
    keywordColor: '#881280',
    numberColor: '#1c00cf',
    operatorColor: '#808080',
    linkColor: '#1155cc',
    textColor: '#8097bd',
    tagNameColor: '#881280',
    functionColor: '#222',
    attributeNameColor: '#994500',
    commentColor: '#236e25',
    cssProperty: '#c80000',
  }),
  Dark: createDarkTheme({
    darkerBackground: '#333',
    background: '#242424',
    foreground: '#a5a5a5',
    selectForeground: '#eaeaea',
    accent: '#7cacf8',
    highlight: '#000',
    border: '#3d3d3d',
    primary: '#ccc',
    contrast: '#0b2544',
    varColor: '#e36eec',
    stringColor: '#f29766',
    keywordColor: '#9980ff',
    numberColor: '#9980ff',
    operatorColor: '#7f7f7f',
    linkColor: '#ababab',
    textColor: '#42597f',
    tagNameColor: '#5db0d7',
    functionColor: '#d5d5d5',
    attributeNameColor: '#9bbbdc',
    commentColor: '#747474',
  }),
  'Material Oceanic': createDarkTheme([
    '#263238', '#B0BEC5', '#FFFFFF', '#009688', '#425B67',
    '#2A373E', '#607D8B', '#1E272C', '#eeffff', '#c3e88d',
    '#c792ea', '#f78c6c', '#89ddff', '#80cbc4', '#B0BEC5',
    '#f07178', '#82aaff', '#ffcb6b', '#546e7a',
  ]),
  'Material Darker': createDarkTheme([
    '#212121', '#B0BEC5', '#FFFFFF', '#FF9800', '#3F3F3F',
    '#292929', '#727272', '#1A1A1A', '#eeffff', '#c3e88d',
    '#c792ea', '#f78c6c', '#89ddff', '#80cbc4', '#B0BEC5',
    '#f07178', '#82aaff', '#ffcb6b', '#616161',
  ]),
  'Material Lighter': createLightTheme([
    '#FAFAFA', '#546E7A', '#546e7a', '#00BCD4', '#E7E7E8',
    '#d3e1e8', '#94A7B0', '#F4F4F4', '#272727', '#91B859',
    '#7C4DFF', '#F76D47', '#39ADB5', '#39ADB5', '#546E7A',
    '#E53935', '#6182B8', '#F6A434', '#AABFC9',
  ]),
  'Material Palenight': createDarkTheme([
    '#292D3E', '#A6ACCD', '#FFFFFF', '#ab47bc', '#444267',
    '#2b2a3e', '#676E95', '#202331', '#eeffff', '#c3e88d',
    '#c792ea', '#f78c6c', '#89ddff', '#80cbc4', '#A6ACCD',
    '#f07178', '#82aaff', '#ffcb6b', '#676E95',
  ]),
  'Material Deep Ocean': createDarkTheme([
    '#0F111A', '#8F93A2', '#FFFFFF', '#84ffff', '#1F2233',
    '#41465b', '#4B526D', '#090B10', '#eeffff', '#c3e88d',
    '#c792ea', '#f78c6c', '#89ddff', '#80cbc4', '#8F93A2',
    '#f07178', '#82aaff', '#ffcb6b', '#717CB4',
  ]),
  'Monokai Pro': createDarkTheme([
    '#2D2A2E', '#fcfcfa', '#FFFFFF', '#ffd866', '#5b595c',
    '#423f43', '#939293', '#221F22', '#FCFCFA', '#FFD866',
    '#FF6188', '#AB9DF2', '#FF6188', '#78DCE8', '#fcfcfa',
    '#FF6188', '#A9DC76', '#78DCE8', '#727072',
  ]),
  Dracula: createDarkTheme([
    '#282A36', '#F8F8F2', '#8BE9FD', '#FF79C5', '#6272A4',
    '#21222C', '#6272A4', '#191A21', '#F8F8F2', '#F1FA8C',
    '#FF79C6', '#BD93F9', '#FF79C6', '#F1FA8C', '#F8F8F2',
    '#FF79C6', '#50FA78', '#50FA7B', '#6272A4',
  ]),
  'Arc Dark': createDarkTheme([
    '#2f343f', '#D3DAE3', '#FFFFFF', '#42A5F5', '#3F3F46',
    '#404552', '#8b9eb5', '#262b33', '#CF6A4C', '#8F9D6A',
    '#9B859D', '#CDA869', '#A7A7A7', '#7587A6', '#D3DAE3',
    '#CF6A4C', '#7587A6', '#F9EE98', '#747C84',
  ]),
  'Atom One Dark': createDarkTheme([
    '#282C34', '#979FAD', '#FFFFFF', '#2979ff', '#383D48',
    '#2e3239', '#979FAD', '#21252B', '#D19A66', '#98C379',
    '#C679DD', '#D19A66', '#61AFEF', '#56B6C2', '#979FAD',
    '#F07178', '#61AEEF', '#E5C17C', '#59626F',
  ]),
  'Atom One Light': createLightTheme([
    '#FAFAFA', '#232324', '#232324', '#2979ff', '#EAEAEB',
    '#DBDBDC', '#9D9D9F', '#FFFFFF', '#986801', '#50A14E',
    '#A626A4', '#986801', '#4078F2', '#0184BC', '#232324',
    '#E4564A', '#4078F2', '#C18401', '#A0A1A7',
  ]),
  'Solarized Dark': createDarkTheme([
    '#002B36', '#839496', '#FFFFFF', '#d33682', '#11353F',
    '#0D3640', '#586e75', '#00252E', '#268BD2', '#2AA198',
    '#859900', '#D33682', '#93A1A1', '#268BD2', '#839496',
    '#268BD2', '#B58900', '#B58900', '#657B83',
  ]),
  'Solarized Light': createLightTheme([
    '#fdf6e3', '#586e75', '#002b36', '#d33682', '#F6F0DE',
    '#f7f2e2', '#93a1a1', '#eee8d5', '#268BD2', '#2AA198',
    '#859900', '#D33682', '#657B83', '#268BD2', '#586e75',
    '#268BD2', '#B58900', '#657B83', '#93A1A1',
  ]),
  Github: createLightTheme([
    '#F7F8FA', '#5B6168', '#FFFFFF', '#79CB60', '#CCE5FF',
    '#DFE1E4', '#292D31', '#FFFFFF', '#24292E', '#032F62',
    '#D73A49', '#005CC5', '#D73A49', '#005CC5', '#5B6168',
    '#22863A', '#6F42C1', '#6F42C1', '#6A737D',
  ]),
  'Night Owl': createDarkTheme([
    '#011627', '#b0bec5', '#ffffff', '#7e57c2', '#152C3B',
    '#2a373e', '#607d8b', '#001424', '#addb67', '#ecc48d',
    '#c792ea', '#f78c6c', '#c792ea', '#80CBC4', '#b0bec5',
    '#7fdbca', '#82AAFF', '#FAD430', '#637777',
  ]),
  'Light Owl': createLightTheme([
    '#FAFAFA', '#546e7a', '#403f53', '#269386', '#E0E7EA',
    '#efefef', '#403F53', '#FAFAFA', '#0C969B', '#c96765',
    '#994cc3', '#aa0982', '#7d818b', '#994cc3', '#546e7a',
    '#994cc3', '#4876d6', '#4876d6', '#637777',
  ]),
  AMOLED: createDarkTheme([
    '#000000', '#8F93A2', '#FFFFFF', '#68FFAE', '#000000',
    '#41465b', '#4B526D', '#000000', '#DEFDF7', '#38ff9f',
    '#ab2eff', '#A76DF7', '#38ff9f', '#86F3C7', '#8F93A2',
    '#ab2eff', '#8293FF', '#38ff9f', '#6575c7',
  ]),
}


================================================
FILE: src/lib/util.js
================================================
import Url from 'licia/Url'
import contain from 'licia/contain'
import escapeJsStr from 'licia/escapeJsStr'
import isUndef from 'licia/isUndef'
import last from 'licia/last'
import map from 'licia/map'
import memStorage from 'licia/memStorage'
import toNum from 'licia/toNum'
import trim from 'licia/trim'
import html from 'licia/html'

// https://stackoverflow.com/questions/46318395/detecting-mobile-device-notch
export function hasSafeArea() {
  let proceed = false
  const div = document.createElement('div')
  if (CSS.supports('padding-bottom: env(safe-area-inset-bottom)')) {
    div.style.paddingBottom = 'env(safe-area-inset-bottom)'
    proceed = true
  } else if (CSS.supports('padding-bottom: constant(safe-area-inset-bottom)')) {
    div.s
Download .txt
gitextract_e87c_nux/

├── .eustia.js
├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── main.yml
│       └── publish.yml
├── .gitignore
├── .gitmodules
├── .prettierignore
├── .prettierrc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build/
│   ├── build.js
│   ├── loaders/
│   │   └── handlebars-minifier-loader.js
│   ├── webpack.analyser.js
│   ├── webpack.base.js
│   ├── webpack.dev.js
│   ├── webpack.polyfill.js
│   └── webpack.prod.js
├── eruda.d.ts
├── eslint.config.mjs
├── karma.conf.js
├── package.json
├── src/
│   ├── Console/
│   │   ├── Console.js
│   │   └── Console.scss
│   ├── DevTools/
│   │   ├── DevTools.js
│   │   ├── DevTools.scss
│   │   └── Tool.js
│   ├── Elements/
│   │   ├── CssStore.js
│   │   ├── Detail.js
│   │   ├── Elements.js
│   │   ├── Elements.scss
│   │   └── util.js
│   ├── EntryBtn/
│   │   ├── EntryBtn.js
│   │   └── EntryBtn.scss
│   ├── Info/
│   │   ├── Info.js
│   │   ├── Info.scss
│   │   └── defInfo.js
│   ├── Network/
│   │   ├── Detail.js
│   │   ├── Network.js
│   │   ├── Network.scss
│   │   └── util.js
│   ├── Resources/
│   │   ├── Cookie.js
│   │   ├── Resources.js
│   │   ├── Resources.scss
│   │   ├── Storage.js
│   │   └── util.js
│   ├── Settings/
│   │   ├── Settings.js
│   │   └── Settings.scss
│   ├── Snippets/
│   │   ├── Snippets.js
│   │   ├── Snippets.scss
│   │   ├── defSnippets.js
│   │   └── searchText.scss
│   ├── Sources/
│   │   ├── Sources.js
│   │   └── Sources.scss
│   ├── eruda.js
│   ├── index.js
│   ├── lib/
│   │   ├── chobitsu.js
│   │   ├── emitter.js
│   │   ├── empty.js
│   │   ├── evalCss.js
│   │   ├── logger.js
│   │   ├── micromark.js
│   │   ├── themes.js
│   │   └── util.js
│   ├── polyfill.js
│   └── style/
│       ├── icon.css
│       ├── icon.json
│       ├── luna.scss
│       ├── mixin.scss
│       ├── reset.scss
│       ├── style.scss
│       └── variable.scss
└── test/
    ├── boot.js
    ├── console.html
    ├── console.js
    ├── data.json
    ├── elements.html
    ├── elements.js
    ├── eruda.html
    ├── eruda.js
    ├── index.html
    ├── info.html
    ├── info.js
    ├── init.js
    ├── inline.html
    ├── manual.html
    ├── network.html
    ├── network.js
    ├── resources.html
    ├── resources.js
    ├── settings.html
    ├── settings.js
    ├── snippets.html
    ├── snippets.js
    ├── sources.html
    ├── sources.js
    ├── style.css
    └── util.js
Download .txt
SYMBOL INDEX (356 symbols across 30 files)

FILE: eruda.d.ts
  type InitDefaults (line 6) | interface InitDefaults {
  type InitOptions (line 21) | interface InitOptions {
  type Position (line 48) | interface Position {
  type AnyFn (line 53) | type AnyFn = (...args: any[]) => any
  type Emitter (line 55) | interface Emitter {
  type Tool (line 67) | interface Tool {
  type ToolConstructor (line 91) | interface ToolConstructor {
  type ConsoleConfig (line 98) | interface ConsoleConfig {
  type Log (line 141) | interface Log {
  type ErudaConsole (line 145) | interface ErudaConsole extends Tool, Console {
  type ErudaConsoleConstructor (line 159) | interface ErudaConsoleConstructor {
  type ElementsConfig (line 164) | interface ElementsConfig {
  type Elements (line 175) | interface Elements extends Tool {
  type ElementsConstructor (line 188) | interface ElementsConstructor {
  type Network (line 193) | interface Network extends Tool {
  type NetworkConstructor (line 204) | interface NetworkConstructor {
  type ResourcesConfig (line 209) | interface ResourcesConfig {
  type Resources (line 220) | interface Resources extends Tool {
  type ResourcesConstructor (line 229) | interface ResourcesConstructor {
  type SourcesConfig (line 234) | interface SourcesConfig {
  type Sources (line 249) | interface Sources extends Tool {
  type SourcesConstructor (line 255) | interface SourcesConstructor {
  type InfoItem (line 260) | interface InfoItem {
  type Info (line 265) | interface Info extends Tool {
  type InfoConstructor (line 285) | interface InfoConstructor {
  type Snippets (line 290) | interface Snippets extends Tool {
  type SnippetsConstructor (line 314) | interface SnippetsConstructor {
  type SettingsRangeOptions (line 319) | interface SettingsRangeOptions {
  type Settings (line 325) | interface Settings extends Tool {
  type SettingsConstructor (line 374) | interface SettingsConstructor {
  type EntryBtn (line 379) | interface EntryBtn extends Emitter {
  type EntryBtnConstructor (line 387) | interface EntryBtnConstructor {
  type DevTools (line 392) | interface DevTools extends Emitter {
  type DevToolsConstructor (line 406) | interface DevToolsConstructor {
  type Util (line 415) | interface Util {
  type IToolNameMap (line 422) | interface IToolNameMap {
  type ErudaApis (line 438) | interface ErudaApis {
  type Eruda (line 485) | interface Eruda extends ErudaApis {

FILE: src/Console/Console.js
  class Console (line 24) | class Console extends Tool {
    method constructor (line 25) | constructor({ name = 'console' } = {}) {
    method init (line 33) | init($el, container) {
    method show (line 45) | show() {
    method overrideConsole (line 49) | overrideConsole() {
    method setGlobal (line 67) | setGlobal(name, val) {
    method restoreConsole (line 70) | restoreConsole() {
    method catchGlobalErr (line 80) | catchGlobalErr() {
    method ignoreGlobalErr (line 85) | ignoreGlobalErr() {
    method filter (line 90) | filter(filter) {
    method destroy (line 105) | destroy() {
    method _enableJsExecution (line 125) | _enableJsExecution(enabled) {
    method _appendTpl (line 137) | _appendTpl() {
    method _initLogger (line 178) | _initLogger() {
    method _exposeLogger (line 211) | _exposeLogger() {
    method _bindEvent (line 225) | _bindEvent() {
    method _hideInput (line 285) | _hideInput() {
    method _showInput (line 289) | _showInput() {
    method _rmCfg (line 293) | _rmCfg() {
    method _initCfg (line 312) | _initCfg() {
  constant CONSOLE_METHOD (line 381) | const CONSOLE_METHOD = [

FILE: src/DevTools/DevTools.js
  class DevTools (line 33) | class DevTools extends Emitter {
    method constructor (line 34) | constructor($container, { defaults = {}, inline = false } = {}) {
    method show (line 66) | show() {
    method hide (line 81) | hide() {
    method toggle (line 94) | toggle() {
    method add (line 97) | add(tool) {
    method remove (line 134) | remove(name) {
    method removeAll (line 151) | removeAll() {
    method get (line 156) | get(name) {
    method showTool (line 161) | showTool(name) {
    method initCfg (line 191) | initCfg(settings) {
    method notify (line 249) | notify(content, options) {
    method destroy (line 252) | destroy() {
    method _setTheme (line 269) | _setTheme(t) {
    method _setTransparency (line 283) | _setTransparency(opacity) {
    method _setDisplaySize (line 289) | _setDisplaySize(height) {
    method _initTpl (line 298) | _initTpl() {
    method _initTab (line 316) | _initTab() {
    method _initNotification (line 328) | _initNotification() {
    method _initModal (line 339) | _initModal() {
    method _bindEvent (line 342) | _bindEvent() {

FILE: src/DevTools/Tool.js
  method init (line 4) | init($el) {
  method show (line 7) | show() {
  method hide (line 12) | hide() {
  method destroy (line 17) | destroy() {

FILE: src/Elements/CssStore.js
  function formatStyle (line 4) | function formatStyle(style) {
  class CssStore (line 30) | class CssStore {
    method constructor (line 31) | constructor(el) {
    method getComputedStyle (line 34) | getComputedStyle() {
    method getMatchedCSSRules (line 39) | getMatchedCSSRules() {
    method _elMatchesSel (line 71) | _elMatchesSel(selText) {
  function sortStyleKeys (line 76) | function sortStyleKeys(style) {
  function cmpCode (line 99) | function cmpCode(a, b) {
  function transCode (line 108) | function transCode(code) {

FILE: src/Elements/Detail.js
  class Detail (line 30) | class Detail {
    method constructor (line 31) | constructor($container, devtools) {
    method show (line 40) | show(el) {
    method destroy (line 53) | destroy() {
    method overrideEventTarget (line 58) | overrideEventTarget() {
    method restoreEventTarget (line 74) | restoreEventTarget() {
    method _initTpl (line 110) | _initTpl() {
    method _toggleAllComputedStyle (line 137) | _toggleAllComputedStyle() {
    method _render (line 142) | _render() {
    method _getData (line 266) | _getData(el) {
    method _bindEvent (line 309) | _bindEvent() {
    method _initObserver (line 349) | _initObserver() {
    method _enableObserver (line 354) | _enableObserver() {
    method _disableObserver (line 361) | _disableObserver() {
    method _handleMutation (line 364) | _handleMutation(mutation) {
    method _rmCfg (line 372) | _rmCfg() {
    method _initCfg (line 384) | _initCfg() {
  function processStyleRules (line 409) | function processStyleRules(style) {
  function processStyleRule (line 430) | function processStyleRule(val) {
  function getInlineStyle (line 442) | function getInlineStyle(style) {
  function rmDefComputedStyle (line 457) | function rmDefComputedStyle(computedStyle, styles) {
  constant NO_STYLE_TAG (line 475) | const NO_STYLE_TAG = ['script', 'style', 'meta', 'title', 'link', 'head']
  function addEvent (line 483) | function addEvent(el, type, listener, useCapture = false) {
  function rmEvent (line 496) | function rmEvent(el, type, listener, useCapture = false) {

FILE: src/Elements/Elements.js
  class Elements (line 21) | class Elements extends Tool {
    method constructor (line 22) | constructor() {
    method init (line 34) | init($el, container) {
    method show (line 55) | show() {
    method hide (line 65) | hide() {
    method select (line 71) | select(node) {
    method destroy (line 77) | destroy() {
    method _updateButtons (line 89) | _updateButtons() {
    method _initTpl (line 125) | _initTpl() {
    method _renderCrumbs (line 149) | _renderCrumbs() {
    method _bindEvent (line 171) | _bindEvent() {
    method _updateHistory (line 289) | _updateHistory() {
  function getCrumbs (line 304) | function getCrumbs(el) {

FILE: src/Elements/util.js
  function formatNodeName (line 6) | function formatNodeName(node, { noAttr = false } = {}) {

FILE: src/EntryBtn/EntryBtn.js
  class EntryBtn (line 13) | class EntryBtn extends Emitter {
    method constructor (line 14) | constructor($container) {
    method hide (line 24) | hide() {
    method show (line 27) | show() {
    method setPos (line 30) | setPos(pos) {
    method getPos (line 42) | getPos() {
    method destroy (line 45) | destroy() {
    method _isOutOfRange (line 50) | _isOutOfRange(pos) {
    method _registerListener (line 58) | _registerListener() {
    method _unregisterListener (line 65) | _unregisterListener() {
    method _initTpl (line 68) | _initTpl() {
    method _resetPos (line 76) | _resetPos(orientationChanged) {
    method _bindEvent (line 150) | _bindEvent() {
    method initCfg (line 158) | initCfg(settings) {
    method _getDefPos (line 168) | _getDefPos() {

FILE: src/Info/Info.js
  class Info (line 14) | class Info extends Tool {
    method constructor (line 15) | constructor() {
    method init (line 23) | init($el, container) {
    method destroy (line 30) | destroy() {
    method add (line 35) | add(name, val) {
    method get (line 52) | get(name) {
    method remove (line 67) | remove(name) {
    method clear (line 78) | clear() {
    method _addDefInfo (line 85) | _addDefInfo() {
    method _render (line 88) | _render() {
    method _bindEvent (line 107) | _bindEvent() {
    method _renderHtml (line 118) | _renderHtml(html) {

FILE: src/Info/defInfo.js
  method val (line 11) | val() {
  method val (line 42) | val() {

FILE: src/Network/Detail.js
  class Detail (line 12) | class Detail extends Emitter {
    method constructor (line 13) | constructor($container, devtools) {
    method show (line 21) | show(data) {
    method hide (line 96) | hide() {
    method _bindEvent (line 123) | _bindEvent() {
  constant MAX_RES_LEN (line 166) | const MAX_RES_LEN = 100000

FILE: src/Network/Network.js
  class Network (line 23) | class Network extends Tool {
    method constructor (line 24) | constructor() {
    method init (line 34) | init($el, container) {
    method show (line 85) | show() {
    method clear (line 89) | clear() {
    method requests (line 93) | requests() {
    method _updateDataGridHeight (line 100) | _updateDataGridHeight() {
    method _updateType (line 159) | _updateType(request) {
    method url (line 224) | url() {
    method requestFormData (line 227) | requestFormData() {
    method requestHeaders (line 230) | requestHeaders() {
    method _updateButtons (line 249) | _updateButtons() {
    method _bindEvent (line 275) | _bindEvent() {
    method destroy (line 344) | destroy() {
    method _initTpl (line 359) | _initTpl() {

FILE: src/Network/util.js
  function getType (line 5) | function getType(contentType) {
  function curlStr (line 16) | function curlStr(request) {

FILE: src/Resources/Cookie.js
  class Cookie (line 12) | class Cookie {
    method constructor (line 13) | constructor($container, devtools) {
    method refresh (line 38) | refresh() {
    method _initTpl (line 64) | _initTpl() {
    method _updateButtons (line 96) | _updateButtons() {
    method _getVal (line 113) | _getVal(key) {
    method _bindEvent (line 124) | _bindEvent() {

FILE: src/Resources/Resources.js
  class Resources (line 21) | class Resources extends Tool {
    method constructor (line 22) | constructor() {
    method init (line 31) | init($el, container) {
    method refresh (line 55) | refresh() {
    method destroy (line 64) | destroy() {
    method refreshScript (line 73) | refreshScript() {
    method refreshStylesheet (line 111) | refreshStylesheet() {
    method refreshIframe (line 149) | refreshIframe() {
    method refreshLocalStorage (line 184) | refreshLocalStorage() {
    method refreshSessionStorage (line 189) | refreshSessionStorage() {
    method refreshCookie (line 194) | refreshCookie() {
    method refreshImage (line 199) | refreshImage() {
    method show (line 257) | show() {
    method hide (line 263) | hide() {
    method _initTpl (line 268) | _initTpl() {
    method _bindEvent (line 287) | _bindEvent() {
    method _rmCfg (line 349) | _rmCfg() {
    method _initCfg (line 361) | _initCfg() {
    method _initObserver (line 388) | _initObserver() {
    method _handleMutation (line 395) | _handleMutation(mutation) {
    method _enableObserver (line 425) | _enableObserver() {
    method _disableObserver (line 432) | _disableObserver() {
  function getLowerCaseTagName (line 437) | function getLowerCaseTagName(el) {

FILE: src/Resources/Storage.js
  class Storage (line 13) | class Storage {
    method constructor (line 14) | constructor($container, devtools, resources, type) {
    method destroy (line 42) | destroy() {
    method refresh (line 45) | refresh() {
    method _refreshStorage (line 63) | _refreshStorage() {
    method _updateButtons (line 91) | _updateButtons() {
    method _initTpl (line 108) | _initTpl() {
    method _getVal (line 141) | _getVal(key) {
    method _bindEvent (line 152) | _bindEvent() {

FILE: src/Resources/util.js
  function setState (line 3) | function setState($el, state) {
  function getState (line 11) | function getState(type, len) {

FILE: src/Settings/Settings.js
  class Settings (line 13) | class Settings extends Tool {
    method constructor (line 14) | constructor() {
    method init (line 22) | init($el) {
    method remove (line 29) | remove(config, key) {
    method destroy (line 53) | destroy() {
    method clear (line 59) | clear() {
    method switch (line 63) | switch(config, key, desc) {
    method select (line 71) | select(config, key, desc, selections) {
    method range (line 87) | range(config, key, desc, { min = 0, max = 1, step = 0.1 }) {
    method button (line 100) | button(text, handler) {
    method separator (line 105) | separator() {
    method text (line 110) | text(text) {
    method _cleanSeparator (line 116) | _cleanSeparator() {
    method _genId (line 129) | _genId() {
    method _getSetting (line 132) | _getSetting(id) {
    method _bindEvent (line 141) | _bindEvent() {
    method createCfg (line 147) | static createCfg(name, data) {

FILE: src/Snippets/Snippets.js
  class Snippets (line 11) | class Snippets extends Tool {
    method constructor (line 12) | constructor() {
    method init (line 21) | init($el) {
    method destroy (line 27) | destroy() {
    method add (line 32) | add(name, fn, desc) {
    method remove (line 39) | remove(name) {
    method run (line 46) | run(name) {
    method clear (line 55) | clear() {
    method _bindEvent (line 61) | _bindEvent() {
    method _run (line 70) | _run(idx) {
    method _addDefSnippets (line 73) | _addDefSnippets() {
    method _render (line 78) | _render() {
    method _renderHtml (line 94) | _renderHtml(html) {

FILE: src/Snippets/defSnippets.js
  method fn (line 19) | fn() {
  method fn (line 35) | fn() {
  method fn (line 45) | fn() {
  method fn (line 58) | fn() {
  method fn (line 68) | fn() {
  method fn (line 98) | fn() {
  method fn (line 105) | fn() {
  method fn (line 112) | fn() {
  method fn (line 119) | fn() {
  method fn (line 126) | fn() {
  method fn (line 133) | fn() {
  method fn (line 140) | fn() {
  method fn (line 147) | fn() {
  method fn (line 154) | fn() {
  function search (line 163) | function search(text) {
  function traverse (line 194) | function traverse(root, processor) {
  function loadPlugin (line 207) | function loadPlugin(name) {

FILE: src/Sources/Sources.js
  class Sources (line 15) | class Sources extends Tool {
    method constructor (line 16) | constructor() {
    method init (line 24) | init($el, container) {
    method destroy (line 31) | destroy() {
    method set (line 37) | set(type, val) {
    method show (line 73) | show() {
    method _renderDef (line 82) | _renderDef() {
    method _bindEvent (line 106) | _bindEvent() {
    method _rmCfg (line 113) | _rmCfg() {
    method _initCfg (line 122) | _initCfg() {
    method _render (line 143) | _render() {
    method _renderImg (line 163) | _renderImg() {
    method _renderCode (line 174) | _renderCode() {
    method _renderObj (line 213) | _renderObj() {
    method _renderRaw (line 237) | _renderRaw() {
    method _renderIframe (line 256) | _renderIframe() {
    method _renderHtml (line 259) | _renderHtml(html, cache = true) {
  constant MAX_BEAUTIFY_LEN (line 268) | const MAX_BEAUTIFY_LEN = 30000
  constant MAX_LINE_NUM_LEN (line 269) | const MAX_LINE_NUM_LEN = 80000
  constant MAX_RAW_LEN (line 270) | const MAX_RAW_LEN = 100000

FILE: src/eruda.js
  method init (line 34) | init({
  method isDarkTheme (line 71) | isDarkTheme(theme) {
  method get (line 100) | get(name) {
  method add (line 109) | add(tool) {
  method remove (line 118) | remove(name) {
  method show (line 123) | show(name) {
  method hide (line 132) | hide() {
  method destroy (line 139) | destroy() {
  method scale (line 151) | scale(s) {
  method position (line 160) | position(p) {
  method _autoScale (line 170) | _autoScale() {
  method _registerListener (line 175) | _registerListener() {
  method _unregisterListener (line 183) | _unregisterListener() {
  method _checkInit (line 188) | _checkInit() {
  method _initContainer (line 192) | _initContainer(container, useShadowDom) {
  method _initDevTools (line 243) | _initDevTools(defaults, inline) {
  method _initStyle (line 249) | _initStyle() {
  method _initEntryBtn (line 277) | _initEntryBtn() {
  method _initSettings (line 281) | _initSettings() {
  method _initTools (line 290) | _initTools(

FILE: src/lib/evalCss.js
  function resetStyles (line 65) | function resetStyles() {
  function resetStyle (line 69) | function resetStyle({ css, el }) {

FILE: src/lib/micromark.js
  function micromark (line 1) | function micromark(str) {

FILE: src/lib/themes.js
  function arrToMap (line 29) | function arrToMap(arr) {
  function createDarkTheme (line 39) | function createDarkTheme(theme) {
  function createLightTheme (line 57) | function createLightTheme(theme) {
  function isDarkTheme (line 90) | function isDarkTheme(theme) {

FILE: src/lib/util.js
  function hasSafeArea (line 13) | function hasSafeArea() {
  function escapeJsonStr (line 36) | function escapeJsonStr(str) {
  function safeStorage (line 40) | function safeStorage(type, memReplacement) {
  function getFileName (line 69) | function getFileName(url) {
  function pxToNum (line 80) | function pxToNum(str) {
  function isErudaEl (line 84) | function isErudaEl(el) {
  function isChobitsuEl (line 93) | function isChobitsuEl(el) {
  function classPrefix (line 108) | function classPrefix(str) {
  function traverseTree (line 126) | function traverseTree(tree, handler) {
  function processClass (line 136) | function processClass(str) {
  function eventClient (line 148) | function eventClient(type, e) {
  function eventPage (line 161) | function eventPage(type, e) {

FILE: test/boot.js
  function boot (line 1) | function boot(name, cb) {
  function loadJs (line 38) | function loadJs(src, cb) {

FILE: test/console.js
  function log (line 7) | function log(i) {
  function logs (line 11) | function logs() {
  function add (line 58) | function add(num) {

FILE: test/util.js
  function noop (line 291) | function noop() {}
  function flat (line 833) | function flat(arr, res) {
  function defineProp (line 1325) | function defineProp(obj, prop, descriptor) {
  function shallowProperty (line 1821) | function shallowProperty(key) {
  function isEqual (line 1980) | function isEqual(a, b) {
  function makeClass (line 2311) | function makeClass(parent, methods, statics) {
  function encodeCodePoint (line 2537) | function encodeCodePoint(codePoint) {
  function decodeCodePoint (line 2580) | function decodeCodePoint(safe) {
  function goBack (line 2643) | function goBack() {
  function decode (line 2698) | function decode(str) {
  function hexToInt (line 2706) | function hexToInt(numStr) {
  function setCookie (line 2781) | function setCookie(key, val, options) {
  function parseArgs (line 3202) | function parseArgs(url, data, success, dataType) {
  function iterateObj (line 3635) | function iterateObj(name, keys, obj, options) {
  function wrapKey (line 3682) | function wrapKey(key) {
  function wrapStr (line 3686) | function wrapStr(str) {
  function escapeJsonStr (line 3690) | function escapeJsonStr(str) {
  function correctReference (line 3733) | function correctReference(map) {
  function parse (line 3759) | function parse(obj, options) {
  function retTimeout (line 3910) | function retTimeout() {
  function strToRegExp (line 3914) | function strToRegExp(str) {
Condensed preview — 99 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (380K chars).
[
  {
    "path": ".eustia.js",
    "chars": 194,
    "preview": "module.exports = {\n  test: {\n    library: ['node_modules/eustia-module'],\n    files: ['test/*.js', 'test/*.html'],\n    e"
  },
  {
    "path": ".gitattributes",
    "chars": 18,
    "preview": "* text=auto eol=lf"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 74,
    "preview": "open_collective: eruda\nko_fi: surunzi\ncustom: [surunzi.com/wechatpay.html]"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 463,
    "preview": "name: CI\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - 'master'\n    paths:\n      - 'src/**/*'\n      - 'test/**"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 515,
    "preview": "name: Publish to NPM\n\non:\n  workflow_dispatch:\n  release:\n    types: [created]\n\njobs:\n  publish:\n\n    runs-on: ubuntu-la"
  },
  {
    "path": ".gitignore",
    "chars": 99,
    "preview": ".idea/\ndist/\nnode_modules/\ntest/lib/\ncoverage/\ntest/playground.html\nnpm-debug.log\npackage-lock.json"
  },
  {
    "path": ".gitmodules",
    "chars": 102,
    "preview": "[submodule \"src/style/icon\"]\n\tpath = src/style/icon\n\turl = https://github.com/liriliri/icon-share.git\n"
  },
  {
    "path": ".prettierignore",
    "chars": 12,
    "preview": "test/util.js"
  },
  {
    "path": ".prettierrc.json",
    "chars": 60,
    "preview": "{\n  \"singleQuote\": true,\n  \"tabWidth\": 2,\n  \"semi\": false\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 13439,
    "preview": "## 3.4.3 (15 Jun 2025)\n\n* fix: redundant code imported\n\n## 3.4.2 (15 Jun 2025)\n\n* fix: elements horizontal scrollbar [#5"
  },
  {
    "path": "LICENSE",
    "chars": 1083,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016-present liriliri\n\nPermission is hereby granted, free of charge, to any person "
  },
  {
    "path": "README.md",
    "chars": 3087,
    "preview": "<div align=\"center\">\n  <a href=\"https://eruda.liriliri.io/\" target=\"_blank\">\n    <img src=\"https://eruda.liriliri.io/ico"
  },
  {
    "path": "build/build.js",
    "chars": 256,
    "preview": "const path = require('path')\nconst fs = require('licia/fs')\n\nconst pkg = require('../package.json')\n\ndelete pkg.scripts\n"
  },
  {
    "path": "build/loaders/handlebars-minifier-loader.js",
    "chars": 145,
    "preview": "module.exports = function (src) {\n    return src.replace(/\"loc\":\\{\"start\":\\{\"line\":\\d+,\"column\":\\d+},\"end\":\\{\"line\":\\d+,"
  },
  {
    "path": "build/webpack.analyser.js",
    "chars": 200,
    "preview": "const BundleAnalyzerPlugin =\n  require('webpack-bundle-analyzer').BundleAnalyzerPlugin\n\nexports = require('./webpack.pro"
  },
  {
    "path": "build/webpack.base.js",
    "chars": 2915,
    "preview": "const autoprefixer = require('autoprefixer')\nconst prefixer = require('postcss-prefixer')\nconst clean = require('postcss"
  },
  {
    "path": "build/webpack.dev.js",
    "chars": 303,
    "preview": "const webpack = require('webpack')\n\nexports = require('./webpack.base')\n\nexports.mode = 'development'\nexports.output.fil"
  },
  {
    "path": "build/webpack.polyfill.js",
    "chars": 198,
    "preview": "const path = require('path')\n\nmodule.exports = {\n  mode: 'production',\n  entry: './src/polyfill',\n  output: {\n    path: "
  },
  {
    "path": "build/webpack.prod.js",
    "chars": 481,
    "preview": "const webpack = require('webpack')\nconst TerserPlugin = require('terser-webpack-plugin')\n\nexports = require('./webpack.b"
  },
  {
    "path": "eruda.d.ts",
    "chars": 11881,
    "preview": "/**\n * Type definitions for Eruda\n * @see https://github.com/liriliri/eruda\n */\ndeclare module 'eruda' {\n  export interf"
  },
  {
    "path": "eslint.config.mjs",
    "chars": 812,
    "preview": "import babelEslintParser from '@babel/eslint-parser'\nimport eslintJs from '@eslint/js'\nimport globals from 'globals'\n\nex"
  },
  {
    "path": "karma.conf.js",
    "chars": 1576,
    "preview": "const webpackCfg = require('./build/webpack.dev')\nwebpackCfg.devtool = 'inline-source-map'\nwebpackCfg.module.rules.push("
  },
  {
    "path": "package.json",
    "chars": 3238,
    "preview": "{\n  \"name\": \"eruda\",\n  \"version\": \"3.4.3\",\n  \"description\": \"Console for Mobile Browsers\",\n  \"main\": \"eruda.js\",\n  \"brow"
  },
  {
    "path": "src/Console/Console.js",
    "chars": 10799,
    "preview": "import Tool from '../DevTools/Tool'\nimport noop from 'licia/noop'\nimport $ from 'licia/$'\nimport toStr from 'licia/toStr"
  },
  {
    "path": "src/Console/Console.scss",
    "chars": 3095,
    "preview": "@use '../style/variable' as *;\n@use '../style/mixin' as *;\n\n#console {\n  padding-top: 40px;\n  padding-bottom: 24px;\n  wi"
  },
  {
    "path": "src/DevTools/DevTools.js",
    "chars": 9503,
    "preview": "import logger from '../lib/logger'\nimport Tool from './Tool'\nimport Settings from '../Settings/Settings'\nimport Emitter "
  },
  {
    "path": "src/DevTools/DevTools.scss",
    "chars": 687,
    "preview": "@use '../style/variable' as *;\n@use '../style/mixin' as *;\n\n.dev-tools {\n  position: absolute;\n  width: 100%;\n  height: "
  },
  {
    "path": "src/DevTools/Tool.js",
    "chars": 248,
    "preview": "import Class from 'licia/Class'\n\nexport default Class({\n  init($el) {\n    this._$el = $el\n  },\n  show() {\n    this._$el."
  },
  {
    "path": "src/Elements/CssStore.js",
    "chars": 2385,
    "preview": "import each from 'licia/each'\nimport sortKeys from 'licia/sortKeys'\n\nfunction formatStyle(style) {\n  const ret = {}\n\n  f"
  },
  {
    "path": "src/Elements/Detail.js",
    "chars": 14781,
    "preview": "import isEmpty from 'licia/isEmpty'\nimport lowerCase from 'licia/lowerCase'\nimport pick from 'licia/pick'\nimport toStr f"
  },
  {
    "path": "src/Elements/Elements.js",
    "chars": 8453,
    "preview": "import Tool from '../DevTools/Tool'\nimport $ from 'licia/$'\nimport isEl from 'licia/isEl'\nimport nextTick from 'licia/ne"
  },
  {
    "path": "src/Elements/Elements.scss",
    "chars": 4918,
    "preview": "@use '../style/variable' as *;\n@use '../style/mixin' as *;\n\n#elements {\n  .elements {\n    @include absolute();\n    paddi"
  },
  {
    "path": "src/Elements/util.js",
    "chars": 1389,
    "preview": "import each from 'licia/each'\nimport isStr from 'licia/isStr'\nimport isShadowRoot from 'licia/isShadowRoot'\nimport { cla"
  },
  {
    "path": "src/EntryBtn/EntryBtn.js",
    "chars": 4298,
    "preview": "import emitter from '../lib/emitter'\nimport Settings from '../Settings/Settings'\nimport Emitter from 'licia/Emitter'\nimp"
  },
  {
    "path": "src/EntryBtn/EntryBtn.scss",
    "chars": 397,
    "preview": ".container {\n  .entry-btn {\n    touch-action: none;\n    width: 40px;\n    height: 40px;\n    display: flex;\n    background"
  },
  {
    "path": "src/Info/Info.js",
    "chars": 2551,
    "preview": "import Tool from '../DevTools/Tool'\nimport defInfo from './defInfo'\nimport each from 'licia/each'\nimport isFn from 'lici"
  },
  {
    "path": "src/Info/Info.scss",
    "chars": 1135,
    "preview": "@use '../style/variable' as *;\n@use '../style/mixin' as *;\n\n#info {\n  @include overflow-auto(y);\n  li {\n    margin: 10px"
  },
  {
    "path": "src/Info/defInfo.js",
    "chars": 1988,
    "preview": "import detectBrowser from 'licia/detectBrowser'\nimport detectOs from 'licia/detectOs'\nimport escape from 'licia/escape'\n"
  },
  {
    "path": "src/Network/Detail.js",
    "chars": 4424,
    "preview": "import trim from 'licia/trim'\nimport isEmpty from 'licia/isEmpty'\nimport map from 'licia/map'\nimport each from 'licia/ea"
  },
  {
    "path": "src/Network/Network.js",
    "chars": 10191,
    "preview": "import Tool from '../DevTools/Tool'\nimport $ from 'licia/$'\nimport ms from 'licia/ms'\nimport each from 'licia/each'\nimpo"
  },
  {
    "path": "src/Network/Network.scss",
    "chars": 3567,
    "preview": "@use '../style/variable' as *;\n@use '../style/mixin' as *;\n\n#network {\n  .network {\n    @include absolute();\n    padding"
  },
  {
    "path": "src/Network/util.js",
    "chars": 2681,
    "preview": "import last from 'licia/last'\nimport detectOs from 'licia/detectOs'\nimport arrToMap from 'licia/arrToMap'\n\nexport functi"
  },
  {
    "path": "src/Resources/Cookie.js",
    "chars": 4985,
    "preview": "import map from 'licia/map'\nimport trim from 'licia/trim'\nimport isNull from 'licia/isNull'\nimport each from 'licia/each"
  },
  {
    "path": "src/Resources/Resources.js",
    "chars": 11322,
    "preview": "import Tool from '../DevTools/Tool'\nimport Settings from '../Settings/Settings'\nimport $ from 'licia/$'\nimport escape fr"
  },
  {
    "path": "src/Resources/Resources.scss",
    "chars": 1828,
    "preview": "@use '../style/variable' as *;\n@use '../style/mixin' as *;\n\n#resources {\n  @include overflow-auto(y);\n  padding: 10px;\n "
  },
  {
    "path": "src/Resources/Storage.js",
    "chars": 5941,
    "preview": "import each from 'licia/each'\nimport isStr from 'licia/isStr'\nimport startWith from 'licia/startWith'\nimport truncate fr"
  },
  {
    "path": "src/Resources/util.js",
    "chars": 662,
    "preview": "import { classPrefix as c } from '../lib/util'\n\nexport function setState($el, state) {\n  $el\n    .rmClass(c('ok'))\n    ."
  },
  {
    "path": "src/Settings/Settings.js",
    "chars": 3406,
    "preview": "import Tool from '../DevTools/Tool'\nimport $ from 'licia/$'\nimport LocalStore from 'licia/LocalStore'\nimport uniqId from"
  },
  {
    "path": "src/Settings/Settings.scss",
    "chars": 172,
    "preview": "@use '../style/variable' as *;\n@use '../style/mixin' as *;\n\n#settings {\n  @include overflow-auto(y);\n}\n\n.safe-area #sett"
  },
  {
    "path": "src/Snippets/Snippets.js",
    "chars": 2081,
    "preview": "import Tool from '../DevTools/Tool'\nimport defSnippets from './defSnippets'\nimport $ from 'licia/$'\nimport each from 'li"
  },
  {
    "path": "src/Snippets/Snippets.scss",
    "chars": 968,
    "preview": "@use '../style/variable' as *;\n@use '../style/mixin' as *;\n\n#snippets {\n  @include overflow-auto(y);\n  padding: $padding"
  },
  {
    "path": "src/Snippets/defSnippets.js",
    "chars": 5313,
    "preview": "import logger from '../lib/logger'\nimport emitter from '../lib/emitter'\nimport Url from 'licia/Url'\nimport now from 'lic"
  },
  {
    "path": "src/Snippets/searchText.scss",
    "chars": 187,
    "preview": "@use '../style/variable' as *;\n\n.search-highlight-block {\n  display: inline;\n  .keyword {\n    background: var(--console-"
  },
  {
    "path": "src/Sources/Sources.js",
    "chars": 6183,
    "preview": "import Tool from '../DevTools/Tool'\nimport LunaObjectViewer from 'luna-object-viewer'\nimport Settings from '../Settings/"
  },
  {
    "path": "src/Sources/Sources.scss",
    "chars": 1092,
    "preview": "@use '../style/variable' as *;\n@use '../style/mixin' as *;\n\n#sources {\n  font-size: 0;\n  @include overflow-auto(y);\n  co"
  },
  {
    "path": "src/eruda.js",
    "chars": 8004,
    "preview": "import EntryBtn from './EntryBtn/EntryBtn'\nimport DevTools from './DevTools/DevTools'\nimport Tool from './DevTools/Tool'"
  },
  {
    "path": "src/index.js",
    "chars": 130,
    "preview": "const eruda = require('./eruda').default\nmodule.exports = eruda\nmodule.exports.default = eruda\n\n//# sourceMappingURL=ind"
  },
  {
    "path": "src/lib/chobitsu.js",
    "chars": 511,
    "preview": "import Chobitsu from 'chobitsu/Chobitsu'\nimport * as Network from 'chobitsu/domains/Network'\nimport * as Overlay from 'c"
  },
  {
    "path": "src/lib/emitter.js",
    "chars": 157,
    "preview": "import Emitter from 'licia/Emitter'\n\nconst emitter = new Emitter()\nemitter.ADD = 'ADD'\nemitter.SHOW = 'SHOW'\nemitter.SCA"
  },
  {
    "path": "src/lib/empty.js",
    "chars": 18,
    "preview": "export default {}\n"
  },
  {
    "path": "src/lib/evalCss.js",
    "chars": 1759,
    "preview": "import toStr from 'licia/toStr'\nimport each from 'licia/each'\nimport filter from 'licia/filter'\nimport isStr from 'licia"
  },
  {
    "path": "src/lib/logger.js",
    "chars": 236,
    "preview": "import Logger from 'licia/Logger'\n\nlet logger\n\nexport default logger = new Logger(\n  '[Eruda]',\n  ENV === 'production' ?"
  },
  {
    "path": "src/lib/micromark.js",
    "chars": 48,
    "preview": "export function micromark(str) {\n  return str\n}\n"
  },
  {
    "path": "src/lib/themes.js",
    "chars": 7345,
    "preview": "import extend from 'licia/extend'\nimport isArr from 'licia/isArr'\nimport contain from 'licia/contain'\n\nconst keyMap = [\n"
  },
  {
    "path": "src/lib/util.js",
    "chars": 3687,
    "preview": "import Url from 'licia/Url'\nimport contain from 'licia/contain'\nimport escapeJsStr from 'licia/escapeJsStr'\nimport isUnd"
  },
  {
    "path": "src/polyfill.js",
    "chars": 64,
    "preview": "import 'core-js/modules/es.map'\nimport 'core-js/stable/promise'\n"
  },
  {
    "path": "src/style/icon.css",
    "chars": 6293,
    "preview": "@font-face {\n  font-family: 'eruda-icon';\n  src: url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAA7U"
  },
  {
    "path": "src/style/icon.json",
    "chars": 318,
    "preview": "[\n  \"left.svg\",\n  \"right.svg\",\n  \"caret-down.svg\",\n  \"caret-right.svg\",\n  \"clear.svg\",\n  \"compress.svg\",\n  \"copy.svg\",\n "
  },
  {
    "path": "src/style/luna.scss",
    "chars": 10186,
    "preview": "@use './variable' as *;\n\n.container {\n  .luna-console {\n    background: var(--background);\n  }\n\n  @mixin luna-console-hi"
  },
  {
    "path": "src/style/mixin.scss",
    "chars": 1980,
    "preview": "@use './variable' as *;\n\n@mixin absolute($width: 100%, $height: 100%) {\n  position: absolute;\n  width: $width;\n  height:"
  },
  {
    "path": "src/style/reset.scss",
    "chars": 1341,
    "preview": ".container {\n  span,\n  applet,\n  object,\n  iframe,\n  h1,\n  h2,\n  h3,\n  h4,\n  h5,\n  h6,\n  p,\n  blockquote,\n  pre,\n  a,\n  "
  },
  {
    "path": "src/style/style.scss",
    "chars": 1222,
    "preview": "@use 'variable' as *;\n@use 'mixin' as *;\n@use 'luna' as *;\n\n.container {\n  min-width: 320px;\n  pointer-events: none;\n  p"
  },
  {
    "path": "src/style/variable.scss",
    "chars": 254,
    "preview": "$padding: 10px;\n\n$font-size: 14px;\n$font-size-s: 12px;\n$font-size-l: 16px;\n\n$font-family: -apple-system, system-ui, Blin"
  },
  {
    "path": "test/boot.js",
    "chars": 940,
    "preview": "function boot(name, cb) {\n  // Need a little delay to make sure width and height of webpack dev server iframe are initia"
  },
  {
    "path": "test/console.html",
    "chars": 566,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device"
  },
  {
    "path": "test/console.js",
    "chars": 1585,
    "preview": "describe('console', function () {\n  let tool = eruda.get('console')\n  tool.config.set('asyncRender', false)\n  let $tool "
  },
  {
    "path": "test/data.json",
    "chars": 486,
    "preview": "[\n    {\n        \"name\": \"Test\",\n        \"author\": {\n            \"name\": \"Redhoodsu\",\n            \"email\": \"surunzi@foxma"
  },
  {
    "path": "test/elements.html",
    "chars": 568,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device"
  },
  {
    "path": "test/elements.js",
    "chars": 250,
    "preview": "describe('elements', function () {\n  let tool = eruda.get('elements')\n\n  beforeEach(function () {\n    eruda.show('elemen"
  },
  {
    "path": "test/eruda.html",
    "chars": 565,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device"
  },
  {
    "path": "test/eruda.js",
    "chars": 1590,
    "preview": "describe('devTools', function () {\n  describe('init', function () {\n    it('destroy', function () {\n      eruda.destroy("
  },
  {
    "path": "test/index.html",
    "chars": 1814,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device"
  },
  {
    "path": "test/info.html",
    "chars": 560,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device"
  },
  {
    "path": "test/info.js",
    "chars": 1601,
    "preview": "describe('info', function () {\n  let tool = eruda.get('info')\n  let $tool = $('.eruda-info')\n\n  describe('default', func"
  },
  {
    "path": "test/init.js",
    "chars": 39,
    "preview": "eruda.init({\n  useShadowDom: false,\n})\n"
  },
  {
    "path": "test/inline.html",
    "chars": 982,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "test/manual.html",
    "chars": 5471,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device"
  },
  {
    "path": "test/network.html",
    "chars": 602,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device"
  },
  {
    "path": "test/network.js",
    "chars": 425,
    "preview": "describe('network', function () {\n  beforeEach(function () {\n    eruda.show('network')\n  })\n\n  describe('request', funct"
  },
  {
    "path": "test/resources.html",
    "chars": 606,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device"
  },
  {
    "path": "test/resources.js",
    "chars": 935,
    "preview": "describe('resources', function () {\n  let $tool = $('.eruda-resources')\n\n  beforeEach(function () {\n    eruda.show('reso"
  },
  {
    "path": "test/settings.html",
    "chars": 568,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device"
  },
  {
    "path": "test/settings.js",
    "chars": 867,
    "preview": "describe('settings', function () {\n  let tool = eruda.get('settings')\n  let $tool = $('.eruda-settings')\n\n  let cfg = er"
  },
  {
    "path": "test/snippets.html",
    "chars": 568,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device"
  },
  {
    "path": "test/snippets.js",
    "chars": 1612,
    "preview": "describe('snippets', function () {\n  let tool = eruda.get('snippets')\n  let $tool = $('.eruda-snippets')\n\n  describe('de"
  },
  {
    "path": "test/sources.html",
    "chars": 566,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device"
  },
  {
    "path": "test/sources.js",
    "chars": 230,
    "preview": "describe('sources', function () {\n  let tool = eruda.get('sources')\n  let $tool = $('.eruda-sources')\n\n  beforeEach(func"
  },
  {
    "path": "test/style.css",
    "chars": 977,
    "preview": "body, html {\n    padding: 0;\n    margin: 0;\n    font-family: 'Avenir Next', Avenir, 'Helvetica Neue', Helvetica, 'Frankl"
  },
  {
    "path": "test/util.js",
    "chars": 119209,
    "preview": "// Built by eustia.\n(function(root, factory)\n{\n    if (typeof define === 'function' && define.amd)\n    {\n        define("
  }
]

About this extraction

This page contains the full source code of the liriliri/eruda GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 99 files (352.1 KB), approximately 94.7k tokens, and a symbol index with 356 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!