1
0
mirror of synced 2025-12-29 04:44:02 +08:00

Compare commits

...

21 Commits

Author SHA1 Message Date
redhoodsu
8b875b68bc release: v2.8.3 2022-12-13 00:05:16 +08:00
redhoodsu
6175dfd430 chore: update luna console and luna object viewer 2022-12-13 00:02:45 +08:00
redhoodsu
d7ecb9f91d fix(network): remove data grid ios outline 2022-12-12 13:03:20 +08:00
redhoodsu
fe05016000 chore: small changes 2022-12-12 08:34:23 +08:00
redhoodsu
16d0f73509 release: v2.8.2 2022-12-12 08:24:13 +08:00
redhoodsu
64a3279f18 release: v2.8.1 2022-12-12 01:09:49 +08:00
redhoodsu
2aa61cf2dc chore: small changes 2022-12-12 00:53:15 +08:00
redhoodsu
e7e949002c feat: remove luna syntax highlighter 2022-12-12 00:52:13 +08:00
redhoodsu
82d0a1dd73 release: v2.8.0 2022-12-11 23:36:22 +08:00
redhoodsu
af81c74193 fix(network): recognize JSON #201 2022-12-11 23:26:26 +08:00
redhoodsu
b8301620c6 feat(network): copy as curl #220 2022-12-11 21:49:17 +08:00
redhoodsu
40bd9e6ddf chore: small changes 2022-12-11 20:13:47 +08:00
redhoodsu
705b0e1915 feat(network): use luna data grid 2022-12-11 18:55:22 +08:00
redhoodsu
b0ebd37c2b chore: small changes 2022-12-11 14:05:19 +08:00
redhoodsu
08ed039023 chore: remove network hbs template 2022-12-11 13:54:15 +08:00
redhoodsu
8739533aad refactor: network 2022-12-11 12:19:03 +08:00
redhoodsu
a7bdd77c8f chore: update luna data grid 2022-12-11 10:39:55 +08:00
redhoodsu
73be39bf98 chore: small changes 2022-12-11 00:44:15 +08:00
redhoodsu
7ada0f689f feat(sources): use luna syntax highlighter 2022-12-10 21:44:35 +08:00
redhoodsu
6168d2b1c8 chore: small changes 2022-12-10 18:05:30 +08:00
redhoodsu
08badcba4b feat(info): copy 2022-12-10 11:37:37 +08:00
34 changed files with 669 additions and 417 deletions

View File

@@ -1,3 +1,25 @@
## 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)

View File

