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

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
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
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.