@@ -63,11 +63,14 @@ module.exports = {
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'),
],
use: [
{
loader: 'babel-loader',
options: {
sourceType: 'unambiguous',
presets: ['@babel/preset-env'],
plugins: [
'@babel/plugin-transform-runtime',

View File

@@ -1,6 +1,6 @@
{
"name": "eruda",
"version": "2.7.4",
"version": "2.8.3",
"description": "Console for Mobile Browsers",
"main": "eruda.js",
"browserslist": [
@@ -44,7 +44,7 @@
"autoprefixer": "^9.7.4",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.5",
"chobitsu": "^1.4.1",
"chobitsu": "^1.4.2",
"core-js": "^3.26.1",
"css-loader": "^3.4.2",
"draggabilly": "^2.2.0",
@@ -68,12 +68,12 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^5.0.0",
"licia": "^1.37.0",
"luna-console": "^1.1.3",
"luna-data-grid": "^0.2.1",
"luna-console": "^1.2.0",
"luna-data-grid": "^0.3.1",
"luna-dom-viewer": "^1.0.2",
"luna-modal": "^1.0.0",
"luna-notification": "^0.1.4",
"luna-object-viewer": "^0.2.2",
"luna-object-viewer": "^0.2.4",
"luna-tab": "^0.1.2",
"node-sass": "^7.0.1",
"postcss-clean": "^1.1.0",

View File

@@ -13,28 +13,8 @@
padding-bottom: 0;
}
.control {
@include absolute(100%, 40px);
cursor: default;
font-size: 0;
@include control();
padding: 10px 10px 10px 35px;
background: var(--darker-background);
color: var(--primary);
line-height: 20px;
border-bottom: 1px solid var(--border);
.icon-clear,
.icon-filter,
.icon-copy {
display: inline-block;
padding: 10px;
font-size: $font-size-l;
position: absolute;
top: 0;
cursor: pointer;
transition: color $anim-duration;
&:active {
color: var(--accent);
}
}
.icon-clear {
padding-right: 0px;
left: 0;
@@ -122,7 +102,7 @@
&:last-child {
border-right: none;
}
transition: background $anim-duration, color $anim-duration;
transition: background-color $anim-duration, color $anim-duration;
&:active {
color: var(--select-foreground);
background: var(--highlight);

View File

@@ -43,7 +43,7 @@ export default class DevTools extends Emitter {
this._resizeStartY = 0
this._resizeStartSize = 0
this._appendTpl()
this._initTpl()
this._initTab()
this._initNotification()
this._initModal()
@@ -228,7 +228,7 @@ export default class DevTools extends Emitter {
this._$el.css({ height: height + '%' })
}
_appendTpl() {
_initTpl() {
const $container = this.$container
$container.append(

View File

@@ -38,7 +38,7 @@
.breadcrumb {
@include breadcrumb();
cursor: pointer;
transition: background $anim-duration, color $anim-duration;
transition: background-color $anim-duration, color $anim-duration;
&:active {
background: var(--highlight);
color: var(--select-foreground);

View File

@@ -1,8 +0,0 @@
<ul>
{{#each infos}}
<li>
<h2 {{{class 'title'}}}>{{name}}</h2>
<div {{{class 'content'}}}>{{{val}}}</div>
</li>
{{/each}}
</ul>

View File

@@ -5,6 +5,11 @@ 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() {
@@ -13,13 +18,14 @@ export default class Info extends Tool {
this._style = evalCss(require('./Info.scss'))
this.name = 'info'
this._tpl = require('./Info.hbs')
this._infos = []
}
init($el) {
init($el, container) {
super.init($el)
this._container = container
this._addDefInfo()
this._bindEvent()
}
destroy() {
super.destroy()
@@ -88,7 +94,26 @@ export default class Info extends Tool {
infos.push({ name, val })
})
this._renderHtml(this._tpl({ infos }))
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')
})
}
_renderHtml(html) {
if (html === this._lastHtml) return

View File

@@ -11,14 +11,26 @@
padding: $padding;
}
.title {
position: relative;
padding-bottom: 0;
font-size: $font-size-l;
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%;

164
src/Network/Detail.js Normal file
View File

@@ -0,0 +1,164 @@
import trim from 'licia/trim'
import isEmpty from 'licia/isEmpty'
import map from 'licia/map'
import escape from 'licia/escape'
import copy from 'licia/copy'
import extend from 'licia/extend'
import isJson from 'licia/isJson'
import { classPrefix as c } from '../lib/util'
import { curlStr } from './util'
export default class Detail {
constructor($container, devtools) {
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) {
resTxt = `<pre class="${c('response')}">${escape(data.resTxt)}</pre>`
}
const html = `<div class="${c('control')}">
<span class="${c('icon-arrow-left back')}"></span>
<span class="${c('url')}">${escape(data.url)}</span>
<span class="${c('icon-copy copy-curl')}"></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()
}
_copyCurl = () => {
const detailData = this._detailData
copy(
curlStr({
requestMethod: detailData.method,
url() {
return detailData.url
},
requestFormData() {
return detailData.data
},
requestHeaders() {
const reqHeaders = detailData.reqHeaders || {}
extend(reqHeaders, {
'User-Agent': navigator.userAgent,
Referer: location.href,
})
return map(reqHeaders, (value, name) => {
return {
name,
value,
}
})
},
})
)
this._devtools.notify('Copied')
}
_bindEvent() {
const devtools = this._devtools
this._$container
.on('click', c('.back'), () => this.hide())
.on('click', c('.copy-curl'), this._copyCurl)
.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')
}
}
}

View File

@@ -1,8 +0,0 @@
<div {{{class 'title'}}}>
Request
<div {{{class 'btn clear-request'}}}>
<span {{{class 'icon-clear'}}}></span>
</div>
</div>
<ul {{{class 'requests'}}}></ul>
<div {{{class 'detail'}}}></div>

View File

@@ -1,13 +1,15 @@
import Tool from '../DevTools/Tool'
import isEmpty from 'licia/isEmpty'
import $ from 'licia/$'
import ms from 'licia/ms'
import trim from 'licia/trim'
import each from 'licia/each'
import last from 'licia/last'
import { getFileName } from '../lib/util'
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 LunaDataGrid from 'luna-data-grid'
import ResizeSensor from 'licia/ResizeSensor'
import { getType } from './util'
export default class Network extends Tool {
constructor() {
@@ -17,26 +19,63 @@ export default class Network extends Tool {
this.name = 'network'
this._requests = {}
this._tpl = require('./Network.hbs')
this._detailTpl = require('./detail.hbs')
this._requestsTpl = require('./requests.hbs')
this._detailData = {}
}
init($el, container) {
super.init($el)
this._container = container
this._initTpl()
this._detail = new Detail(this._$detail, container)
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()
this._appendTpl()
}
show() {
super.show()
this._render()
this._updateDataGridHeight()
}
clear() {
this._requests = {}
this._render()
this._requestDataGrid.clear()
}
requests() {
const ret = []
@@ -45,8 +84,13 @@ export default class Network extends Tool {
})
return ret
}
_updateDataGridHeight() {
const height = this._$el.offset().height - 41
this._requestDataGrid.setOption('minHeight', height)
this._requestDataGrid.setOption('maxHeight', height)
}
_reqWillBeSent = (params) => {
this._requests[params.requestId] = {
const request = {
name: getFileName(params.request.url),
url: params.request.url,
status: 'pending',
@@ -62,106 +106,103 @@ export default class Network extends Tool {
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 target = this._requests[params.requestId]
if (!target) {
const request = this._requests[params.requestId]
if (!request) {
return
}
target.resHeaders = params.headers
request.resHeaders = params.headers
this._updateType(target)
this._render()
this._updateType(request)
request.render()
}
_updateType(target) {
const contentType = target.resHeaders['content-type'] || ''
_updateType(request) {
const contentType = request.resHeaders['content-type'] || ''
const { type, subType } = getType(contentType)
target.type = type
target.subType = subType
request.type = type
request.subType = subType
}
_resReceived = (params) => {
const target = this._requests[params.requestId]
if (!target) {
const request = this._requests[params.requestId]
if (!request) {
return
}
const { response } = params
const { status, headers } = response
target.status = status
request.status = status
if (status < 200 || status >= 300) {
target.hasErr = true
request.hasErr = true
}
if (headers) {
target.resHeaders = headers
this._updateType(target)
request.resHeaders = headers
this._updateType(request)
}
this._render()
request.render()
}
_loadingFinished = (params) => {
const target = this._requests[params.requestId]
if (!target) {
const request = this._requests[params.requestId]
if (!request) {
return
}
const time = params.timestamp * 1000
target.time = time - target.startTime
target.displayTime = ms(target.time)
request.time = time - request.startTime
request.displayTime = ms(request.time)
target.size = params.encodedDataLength
target.done = true
target.resTxt = chobitsu.domain('Network').getResponseBody({
request.size = params.encodedDataLength
request.done = true
request.resTxt = chobitsu.domain('Network').getResponseBody({
requestId: params.requestId,
}).body
this._render()
request.render()
}
_bindEvent() {
const $el = this._$el
const container = this._container
const self = this
$el
.on('click', '.eruda-request', function () {
const id = $(this).data('id')
const data = self._requests[id]
$el.on('click', c('.clear-request'), () => this.clear())
if (!data.done) return
this._requestDataGrid.on('select', (node) => {
const id = $(node.container).data('id')
const request = self._requests[id]
if (!request.done) {
return
}
self._detail.show(request)
})
self._showDetail(data)
})
.on('click', '.eruda-clear-request', () => this.clear())
.on('click', '.eruda-back', () => this._hideDetail())
.on('click', '.eruda-http .eruda-response', () => {
const data = this._detailData
const resTxt = data.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)
}
})
function showSources(type, data) {
const sources = container.get('sources')
if (!sources) return
sources.set(type, data)
container.showTool('sources')
}
this._resizeSensor.addListener(
throttle(() => this._updateDataGridHeight(), 15)
)
chobitsu.domain('Network').enable()
@@ -174,6 +215,7 @@ export default class Network extends Tool {
destroy() {
super.destroy()
this._resizeSensor.destroy()
evalCss.remove(this._style)
const network = chobitsu.domain('Network')
@@ -182,51 +224,19 @@ export default class Network extends Tool {
network.off('responseReceived', this._resReceived)
network.off('loadingFinished', this._loadingFinished)
}
_showDetail(data) {
if (data.resTxt && trim(data.resTxt) === '') {
delete data.resTxt
}
if (isEmpty(data.resHeaders)) {
delete data.resHeaders
}
if (isEmpty(data.reqHeaders)) {
delete data.reqHeaders
}
this._$detail.html(this._detailTpl(data)).show()
this._detailData = data
}
_hideDetail() {
this._$detail.hide()
}
_appendTpl() {
_initTpl() {
const $el = this._$el
$el.html(this._tpl())
this._$detail = $el.find('.eruda-detail')
this._$requests = $el.find('.eruda-requests')
}
_render() {
if (!this.active) return
const renderData = {}
if (!isEmpty(this._requests)) renderData.requests = this._requests
this._renderHtml(this._requestsTpl(renderData))
}
_renderHtml(html) {
if (html === this._lastHtml) return
this._lastHtml = html
this._$requests.html(html)
}
}
function getType(contentType) {
if (!contentType) return 'unknown'
const type = contentType.split(';')[0].split('/')
return {
type: type[0],
subType: last(type),
$el.html(
c(`<div class="title">
Request
<div class="btn clear-request">
<span class="icon-clear"></span>
</div>
</div>
<div class="requests"></div>
<div class="detail"></div>`)
)
this._$detail = $el.find(c('.detail'))
this._$requests = $el.find(c('.requests'))
}
}

View File

@@ -2,79 +2,56 @@
@import '../style/mixin';
#network {
padding-top: 36px;
padding-top: 40px;
.title {
@include absolute(100%, 36px);
@include absolute(100%, 40px);
@include right-btn();
background: var(--darker-background);
padding: $padding;
color: var(--primary);
height: 36px;
border-bottom: 1px solid var(--border);
height: 40px;
}
.requests {
@include overflow-auto(y);
height: 100%;
border-bottom: 1px solid var(--border);
margin-bottom: 10px;
li {
display: flex;
width: 100%;
cursor: pointer;
border-bottom: 1px solid var(--border);
height: 41px;
color: var(--foreground);
white-space: nowrap;
&.error {
span {
color: var(--console-error-foreground);
}
}
span {
display: block;
line-height: 40px;
height: 40px;
padding: 0 5px;
font-size: $font-size-s;
vertical-align: top;
text-overflow: ellipsis;
overflow: hidden;
}
.name {
flex: 1;
padding-left: $padding;
}
.status {
width: 40px;
}
.method,
.type {
width: 50px;
}
.size {
width: 70px;
}
.time {
width: 60px;
padding-right: $padding;
}
&:nth-child(even) {
background: var(--contrast);
.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-bottom: 40px;
padding-top: 40px;
background: var(--background);
.control {
@include control();
padding: 10px 35px;
.url {
font-size: $font-size-s;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
display: inline-block;
}
.icon-arrow-left {
left: 0;
}
.icon-copy {
right: 0;
}
}
.http {
@include overflow-auto(y);
height: 100%;
.breadcrumb {
@include breadcrumb();
}
.section {
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
@@ -84,6 +61,7 @@
padding: $padding;
font-size: $font-size;
}
margin-top: 10px;
margin-bottom: 10px;
table {
color: var(--foreground);
@@ -108,32 +86,12 @@
@include overflow-auto(x);
padding: $padding;
font-size: $font-size-s;
margin-bottom: 10px;
margin: 10px 0;
white-space: pre-wrap;
border-top: 1px solid var(--border);
color: var(--foreground);
border-bottom: 1px solid var(--border);
}
}
.back {
position: absolute;
left: 0;
bottom: 0;
color: var(--foreground);
width: 100%;
border-top: 1px solid var(--border);
background: var(--darker-background);
display: block;
height: 40px;
line-height: 40px;
text-decoration: none;
text-align: center;
margin-top: 10px;
transition: background 0.3s;
cursor: pointer;
&:active {
color: var(--select-foreground);
}
}
}
}

View File

@@ -1,46 +0,0 @@
<div {{{class 'http'}}}>
<div {{{class 'breadcrumb'}}}>{{url}}</div>
{{#if data}}
<pre {{{class 'data'}}}>{{data}}</pre>
{{/if}}
<div {{{class 'section'}}}>
<h2>Request Headers</h2>
<table {{{class 'headers'}}}>
<tbody>
{{#if reqHeaders}}
{{#each reqHeaders}}
<tr>
<td {{{class 'key'}}}>{{@key}}</td>
<td>{{.}}</td>
</tr>
{{/each}}
{{else}}
<tr>
<td>Empty</td>
</tr>
{{/if}}
</tbody>
</table>
<h2>Response Headers</h2>
<table {{{class 'headers'}}}>
<tbody>
{{#if resHeaders}}
{{#each resHeaders}}
<tr>
<td {{{class 'key'}}}>{{@key}}</td>
<td>{{.}}</td>
</tr>
{{/each}}
{{else}}
<tr>
<td>Empty</td>
</tr>
{{/if}}
</tbody>
</table>
</div>
{{#if resTxt}}
<pre {{{class 'response'}}}>{{resTxt}}</pre>
{{/if}}
</div>
<div {{{class 'back'}}}>Back to the List</div>

View File

@@ -1,14 +0,0 @@
{{#if requests}}
{{#each requests}}
<li class="eruda-request {{#if hasErr}}eruda-error{{/if}}" data-id="{{@key}}">
<span {{{class 'name'}}}>{{name}}</span>
<span {{{class 'status'}}}>{{status}}</span>
<span {{{class 'method'}}}>{{method}}</span>
<span {{{class 'type'}}}>{{subType}}</span>
<span {{{class 'size'}}}>{{size}}</span>
<span {{{class 'time'}}}>{{displayTime}}</span>
</li>
{{/each}}
{{else}}
<li><span {{{class 'name'}}}>Empty</span></li>
{{/if}}

107
src/Network/util.js Normal file
View File

@@ -0,0 +1,107 @@
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'
}
/* eslint-disable */
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
}
if (/[\0-\x1F\x7F-\x9F!]|\'/.test(str)) {
return (
"$'" +
str
.replace(/\\/g, '\\\\')
.replace(/\'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.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 ') : ' '
)
)
}

View File

@@ -1,12 +0,0 @@
{{#each snippets}}
<div class="eruda-section eruda-run" data-idx="{{@index}}">
<h2 class="eruda-name">{{name}}
<div class="eruda-btn">
<span class="eruda-icon-play"></span>
</div>
</h2>
<div class="eruda-description">
{{desc}}
</div>
</div>
{{/each}}

View File

@@ -2,7 +2,10 @@ 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 evalCss from '../lib/evalCss'
import { classPrefix as c } from '../lib/util'
export default class Snippets extends Tool {
constructor() {
@@ -13,7 +16,6 @@ export default class Snippets extends Tool {
this.name = 'snippets'
this._snippets = []
this._tpl = require('./Snippets.hbs')
}
init($el) {
super.init($el)
@@ -77,11 +79,20 @@ export default class Snippets extends Tool {
})
}
_render() {
this._renderHtml(
this._tpl({
snippets: this._snippets,
})
)
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

View File

@@ -19,7 +19,7 @@
padding: $padding;
color: var(--primary);
background: var(--darker-background);
transition: background $anim-duration;
transition: background-color $anim-duration;
.btn {
margin-left: 10px;
float: right;
@@ -31,6 +31,7 @@
}
}
.description {
font-size: $font-size-s;
color: var(--foreground);
padding: $padding;
transition: background $anim-duration;

View File

@@ -2,11 +2,13 @@ import Tool from '../DevTools/Tool'
import LunaObjectViewer from 'luna-object-viewer'
import Settings from '../Settings/Settings'
import ajax from 'licia/ajax'
import isStr from 'licia/isStr'
import escape from 'licia/escape'
import trim from 'licia/trim'
import isStr from 'licia/isStr'
import map from 'licia/map'
import highlight from 'licia/highlight'
import evalCss from '../lib/evalCss'
import { classPrefix as c } from '../lib/util'
export default class Sources extends Tool {
constructor() {
@@ -16,8 +18,6 @@ export default class Sources extends Tool {
this.name = 'sources'
this._showLineNum = true
this._loadTpl()
}
init($el, container) {
super.init($el)
@@ -108,13 +108,6 @@ export default class Sources extends Tool {
}
})
}
_loadTpl() {
this._codeTpl = require('./code.hbs')
this._imgTpl = require('./image.hbs')
this._objTpl = require('./object.hbs')
this._rawTpl = require('./raw.hbs')
this._iframeTpl = require('./iframe.hbs')
}
_rmCfg() {
const cfg = this.config
@@ -166,7 +159,15 @@ export default class Sources extends Tool {
}
}
_renderImg() {
this._renderHtml(this._imgTpl(this._data.val))
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
@@ -188,7 +189,9 @@ export default class Sources extends Tool {
code = escape(code)
}
if (len < MAX_LINE_NUM_LEN && this._showLineNum) {
const showLineNum = len < MAX_LINE_NUM_LEN && this._showLineNum
if (showLineNum) {
code = code.split('\n').map((line, idx) => {
if (trim(line) === '') line = '&nbsp;'
@@ -199,16 +202,35 @@ export default class Sources extends Tool {
})
}
this._renderHtml(
this._codeTpl({
code,
showLineNum: len < MAX_LINE_NUM_LEN && this._showLineNum,
})
)
let html
if (showLineNum) {
const lineNum = map(code, ({ idx }) => {
return `<div class="${c('line-num')}">${idx}</div>`
}).join('')
const codeLine = map(code, ({ val }) => {
return `<pre class="${c('code-line')}">${val}</pre>`
}).join('')
html = `<div class="${c('code-wrapper')}">
<table class="${c('code')}">
<tbody>
<tr>
<td class="${c('gutter')}">${lineNum}</td>
<td class="${c('content')}">${codeLine}</td>
</tr>
</tbody>
</table>
</div>`
} else {
html = `<div class="${c('code-wrapper')}">
<pre class="${c('code')}">${code}</pre>
</div>`
}
this._renderHtml(html)
}
_renderObj() {
// Using cache will keep binding events to the same elements.
this._renderHtml(this._objTpl(), false)
this._renderHtml(`<ul class="${c('json')}"></ul>`, false)
let val = this._data.val
@@ -229,10 +251,12 @@ export default class Sources extends Tool {
objViewer.set(val)
}
_renderRaw() {
this._renderHtml(this._rawTpl({ val: this._data.val }))
this._renderHtml(`<div class="${c('raw-wrapper')}">
<div class="${c('raw')}">${escape(this._data.val)}</div>
</div>`)
}
_renderIframe() {
this._renderHtml(this._iframeTpl({ src: this._data.val }))
this._renderHtml(`<iframe src="${escape(this._data.val)}"></iframe>`)
}
_renderHtml(html, cache = true) {
if (cache && html === this._lastHtml) return

View File

@@ -2,6 +2,7 @@
@import '../style/mixin';
#sources {
font-size: 0;
@include overflow-auto(y);
color: var(--foreground);
.code-wrapper,
@@ -11,6 +12,7 @@
min-height: 100%;
}
.raw {
font-size: $font-size-s;
user-select: text;
padding: $padding;
}
@@ -40,6 +42,7 @@
}
}
.image {
font-size: $font-size-s;
.breadcrumb {
@include breadcrumb();
}

View File

@@ -1,24 +0,0 @@
{{#if showLineNum}}
<div {{{class 'code-wrapper'}}}>
<table {{{class 'code'}}}>
<tbody>
<tr>
<td {{{class 'gutter'}}}>
{{#each code}}
<div {{{class 'line-num'}}}>{{idx}}</div>
{{/each}}
</td>
<td {{{class 'content'}}}>
{{#each code}}
<pre {{{class 'code-line'}}}>{{{val}}}</pre>
{{/each}}
</td>
</tr>
</tbody>
</table>
</div>
{{else}}
<div {{{class 'code-wrapper'}}}>
<pre {{{class 'code'}}}>{{{code}}}</pre>
</div>
{{/if}}

View File

@@ -1 +0,0 @@
<iframe src="{{{src}}}"></iframe>

View File

@@ -1,7 +0,0 @@
<div {{{class 'image'}}}>
<div {{{class 'breadcrumb'}}}>{{src}}</div>
<div {{{class 'img-container'}}} data-exclude="true">
<img src="{{src}}">
</div>
<div {{{class 'img-info'}}}>{{width}} × {{height}}</div>
</div>

View File

@@ -1 +0,0 @@
<ul {{{class 'json'}}}></ul>

View File

@@ -1,3 +0,0 @@
<div {{{class 'raw-wrapper'}}}>
<div {{{class 'raw'}}}>{{val}}</div>
</div>

View File

@@ -103,9 +103,11 @@ export default {
this._entryBtn.destroy()
delete this._entryBtn
this._unregisterListener()
this._$el.remove()
$(this._container).remove()
evalCss.clear()
this._isInit = false
this._container = null
this._shadowRoot = null
},
scale(s) {
if (isNum(s)) {
@@ -148,19 +150,23 @@ export default {
if (!this._isInit) logger.error('Please call "eruda.init()" first')
return this._isInit
},
_initContainer(el, useShadowDom) {
if (!el) {
el = document.createElement('div')
document.documentElement.appendChild(el)
el.style.all = 'initial'
_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 (el.attachShadow) {
shadowRoot = el.attachShadow({ mode: 'open' })
} else if (el.createShadowRoot) {
shadowRoot = el.createShadowRoot()
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.
@@ -174,8 +180,12 @@ export default {
}
}
if (!this._shadowRoot) {
el = document.createElement('div')
container.appendChild(el)
}
extend(el, {
id: 'eruda',
className: 'eruda-container',
contentEditable: false
})

View File

@@ -190,8 +190,6 @@ export function safeStorage(type, memReplacement) {
export function getFileName(url) {
let ret = last(url.split('/'))
if (ret.indexOf('?') > -1) ret = trim(ret.split('?')[0])
if (ret === '') {
url = new Url(url)
ret = url.hostname

View File

@@ -94,6 +94,22 @@
.luna-console-code {
@include luna-console-highlight();
}
.luna-console-log-content {
.luna-console-undefined,
.luna-console-null {
color: var(--operator-color);
}
.luna-console-number {
color: var(--number-color);
}
.luna-console-boolean {
color: var(--keyword-color);
}
.luna-console-symbol,
.luna-console-regexp {
color: var(--var-color);
}
}
}
.luna-console-preview {
@@ -132,12 +148,6 @@
border-top-color: transparent;
border-left-color: var(--foreground);
}
.luna-object-viewer-icon-caret-right {
top: 0;
}
.luna-object-viewer-icon-caret-down {
top: 1px;
}
.luna-notification {
pointer-events: none !important;
@@ -166,6 +176,13 @@
color: var(--foreground);
background: var(--background);
border-color: var(--border);
&:focus {
.luna-data-grid-data-container {
.luna-data-grid-node.luna-data-grid-selected {
background: var(--accent);
}
}
}
th,
td {
border-color: var(--border);
@@ -173,12 +190,17 @@
th {
background: var(--darker-background);
&.luna-data-grid-sortable {
&:hover {
background: var(--darker-background);
&:hover,
&:active {
color: var(--select-foreground);
background: var(--highlight);
}
}
}
.luna-data-grid-data-container {
.luna-data-grid-node.luna-data-grid-selected {
background: var(--highlight);
}
tr:nth-child(even) {
background: var(--contrast);
}

View File

@@ -29,6 +29,29 @@
border-bottom: 1px solid var(--border);
}
@mixin control {
@include absolute(100%, 40px);
cursor: default;
font-size: 0;
background: var(--darker-background);
color: var(--primary);
line-height: 20px;
border-bottom: 1px solid var(--border);
[class^='icon-'],
[class*=' icon-'] {
display: inline-block;
padding: 10px;
font-size: $font-size-l;
position: absolute;
top: 0;
cursor: pointer;
transition: color $anim-duration;
&:active {
color: var(--accent);
}
}
}
@mixin clear-float {
&:after {
content: '';

View File

@@ -36,6 +36,13 @@
h4 {
margin: 0;
}
h2 {
font-size: $font-size;
[class^='icon-'],
[class*=' icon-'] {
font-weight: normal;
}
}
}
.hidden {
@@ -69,10 +76,3 @@
.string-color {
color: var(--string-color);
}
h2 {
[class^='icon-'],
[class*=' icon-'] {
font-weight: normal;
}
}

View File

@@ -126,6 +126,11 @@
addClickEvent('log', function () {
console.clear()
console.log('log')
console.log('number:', 5)
console.log('boolean:', true, false)
console.log('null:', null)
console.log('undefined:', undefined)
console.log('regexp:', /test/gi)
for (var i = 0; i < 10; i++) {
console.log('repeat log')
}

View File

@@ -7,7 +7,7 @@ describe('network', function () {
it('xhr', function (done) {
$('.eruda-clear-xhr').click()
util.ajax.get(window.location.toString(), function () {
expect($('.eruda-requests li')).toHaveLength(1)
expect($('.eruda-requests .luna-data-grid-node')).toHaveLength(1)
done()
})
})

View File

@@ -6,10 +6,8 @@ describe('sources', function () {
eruda.show('sources')
})
describe('js', function () {
it('highlight', function () {
tool.set('js', '/* test */')
expect($tool.find('.eruda-content')).toContainHtml('/* test */')
})
it('raw', function () {
tool.set('raw', '/* test */')
expect($tool.find('.eruda-raw')).toContainHtml('/* test */')
})
})